一、为什么要做这个项目
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提供了两个关键能力:
- 统一的LLM接口(不管用Ollama还是其他模型,代码不需要改)
- 现成的文本分块工具(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 第二次尝试:提示词优化(改进但不够)
优化策略:
- 增加示例(few-shot learning)
- 明确列出规则
- 降低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)时,遇到问题:
- qwen2.5:3b的上下文窗口是8K tokens(约16000个英文单词)
- 8000行文档转换为tokens约为12000
- 直接翻译会报错:"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),则进一步切分:
优先级顺序:
- 按双换行符切分(段落边界)
- 按单换行符切分(句子边界)
- 按句号切分
- 按空格切分(实在没办法了)
重叠策略(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"
错误的翻译输出:
定义 你好():
返回 "世界"
调试思路:
-
检查解析器:是否正确识别了代码块?
- 添加日志:打印解析后的元素列表
- 发现:代码块被正确识别为CODE_BLOCK类型
-
检查翻译器:是否跳过了CODE_BLOCK?
- 添加日志:打印每个元素的翻译前后内容
- 发现:翻译器没有跳过CODE_BLOCK,仍然发送给了LLM
-
定位问题:翻译器的判断逻辑有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分钟
瓶颈在哪里:
- 每个chunk都要单独调用LLM(网络往返开销)
- chunk_size设置为500 tokens太小(切分太细)
优化方案:
-
增大chunk_size:
- 从500增加到1500
- chunk数量从150降到50
- 时间从40分钟降到13分钟
-
批处理翻译(结构化模式):
- 原来:每个段落单独翻译
- 改进:每次翻译10个段落
- 网络往返次数减少90%
-
调整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(良好)
处理时间:2分13秒
文档大小:英文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
- 技术博客
不适合的场景:
- 文学作品(需要更多创造性翻译)
- 口语化内容(技术文档风格较正式)
- 图表密集的文档(目前只处理文本)