最近在处理一批压箱底的文档翻译任务时,我彻底重构了一遍自己的工作流。手头是7份技术手册和产品规范,加起来将近400页,里面各种大段说明文字、数学公式、代码清单和参数表格混在一起。一开始想偷个懒,用在线翻译工具凑合跑,结果第一份文档翻完就傻眼了:公式乱掉、术语前后不统一、表格排得面目全非。后来我改成基于大模型API做批量翻译,顺手搭了一个叫LLMFanyi的小工具,把"文档输入、分块、翻译、还原"整条链路跑通之后,原本要磨一周的活,一个晚上就处理完了。这篇文章就是把这套批量翻译方案的选型思路、工程实现、常见坑位完整拆一遍,适合需要批量处理外文技术文档、论文和产品说明书的同学参考。
1. 先搞懂批量翻译到底难在哪
1.1 为什么人工翻译和普通机器翻译都撑不住
回到最朴素的场景:一份外文技术手册,满屏小标题、图表编号、数学公式,还有一堆特有的缩写词。人工翻译的质量确实是最好的,但瓶颈非常明显——慢、费脑、极其依赖术语一致性。术语一致性这个东西,文档越长越要命。前面第二章把某个词翻成"处理器",后面第四章又变成"中央处理单元",读者马上就会被绕晕。一个人从头翻译到尾都容易这样,更别提多人按章节分工协作,每个人各翻各的,术语风格很难统一。
普通机器翻译又走到另一个极端:速度快、成本低,但几乎不真正理解上下文。它对公式、代码块、内联标记(比如LaTeX的$...$、Markdown里的代码块)处理得非常莽,要么把公式也强行"翻译"一遍,要么把代码里的变量名改成英文直译,甚至直接把标记符号吞掉。更难受的是长句子,经常前半句和后半句语义对不上,读起来像是被揉成一团的纸。
大模型接口的出现,算是同时补上了这两块短板:它带上下文窗口,能理解一段话前后的关系;指令跟随能力也强,你告诉它"公式保持原样、代码不要翻译、术语按对照表来",它基本会照做。但大模型API也不是开箱即成的翻译工具。它不会自动把400页文档变成几十个请求发出去,更不会帮你处理并发、重试、断点续传这些问题。所以批量翻译真正的难点,其实不在"翻译"这两个字上,而是在于怎么设计一套流程,把长文档拆成模型能吃进去的一块块内容,再把结果安全地拼回来。
1.2 批量翻译的"公式"长什么样
我做这个批量翻译工具时,把所有问题收敛成了一句话:先把脏乱的长文档变成规格统一的中间格式,再把它拆成适合模型处理的小块,逐块翻译完成后按原顺序合并。这套流程我拆成了四个环节:输入清洗、分块调度、模型翻译、还原合并。任何一篇文档的批量翻译,都可以套进这四个环节,区别只在于每一环的具体做法。
输入清洗是第一个容易被忽略的环节。PDF转出来的文本经常带着莫名其妙的换行和空格,Word文档里残留各种样式标记,不先统一成干净文本,后面分块和翻译都会埋坑。我习惯把所有输入统一转成Markdown,因为Markdown能用结构化语法表达标题层级、代码块、表格、公式,而这些结构正是翻译时需要重点保护的。
分块调度是决定质量上限的关键环节。模型有上下文长度限制,一次性塞整章内容大概率超限;反过来,如果把文本按一行一行切,上下文完全丢失,术语一致性一定会崩。我的处理方法是按"标题加自然段落"切块,单块控制在千字以内,同时把前一块译文摘要带进请求,给模型续上上下文。
模型翻译这步,质量主要由提示词决定。一份合格的翻译提示词,至少要包含角色设定、翻译规则、术语对照表、输出格式要求。温度参数要调低,翻译任务不需要模型自由发挥。
还原合并听起来机械,但不做的话前功尽弃。核心思路是分块时用占位符保护公式和代码,翻译完成后再按标记替换回原始内容。
这四个环节组合起来,就是我说的批量翻译"公式"。你可以把它理解成一条流水线:脏的原料进来,经过四道工序,变成干净的成品。每一道工序都能单独调试优化,这也是我建议你借鉴整体思路、而不是照抄某一份脚本的原因。
2. 工具选型与环境准备:模型接口和运行依赖怎么选
2.1 模型接口的三项权衡:质量、成本、速度
选哪家模型接口,可以从三个维度来评估。
质量方面,翻译很吃上下文理解,所以优先选指令跟随能力强、支持长上下文的模型接口。我实测下来的感受是,能处理较长上下文、又专门做过语言语料优化的模型,翻译技术文档时要稳很多,术语不容易乱飘。普通在线翻译引擎完全做不到这个程度。
成本方面,大模型接口按token计费,翻译任务的消耗分输入token和输出token。一个粗略的估算方法是:1个汉字大约对应0.7到1个token,英文则大约3到4个字符对应1个token。跑全量之前,先用几页样本估算一下总token量,再乘以单价,心里就有底了。千万别上来就全量跑,否则账单出来会很肉疼。
速度方面,串行一条条发请求最稳但慢,并发能大幅提速,但容易触发限流。关键是"稳定地压着配额跑",而不是一味追求并发数。我一般从并发4到6条开始跑,观察错误率和响应耗时,再逐步往上加,加到自己能接受的抖动水平为止。
这三者说白了是个三角:要质量好,往往价格不便宜;要便宜,质量可能打折;要速度快,就得接受并发带来的限流风险。我的折中方案是:正式交付走质量优先的接口,草稿和初翻走更便宜的备用接口,代码不变,只改配置项里的模型名。
2.2 云端API还是本地推理框架
这一步的选择直接决定成本和隐私边界。
云端API的好处是零运维、按量付费、有问题不用自己扛。坏处是每次请求都要把文本送出内网,如果是内部规范、未公开的技术方案这类敏感文档,就不太合适。
本地推理框架的好处是数据不出网,长期跑大量任务时边际成本低。坏处是你需要一张能打的显卡,并且要自己处理模型部署、量化、版本升级这些事。我一开始是纯云端把流程跑通,后来量大了,才把一部分重复性工作转回本地。如果你是个人使用、量也不大,建议先走云端,省下的那些钱可能还不够你折腾推理环境的时间成本。
2.3 最小运行环境准备
到代码层面,依赖其实非常轻。只需要Python环境,外加两个常见库:一个负责发HTTP请求,一个负责解析JSON,不需要引入重型框架。
我搭的项目结构大概长这样:
LLMFanyi/ ├── config.yaml # 模型名、接口地址、Key、并发数等配置 ├── utils.py # 公共工具函数 ├── translator.py # 翻译主流程 ├── splitter.py # 文档分块与占位符保护 ├── resume.py # 断点续传逻辑 └── output/ # 翻译结果与进度记录配置文件的写法很直白,核心字段包括接口地址、模型名称、Key、并发数、温度。这里要特别强调:Key千万不要写死在代码里,放进配置文件并加入忽略清单,避免某天把脚本分享出去时把密钥一起泄漏出去。我在早期吃过这个亏,后来养成了习惯,所有密钥一律走环境变量或者外部配置。
3. 实操过程:一个可落地的批量翻译脚本是怎么跑通的
3.1 输入清洗:先把文档统一成Markdown
接到一批文档,我做的第一件事不是写翻译代码,而是先把格式理干净。常用的转换路径是PDF走解析工具提取文本,docx用脚本直接读。但无论哪种方式,转换出来的Markdown都要人工抽查几页,因为PDF里的多栏排版、页眉页脚、图片旁边的说明文字,机器转换后经常错位。
清洗阶段我有三条原则:一是去掉页眉页脚和多余空行;二是在标题、代码块、公式前后补上正确的Markdown标记;三是清掉全角空格和行尾杂散空格。这些动作看着小,但输入越干净,后面分块越省心,翻译质量也会更稳定。我见过有人跳过清洗直接切分,结果整个表格被拆成碎片,翻完根本没法看。
3.2 分块与上下文管理:怎么切才能保证术语一致
分块逻辑可以总结成一句话:以自然段落为基本单位,不要硬按固定字数切。自然段落在语义上是完整的,模型翻译时理解得更好。如果某个段落特别长,超过模型窗口,再按句子边界做二次切分。
上下文管理是术语一致性的关键。我用的是"双段落地"方式:请求体里包含当前块要翻译的内容,同时把前一条已译好的译文摘要放到一个"上文参考"字段里。这样模型能看出"这段还是在讲车间调度的事",相同术语的处理方式就能延续下来。
下面是我用过的分块函数简化版:
import re def split_blocks(text, max_chars=800): paragraphs = re.split(r'\n{2,}', text.strip()) blocks = [] current = [] current_len = 0 for p in paragraphs: if current_len + len(p) > max_chars and current: blocks.append("\n\n".join(current)) current = [] current_len = 0 current.append(p) current_len += len(p) if current: blocks.append("\n\n".join(current)) return blocks逻辑很好懂:先按空行切出段落,再按800字上限把多个段落叠成一个块。块太小,请求次数暴增,成本上升;块太大,容易顶到模型窗口上限。800字是我反复试出来的平衡点,你可以根据自己的模型窗口调整。这里顺便提醒一句:切分时千万小心,不要让代码块标记或者公式标记落在块边界上,否则后面还原会出问题。
3.3 模型接口调用与提示词设计:翻译质量的上限
接口调用本身没什么好说的,无非是把消息组装好发出去,拿到返回文本存起来。真正决定翻译质量的,是提示词。我用的系统提示词包含四层内容:
- 角色设定:你是资深技术翻译,擅长把外文技术文档译成中文。
- 硬性规则:保留公式、代码、编号、表格结构不变;术语必须按给定对照表翻译;保持原文排版格式。
- 上文参考:前一块译文摘要。
- 输出要求:只输出译文正文,不要解释或补充。
请求之前,我还会把术语对照表拼进提示词。每一条术语用"原词:指定译词"的格式列出来,模型基本会遵守。温度参数设置为0或接近0,翻译任务不需要创造性,稳定压倒一切。
def build_system_prompt(terms, context): term_lines = "\n".join(f"{k}: {v}" for k, v in terms.items()) return ( "你是资深技术翻译,擅长技术文档翻译。\n" "规则:公式、代码、编号、表格结构保持原样;" "术语必须使用以下对照表:\n" + term_lines + "\n上文参考译文摘要:\n" + context ) messages = [ {"role": "system", "content": build_system_prompt(terms, context)}, {"role": "user", "content": block_text} ] payload = { "model": config["model_name"], "messages": messages, "temperature": 0 }3.4 并发、重试与断点续传:批量任务的稳定性
批量任务跑到一半断掉,是最崩溃的事。所以我的脚本从一开始就带断点续传:每翻译完一个块,就把结果连带块ID写进本地进度文件。下次启动时先读进度文件,跳过已完成块,只跑未完成的。这样即使电脑重启、接口报错、进程被杀,任务也能接着跑,不会一晚上白干。
并发用线程池实现,但要限制上限。为了不让接口限流,我在请求失败时做退避重试:第一次失败等3秒,第二次等6秒,第三次等12秒,最多试5次。如果5次依然失败,把这个块标记为"待人工处理",然后继续跑后面的块。这样接口抽风时,整个任务也不会卡死,最后再集中处理那些没跑成的极少数块。
进度文件用JSON格式保存,块ID是原文档里的绝对顺序编号,这样合并还原时能按顺序恢复,不会乱序。
3.5 公式和代码的占位符保护:让模型"看不见"不该动的部分
承接着标题里"公式"两个字,这里其实是实操中最值钱的一段。翻译技术文档时,公式和代码直接出现在文本里,模型经常忍不住去改。最简单的办法是分块时先把这些内容换成占位符。比如把E = mc^2替换成{{FORMULA_001}},把代码块替换成{{CODE_BLOCK_002}},翻译完成后再替换回来。
占位符要设计成一个整体,不能拆进上下文里。我的保护逻辑是:先用正则匹配出代码块和公式,存成列表;然后把原文中的匹配区域替换为占位符;翻译结束后,再按标记从列表里取回原始内容。模型全程看到的都是"这个位置是一串看不懂的标记",自然就不会乱动。
这里给一个精简的示例:
placeholders = re.findall( r'(\$\$.*?\$\$|\$.*?\$|```.*?```)', text, re.S ) for idx, ph in enumerate(placeholders): text = text.replace(ph, f"{{{{FORMULA_{idx}}}}}")做这一步时要注意顺序:先匹配代码块,再匹配行内公式,最后再匹配整段公式,避免嵌套替换出错。我最初因为顺序反了,导致不少公式被代码规则提前吞掉,检查返工花了不少时间。
4. 常见问题与排查技巧实录
4.1 翻译结果出现乱码或格式丢失
症状很容易认:译文里出现成对的方括号、半个LaTeX标签,或者表格整个散掉。排查顺序一般是先看原始块是否干净,再检查分块时有没有把标记拦腰切断,最后看后处理替换有没有遗漏。大多数情况下问题出在第二步,所以我给分块函数加了一道"标记完整性检查":切出来的每个块,所有代码块标记和公式标记必须成对出现,否则强制把切分点挪到标记逻辑结束的位置。
4.2 术语翻译前后不一致
造成这个现象的原因有两个:一是块与块之间没有上下文承接,二是同一个词在不同位置被模型理解成不同含义。解决办法说来说去还是术语表前置:把所有术语对照直接写进系统提示词,并明确要求"必须严格遵守,不允许引入其他译法"。再补一个事后校验技巧:翻译完成后跑一遍术语校验脚本,扫描译文中是否出现术语表之外的同义表达,一旦发现就自动标记出来,人工复核。经验之谈,术语表越早建立,返工越少。动手翻译前先通读一遍文档,把高频术语和缩写整理成表,这笔时间花得相当值。
4.3 接口限流、超时与网络异常
症状一般是连续发出请求时报限流错误或者超时。处理方法有三个:调低并发数、加指数退避重试、在请求之间加固定停顿削峰。我常把后两种结合使用。贴一个重试封装的简化思路:
import time for attempt in range(config["max_retries"]): try: resp = http_post(payload) return resp except Exception: if attempt == config["max_retries"] - 1: mark_manual(block_id) return None wait_time = config["base_wait"] * (2 ** attempt) time.sleep(wait_time)网络异常不用在代码层面硬撑,重试到上限后直接标记人工处理,这是保底逻辑。日志一定要打全:块ID、失败原因、第几次重试,后面排查会省很多力气。
4.4 成本超支怎么控制
最实用的控费手段是缓存。同一篇文档如果被重复翻译,先查缓存,命中就直接返回历史结果。批次控制也很关键:先跑一个十几页的样本,估算出每页消耗的token量和对应费用,再决定全量怎么跑。还有一点很多人不知道:不要盲目调大输出长度限制。翻译的输出token和输入基本成正比,设得过大只会浪费额度。
4.5 断点续传里的两个隐蔽坑
第一个坑是进度文件写得太频繁,会拖慢整体速度。因为每写一次都是磁盘I/O,并发场景下频繁写还可能造成文件锁冲突。我的方案是每完成一个块就写一次,但不做实时同步刷盘。第二个坑是任务中断后,进度文件里可能残留半条写入记录。我的做法是写入时先写完临时文件再改名,避免读到残缺JSON导致整个进度崩溃。另外,合并结果时也要校验块数量是否等于总块数,一旦少了一块,说明有块被误标成"已完成",必须回查进度记录。
| 问题 | 现象 | 优先排查方向 |
|---|---|---|
| 乱码与格式丢失 | 译文出现标签碎片、表格散掉 | 分块是否切断标记、后处理是否漏替换 |
| 术语不一致 | 同一个词有多种译法 | 术语表是否进入提示词、上下文是否连贯 |
| 限流与超时 | 大量限流错误或超时 | 并发数、重试策略、请求间隔 |
| 成本超支 | 费用远超出预算 | 缓存命中率、批次样本估算 |
我个人在跑完这批文档后的体会是:大模型翻译本身已经不是瓶颈,瓶颈几乎都集中在"怎么把一批文档安全可靠地送进去再接回来"这个工程问题上。批量翻译、公式保护、断点续传,这些听起来有点技术感的说法,落到地上全是朴素的流程管理。最后再分享一个小经验:第一次跑全量之前,先挑50到100个块做预热,人工翻看质量、记录耗时,拿这组数据去估算全量的成本和时间,再决定要不要调整并发。这个动作帮我避掉过好几次大返工,强烈建议你也试试。