Gegenrede: Architektur-Brief KI-Recherche

Adressiert an Tech-Redaktionen, die die Datenschutz- und EU-Hosting-Aussagen aus der Pressemappe technisch nachvollziehen möchten. Zusammengefasst aus der internen Architektur-Dokumentation; Originalquellen einzelner Aussagen auf Anfrage.


1. Überblick

Die KI-Recherche prüft eine vom Nutzer eingegebene Behauptung gegen eine Allowlist offizieller deutscher und europäischer Quellen und liefert eine strukturierte Antwort in drei Tonarten zurück. Die gesamte Verarbeitung läuft in der EU; auf dem Server werden weder die Eingabe noch die IP-Adresse gespeichert.

Der Verarbeitungsweg in Schritten:

  1. App: Pre-flight-Guardrails (Mindestlänge, PII-Heuristik) und lokaler Tages-Quota-Check.
  2. App: HTTPS-Streaming-POST an die Cloud Function aiLookupStream, Region europe-west3 (Frankfurt).
  3. Function: App-Check-Token-Validierung (App Attest auf iOS, Play Integrity auf Android).
  4. Function: Server-Guardrails (Längen-Cap, Injection-Regex, Topic-Gate).
  5. Function: Tages-Quota-Decrement im Firestore-Counter (per device + global).
  6. Function: Aufruf der Mistral Agents API mit stream: true und dem eingebauten web_search-Tool.
  7. Function: Filter aller vom Modell zitierten URLs gegen die Allowlist (filterToAllowlist), anschließend HEAD-Verifikation mit Host-Root-Fallback (verifyAndNormalizeSources).
  8. Function: SSE-Events zurück an die App, sobald die jeweiligen Sektionen geschlossen sind.
  9. App: Progressive Rendering Block für Block; beim done-Event Cross-Fade in den finalen Antwort-Screen.

2. Pipeline-Diagramm

flowchart LR
    A["iOS / Android App"] -->|"HTTPS POST + App Check Token + SHA-256(device UUID) + claim text"| B["aiLookupStream<br/>(Cloud Function Gen 2, europe-west3 / Frankfurt)"]
    B --> C{"Guardrails<br/>len ≤ 500, injection regex, topic gate"}
    C -->|reject| Z1["SSE: error event"]
    C -->|pass| D{"Rate Limit<br/>per-device + global (Firestore counters)"}
    D -->|exhausted| Z2["SSE: rateLimited / quotaExhausted"]
    D -->|allow| E["Mistral Agents API<br/>mistral-large-latest, stream: true<br/>(France / Frankfurt)"]
    E --> F["web_search Tool<br/>(Brave-backed, EU-only)"]
    F --> E
    E -->|streamed tokens| G["Marker Parser<br/>«META», «RESPONSE:tone», «FACT», «SOURCE» ..."]
    G --> H["Allowlist Filter<br/>trusted_sources.json (75 domains)"]
    H --> I["HEAD Verifier<br/>host-root fallback"]
    I -->|"no source survives"| Z3["SSE: noEvidence + reformulation chips"]
    I -->|"≥1 source ok"| J["SSE: typed events back to client"]
    J --> A
    Z1 --> A
    Z2 --> A
    Z3 --> A

3. Streaming-Protokoll (Server-Sent Events)

Die Function liefert die Antwort nicht in einem Block, sondern als typisierte SSE-Frames. Damit erscheint das Verdikt-Chip nach ~5–10 Sekunden, während Quellen und Perspektiven im Hintergrund weiter eintrudeln.

Frame-Reihenfolge:

meta          → optional rephrased   → 3 × response   → fact   → N × source   → M × perspective   → conversation starters   → keywords   → done | error

Beide Plattformen rollen ihren eigenen chunk-basierten SSE-Parser (AILookupSSEParser auf iOS, AILookupSseParser auf Android), weil die System-Line-Iteratoren in beiden Stacks Leerzeilen verschlucken und genau diese Leerzeilen sind in SSE der Frame-Separator.


4. Allowlist und Citation Gating

Die harte Regel: keine Antwort ohne mindestens eine durch die Allowlist + HEAD-Pipeline gelaufene Quelle.


