Zum Inhalt springen

Duplikat-Check beim Verfassen von Argumenten

Erkennt beim Verfassen eines neuen Arguments, ob für dieselbe Vorlage bereits ein inhaltlich (nahezu) gleiches Argument gleicher Position existiert, und zeigt es dem Nutzer als weichen Hinweis an. Erster Konsument der Argument-Embeddings; Die Prüfstufe des Composers baut der Client darum herum.

Dies ist Kriterium 5 („Kein Duplikat”) der fünf offiziellen Kriterien für neue Argumente → ARGUMENT_CRITERIA.md. Dieses Dokument beschreibt nur das Duplikat-Detail.

  • Embedding + Schwelle, kein LLM. Cosine-Ähnlichkeit über pgvector-Embeddings; nur Treffer ≥ Schwelle werden gezeigt. Das genügt für die Frage „gibt es ein ähnliches Argument?” — die letzte Entscheidung trifft der Nutzer.
  • Weicher, nicht-blockierender Hinweis. Der Nutzer kann das bestehende Argument ansehen oder seines trotzdem einreichen. Nichts wird verhindert.
  • Der Nutzer urteilt, nicht die Maschine. Es wird kein „ist Duplikat = ja/nein” behauptet; es wird das ähnlichste bestehende Argument vorgelegt.

Verglichen wird strikt eingegrenzt, alle drei Filter zusammen:

Filter Warum
Gleiche Vorlage (scope_rkey = dossier_rkey) Argumente anderer Vorlagen sind irrelevant.
Gleiche Position (a.type = PRO/CONTRA) Gleiches Thema mit gegenteiliger Position ist kein Duplikat, sondern das Gegenargument. Ohne diesen Filter wäre der nächste Treffer oft das Gegenteil (Confound).
Gleiche Sprache (lang, kanonisch z.B. de-CH) Gleichsprachiger Vergleich ist am präzisesten; Embeddings liegen pro Sprache vor.

Seit 2026-11 ist der Store nur noch das neutrale Werkzeug: Er sucht ähnliche Argumente. Die übrigen Kriterien (Stimmigkeit, Thema, Ton, Fokus) prüft der Client selbst (ARGUMENT_CRITERIA.md).

Composer (Schreiben) → "Prüfen"
│ Client (smartinfo: client-api POST /arguments/precheck, mit eigener LLM-Prüfung)
▼
AppView POST /xrpc/ch.atvoto.argument.findSimilar (Viewer nötig, Rate-Limit)
│ Vektor des Entwurfs vom Writer
▼
Writer POST /internal/embed (clusterintern, Service `writer`, Infomaniak)
▲
│ zurück an die AppView: Cosine gegen app_embeddings
▼
Postgres / pgvector (gefiltert: Vorlage + Position + Sprache + Modell, ohne
zurückgezogene Vorlagen, >= Schwelle, ORDER BY <=>)

Das Duplikat im Peer Review (peerreview.duplicateCandidate) braucht keinen Embedding-Aufruf: Es vergleicht den gespeicherten Vektor des Arguments per SQL.

  • Top-1: das eine ähnlichste gleichgesinnte Argument über der Schwelle (Titel, PRO/CONTRA-Badge, Ähnlichkeit %). Empirisch deckt Top-1 ~99 % ab (siehe Kalibrierung).
  • Keine Treffer → grüner „keine Duplikate”-Hinweis.
  • Prüfung nicht verfügbar (Writer oder Anbieter nicht erreichbar) → grauer Hinweis „du kannst trotzdem einreichen”. Nicht-blockierend.
Service Datei Rolle
AppView routes/participation/similar.py XRPC ch.atvoto.argument.findSimilar
AppView core/similarity.py _SIMILAR_SQL, _DUP_SQL (Cosine, Filter, Schwelle), embed_draft()
Writer internal_api.py POST /internal/embed
Writer embedding/ Backfill der Argument-Embeddings (jede Minute), Infomaniak-Client
Client smartinfo: services/client-api/src/routes/precheck.py, services/frontend/src/components/add-argument-modal.tsx Check-Bündel und Composer

Datenbasis: app_embeddings (ein Vektor je (argument, lang), Modell in der Zeile, Qwen/Qwen3-Embedding-8B @ 1024).

{ "lang": "de-CH", "status": "ok", "items": [ {uri, title, body, type, similarity} ] }

status: "unavailable", wenn der Writer oder sein Anbieter nicht antwortet. So lässt sich „echt keine Treffer” von „Prüfung fehlgeschlagen” unterscheiden.

  • AppView: Writer nicht erreichbar / non-200 → {status:"unavailable"}, Ursache im AppView-Log (embed_draft:).
  • Writer: Anbieter langsam oder fehlerhaft → 502 (Timeout 10 s, keine Wiederholung, eine Person wartet).
  • Client: eigener Fallback auf „nicht verfügbar”.
Env-Var Default Wirkung
APPVIEW_DEDUP_SIM_THRESHOLD (AppView) 0.66 Anzeige-Schwelle. Darunter → „keine Duplikate”.
WRITER_INTERNAL_URL (AppView) http://writer Writer-Adresse; lokal z.B. http://localhost:3000.
APPVIEW_EMBEDDING_MODEL / _DIMENSIONS (Writer) Qwen/Qwen3-Embedding-8B / 1024 Modell; ein neues Modell rechnet alles neu, die Dimension muss zu vector(1024) passen.
APPVIEW_EMBEDDING_PRODUCT_ID / _API_KEY (Writer) sonst APPVIEW_TRANSLATE_* Infomaniak-Zugang. Fehlt er → 503 → „nicht verfügbar”.

limit (Top-N) ist auf 1 defaultet, höchstens 10.

Empirische Kalibrierung (Ballot 663.1, de-CH, same-stance)

Abschnitt betitelt „Empirische Kalibrierung (Ballot 663.1, de-CH, same-stance)“
  • Echte Near-Dupes liegen bei 0.66–0.82; Rauschen darunter. Schwelle 0.66 fängt auch knappe Fälle („Weniger ist mehr”, „ÖV”); recall-orientiert (weicher Hinweis, Fehltreffer = ein Klick).
  • Von 111 Argumenten haben 21 (~19 %) mind. einen gleichgesinnten Treffer ≥ 0.66, nur 4 (~3.6 %) mehr als einen.
  • Top-1 vs Top-2: Das Szenario „Top-1 kein Duplikat, Top-2 schon” kann nur bei diesen 4 auftreten und ist dort die unwahrscheinlichere Konstellation → Restrisiko ~1 %. Top-2 wäre praktisch gratis (gleiche Query) als Absicherung.

/internal/embed ist clusterintern (Service writer, kein Ingress, keine Authentisierung): sonst lägen Infomaniak-Kosten offen. Öffentlich ist nur argument.findSimilar, mit Viewer und Rate-Limit. Siehe ARCHITECTURE_SPLIT.md §6.4.

  • Schwellenwert ggf. pro Sprache nachjustieren.
  • „Ansehen”-Link auf das Duplikat (Overlay-Navigation).