双语站翻译管线:给 LLM 译文上质量闸

自用系统 · 开源方法 2026.07 – 至今 生产使用中

以前想让全站中英同步,要么手工翻一遍、要么赌 AI 一次翻对;现在改完中文源跑一条命令,增量翻译加逐字节质量闸挡住走样和丢数字的译文,英文镜像自动跟上。

业务问题

这个站是中文写的,我想给它一份英文镜像——页面、博客、简历数据全套。人工维护不现实:中文源经常改,英文跟着追等于再养一个站。直接丢给 LLM 整页翻译,试过就知道有三个坑:

  • 结构会被改坏——标签、属性、SVG 路径,模型「顺手优化」一下页面就花了
  • 措辞会被升格——中文源老老实实写「AI 辅助开发」,译文动不动就变成「独立开发」「from-scratch」式的吹牛。译文比原文能干,在我这个站上是事故不是惊喜
  • 细节会悄悄丢——数字、链接、限定词,丢了没人发现

所以真正的问题不是「怎么翻译」,而是怎么让机器翻译在没人盯着的情况下不越界

方案:整页翻译 + 确定性重写 + 质量闸

  • 翻译单元是整个文件:HTML、Markdown、JSON 三类源各配一份系统提示词,整页进整页出,不做碎片拼装
  • 增量翻译:manifest 记录每个中文源翻译时的内容哈希,没变的绝不重翻——改一页只花一页的钱
  • 确定性的事不交给模型:路径改写、canonical、语言标记这些机械变换,全部由代码在译文落盘前统一执行,模型只负责语言
  • 四道质量闸,全过才写盘:标签骨架逐字节比对、措辞红线、数字防丢、中文残留检查
  • 纯 Node 标准库,零 npm 依赖——和整个站一个原则,一个一千行上下的脚本就是全部
中文源 唯一事实来源 增量比对 哈希没变就跳过 LLM 翻译 整页进整页出 质量闸 骨架 · 措辞 · 数字 en/ 镜像 纯生成物 没过闸:带反馈重译 错误清单喂回模型 仍未过:拒绝 不写盘不记账 发布闸 过期镜像拒发布 没过闸的译文不落盘、不记账,重跑自动再来;路径、链接等确定性改写不交给模型
模型只坐在中间一格,它两侧全是不信任它的代码。

最硬的仗:防的不是翻错,是吹牛

做之前我以为质量闸主要防翻译错误,做完发现最大的敌人是模型的「热情」。中文源写「用自然语言指挥 Claude Code 完成」,模型翻着翻着就升格成不带任何限定词的「独立开发」——它以为是在帮我润色,实际是在替我撒谎。

所以措辞闸是硬编码的死规矩:「独立开发」「纯手写」「from-scratch」这类英文吹牛短语进禁词表,出现即拦;中文源里带「AI 辅助」限定的内容,译文必须保留 AI-assisted 对应表述,丢了也拦。数字闸把中文源里每个数字拿到译文里找,找不到就拦(认得「万/亿」到 million/billion 的换算形态,防误报)。

这不是杞人忧天。首翻 20 个文件,质量闸拦了 7 次:有想写 from-scratch 的,有把「AI 辅助」翻丢的,有弄丢「1400+」这个数字的。拦下后错误清单原样喂回模型重译一轮;两轮还不过就整个拒绝——不写盘、manifest 不记账,下次重跑从头再来。

让 LLM 干活的系统里,最重要的代码不是调用模型那几行,而是模型出错时把它拦下来的那几行。真实性不能外包给模型,只能交给闸门。

生产加固

  • 发布闸:发布脚本先比对 manifest,中文源变了还没重翻,发布直接停——英文镜像不会悄悄落后于中文
  • 体检常态化:已生成的英文产物并入全站检查,每次全量重过质量闸,外加禁词全局扫描和内链存在性检查
  • 通道容错:长文件生成时线路上几分钟没有字节,代理和中间层会掐掉连接。改走 SSE 流式输出,主通道不通自动回退 curl 备用通道,断了退避重试
  • 防呆:超大源文件直接拒译防截断;输出被截断一律按失败处理,绝不落半个文件

结果与反思

现在的工作流:改中文,跑一条命令,英文镜像自己追上来;忘了跑,发布闸会把我拦住。这个案例没有客户——交付对象就是这个站本身,你正在读的这个页面,也会被这条管线翻成英文。

诚实边界也要写清楚:有两篇归档教程里的 ASCII 流程图和批处理内容,模型连续多轮拒绝翻译,我最终给这两篇开了显式豁免、记成待办,而不是假装它们过了闸。另外,质量闸只保得住「结构与事实」,保不住「译得地道」——文风好坏还得靠人抽查。闸门的边界必须说清楚,否则它给人的安全感会超过它真实提供的安全。