把 A Tour of Go 的翻译范围扩展到 Learn 和 Docs 以后,我很快遇到了一个不能简单沿用“把文本交给 AI”的问题。
Markdown 页面看起来是文本,但它实际上并不只是文本。
一篇技术文档里,可能同时存在普通段落、标题、列表、链接、inline code、代码块、教学代码注释、directive,以及各种不能被翻译模型随意修改的机器结构。
如果把整份 Markdown 原样交给 AI,模型当然有可能正确完成翻译。
但“有可能”对于一个长期运行的自动化翻译系统来说是不够的。
真正需要回答的是:
怎样让模型拥有足够的语言自由,同时又能确定代码、链接和 Markdown 结构不会被破坏?
这也是 Learn / Docs 纳入 v2 以后,parser 和 representation 必须进一步完善的原因。
Markdown 的困难,不是“它里面有一些符号”
如果只是几个 #、* 和反引号,其实并不难处理。
真正麻烦的是,同一段 Markdown 中经常混合了两类完全不同的内容:
需要翻译的自然语言
和
必须保持机器语义的结构
例如:
Use [`go test`](/cmd/go/#hdr-Test_packages) to run the tests.这里至少包含:
- 普通英文;
- inline code;
- link text;
- link destination;
- Markdown link binding。
真正需要翻译的是自然语言。
go test 不能被翻译。
URL 通常也不能被修改。
但最终中文又不能被强迫机械保持英文词序。
合理的译文可能是:
运行测试时,可以使用 [`go test`](/cmd/go/#hdr-Test_packages)。也就是说:
结构必须稳定,但结构所在的位置不一定固定。
这件事情其实在之前的 Tour 翻译中就已经存在。
Tour 阶段并不是要求所有 protected token 都严格保持英文中的原始顺序。
只要必要的 pair、binding、token 完整性以及机器语义没有被破坏,就应该允许目标语言按照自己的自然语序表达。
所以 v2 并不是第一次允许“换序”。
真正的新问题是:
Learn / Docs 中需要参与这种保护与重建的结构,比 Tour 更复杂了。
如果保护得太少,AI 很容易“帮忙”修改不该修改的东西
假设直接把整份 Markdown 交给模型。
模型可能会非常自然地做一些人类读者看来似乎无害的修改。
例如调整一个 URL。
改变代码中的大小写。
把一个 identifier 翻译成中文。
把代码注释和周围说明混在一起。
改变 fenced code block 的边界。
或者把 Markdown directive 当成普通文本重新组织。
这些变化单独看可能很小。
但对于机器来说,可能意味着:
页面无法正常渲染。
或者:
示例代码不再正确。
甚至:
文档表面上可以显示,但链接、directive 或其他隐藏语义已经改变。
所以系统需要先明确:
哪些东西可以交给语言模型决定,哪些东西必须由机器负责保护。
但保护得太多,同样会破坏翻译质量
另一个极端也有问题。
最简单的保护方式是:
把所有特殊结构替换成 token,然后要求这些 token 原位置不动。
例如:
Use ⟪TOKEN_1⟫ to run the tests.然后要求中文结果仍然严格是:
使用 ⟪TOKEN_1⟫ 来运行测试。这样机器确实很好验证。
但如果目标语言的自然表达需要调整位置,规则就开始和语言质量冲突。
中文可能更自然的是:
运行测试时,可以使用 ⟪TOKEN_1⟫。如果 validator 把这种变化判为错误,那么系统得到的是:
机器结构非常稳定,但中文越来越像英文语序的投影。
这不是我想要的结果。
因此 v2 中一直需要坚持的原则仍然是:
保护语义,而不是保护不必要的位置。
机器应该规定哪些关系不能破坏。
而不是规定译文必须怎样排列。
真正需要保护的是关系
做到后面以后,我越来越觉得,“protected token”这个名字其实很容易让人把注意力放错地方。
真正重要的并不是某一个 token 本身。
而是 token 和周围结构之间的关系。
例如一个链接:
[Go documentation](https://go.dev/doc/)需要保护的并不只是:
https://go.dev/doc/还包括:
哪一段文本仍然是这个 URL 的 link text。
再例如 inline code:
The `error` value...error 本身必须保持代码语义。
但中文可以是:
`error` 值……也可以根据上下文调整它在句子中的位置。
所以 parser 真正需要保留的是:
this is inline code以及:
this code belongs to this surrounding language structure而不是:
它原来在第 17 个字符,所以翻译以后还必须在第 17 个字符附近这种区别后来对 validator 的设计影响很大。
Pair 也比单个 token 更重要
有些结构必须成对出现。
例如 Markdown link 的:
[text](destination)或者某些 directive、placeholder、内部保护标记。
如果只检查:
所有 token 最后都还存在
其实还不够。
可能出现:
token A 存在
token B 也存在但:
A 和 B 已经不再属于原来的那一组结构。
这种情况特别危险,因为简单 token count 仍然可以 PASS。
所以机器校验需要逐渐从:
token exists升级到:
pair / binding remains valid这也是为什么 Markdown translation 的结构保护不能简单理解成“字符串替换”。
它实际上是在保存 representation 中的一部分语义关系。
Link binding 是一个很典型的例子
链接尤其适合说明这个问题。
假设 source 中有两个链接:
Read the [language specification](...) and the [memory model](...).中文为了自然表达,可以调整句子:
可以继续阅读[语言规范](...)以及[内存模型](...)。这没有问题。
甚至在更复杂的句子里,两个链接在译文中的整体位置也可能发生变化。
但有一件事情不能变:
“语言规范”不能最后绑定到 memory model 的 URL。
只检查:
- 两个 URL 都还在;
- 两段 link text 都还在;
并不能证明结果正确。
必须进一步保证:
link text 和它原来的 destination binding 没有被交叉。
这就是 parser / protector 真正应该解决的问题。
完整代码又是另一类 representation
Learn / Docs 中还有比普通 Markdown 更复杂的内容:
完整代码。
普通段落中的:
`fmt.Println`和 fenced Go code block 中真实存在的:
fmt.Println("Hello")显然不能采用同一种处理方式。
完整代码首先必须保持 Go 语义正确。
绝大部分代码根本不应该翻译。
但教学文档中的代码又可能包含:
需要翻译的人类可读注释。
例如:
// Print the result.
fmt.Println(result)这里:
fmt.Println(result)必须保持代码语义。
而:
// Print the result.又可能是需要本地化的教学内容。
所以一个完整 code block 不能简单判断成:
“代码,全部保护。”
也不能判断成:
“里面有英文,所以全部允许翻译。”
它需要继续理解:
哪些内容属于 Go syntax,哪些内容属于可以翻译的 teaching comment。
这类 representation 也是 Learn / Docs parser 后来需要不断完善的原因之一。
Teaching comment 很容易暴露 parser 的边界
代码注释看起来很好识别。
Go 中有:
// comment和:
/* comment */似乎找到 comment node,然后翻译里面的文字就可以了。
但真正进入完整教学代码以后,情况往往更复杂。
注释可能:
- 单独占一行;
- 位于语句末尾;
- 包含代码 identifier;
- 包含格式约定;
- 与 surrounding code 的教学含义紧密相关。
这意味着 parser 不只是要知道:
这里有一段 comment。
还要知道:
这是不是应该交给语言模型处理的教学内容,以及处理以后如何安全放回原来的代码 representation。
如果这一层判断错误,就很容易出现两种失败。
一种是:
本来应该翻译的教学注释被完全保护,中文页面里留下英文。
另一种则更危险:
为了翻译注释,意外改变了完整代码。
因此这种结构不能依赖“模型应该知道哪里不要动”。
机器必须先把责任边界画清楚。
Parser 和 Generation 的职责不能混在一起
这里其实存在一个非常重要的职责划分。
Parser 应该负责:
确定结构。
Generation 应该负责:
确定语言表达。
例如 parser 可以告诉 Generation:
这里是一段可以翻译的自然语言。
这里是 inline code。
这里是一组 link binding。
这里是 protected code。
这里是一段可翻译的 teaching comment。但 parser 不应该继续规定:
中文必须按照英文的词序排列。
同样,Generation 可以决定:
这句话中文怎样表达更加自然。
但不能决定:
这个 URL 好像不好看,我顺便帮你改一下。
两者各自越界,都会出问题。
所以比较理想的关系是:
Parser
↓
定义机器边界
Generation
↓
在合法边界内完成自然语言表达然后再由 reconstruction 和 validator 检查:
Generation 有没有突破这个边界。
Reconstruction 和 Parse 同样重要
很多人想到 parser,关注的都是:
怎样把输入拆开。
但对于 AI 翻译系统来说,另外一半同样重要:
怎样把结果重新还原成合法页面。
也就是说,整个过程不是:
Markdown
↓
Parse
↓
AI
↓
结束而更像:
Markdown
↓
Parse
↓
Protected / translatable representation
↓
Generation
↓
Reconstruction
↓
Machine validation
↓
Markdown只有 reconstruction 成功,才能说明 Generation 返回的内容仍然能够重新组成合法 representation。
所以 parser 和 reconstruction 实际上是一对能力。
一边定义:
哪些结构被拆开。
另一边证明:
这些结构最终可以正确合回来。
Round-trip 是一个很重要的机器门禁
这也是为什么我后来非常重视 machine round-trip。
一个 TranslationUnit 在进入语言审核以前,机器至少应该先证明:
结构上能够还原。
例如:
- protected 内容没有丢;
- pair 没有断;
- link binding 没有错;
- Markdown 结构仍然合法;
- code representation 没有损坏;
- placeholder 没有缺失。
如果这些基础条件都失败了,就没有必要让 Independent Reviewer 去判断中文质量。
Reviewer 应该把时间花在:
“这是不是好的中文?”
而不是:
“这个 Markdown 还能不能解析?”
能由机器确定的问题,应该尽量在进入语言审核以前解决掉。
机器 PASS 仍然不代表翻译正确
当然,另外一个方向也必须保持清醒。
假设:
ReconstructWorkflow = PASS只能证明:
representation 可以正确恢复。
它不能证明:
中文就是好的。
甚至也不能完全证明:
技术含义一定正确。
一个译文可以做到:
- 所有 inline code 正确;
- 所有 link binding 正确;
- Markdown 完整;
- code 完整;
- placeholders 完整;
但把英文原意理解错了。
这时所有 machine validation 都可能 PASS。
所以 parser / protector 的目标从来不是取代 Reviewer。
它只是尽可能把问题分层:
机器可判断的问题
→ Machine Validation
语言和技术表达问题
→ Independent Reviewer两者各自处理最适合自己的问题。
Internal slot 为什么不能变成正式 TranslationUnit
这又回到了前面那篇关于 Page TranslationUnit 的文章。
Parser 为了完成这些工作,内部可能需要拆出很多 slot。
例如:
slot 1 = paragraph text
slot 2 = inline code
slot 3 = link text
slot 4 = link destination
slot 5 = teaching comment
...这些 slot 对机器非常有用。
但它们不应该因此变成正式语言治理单位。
因为 parser 是会演进的。
今天 parser 把一段内容拆成 4 个 slot。
以后发现 representation 更准确的方式,可能改成 6 个。
如果 slot 就是正式 TranslationUnit,那么一次纯 parser 改进就会导致语言 identity 大量变化。
这显然不合理。
所以 v2 最后逐渐明确:
physical Page 是正式 Page TranslationUnit。
internal slot 只是解析、保护、重建和诊断工具。
Parser 可以进化。
正式语言历史不应该因此被无意义地重写。
Parser 改进以后,也不代表所有 Page 都要重译
这其实和前面 glossary affected scope 的思路非常相似。
假设发现 parser 对某一种 teaching comment 的 representation 有问题。
修复 parser 以后,最简单的办法当然是:
所有 Learn / Docs Page 全部重新翻译。
但如果只有少数页面实际包含这种 representation,就没有必要把整个 package 全部推倒重来。
更合理的方式仍然是判断:
哪些 Page 真正 affected。
未受到影响、并且在新 parser / validator 下仍然能够可靠验证的已有 TranslationUnit,应该继续保留。
这背后仍然是整个 v2 越来越明确的一条原则:
基础设施发生变化,不自动等于所有语言成果失效。
真正需要处理的是受到变化影响的部分。
结构保护的目标不是让 validator 更容易写
这次 parser 工作里,我觉得最容易走偏的一件事情就是:
为了让 validator 简单,设计过强的翻译约束。
例如:
所有 token 必须保持原顺序。
这种规则当然非常容易验证。
直接比较 sequence 就行。
但这并不代表它是正确的语言规则。
如果中文自然表达需要改变顺序,那么 validator 就不应该因为实现方便而禁止这种变化。
换句话说:
validator 应该验证真正不能破坏的语义,而不是把实现方便伪装成语言约束。
这句话对我来说很重要。
因为很多机器规则一开始看起来都很合理。
直到真正出现一种合法的自然语言表达,却被机器拒绝以后,才会发现:
也许不是翻译错了,而是 validator 的模型太窄了。
但放宽 validator 也不能变成“什么都允许”
当然,另外一个极端也同样危险。
发现严格 sequence check 会误伤自然语序以后,不能直接改成:
token 都在就行。
如果这样,pair、binding、link destination 和结构关系都可能发生错位。
所以更合理的改进不是:
strict
→
loose而是:
错误的约束
→
更准确的约束例如从:
token 必须保持原位置。
改成:
token 可以移动,但合法 pair / binding / machine semantics 必须保持。
这不是降低 validation 强度。
实际上要求反而更准确。
只是机器验证的对象从:
表面位置
变成了:
真正需要保持的语义关系。
Example 会比普通 Page 更严格
这里还需要区分另外一种内容:
Example。
普通 Page 的自然语言往往需要较大的语序自由。
但 Example 特别是包含 Go code、预期输出或者机器相关 token 的内容,对 representation 的要求会更严格。
所以“允许 Page 根据自然语言调整合法 binding 的位置”,并不能简单推广成:
所有内容都可以自由移动。
最终仍然需要根据 TranslationUnit kind 判断:
哪些东西属于语言表达,哪些东西属于程序语义。
也就是说,结构保护不是一条全站统一的字符串规则。
它必须理解内容类型。
做到后来,parser 更像一份机器契约
最开始我把 parser 理解成:
把 Markdown 解析出来的工具。
但随着 v2 越做越深,我觉得更准确的理解可能是:
它在 source、Generation 和最终页面之间建立一份机器契约。
这份契约规定:
哪些东西可以改变。
哪些东西不能改变。
哪些内容可以移动。
哪些 binding 必须保持。
哪些结构必须成对存在。
哪些机器语义必须在 round-trip 后完全恢复。
Generation 在这个契约内部拥有语言自由。
Validator 则负责证明它没有越界。
这样一来,parser 就不再只是一个文本预处理器。
它其实是整个 AI 翻译流水线中:
机器安全边界的一部分。
这也是为什么 parser 会不断迭代
对于这种系统,不太可能第一次就定义出完美 representation。
真正的内容会不断暴露新的边界情况。
一开始可能觉得:
inline code 只要保护住就够了。
后来发现需要 link binding。
再后来发现完整 code 中还有 teaching comment。
继续做下去,又可能发现某种 directive 或 nested structure 需要新的 representation。
这种迭代本身并不可怕。
真正需要避免的是:
每一次 parser 改进,都迫使已经稳定的语言成果全部重做。
所以 Page identity、internal representation 和 affected scope 之间的分层非常重要。
Parser 可以继续完善。
但它应该尽量让真正 unaffected 的 TranslationUnit 保持稳定。
最终想要的其实是一个平衡
做到现在,我觉得 Learn / Docs parser 最核心的问题可以浓缩成一句话:
既不能把 Markdown 当成纯文本,也不能为了保护 Markdown 把自然语言锁死。
太自由:
AI may break structure太严格:
machine-safe but unnatural translation真正需要的是:
Protect machine semantics
+
Allow natural language
+
Verify round-trip机器负责保证:
不该坏的东西没有坏。
Generation 负责保证:
中文像中文。
Independent Reviewer 再负责判断:
最终内容是不是真的准确、自然,而且适合技术文档。
这三层职责不能互相替代。
从 Tour 到 Learn / Docs,变化的是 representation 的复杂度
所以最后还有一点需要特别说明。
这次 v2 并不是从:
Tour 阶段严格保持原顺序
变成:
Learn / Docs 才允许自然语序。
Tour 阶段本来就已经允许在保持机器语义的前提下,根据目标语言调整表达顺序。
Learn / Docs 真正带来的新挑战,是:
需要保护和重建的 representation 类型更多、更复杂了。
因此原来已经存在的原则:
机器保护不能牺牲自然语言。
到了 v2,需要被扩展到:
- Markdown;
- inline code;
- links;
- pair / binding;
- complete code;
- teaching comment;
- directive;
- 以及更多结构化内容。
也正是在这个过程中,我越来越确定:
好的结构保护,不应该让读者感觉到结构保护曾经存在。
最终用户看到的,应该仍然是一篇自然、连贯的中文技术文档。
至于为了做到这一点,机器内部用了多少 slot、token、binding 和 reconstruction,那些都应该留在实现层。