5. Rate Limits und Cost Control

Drei Ebenen, die unterschiedliche Angriffsflächen abdecken.

Ebene Limit Wo Zweck
Client (UX-Framing) 3 / Gerät / Tag iOS Keychain, Android EncryptedSharedPreferences Faire Quotenwand für den Nutzer, transparent kommuniziert
Server (per Device) 50 / Gerät / Tag Firestore ai_rate_limits/{deviceHash}, 24h-Rolling-Window Autoritatives Spend-Cap, greift auch dann, wenn die Client-Wand umgangen wird
Server (global) 1.000 / Tag Firestore ai_rate_limits_global/today Cost-Runaway-Verteidigung (~50 € / Tag Cap bei ~0,05 €/Lookup)

Der Abstand zwischen Client-Wand (3) und Server-Cap (50) ist beabsichtigt. Die 3 sind die Wand, die der Nutzer sieht, mit Mitternachts-Reset und klarer Ansage. Die 50 sind die Reißleine für den Fall, dass jemand den Client-Counter aushebelt, etwa durch wiederholte Neuinstallation unter Android, wo die Geräte-Kennung eine Deinstallation nicht überlebt. Ein echter Nutzer kommt dieser Grenze nie nahe; sie ist so gesetzt, dass sie beim normalen Gebrauch und beim Testen nicht im Weg steht, aber die Kosten pro Gerät nach oben hin begrenzt.

Für submitFeedback (Feedback, Inhaltsmeldungen, Themenvorschläge, ab App-Version 2.3.0) laufen zwei weitere, voneinander getrennte Zähler: 5 / Gerät / Tag für Feedback aus den Einstellungen und 15 / Gerät / Tag für Meldungen und Vorschläge an einer Behauptung. Getrennt deshalb, weil das Melden von drei toten Quellen-Links in einer Sitzung normale Nutzung ist und niemandem das Kontingent für einen echten Fehlerbericht wegnehmen soll.


6. App Check und Device Identity

Zwei orthogonale Mechanismen sorgen dafür, dass nur die offizielle App den Endpunkt aufrufen kann und Quoten pro Gerät zugeordnet werden, ohne dass eine persönliche Identität entsteht.


7. Failure Modes

Jeder Fehlerpfad hat einen eigenen UI-State und einen eigenen Telemetrie-Marker, damit sich im Nachhinein nachvollziehen lässt, wie häufig welcher Fall greift.

Auslöser UI-State Backend-Code Quoten-Verbrauch
Quota lokal erschöpft quotaExhausted (Frontend, kein Backend-Hit) nein
Server-Quota pro Gerät erschöpft rateLimited resource-exhausted nein
App-Version unter Minimum unsupportedAppVersion failed-precondition nein
Mistral Timeout / Netzwerk error.transport (Client-Side AbortError) nein
Mistral antwortet, aber keine Allowlist-Quelle noEvidence + Reformulierungs-Chips no_evidence, reason no_sources_verified ja
Modell-Output unparsbar error.rejected internal mit Tag nein

Das absichtliche Verhalten: nur erfolgreiche Mistral-Arbeit kostet einen Quota-Slot. Transport-Fehler oder Server-Rejects, bei denen der User „Server gerade nicht erreichbar" sieht, ziehen keinen Slot, andernfalls wäre die UX einer schlechten Verbindung eine doppelte Bestrafung.


8. Code-Einblicke

Das Code-Repository ist nicht öffentlich. Für Tech-Redaktionen, die einzelne Aussagen tiefer nachvollziehen möchten, teilen wir gezielt einzelne Code-Stellen oder Auszüge aus der internen Architektur-Dokumentation auf Anfrage. Stack-Übersicht:

Kontakt für tiefergehende Fragen: kontakt@gegenrede.app.


9. Was hier bewusst fehlt


Stand

Zuletzt aktualisiert am 25. Juli 2026. Veröffentlicht war zu diesem Zeitpunkt App-Version 2.3.0 für iOS und Android. Bei technischen Rückfragen: kontakt@gegenrede.app.