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 跟踪器,我很乐意私下提供完整的原始/翻译文本对。

2 个赞

补充一个数据点——我们观察到的现象似乎源于相同的底层机制,但表现形式不同。

观察环境: Discourse 核心 + discourse-ai,LLM:通过 OpenAI 接入的 GPT-5.1(自定义 API 连接,非预置模型)。

复现证据

同一源帖子(俄语,约 2000 字符,包含标题/粗体/项目符号列表/链接),通过原生 AI 翻译功能翻译成多种语言:

  • 波兰语 (pl):嵌入图片的 Markdown 结构丢失——从 ![alt text|690x460](upload://...) 变为格式错误的链接,缺失了 ! 前缀和 | 分隔符,替代文本和尺寸混在一起作为纯文本显示。渲染结果为可点击链接而非嵌入图片。
  • 乌克兰语 (uk):整个帖子中出现了字面量的 \n\n 序列,取代了段落换行。同一翻译中的图片 Markdown 保持完整——因此这种损坏并非由单一固定症状引起,而是随每次运行而变化。

与上述描述的截断问题不同,我们的案例中并未出现内容缺失——全文完整存在,但转义序列 / Markdown 语法遭到破坏,而非输出被缩短。不同的服务商(此处为 OpenAI 而非 Google),不同的症状(损坏而非截断),但疑似根本原因相同:StructuredOutput#read_buffered_property 回退至 BestEffortJsonParser#extract_key,该方法未对 JSON 字符串序列进行转义处理(\n 保持字面量),并且在解析中途触发回退时,似乎也无法正确处理 Markdown 语法旁边的特殊字符。

我们这边的 Sidekiq 或 Rails 日志中也没有报错——同样表现为静默失败。