AI 翻译
翻译流程会读取仓库中已有的 Markdown,按照线上页面的实际呈现方式进行渲染,将其拆分,翻译其中人类可读的部分,并按页面和语言存储结果。没有翻译文件、没有消息键,也没有导出步骤。本页面从代码实际执行的层面介绍了这一机制。
一次运行从何开始#
有五种情况,而且每次运行都会记录具体是哪一种——这样面板就能显示由提交 a1b2c3d 触发,而不是把一次推送归因于你。
| 触发器 | 触发原因 |
|---|---|
language_enabled |
你启用了某种语言。 |
commit |
自动模式的扫描器发现仓库头指针发生了移动。 |
manual |
你按下了立即翻译,或者其他人从控制面板执行了此操作。 |
agent |
名为 run_translation_pass 的代理触发。代理的密钥和运行 ID 会写在任务行中。 |
| 恢复运行器 | 每 2 分钟,cron 定时任务会回收心跳已沉默 15 分钟的运行,释放它们的锁,并驱动最多三个已空闲 90 秒的活动任务。 |
扫描器只会检查 15 分钟内未检查过的工作区,并且每次只检查四个工作区,优先检查距离上次扫描时间最长的工作区。当仓库头指针未发生变化时,扫描器会立即停止——而且它用于比较的水印,只有在该提交中确认所有已启用的语言都已同步时才会推进,因此一次因预算耗尽而中止的运行不会将工作区冻结在部分翻译状态,导致它永远不再被检查。
一次运行绝不会同时启动超过三种语言,并且优先处理最紧急的语言:每次运行都需要数分钟的计费模型工作,如果一个代理步骤同时启动十种语言,一次触发就会耗尽整整一个月的预算。其余语言会被报告为 over_language_cap,并在下次处理,这才是“现在不处理”的真实情况。
页面如何分块#
翻译的单位不是页面,而是章节。
- 系统会预处理您的 Markdown,并通过线上页面所使用的同一处理流程进行渲染,包括您的小组件黑名单。因此,翻译所看到的是读者实际看到的页面,而不是对源文件的第二种解读。
- 渲染后的 HTML 会在
<h2>和<h3>边界处分割。第一个标题之前的内容会成为独立的开头分块。 - 超过9,000 个字符的分块会再次分割——但仅在顶层块边界(
</p>、</li>、</table>、</pre>、</figure>、</h1>…</h6>)处分割,因此片段绝不会在标签中间被截断。 - 每个分块都根据其自身内容生成哈希值。哈希值加语言构成缓存键。
- 分块会同时翻译,最多每次 3 个,每个请求的上游超时时间为 30 秒,然后按原顺序重新拼接。
这种设计带来了两点结果,而这正是采用这种设计的全部原因:
- 编辑一个段落只会重新翻译一个章节。其他分块的哈希值均未改变,因此会从缓存中提供。修正一个错字只需处理一个章节,而不是整个页面。每个页面的支出账本会记录一次分割结果——复用了多少分块、发送给模型多少分块——因此节省的成本是经过测量的数字,而不是口头声明。
- 未触及的文本不会发生术语漂移。未编辑的章节与上次翻译时逐字节一致,因为它实际上就是同一个缓存字符串。
请求以温度 0发送,输出令牌预算根据输入长度计算,而不是统一预留固定额度——乘数为 2.6。之所以选择这个数值,是因为与英文源文本相比,西里尔文字和中日韩文字每个字符消耗的令牌要多得多,而 1.5 倍预算会导致页面被截断。
模型受到什么保护#
这里有两种不同类型的保护,将它们混为一谈会导致文档最终承诺超出代码实际能提供的范围。
结构上受到保护——模型永远看不到它#
| 元素 | 机制 |
|---|---|
| 围栏代码块 | 在请求之前提取,并替换为 __CODE_BLOCK_N__;之后逐字节恢复。 |
行内代码 (`like this`) |
同样进行提取,并以完全相同的字节内容恢复。 |
| Frontmatter 键和值 | 根本不会到达模型:翻译在渲染后的 HTML 上运行,而 frontmatter 已在渲染时被处理。 |
小部件标记 (<!-- widget:name -->) |
同样不会到达模型:小部件在分块之前由渲染管线展开为 HTML。没有留下可被破坏的标记。小部件内部的可见文本——例如卡片的标题——属于普通文本,会被翻译。 |
这些都是保证。模型从未获得代码示例,因此无法修改、重新排版或“翻译”它。
受指令保护——请验证这些内容,不要想当然#
提示要求模型将不得翻译或修改 HTML 标签、属性、类名、ID、href 值或数据属性、不得更改 HTML 结构、不得翻译代码标识符和变量名、不得添加评论,并且必须完全复现 __CODE_BLOCK_N__ 占位符,这些都作为绝对规则告知模型。这对于以温度 0 运行的模型来说是一条强指令,在实践中确实有效——但它是一条指令,而不是一种机制,诚实的说法是通常如此。
需要了解的三个后果:
- 链接会保留其目标。
href是一个属性,而属性属于不得触碰的列表。链接的文本是正文内容,会被翻译。 - 标题锚点会保留源语言。 标题的
id属性在翻译前就已设置,并且被要求保持不变,因此指向英文标题锚点的深层链接在翻译后的页面上仍然有效。 - 图像的
alt文本不会被翻译。 它是一个 HTML 属性,而保护href的规则也会一并保护alt。如果读者语言的可访问替代文本对你很重要,那么这是一项缺陷,而不是一项功能。
按组翻译,而不是逐个翻译#
导航标签会在单个请求中作为一个整体进行翻译,该请求必须按相同顺序返回数量相同的标签;如果响应格式错误或不匹配,则保留原文,而不是进行猜测。如果返回的每个标签都与原文完全相同——这表明翻译并未发生——则会丢弃结果而不进行缓存,以便下次尝试时可以重新翻译,而不是永久锁定为英文标签。页面的标题和描述会作为一对一起翻译,因此二者永远不会失去同步。
如何检测过时的翻译#
通过与 git 比较,而不是通过状态标志。
每条存储的翻译记录都保留 source_hash —— 翻译时源文件的 git blob SHA。通过读取 HEAD 处的仓库树并按路径进行比较来计算覆盖率:
| 状态 | 含义 |
|---|---|
current |
存在机器翻译,并且其存储的 SHA 与 HEAD 处文件的 SHA 相等。 |
behind |
存在机器翻译,但对应的是该页面的较早版本。 |
missing |
页面存在于仓库中,但从未翻译成此语言。 |
manual |
手写或上传的翻译。新鲜度由作者决定,因此永远不会被计为落后。 |
orphaned |
源文件在 HEAD 处已不再存在的翻译。 |
覆盖率是 (current + manual) / total,当 behind 和 missing 均为零时,语言处于同步状态。当无法读取仓库时,覆盖率为 null —— 绝不能自信地视为零,每个界面都会报告“未知”,而不是将健康的语言标记为红色。
数据库中的 status = 'outdated' 列有意不用于此目的。产品不会自动写入任何内容,因此每个工作区中该列都显示为零;基于它构建的新鲜度检查将永远报告完美的健康状态。
按照此比较结果安排的处理会先翻译落后项,再翻译缺失项。过时的翻译实际上是在告诉读者一些文档已不再表达的内容;缺失的翻译则会回退到原文,只是未能提供帮助。
审核与批准流程#
已存储的翻译有三种来源,而它们之所以受到不同处理是有意为之。
| 来源 | 编写者 | 提供给读者 | 是否会被后续流程覆盖 |
|---|---|---|---|
docsbook_ai |
翻译流程 | 是 | 是 |
manual_upload |
你,通过面板编辑器或 upload_translation |
请先阅读此处 | 否 — 自动流程不会替换它 |
external_api |
你自己的管道,采用 external 模式 |
请先阅读此处 | 否 |
上传的内容默认会成为草稿。list_pending_translations 返回草稿,approve_translation 将其中一项移至已发布状态,而编辑翻译内容会将该行标记为手动上传,因此后续流程会保留它。在 external 模式下,流程如下:页面即将被翻译时,Docsbook 发出 translation.needed,你的管道执行相关工作,然后 upload_translation 将结果发回。
机器翻译不会进入此队列。流程会写入状态为 auto 而非 draft 的行,因此 list_pending_translations 永远不会列出它们。批准流程是对从外部传入的翻译设置的一道关卡,而不是在 AI 输出前设置的人工审核步骤。如果你希望在读者看到 AI 输出之前对其进行审核,external 模式正是实现这一目标的方式;默认的 auto 模式会边处理边发布。
运行失败时会发生什么#
下面的每种失败模式,都是为了提供原始内容,而不是存储损坏内容而有意做出的决定。
| 失败情况 | 处理方式 |
|---|---|
模型达到其输出令牌上限(finish_reason: length) |
将其视为硬失败并拒绝,而不是存储。曾经有一次,被截断的内容块导致半页内容永远作为译文缓存在系统中。 |
| 模型返回空内容 | 同样视为硬失败。曾有一个作为成功结果传播的空字符串,导致某个项目累计产生了 808 条空白译文记录。 |
| 某个内容块失败 | 页面会使用该内容块的原文进行组装,并且仅针对本次请求提供。它不会写入 Redis,不会写入 Postgres,也不会被编入索引。 |
| 组装后的页面为空,而源页面不为空 | 不存储;改为提供源页面。 |
| 某个内容块最近失败过 | 10 分钟的负缓存会防止重试风暴。缓存过期后,下一次访问只会重新翻译缺失的内容块。 |
| 提供商针对 Docsbook 的共享密钥返回 402/403 | 系统会设置4 小时的全局暂停,所有待处理的运行都会立即停止,而不是继续频繁请求已耗尽额度的账户。使用自有密钥的工作区不受影响。 |
| 您自己的密钥额度已耗尽 | 只有您项目的运行会失败。其他人的额度耗尽不会暂停您的运行,您的额度耗尽也不会暂停他们的运行。 |
| 项目支出预算已耗尽 | 运行会以文字说明该原因并暂停,剩余页面会在余额允许后于后续运行中翻译。已经付费的内容不会丢失。 |
| 调用在运行过程中被终止 | 任务会保存游标和心跳。运行器每 2 分钟检查一次,对静默 15 分钟的任务进行回收、释放其锁,并从下一个尚未翻译的页面重新启动。 |
运行只完成一部分是正常现象,而不是错误状态:大型网站需要多次调用,每次调用都会在墙上时钟允许的时间内尽可能继续处理,下一次调度会接着进行。您不应该看到的情况是一个页面一半为英语、另一半为其他语言,因为代码正是拒绝持久化这种组装结果。
页面尚无译文时,读者会看到原文——没有加载指示器,没有错误,也没有横幅承诺提供本次请求无法生成的译文。如果只有较旧的译文存在,读者会立即获得该旧译文,而不是被退回原文:以正确语言提供可阅读的内容,胜过让读者等待。
限制#
- 术语一致性背后没有术语表支持。 没有术语库、没有可供你提供的禁止翻译列表,也没有跨页面一致性检查。现有的一致性来自温度 0、从缓存中原样提供未编辑的部分,以及将标签和标题/描述作为整体进行翻译。使用相同术语的两个不同页面是分别独立翻译的,因此可能并不一致。
- “不要翻译标识符”是一条指令,而不是保证。 围栏代码和反引号中的代码在机制上是安全的。作为普通散文书写的裸标识符——例如句子中的参数名,且未使用反引号——只能依靠提示词来保护。将标识符写在反引号中,是你能为自己的翻译采取的最有价值的措施。
- 图片
alt文本会保留源语言。 请参见上文;这是完整保护 HTML 属性的结果。 - 缓存的翻译是在写入时生效的小组件屏蔽列表下呈现的。 关闭小组件不会重写已经翻译的页面;这些页面会在下一次处理时应用新的设置。根据屏蔽列表重新生成缓存键,将导致每种语言的每个页面都因显示切换而重新翻译。
- 自动模式会响应轮询,而不是响应你的推送。 请参见翻译设置。