造了一条翻译流水线,然后把它关掉了
用一周时间构建分片翻译系统,翻译了十几篇长文,最后发现不如直接丢进去
起点:一篇八万字的播客文字稿
我有一个个人知识库,里面有一个目录专门存放从网上收藏的外文文章,积攒了不少播客访谈和演讲的文字稿。我习惯把这些东西翻译成中文再读,一方面是阅读速度更快,另一方面是翻译本身就是一种深度阅读——你不得不去理解每一句话到底在说什么。
我之前经常直接把东西甩到chatbot中直接翻译,这一套当然问题不大,但是存在潜在的对内容压缩的风险。另外一点,我有超多配额的国产大模型,心想其实翻译不那么考验大模型的能力,因此不妨开发一套skills,来直接对内容进行翻译。当然,有这种方法直接短文章没问题,几千字的博客帖子扔进去,几分钟就出来了。但当我把一篇 超长的YouTube视频脚本或者一本书丢过去,就很难避免它翻到一半就开始丢内容、混术语,翻译质量明显下降。当然,还有一种情况:直接卡住,几个世纪过后告诉你失败...
于是我决定写一个分片翻译系统。
分片的基本想法
核心思路:把长文按章节标题切成若干片段,每片独立调用一次 LLM,翻译完再拼起来。每片有自己独立的上下文,不会溢出。
但"切开再拼回去"这件事,远没有听起来那么简单。最直接的问题是:切开之后,后一片看不到前一片的内容,怎么保证术语统一?怎么保证前后文衔接自然?
我用了两个机制。第一个是术语表接力:每片翻译完成后,脚本会从输出中提取新出现的术语对照(比如 "scaling law → 规模定律"),存入一个不断增长的术语表,注入到下一片的提示词里。这样模型在翻译后续片段时,已经知道前面把哪个词翻成了什么。
第二个是重叠段落:把前一片译文的最后三段文字带给下一片,标注"这是前文结尾,不要重复翻译,仅用于衔接语气"。这样模型能感知到前文的口吻和节奏,避免每片的开头都像是文章的第一段。
真正的文本什么样都有
系统写出来之后,拿几篇标准的博客文章测试,运行得很顺畅。但实际使用中很快遇到了各种边界情况。
第一个是播客文字稿的时间戳。YouTube 自动转录出来的文字稿里密布着 "0:00"、"1:23" 这样的时间戳,它们出现在原文的各个位置,需要在翻译时全部移除。我写了一个检测函数,扫描原文中的时间戳模式——粗体时间戳、裸时间戳、方括号时间戳、括号章节目录——出现三次以上就判定为视频脚本,自动切换到去时间戳、合并短句、跳过广告的处理模式。
第二个是没有章节标题的文档。我的切分逻辑以 ## 标题为边界,但有些演讲稿(比如 David Patterson 的那篇)是 58K 字符的纯文本,没有任何标题,也没有段落分隔。切分函数拿到这样的输入,一刀都切不下去。
为此我做了一个四级回退机制:先试按空行切,切不动就按换行切,再不行就按句号问号等标点切,最后兜底是强制按字符数在空格处截断。每一级都检查切出来的最大片段是否在限制以内,满足就停下来。
第三个是说话人识别。播客和访谈里有多个人在说话,翻译完之后读者分不清谁是谁。一开始我做了一个独立的后处理脚本,对已翻译的文本再跑一遍 LLM 来推断说话人。后来发现这条路不太对——中文译文里的说话人线索远不如英文原文丰富。英文原文中,提问的句式、自我介绍、对他人的称呼,这些线索都很明确。到了中文里,这些特征被翻译抹平了不少。
最终的方案是在翻译的同时就做说话人识别,把识别指令直接写进翻译提示词里,而不是翻译完再补。
断点续传:从简单到复杂
长文翻译经常中断——网络超时、API 限额、手动终止。翻译到第 8 片的时候断了,总不能从头再来。所以我给系统加了断点续传:每片翻译完成后立即写一个 checkpoint 文件,记录原文内容、原文的哈希值、译文和术语。下次运行时,检测到 checkpoint 存在且哈希匹配,就跳过这一片。
最初这个功能需要手动加 --resume 参数。很快发现自己总是忘记加,于是改成了自动检测:每次运行都去工作目录里看有没有已完成的 checkpoint,有就跳过,没有就翻译。
后来又遇到一个更微妙的问题。第一次用 10K 字符切分,翻译了一部分之后觉得片段太大、模型处理不好,想改成 5K 重新来过。但已经完成的那几片没问题,只是后面的需要切小一点。如果简单地重新切分,片段编号就全变了,已有的 checkpoint 对不上。
最终的解决方案是:保留已完成片段不动,把剩余未翻译的原文拼在一起,按新的尺寸重新切分,清理掉旧的未完成 checkpoint。这样已完成的工作一点不浪费,新的片段从正确的位置接上去。
回头看这个续传系统,它的复杂度已经超过了翻译本身。原文指纹校验、chunk manifest 持久化、动态重切、checkpoint 清理——这些机制解决的都是真实遇到的问题,但它们叠加起来之后,系统变得很难一眼看清全貌。
便宜的模型撑不住
系统设计了多模型支持,可以通过一个参数切换到不同的模型提供商。这个功能的初衷很实际:翻译是一个 token 消耗量很大的任务,如果能用更便宜的模型完成,成本可以降低一个数量级。
我试过用 MiniMax 翻译了一篇文章。结果很难接受:术语译法不稳定,同一个词前后出现了三四种不同的翻译;口语化的段落被改写成了不像中文的奇怪句式;说话人识别几乎完全失败,该标注的地方没标注,不该标注的地方乱标。当然,更关键的还是有些段落完全不讲人话。
不得已,所有正式翻译都回到了 Claude Sonnet。Sonnet 的翻译质量确实好得多,但代价是 token 单价高出很多倍。对于一篇普通的博客文章,这个成本还在可接受范围内。但 YouTube 上那些动辄两三个小时的长访谈——文字稿 70K、80K 字符起步——切成十几个片段逐一翻译,每片都用 Sonnet,总成本就变得很不经济了。
这让整个分片翻译系统陷入了一个两难:用便宜的模型质量不够用,用好的模型又太贵。而且片数越多,这个矛盾越突出。
每片的固定开销
这笔账其实越算越不乐观。
每片翻译的提示词不只有待翻译的原文,还包括一套固定的"脚手架":翻译原则(直译优先、不要中介转写、术语处理规则等),视频脚本处理指令(去时间戳、合并短句、说话人识别),章节标题策略,已有的术语表,前一片的末尾三段。这些加起来大约 700 到 1200 tokens,与原文内容无关。
7 个 10K 片段的文章,固定开销合计约 5600 tokens。如果同一篇文章切成 22 个 5K 片段,固定开销变成约 17600 tokens。多出来的一万多 tokens 全是在重复念同一套指令。而且术语表会随着翻译不断膨胀,后面的片段比前面的片段更贵。
这还只是输入端。输出端每片也有格式指令产生的额外开销。算下来,分片翻译比一次性翻译多消耗 15% 到 20% 的 token,而且片切得越小,比例越高。
关掉它的原因
最终让我决定放弃这套系统的,不是某一个具体的技术问题,而是一个整体判断:性价比不够。
速度慢是第一个原因。13 个片段的文章需要 13 次串行的 API 调用,每次都要等模型生成完整的翻译,加上重试和最后的导读生成,一篇文章翻译下来要几十分钟。这段时间不需要人工干预,但你得等着它跑完才能看到结果,没法边看边调整。
token 消耗大是第二个原因。分片的固有开销——每片重复的系统指令、不断膨胀的术语表、衔接段落——这些都是用 token 换来的跨片一致性。在上下文窗口只有 8K 的时代,这个代价值得付。但现在主流模型的上下文窗口已经到了 128K 甚至更大,一篇 70K 的文章可以一次性放进去,根本不需要切分。
翻译质量没有明显优势是第三个原因。我对比了分片翻译的结果和直接在交互式工具(比如 AI Studio)里翻译的结果,差别不大。分片系统在术语一致性上有一点优势——术语表接力确实能保证前后统一——但这个优势在一次性翻译中也基本能做到,因为模型本身就能在长上下文里保持一致。
系统复杂度高是第四个原因。断点续传、chunk manifest、动态重切、四级回退切分、视频脚本检测、说话人识别——每一个功能都是为了解决一个真实的问题,但它们合在一起形成了一个需要持续维护的系统。而我要做的事情只是翻译文章。
留下来的东西
系统关掉了,但过程中积累的翻译理念还是有用的。
比如"非中介翻译"这个约束。LLM 在翻译非英语文本时,有一种倾向:先把原文理解为英文,再从英文翻译成中文。日语翻中文的时候尤其明显——翻译结果读起来像是从英文转过来的,日语原文特有的省略结构和语气词全丢了。在提示词里明确写"不要借助英语或其他中介语言",能一定程度上缓解这个问题。
再比如"直译优先"的层次。很多人让 AI 翻译的时候会说"翻译要通顺",但"通顺"这个要求往往导致模型过度改写——补出原文没有的主语、加上原文没有的因果连接词、把原文的省略结构展开。我在提示词里建立了一个优先级:先尽量保留原文的语序和结构,实在不通顺再调整语序,最后才是为了可读性牺牲原文形式。这个分层让模型的翻译行为更可控。
还有术语表的处理策略。"首次出现时写成'中文翻译(原文术语)',后续直接使用中文"——这个规则在任何翻译场景下都适用,不限于分片系统。人名的处理也是一样:只有被广泛认可的译名(比如"欧拉")才翻译,其余一律保留原文,避免读者无法回溯原始人名。
这些规则不需要一个复杂的脚本来执行,写在任何翻译提示词里都能用。
一个关于工程判断的小教训
这个项目从开始到弃用,前后大约一周。期间翻译了十几篇文章,其中包括几篇八万字以上的长访谈。系统本身是能用的,翻译质量也可以接受。但最终的结论是:用一个交互式窗口手动操作,效率反而更高。
回头看,问题出在对环境变化速度的低估上。我在设计分片翻译的时候,心里的参照系是"上下文窗口不够长"这个前提。但是这种焦虑在可以轻松使用AI Studio这种提供1M上下文的平台的前提下,意义不大。
另一个教训是关于工程复杂度的。每一个功能——断点续传、动态重切、四级回退——单独看都很合理,每一个都是为了解决一个真实的、在使用中遇到的问题。但它们叠加起来之后,系统的维护成本开始超过使用收益。什么时候该停下来不再加功能,这个判断比"该加什么功能"更难做。
留下技术文档不是因为这个系统以后还会用,而是因为里面的一些设计思路——术语表接力、重叠衔接、分级回退切分——在其他需要分片处理长文本的场景下可能有参考价值。至于翻译本身,就回到最简单的方式:打开一个足够大的上下文窗口,把全文丢进去。另外,我也会偶尔翻出来再测试一下,把它接上claude或者国产的大模型,日后也可能会突然发现这套系统变得丝滑好用,届时可以用来翻译一些图书级别的长文档。