三语都发成功了,英文代码块却没了:翻译输出的结构漂移没被看见
周日的 20 点档跑完,发布脚本记了三语全部成功,封面也关联上了。第二天早上读者留言说英文版里那段 curl 示例是一整块纯文本,没换行也没高亮,粘走就跑不起来。回 CMS 翻原文,英文的 content_md 里 围栏根本没剩——翻译那一步把代码块拆成了普通段落,揉进了上下文中。

三语都发成功了,英文代码块却没了:翻译输出的结构漂移没被看见
周日的 20 点档跑完,发布脚本记了三语全部成功,封面也关联上了。第二天早上读者留言说英文版里那段 curl 示例是一整块纯文本,没换行也没高亮,粘走就跑不起来。回 CMS 翻原文,英文的 content_md 里 ``` 围栏根本没剩——翻译那一步把代码块拆成了普通段落,揉进了上下文中。
现场还原
我们的三语链路是:zh-CN 为源稿,router 拉模型翻出 zh-TW 和 en,再用同一个 slug 往 CMS 发三行数据。出事的当天,中文源稿里放了一段 yaml 配置加两行 curl,就是交付文档里最常见的东西。这个普通内容正好把两个薄弱点露了出来:
- 翻译 prompt 只写了"保留 markdown 格式",后面没有任何校验
- 发布脚本只看 HTTP 200,200 代表的是"我收到并写进去了",不代表"结构是对的"
对模型来说,把围栏块揉进段落是一种"低风险改动":意思没变,token 数量也差不多,在 temperature 0.3 下完全可能产出这种变体。更要命的是,前端渲染出来的 HTML 看起来还算能读,不逐字符对原文根本发现不了。
补的这道关
我们在三语进 CMS 之前加了一个结构指纹比对,指纹是三个列表:
1. 标题层级序列(h1/h2/h3 的次序和数量)
2. ``` 围栏块的数量,以及每块的第一行和最后一行
3. 表格数量及每张表的行数
先用 zh-CN 提取基准,每种译文各取一次,逐项对比。对不上就先自动重翻一次;重翻还不对就拦下这一语种并告警,不硬发——宁可缺一个语种提醒人工补,也不能发一个结构坏了的版本。实现很小:指纹提取就是一个按行扫描的函数,diff 就是比三个元组,总共不到一百行。它防的不是罕见 corner case,而是"同一个 prompt 长尾地差一步"这种变化。
这里有个容易踩的坑,说下我们自己的经历:最开始只比"围栏块数量",结果连续两天被误报——一次是译文把围栏外的一行说明句并进了块里,另一次是块内少了一行结尾。只比数量抓不住"多了一对围栏"或"块内容被挪走",所以每块的锚点必须带上首尾内容。阈值也别一上来就收紧:先并行跑一周,只记日志不拦截,拿真实漂移分布定好规则,再升成硬门禁。
数据
并行模式开了十天的记录:13 条翻译产出,9 条一次通过,4 条结构漂移。漂移的类型全部落在同一类——标题数对了但围栏内容被合并、列表被改写成行文。把门禁从"整单拦截"改成"单语种自动重试一次 + 留 diff 日志"之后,第二次通过率百分之百,人工介入为零。一期初通过率看着不低,但失败模式高度集中,说明这道关拦的是真问题,不是噪音。
给同跑道的人
如果你的管线里也用 LLM 做翻译或改写,别把"模型说它保留了格式"当验收标准。十分钟能搭的结构 diff:源稿和每份译文各提一次标题序列、围栏块、表格三个指纹,逐项比。好处是问题在进生产前被截住,而且你手上有上下文——哪个语种、哪一步、具体 diff 在哪。内容管线的成本和工程运维一样,难点不在单步多难,而在每一步"看起来都还行",而"看起来"不等于被验证过。
留言区
欢迎分享你的想法!
加载留言中…