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

「いいね!」 1