JSONストリーム解析が失敗するとエラーなしで翻訳が静かに切り捨てられる

概要
DiscourseAi::Translation::PostLocalizer(コンテンツのローカライズ)は、稀にエラーがどこにもログに記録されないまま(Rails ログ、Sidekiq ログ、Sidekiqのdead/retryキュー)、切り捨てられた翻訳を出力します。この切り捨ては文の途中で静かに発生し、部分的な結果が完全で成功した翻訳であるかのように保存されます。

確認環境: Discourse core + discourse-ai 46bea6613(2026-07-09)、LLM: Googleプロバイダ経由のGemini 2.5 Flash。

再現のエビデンス
当インスタンスでの2つの実際のケース:

  • 約2090文字のドイツ語投稿をスペイン語に翻訳したところ、50文字しか生成されず、ソーステキスト内の引用語(“aufwendiger”)の直前で途切れました。
  • 約1830文字のドイツ語投稿を英語(83文字)およびスロバキア語(97文字)に翻訳したところ、両方とも文の全く同じ地点、ソーステキスト内の絵文字ショートコード(:collision:)の直前で途切れました。同じ投稿の他のロケール(fr, it, pt, ru, he)はすべて完全に翻訳されていました(1600~1950文字)。

根本原因(私が追跡できる範囲で)
lib/completions/structured_output.rbにおいて、StructuredOutput#read_buffered_propertyは、@partial_json_tracker.broken?がtrueの場合、DiscourseAi::Utils::BestEffortJsonParser.extract_keyにフォールバックします。このフォールバックは、JSONストリームが破損する前に解析されたものを返しますが、例外、警告、または出力が不完全であることを示す何らかのインジケーターは一切ありません。lib/translation/base_translator.rb#get_translationでは、この値がそのまま最終翻訳として使用され、PostLocalization#save!を介して永続化されます。

当方のデータにおける2つの切り捨てポイントは、どちらもソーステキスト内の「特殊」文字(ストレートクォート、絵文字ショートコード)の直前に位置しており、JSONストリーミングトラッカーがモデルのストリーミング出力における特定の文字/シーケンスによって混乱し、ストリームが prematurely(早期に)破損したとマークされていることを示唆しています。

影響
目に見えて不完全で、場合によっては意味不明な翻訳が公開され、これを検出するための運用上のシグナル(ログ、deadジョブ、管理者通知)が存在しません。コミュニティメンバーがUI内の文途中の無意味なデータに気づいたため、発見に至りました。

提案される修正

  • StructuredOutput#broken?がtrueであり、ベストエフォートフォールバックが使用された場合に、警告/エラーを出力する(Rails.logger + Discourse-error-log)。これにより、管理者が部分的なコンテンツを静かに配信するのではなく、これを検出できます。
  • PostLocalizationを保存する前に、翻訳後の長さがソースの長さに対する健全性閾値に対して検証することを検討してください。

JSONトラッカーのデバッグに役立つ場合は、完全な生テキスト/翻訳テキストのペアを非公開で提供できます。

「いいね!」 2

この件に関するデータポイントを一つ追加します。私どもは、同じ根本的なメカニズムが異なった形で現れているように見受けられます。

観測環境: Discourse コア + discourse-ai、LLM: OpenAI 経由の GPT-5.1(カスタム API 接続、シードモデルではありません)。

再現の証拠

同じ元の投稿(ロシア語、約2000文字、見出し/太字/箇条書き/リンクを含む)を、ネイティブ AI 翻訳機能を用いて複数のロケールに翻訳した結果:

  • ポーランド語(pl):埋め込み画像の Markdown が構造を失いました。![alt text|690x460](upload://...) から、! プレフィックスと | セパレータが欠落し、代替テキストと寸法がプレーンテキストとして連結された不正なリンクに変わりました。結果として、埋め込み画像ではなくクリック可能なリンクとしてレンダリングされました。
  • ウクライナ語(uk):段落区切りの代わりに、投稿全体にリテラルの \n\n シーケンスがプレーンテキストとして表示されました。この翻訳では画像の Markdown は無事でした。つまり、破損は特定の症状に固定されているわけではなく、実行によって変動します。

上記の切り捨て問題とは異なり、私どものケースでは内容の欠落はありません。全文は存在しますが、エスケープシーケンスや Markdown 構文が破損しており、出力が短くなるわけではありません。プロバイダーが異なります(OpenAI vs Google)、症状も異なります(破損 vs 切り捨て)が、疑われる根本原因は同じです。StructuredOutput#read_buffered_propertyBestEffortJsonParser#extract_key にフォールバックしており、このフォールバックは JSON 文字列シーケンス(\n がリテラルのまま)をアンエスケープせず、またフォールバックがパーシング途中にトリガーされた場合、Markdown 構文の隣接する特殊文字も適切に処理できていないようです。

Sidekiq や Rails のログにもエラーは記録されていません。これもまた、サイレントフェイルオーバーの挙動を示しています。