从零实现英文技术文档自动翻译系统

64 阅读19分钟

一、为什么要做这个项目

1.1 实际需求场景

在学习技术时,经常需要阅读官方文档,比如Python官方教程、FastAPI文档、React文档等。这些文档的特点是:

  • 英文原版,阅读速度慢,理解不够直观
  • 内容包含大量代码示例和专业术语
  • 文档以Markdown格式编写,有复杂的格式结构
  • 更新频繁,人工翻译跟不上

如果能有一个工具,把这些英文文档网站自动翻译成中文Markdown文件,既方便阅读,又可以保存到本地。

1.2 核心诉求

翻译工具需要满足以下要求:

格式完整性:假设原文是这样的Markdown:

## Getting Started

Follow these steps:
- Install `python`
- Run `pip install fastapi`

Example code:
```python
def hello():
    return "world"
```

翻译后应该是:

## 开始使用

按照以下步骤操作:
- 安装 `python`
- 运行 `pip install fastapi`

示例代码:
```python
def hello():
    return "world"
```

注意几个关键点:

  • 标题的##保持不变
  • 列表的-标记保持不变
  • 代码块中的python关键字和代码内容完全不变
  • 行内代码`python`保持不变
  • 只翻译纯文本内容

内容准确性

  • 代码不能被翻译(def hello():不能变成定义 你好():
  • URL链接不能被修改
  • 数字和版本号不能改变(如Python 3.11不能变成Python 三点一一)
  • 专业术语保持一致(function始终翻译为"函数",不能一会儿是"函数"一会儿是"方法")

本地化部署

  • 不依赖付费API(如OpenAI、Claude等)
  • 可以在本地电脑运行
  • 数据不上传到外部服务器

批量处理能力

  • 一次翻译多个网站
  • 某个网站翻译失败不影响其他网站
  • 能够评估每个翻译的质量

1.3 为什么不直接用现有工具

尝试过几种方案:

方案1:浏览器翻译插件

  • 问题:翻译后是HTML页面,无法保存为Markdown
  • 问题:代码块会被翻译破坏
  • 问题:无法批量处理

方案2:复制到翻译网站

  • 问题:Markdown格式会丢失
  • 问题:无法批量处理
  • 问题:代码块和链接会被破坏

方案3:使用GPT直接翻译

  • 问题:需要付费API
  • 问题:长文档超过token限制
  • 问题:格式破坏率高(LLM会把#等格式符号当作普通文本)

因此决定从零开发一个专门的工具。

二、技术方案设计思路

2.1 整体流程设计

把翻译任务拆解成6个步骤,每一步完成一个独立的功能:

步骤1:检查环境 → 确保Python版本、Ollama服务、LLM模型都已就绪
步骤2:读取URL列表 → 从配置文件读取要翻译的网站地址
步骤3:抓取网页 → 下载HTML源码
步骤4:转换为Markdown → 从HTML提取正文并转换为Markdown格式
步骤5:翻译 → 调用本地LLM翻译成中文
步骤6:质量校验 → 检查格式和内容是否正确
步骤7:生成报告 → 输出HTML和JSON格式的质量报告

这样设计的好处是:

  • 每一步可以单独调试
  • 中间结果都会保存(HTML、英文Markdown、中文Markdown),方便检查
  • 某一步失败了可以从失败的地方继续,不用全部重来

2.2 技术选型的考虑

为什么选择本地LLM(Ollama + qwen2.5:3b)

对比了几个方案:

方案优点缺点是否采用
OpenAI API质量最好需要付费,数据上传外网
本地大模型(70B)质量好需要32GB显存,普通电脑跑不动
Ollama + qwen2.5:3b免费,4GB内存可运行,中文质量好质量略逊于大模型
传统翻译引擎快速无法理解上下文,质量差

最终选择Ollama + qwen2.5:3b的原因:

  • 阿里的千问模型中文翻译质量稳定
  • 3B参数的模型在普通笔记本上可以运行(4GB内存)
  • 本地运行,不担心数据泄露
  • 完全免费

为什么使用LangChain

LangChain提供了两个关键能力:

  1. 统一的LLM接口(不管用Ollama还是其他模型,代码不需要改)
  2. 现成的文本分块工具(MarkdownHeaderTextSplitter等)

为什么选择Trafilatura抓取网页

尝试过几个HTML解析库:

  • BeautifulSoup:通用HTML解析,但需要手动写规则识别正文
  • newspaper3k:新闻类网站效果好,技术文档效果差
  • Trafilatura:专门用于提取网页正文,识别准确率高

Trafilatura的优势:

  • 自动识别正文区域(过滤掉导航栏、侧边栏、广告等)
  • 保留HTML结构信息,方便后续转换为Markdown
  • 支持多种编码格式

2.3 核心难点:如何保证格式不丢失

这是整个项目最大的技术挑战。

2.3.1 第一次尝试:直接翻译(失败)

最开始的想法很简单:把整个Markdown文本发给LLM,让它翻译成中文。

提示词设计:

你是翻译专家。将下面的Markdown翻译成中文,保持所有格式不变。

原文:
## Getting Started
- Install `python`

翻译:

测试结果:

  • 有时LLM会输出## 开始使用(正确)
  • 有时输出#开始使用(少了空格)
  • 有时输出开始使用##符号丢失)
  • 代码块中的python有概率被翻译成蟒蛇

失败原因分析: LLM本质是语言模型,训练目标是生成自然语言。它把## Getting Started理解为"带有两个井号的文本",而不是"二级标题"。在生成中文时,概率上既可能输出##,也可能不输出。

即使在提示词中反复强调"保持格式",LLM仍然会在长文本翻译中逐渐"忘记"这个约束。

2.3.2 第二次尝试:提示词优化(改进但不够)

优化策略:

  1. 增加示例(few-shot learning)
  2. 明确列出规则
  3. 降低temperature参数(从1.0降到0.3)

改进后的提示词:

你是技术文档翻译专家。将下面的英文Markdown翻译成中文,严格保持所有格式标记。

【关键规则】
1. 标题的#号必须保留:# Title → # 标题
2. 列表的-号必须保留:- item → - 项目
3. 代码`code`不翻译:`hello()` → `hello()`
4. 链接[text](url)只翻译text:[Click](url) → [点击](url)

【示例】
英文:## Getting Started
中文:## 开始使用

英文:- Install the `package`
中文:- 安装 `package`

【英文原文】
{text}

【中文翻译】

测试结果:

  • 格式破坏率从50%降到20%
  • 但仍然不够稳定,长文档(超过500行)几乎必然出现格式错误
  • 代码块内部的代码被翻译的情况减少但没有完全消除

仍然不够的原因: LLM看到的是一整段混合了格式符号和内容的文本。虽然提示词强调了规则,但LLM在生成时仍然需要"同时考虑翻译内容"和"记住格式规则",注意力会分散。

2.3.3 最终方案:结构化解析(成功)

关键思路:把格式和内容完全分离

设计思想: 不让LLM看到任何格式符号,它只需要翻译纯文本。格式的重建由程序代码保证。

三阶段流程

阶段1:解析Markdown为结构化数据

原始Markdown:

## Getting Started
- Install `python`

解析后的数据结构:

元素1:
  类型: 标题
  纯文本内容: "Getting Started"
  格式信息: {级别: 2, 前缀: "##"}

元素2:
  类型: 列表项
  纯文本内容: "Install python"
  格式信息: {标记: "-", 行内元素: [{类型: 行内代码, 内容: "python", 位置: 8-13}]}

关键点:

  • 提取出纯文本"Getting Started"和"Install python"
  • 格式信息(##-`)单独保存
  • 行内代码`python`被替换成占位符,记录原始内容和位置

阶段2:只翻译纯文本

发送给LLM的内容:

翻译:"Getting Started"
翻译:"Install python"

LLM返回:

"开始使用"
"安装 python"

此时LLM完全看不到任何格式符号,只需要做纯文本翻译,不会被格式干扰。

阶段3:用原始格式重建Markdown

根据格式信息重建:

元素1: 前缀"##" + 翻译后的文本"开始使用""## 开始使用"
元素2: 标记"-" + 翻译后的文本"安装 python""- 安装 python"
       然后恢复行内代码: "安装 python""安装 `python`"

最终输出:

## 开始使用
- 安装 `python`

这个方案的效果

  • 格式准确率:100%(由代码保证,不依赖LLM)
  • 代码块:100%不会被翻译(解析时直接标记为"不翻译")
  • URL:100%不会被修改(保存在格式信息中)

2.4 第二个难点:长文档的处理

2.4.1 问题发现

当翻译Python官方教程(约8000行Markdown)时,遇到问题:

  1. qwen2.5:3b的上下文窗口是8K tokens(约16000个英文单词)
  2. 8000行文档转换为tokens约为12000
  3. 直接翻译会报错:"context length exceeded"(上下文长度超限)

即使把上下文窗口扩大到32K(使用更大的模型),还有另一个问题:

测试发现,当文档超过5000行时,LLM翻译质量明显下降:

  • 前面的翻译正常
  • 中间部分开始出现重复
  • 后面部分会生成无关内容(幻觉)

原因分析: Transformer模型的注意力机制对长文本的处理能力有限。即使上下文窗口足够大,模型对远距离信息的关注度也会衰减。

2.4.2 解决方案:智能分块

核心思路:把长文档切分成多个小块,分别翻译,然后合并。

但是怎么切分是关键

错误的切分方式:按字数切

假设每500个单词切一块,可能出现:

块1: ...follow these steps:
     - Install Python
     - Run the

块2: following command:
     pip install fastapi

问题:

  • 列表项被切断,翻译不连贯
  • 上下文丢失(块2不知道"following command"是列表的一部分)

正确的切分方式:按Markdown结构切

利用标题作为天然的分界线:

块1: ## Getting Started
     (这个章节的所有内容)

块2: ## Installation
     (这个章节的所有内容)

块3: ## Usage
     (这个章节的所有内容)

优点:

  • 每个块是完整的章节,上下文完整
  • 不会在段落或列表中间切断

处理超大章节:递归切分

如果某个章节本身超过chunk_size(1500 tokens),则进一步切分:

优先级顺序:

  1. 按双换行符切分(段落边界)
  2. 按单换行符切分(句子边界)
  3. 按句号切分
  4. 按空格切分(实在没办法了)

重叠策略(chunk_overlap)

设置300 tokens的重叠区域:

块1: [内容A][内容B][内容C]
                     ↑
                  重叠区域
                     ↓
块2:         [内容C][内容D][内容E]

好处:

  • 块之间的衔接更自然
  • 避免在边界处的翻译不连贯

如何计算tokens数量

使用tiktoken库(OpenAI的token计数工具):

encoding = tiktoken.get_encoding("cl100k_base")
token_count = len(encoding.encode(text))

如果tiktoken库加载失败(网络问题),使用简单估算:

token_count ≈ 字符数 / 2

(英文平均每个token约2个字符)

2.5 第三个难点:术语一致性

2.5.1 问题场景

翻译Python文档时发现:

  • 第1章:"function"翻译成"函数"
  • 第3章:"function"翻译成"方法"
  • 第5章:"function"翻译成"功能"

原因:每个chunk是独立翻译的,LLM没有"记忆"之前的翻译。

2.5.2 解决方案:术语表注入

建立术语对照表(terminology.json):

{
  "function": "函数",
  "class": "类",
  "method": "方法",
  "API": "API"
}

在每次翻译时,把术语表加入提示词:

Terminology:
function = 函数
class = 类
method = 方法
API = API

Text to translate:
A function is different from a method.

Translation:

LLM看到术语表后,会按照指定的翻译:

函数与方法不同。

优化:限制术语表大小

如果术语表有100个条目,全部加入提示词会占用太多tokens。

解决方法:只加入最常用的20个术语,其他的依赖LLM自己判断。

三、实现过程中遇到的问题

3.1 格式问题的调试过程

问题1:代码块被翻译

现象

输入的代码块:

def hello():
    return "world"

错误的翻译输出:

定义 你好():
    返回 "世界"

调试思路

  1. 检查解析器:是否正确识别了代码块?

    • 添加日志:打印解析后的元素列表
    • 发现:代码块被正确识别为CODE_BLOCK类型
  2. 检查翻译器:是否跳过了CODE_BLOCK?

    • 添加日志:打印每个元素的翻译前后内容
    • 发现:翻译器没有跳过CODE_BLOCK,仍然发送给了LLM
  3. 定位问题:翻译器的判断逻辑有bug

    • 原始逻辑:只跳过type为空的元素
    • 修改后:明确跳过CODE_BLOCK类型

解决方案: 在翻译器中增加类型判断:

if element['type'] == 'CODE_BLOCK':
    return element  # 直接返回,不翻译

问题2:链接URL被修改

现象: 输入:[点击这里](https://example.com) 输出:[点击这里](https://示例.com)

原因: 解析器虽然提取了链接,但在占位符替换时,LLM看到了完整的[text](url),把URL也翻译了。

解决方案: 改进解析策略:

  • 不使用占位符,而是直接提取链接文本
  • 只把"点击这里"发给LLM翻译
  • URL存储在metadata中,重建时直接使用原始URL

3.2 性能问题的优化

问题:翻译速度太慢

现象: 翻译一个5000行的文档需要40分钟。

分析

  • 文档被分成150个chunk
  • 每个chunk翻译需要16秒
  • 总时间:150 × 16 = 2400秒 = 40分钟

瓶颈在哪里

  1. 每个chunk都要单独调用LLM(网络往返开销)
  2. chunk_size设置为500 tokens太小(切分太细)

优化方案

  1. 增大chunk_size:

    • 从500增加到1500
    • chunk数量从150降到50
    • 时间从40分钟降到13分钟
  2. 批处理翻译(结构化模式):

    • 原来:每个段落单独翻译
    • 改进:每次翻译10个段落
    • 网络往返次数减少90%
  3. 调整temperature参数:

    • 原来:temperature=0.7(生成多样性高,但慢)
    • 改进:temperature=0.3(生成更确定,更快)

最终效果:

  • 5000行文档翻译时间:约8分钟
  • 翻译速度:约150-200 tokens/秒

3.3 稳定性问题的处理

问题:批量翻译时偶发失败

现象: 批量翻译10个网站,第3个网站失败后,整个流程中断,后面7个网站没有翻译。

原因: 代码中使用了简单的for循环:

for url in urls:
    translate(url)  # 如果这里抛出异常,整个循环终止

解决方案1:异常捕获

改为:

for url in urls:
    try:
        translate(url)
    except Exception as e:
        记录错误日志
        继续处理下一个URL

解决方案2:重试机制

对于临时性故障(网络波动、Ollama服务临时不可用),增加重试:

重试3次
每次失败后等待时间加倍(2秒 → 4秒 → 8秒)
如果3次都失败,返回英文原文(而不是报错)

解决方案3:降级策略

定义多级降级方案:

优先级1: 结构化翻译模式
如果失败 → 降级到直接翻译模式
如果还失败 → 降级到分块翻译
如果还失败 → 返回英文原文

实现效果:

  • 批量翻译10个网站,即使个别失败也不影响整体
  • 稳定性从60%提升到95%

3.4 LLM输出不稳定的处理

问题:LLM有时输出多余内容

现象: 期望输出:开始使用 实际输出:翻译:开始使用【中文翻译】开始使用

原因: LLM有时会"模仿"提示词中的格式,把"翻译:"这样的标记也输出。

解决方案:后处理清洗

使用正则表达式清理:

删除开头的"翻译:""中文:"等标记
删除【】包裹的标记
删除多余的空行
如果结果为空或太短,返回原文

清洗后的效果:

  • 输出格式统一
  • 避免因为多余标记导致的Markdown格式错误

四、翻译质量如何保证

光靠LLM翻译还不够,需要有自动化的质量检查。

4.1 设计质量评分体系

设计思路: 把翻译质量拆解为可量化的指标,每项指标给出分数,最后汇总。

评分机制

  • 基准分:100分
  • 发现一个严重问题(issue):扣15分
  • 发现一个轻微问题(warning):扣5分
  • 最低分:0分

质量等级

  • 90-100分:优秀(可以直接使用)
  • 75-89分:良好(可能有小瑕疵)
  • 60-74分:一般(需要人工检查)
  • 0-59分:差(需要重新翻译)

4.2 格式完整性检查

检查格式是否被破坏。

检查项1:标题数量

逻辑:

统计英文Markdown中的标题数量(以#开头的行)
统计中文Markdown中的标题数量
两者应该相等

如果不等:

  • 说明有标题的#符号丢失了
  • 记录为issue,扣15分

检查项2:代码块数量

逻辑:

统计英文中的代码块数量(用三个反引号包裹的部分)
统计中文中的代码块数量
两者应该相等

如果不等:

  • 说明有代码块丢失或格式错误
  • 记录为issue,扣15分

检查项3:链接数量

逻辑:

统计英文中的链接数量([text](url)格式)
统计中文中的链接数量
两者应该相等或接近

如果差异较大:

  • 允许±1的误差(有些链接可能在文本描述中)
  • 超过误差范围记录为warning,扣5分

检查项4:列表数量

逻辑:

统计英文中的列表项数量(以 - 或 * 开头的行)
统计中文中的列表项数量
两者应该相等或接近

如果差异超过2:

  • 记录为warning,扣5分

检查项5:长度合理性

逻辑:

计算中文字符数 / 英文字符数的比例
正常范围:0.4 - 1.2
(中文通常比英文短,因为中文表达更简洁)

如果超出范围:

  • 可能翻译不完整或有大量重复
  • 记录为warning,扣5分

4.3 内容准确性检查

检查内容是否被错误翻译。

检查项1:代码内容一致性

逻辑:

提取英文代码块的内容
提取中文代码块的内容
移除注释后比较(注释可以翻译)
代码部分应该完全一致

实现细节:

for 每个代码块:
    移除 # 开头的注释行
    比较剩余代码
    如果不一致,记录warning

检查项2:数字保留检查

逻辑:

提取英文中的所有数字(如3.11、100、2023等)
提取中文中的所有数字
两者应该相等

如果不等:

  • 说明有数字被翻译了(如"3.11"变成"三点一一")
  • 记录为warning

检查项3:URL完整性

逻辑:

提取英文中的所有URL(https://开头)
提取中文中的所有URL
两者应该完全相同

如果不同:

  • 说明URL被修改或删除
  • 记录为issue,扣15分

检查项4:未翻译内容检测

逻辑:

在中文Markdown中查找连续超过10个英文单词的片段
如果存在,可能是漏翻译的段落

如果发现:

  • 记录为warning,扣5分

检查项5:格式标记保留

逻辑:

统计英文中的粗体标记**、斜体标记*、行内代码`等
统计中文中的这些标记
两者应该数量相等

如果不等:

  • 说明格式标记丢失
  • 记录为warning

4.4 生成可视化报告

检查完成后,生成两种报告:

HTML报告(给人看)

包含内容:

总体评分:85.0/100(良好)
处理时间:213秒
文档大小:英文12345字符,中文8765字符

格式检查:
  ✓ 标题数量:12 vs 12(正常)
  ✓ 代码块数量:5 vs 5(正常)
  ⚠ 链接数量:10 vs 9(警告)
  ✓ 列表项:23 vs 23(正常)

内容检查:
  ✓ 代码内容一致:5个代码块检查通过
  ✓ URL完整性:10个URL全部保留
  ⚠ 数字数量:58 vs 56(警告)

问题列表:
  - [警告] 链接数量不匹配:英文10个,中文9个
  - [警告] 数字数量不匹配:英文58个,中文56个

随机采样(5对段落对比):
  [英文] Getting started with Python is easy...
  [中文] 开始使用Python很简单...

  [英文] Install the package using pip...
  [中文] 使用pip安装包...
  ...

JSON报告(给程序看)

机器可读的结构化数据:

{
  "url": "https://docs.python.org/3/tutorial/",
  "score": 85.0,
  "quality_level": "良好",
  "format_check": {
    "headers": {"en": 12, "zh": 12, "match": true},
    "code_blocks": {"en": 5, "zh": 5, "match": true},
    "links": {"en": 10, "zh": 9, "match": false}
  },
  "content_check": {
    "code_integrity": true,
    "url_integrity": true,
    "numbers": {"en": 58, "zh": 56}
  },
  "issues": [],
  "warnings": [
    "链接数量不匹配:英文10个,中文9个",
    "数字数量不匹配:英文58个,中文56个"
  ]
}

便于后续批处理或CI/CD集成。

4.5 实际效果评估

测试数据(翻译10个技术文档网站):

结构化模式(推荐)

  • 平均得分:88.5分
  • 格式issue率:0.2%(几乎没有)
  • 内容warning率:5%(主要是链接数量微差)
  • 代码块破坏率:0%
  • 推荐使用场景:所有情况

直接翻译模式

  • 平均得分:76.3分
  • 格式issue率:8%(标题、列表偶有丢失)
  • 内容warning率:12%
  • 代码块破坏率:2%
  • 推荐使用场景:快速翻译、对格式要求不高的情况

校验系统的价值

  • 自动发现95%的翻译问题
  • 避免人工逐行检查
  • 提供量化指标,方便评估不同模型的翻译质量

五、技术方案总结

5.1 核心创新点

1. 格式与内容分离的解析-翻译-重组架构

传统方案:LLM同时处理内容和格式 → 格式破坏率高 本方案:格式由代码保证,LLM只翻译纯文本 → 格式准确率100%

2. 智能分块策略

传统方案:按固定字数切分 → 上下文断裂 本方案:按Markdown结构切分,支持多级策略 → 保持完整性

3. 术语表注入机制

传统方案:每个chunk独立翻译 → 术语不一致 本方案:每次翻译都注入术语表 → 术语一致性显著提升

4. 多层质量校验体系

传统方案:人工检查或无检查 本方案:自动化格式+内容双重检查 → 质量可量化

5.2 适用场景

适合翻译的文档类型:

  • 技术文档(如Python、FastAPI、React官方文档)
  • API参考手册
  • 编程教程
  • 开源项目README
  • 技术博客

不适合的场景:

  • 文学作品(需要更多创造性翻译)
  • 口语化内容(技术文档风格较正式)
  • 图表密集的文档(目前只处理文本)