JSON 流解析中断时翻译被静默截断(未引发错误)

摘要
DiscourseAi::Translation::PostLocalizer(内容本地化)偶尔会产生截断的翻译结果,且未在任何地方记录错误日志(Rails 日志、Sidekiq 日志、Sidekiq 死信/重试队列)。截断发生在句子中间,静默进行,部分结果被保存,仿佛是一次完整且成功的翻译。

观察到的版本: Discourse 核心 + discourse-ai 46bea6613 (2026-07-09),LLM:通过 Google 提供商使用的 Gemini 2.5 Flash。

复现证据
我们实例上的两个真实案例:

  • 一篇约 2090 字符的德语帖子翻译成西班牙语后仅产生 50 个字符,在源文本中一个引用词(“aufwendiger”)之前立即截断。
  • 一篇约 1830 字符的德语帖子翻译成英语(83 个字符)和斯洛伐克语(97 个字符)——两者均在句子的完全相同位置截断,即在源文本中的一个表情符号简码 (:collision:) 之前立即截断。同一帖子的所有其他语言(法语、意大利语、葡萄牙语、俄语、希伯来语)均完整翻译(1600–1950 个字符)。

根本原因(据我所能追溯)
lib/completions/structured_output.rb 中,当 @partial_json_tracker.broken? 为 true 时,StructuredOutput#read_buffered_property 会回退到 DiscourseAi::Utils::BestEffortJsonParser.extract_key。此回退返回 JSON 流中断之前解析到的任何内容——没有异常、没有警告、也没有任何地方表明输出不完整。在 lib/translation/base_translator.rb#get_translation 中,该值被直接用作最终翻译结果,并通过 PostLocalization#save! 持久化保存。

我们数据中的两个截断点都位于源文本中某个“特殊”字符(直引号、表情符号简码)之前,这表明 JSON 流跟踪器因模型流式输出中的某些字符/序列而混乱,并过早地将流标记为 broken?

影响
内容以明显不完整、有时甚至不合逻辑的翻译形式发布,且没有任何操作信号(日志、死信任务、管理员通知)来捕获此问题。我们之所以发现它,是因为一位社区成员在 UI 中注意到了句子中间的乱码。

建议的修复方案

  • StructuredOutput#broken? 为 true 且使用尽力而为的回退机制时,输出警告/错误(Rails.logger + Discourse-error-log),以便管理员能够检测到该问题,而不是静默发布部分内容。
  • 考虑在保存 PostLocalization 之前,根据源文本长度对翻译长度进行合理性阈值验证。

如果有助于调试 JSON 跟踪器,我很乐意私下提供完整的原始/翻译文本对。

1 个赞