Übersetzung wird stillschweigend abgeschnitten, wenn die JSON-Stream-Analyse fehlschlägt (kein Fehler ausgelöst)

Zusammenfassung
DiscourseAi::Translation::PostLocalizer (Inhaltslokalisierung) erzeugt gelegentlich
abgeschnittene Übersetzungen, wobei nirgendwo ein Fehler protokolliert wird (Rails-Log,
Sidekiq-Log, Sidekiq Dead/Retry-Warteschlangen). Der Abbruch erfolgt mitten im Satz,
still und das unvollständige Ergebnis wird so gespeichert, als wäre es eine vollständige,
erfolgreiche Übersetzung.

Beobachtet bei: Discourse Core + discourse-ai 46bea6613 (2026-07-09), LLM:
Gemini 2.5 Flash über Google Provider.

Reproduktionsnachweis
Zwei reale Fälle auf unserer Instanz:

  • Ein ~2090 Zeichen langer deutscher Beitrag wurde ins Spanische übersetzt, ergab jedoch
    nur 50 Zeichen und brach sofort vor einem zitierten Wort („aufwendiger“) im Quelltext
    ab.
  • Ein ~1830 Zeichen langer deutscher Beitrag wurde ins Englische (83 Zeichen) und
    Slowakische (97 Zeichen) übersetzt – beide brachen an der genau gleichen Stelle
    im Satz ab, unmittelbar vor einem Emoji-Shortcode (:collision:) im Quelltext.
    Alle anderen Lokalisierungen (fr, it, pt, ru, he) für denselben Beitrag wurden
    vollständig übersetzt (1600–1950 Zeichen).

Ursache (soweit ich sie nachverfolgen konnte)
In lib/completions/structured_output.rb fällt StructuredOutput#read_buffered_property
auf DiscourseAi::Utils::BestEffortJsonParser.extract_key zurück, sobald
@partial_json_tracker.broken? true ist. Dieser Fallback gibt alles zurück, was
vor dem Abbruch des JSON-Streams geparst wurde – ohne Ausnahme, ohne Warnung und
ohne jegliche Andeutung, dass die Ausgabe unvollständig ist. In
lib/translation/base_translator.rb#get_translation wird dieser Wert unverändert als
endgültige Übersetzung verwendet und über PostLocalization#save! gespeichert.

Beide Abbruchstellen in unseren Daten liegen unmittelbar vor einem „speziellen"
Zeichen im Quelltext (ein gerades Anführungszeichen, ein Emoji-Shortcode),
woraufhin der JSON-Streaming-Tracker durch bestimmte Zeichen/Folgen in der
gestreamten Ausgabe des Modells verwirrt wird und den Stream vorzeitig als
broken? markiert.

Auswirkungen
Inhalte werden mit sichtbar unvollständigen, manchmal unsinnigen Übersetzungen
veröffentlicht, und es gibt kein operationales Signal (Log, Dead-Job, Admin-Benachrichtigung),
um dies zu erkennen. Wir haben es nur entdeckt, weil ein Community-Mitglied
Müll mitten im Satz in der UI bemerkte.

Vorgeschlagene Korrekturen

  • Eine Warnung/einen Fehler (Rails.logger + Discourse-error-log) ausgeben, wenn
    StructuredOutput#broken? true ist und der Best-Effort-Fallback verwendet wird, sodass
    Administratoren dies erkennen können, anstatt stillschweigend unvollständige Inhalte
    zu veröffentlichen.
  • In Betracht ziehen, die Länge der Übersetzung vor dem Speichern einer PostLocalization
    anhand einer Plausibilitätsschwelle relativ zur Quelltextlänge zu validieren.

Ich kann bei Bedarf vollständige Roh-/Übersetzungstextpaare privat zur Verfügung stellen,
um beim Debuggen des JSON-Trackers zu helfen.

2 „Gefällt mir“

Ich möchte hier einen weiteren Datenpunkt hinzufügen – wir beobachten einen Mechanismus, der zwar dem gleichen zugrunde zu liegen scheint, sich aber anders manifestiert.

Beobachtet bei: Discourse Core + discourse-ai, LLM: GPT-5.1 über OpenAI (benutzerdefinierte API-Verbindung, kein vorgefertigtes Modell).

Nachweise zur Reproduktion

Derselbe Quellbeitrag (Russisch, ~2000 Zeichen, Überschriften/Fett/Bullet-Listen/Links),
übersetzt in mehrere Sprachen über die native KI-Übersetzung:

  • Polnisch (pl): Die Markdown-Struktur des eingebetteten Bildes ging verloren – aus
    ![alt text|690x460](upload://...) wurde ein fehlerhafter Link, dem das !-Präfix und der |-Separator fehlten, wobei Alt-Text und Abmessungen als Klartext
    zusammenliefen. Es wurde als anklickbarer Link statt als eingebettetes Bild gerendert.
  • Ukrainisch (uk): Literalen \n\n-Sequenzen erscheinen als Klartext im gesamten Beitrag, anstelle von Absatzumbrüchen. Das Markdown des Bildes in dieser
    gleichen Übersetzung war intakt – die Beschädigung ist also nicht auf ein festes Symptom beschränkt, sondern variiert je nach Ausführung.

Im Gegensatz zum oben beschriebenen Abschneiden zeigt unser Fall keine fehlenden Inhalte –
der vollständige Text ist vorhanden, jedoch mit beschädigten Escape-Sequenzen / Markdown-
Syntax statt einer verkürzten Ausgabe. Anderer Anbieter (OpenAI vs. Google
hier), anderes Symptom (Beschädigung vs. Abschneiden), gleicher vermuteter Grund:
StructuredOutput#read_buffered_property fällt zurück auf
BestEffortJsonParser#extract_key, was JSON-String-
Sequenzen (\n bleibt literal) nicht unescaped und scheinbar auch Sonderzeichen neben Markdown-Syntax falsch behandelt, wenn der Fallback mitten im Parsing ausgelöst wird.

Auch in unseren Sidekiq- oder Rails-Logs keine Fehler – dasselbe Verhalten eines stillen Fehlers.