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.
Prinzip
Abschnitt betitelt „Prinzip“- 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.
Scope der Suche
Abschnitt betitelt „Scope der Suche“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.
Was angezeigt wird
Abschnitt betitelt „Was angezeigt wird“- 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.
Komponenten
Abschnitt betitelt „Komponenten“| 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).
Antwort-Form
Abschnitt betitelt „Antwort-Form“{ "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.
Fehlerhandling
Abschnitt betitelt „Fehlerhandling“- 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”.
Konfiguration
Abschnitt betitelt „Konfiguration“| 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.
Sicherheit
Abschnitt betitelt „Sicherheit“/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.
Offen / später
Abschnitt betitelt „Offen / später“- Schwellenwert ggf. pro Sprache nachjustieren.
- „Ansehen”-Link auf das Duplikat (Overlay-Navigation).