最近继续完善 WordPress 中文文章自动翻译为英文的流程时,我遇到了一篇非常适合作为压力测试的文章。
这篇文章并不是普通的几千字内容,而是一篇结构非常复杂的技术博客:
- Gutenberg 区块很多;
- 包含大量普通段落;
- 包含 48 个引用区块;
- 包含大量列表;
- 包含图片;
- 包含 90 个 Code Block Pro 区块;
- 其中还有需要翻译的 Plaintext 代码区块;
- 正文规模达到 30 多万字符。
而我的目标仍然没有改变:
不拆文章,不逐段调用模型,而是继续让 GLM-5.2 一次翻译完整文章,同时通过自动校验确保 Gutenberg 结构没有被破坏。
结果,这篇文章把现有翻译管线中几个隐藏很深的问题全部暴露了出来。
更有意思的是,最后真正需要修改的,并不是模型本身,而是我对“哪些东西属于结构、哪些东西允许随翻译自然变化”的定义。
一、最开始的问题:模型需要机械复制太多结构标记
为了避免 AI 翻译时破坏 Gutenberg 区块,我之前给大量不可翻译结构增加了受保护标记。
例如:
SWQSTRUCT000001END
SWQSTRUCT000002END
SWQINLINE000001END
……
模型翻译正文时,只需要原样保留这些标记。
翻译完成后,再由程序把这些标记恢复成原来的 Gutenberg 注释、HTML、路径和其他结构。
从设计上看很合理。
但当文章越来越复杂以后,问题也逐渐出现了:
模型不仅需要翻译文章,还必须准确复制上千个没有任何语义的机器标记。
这篇文章第一次完整翻译时,就遇到了典型的受保护标记校验失败。

模型并没有简单地“翻译错一句话”。
真正的问题是:
expected_count
actual_count
missing_tokens
fixed_order
开始出现不一致。
而这类问题随着受保护标记数量增加,会越来越容易出现。
尤其普通 Gutenberg 段落本身就会产生:
<!-- wp:paragraph -->
<p>...</p>
<!-- /wp:paragraph -->
如果每一个普通段落的开头和结尾都变成独立受保护标记,那么一篇长文章中,仅普通段落就会制造数百个额外标记。
这让我开始重新思考:
普通段落的数量,真的必须和中文原文完全一致吗?
二、中文三个段落,英文为什么一定也必须三个?
答案其实是否定的。
例如中文可能写成:
这是第一句话。
这是补充说明。
因此我们得到结论。
翻译成英文以后,模型完全可能自然地组织成两个段落。
反过来也一样。
一个很长的中文段落,在英文中拆成两个段落,有时反而更自然。
所以真正应该严格保持的,并不是:
普通段落数量必须 1:1 不变。
而应该是:
普通段落不能越过标题、引用、列表、图片、代码、表格等真正的结构边界。
基于这个思路,我加入了新的:
SWQPARARUNSTART......
SWQPARARUNSTOP......
也就是“普通段落区域”。
一个安全的连续普通段落区域,不再要求模型维护其中每一个 Gutenberg paragraph 注释。
模型看到的只是:
PARAGRAPH RUN START
<p>第一段</p>
<p>第二段</p>
<p>第三段</p>
PARAGRAPH RUN STOP
在这个区域内部:
- 3 段可以翻译成 2 段;
- 1 段也可以变成 2 段;
- 只要仍然是合法的顶层
<p>; - 不允许跨越引用、标题、列表、图片、代码等结构边界。
翻译完成后,再由本地程序重新生成 Gutenberg paragraph 注释。
三、受保护标记从 1000+ 降到了 506
这个调整上线以后,我重新使用同一篇文章进行真实整篇翻译。
最终成功这一轮的执行数据是:

关键数据如下:
source_chars: 324981
payload_chars: 42137
protected_count: 506
request_count: 1
translation_tail_guard_status: present
translation_tail_guard_count: 1
content_result: string
error_code: None
最值得关注的是:
protected_count: 506
此前这篇文章在另一版输入结构中需要模型维护 1000 多个受保护标记。
普通段落区域上线后,这个数量大幅下降。
而且整篇文章仍然只发起:
request_count: 1
也就是说,我没有因为文章复杂就退回:
- 分段翻译;
- 多次模型请求;
- 按区块独立翻译;
- 人工逐段拼接。
仍然坚持的是:
一篇文章,一次完整模型请求。
但事情还没有结束。
四、模型完整返回了,为什么仍然被判定为 Gutenberg 结构变化?
新的普通段落机制解决了大量结构标记的问题后,下一次真实测试不再报:
Protected token validation failed
而是进入了更后面的最终结构校验:
The Gutenberg block structure changed during full-article translation.
这反而是一个好信号。
因为执行流程已经推进到了:
模型完整返回
→ 尾部完整性检查通过
→ 受保护标记校验通过
→ 普通段落区域校验通过
→ Plaintext 等内容恢复完成
→ Gutenberg 最终结构比较
也就是说,问题范围已经被缩到了最后一步。
于是我增加了专门的 Gutenberg structure drift 诊断。
不再只告诉我:
结构不同
而是记录:
first_mismatch_index
source_item_at_mismatch
restored_item_at_mismatch
source_signature_context
restored_signature_context
结果很快就找到了第一个真正的差异。
五、第一个“结构变化”,实际上根本不是结构变化
诊断显示:
first_mismatch_index: 67
源文章和恢复后的译文在这个位置都是:
block_name: kevinbatdorf/code-block-pro
language: plaintext
没有任何区块类型变化。
真正不同的是内容。

源内容:
正文 A
.image /tour/static/img/tree.png
正文 B
翻译后:
Body text A
.image /tour/static/img/tree.png
Body text B
这显然正是我希望看到的结果。
因为这是一个:
language: plaintext
的 Code Block Pro。
Plaintext 本来就允许翻译。
问题出在最终结构签名。
当时的结构签名会对整个 attributes 做 SHA-256。
而 Code Block Pro 的 attributes 中不仅有:
language
theme
lineNumbers
fontSize
copyButton
这些真正的配置项,还有:
code
codeHTML
highestLineNumber
其中:
code是翻译后的文本;codeHTML会根据译文重新生成;highestLineNumber也可能随译文行数重新计算。
于是出现了一个很典型的逻辑冲突:
Plaintext 恢复逻辑:
这些字段允许并应该变化
最终结构校验:
这些字段变化 = Gutenberg 结构变化
所以模型其实做对了。
是校验器误判了。
六、结构校验不能只看“属性有没有变化”
最终我把 Plaintext Code Block Pro 的结构签名进一步细分。
只有:
kevinbatdorf/code-block-pro
并且语言属于:
plaintext
plain
text
txt
时,最终结构签名才会忽略三个确定允许变化的内容字段:
code
codeHTML
highestLineNumber
其他配置继续严格比较。
例如:
language
theme
bgColor
textColor
fontSize
fontFamily
lineNumbers
copyButton
useTabs
只要这些真正的配置发生变化,仍然会判定为结构异常。
而对于不是 Plaintext 的 Code Block Pro:
attributes 仍然完整严格比较。
所以这并不是:
为了让当前文章通过,干脆把 Code Block Pro 校验放宽。
而是重新定义:
内容字段和结构配置字段不是一回事。
七、另外三个“丢失的结构”,竟然只是空白
诊断中还有另一个很有意思的数据:
source_signature_count: 588
restored_signature_count: 585
也就是说,除了 Code Block Pro 的 attributes hash 不一致以外,译文还少了三个结构签名项。
继续对比以后发现:
并没有少:
- 引用;
- 标题;
- 列表;
- 图片;
- Code Block Pro;
- 分隔符。
少掉的全部都是:
PARAGRAPH_RUN
继续追下去,发现问题来自 WordPress parse_blocks() 返回的一类无名项。
有些 Gutenberg 区块之间只有换行和空格:
paragraph
空白
paragraph
这些空白可能被解析成:
blockName = null
旧逻辑虽然不会把这个无名 block 写进结构签名,却会把:
in_paragraph_run
重置。
结果原文变成:
PARAGRAPH_RUN
空白
PARAGRAPH_RUN
最终签名看起来却只有:
PARAGRAPH_RUN
PARAGRAPH_RUN
而翻译以后,如果空白形式变化了:
PARAGRAPH_RUN
于是校验器误以为:
少了一个段落区域。
实际上什么 Gutenberg 结构都没有丢失。
八、空白可以忽略,但真正的自由内容不能忽略
这里同样不能简单地写:
所有
blockName=null全部忽略。
因为有些无名 block 可能包含真正有意义的旧式 HTML 或自由文本。
所以最终规则是:
纯空白 freeform
满足:
blockName = null
无 innerBlocks
内容只有空白字符
这种内容:
不进入结构签名,也不切断普通段落区域。
有意义的 freeform HTML / 文本
例如:
<div class="legacy">
Visible freeform HTML
</div>
这种内容不会被静默忽略。
而是生成独立的:
FREEFORM:<SHA-256>
结构签名。
也就是说:
无意义的排版空白不应该影响结构判断,但真正的自由内容仍然必须严格保护。
这也是这次调整中我比较满意的一点。
不是为了让当前文章通过就不断“放宽”。
反而是在区分:
什么是真正的结构
什么只是内容
什么只是空白表现
九、所有调整必须有自动回归,而不是只验证这一篇文章
最终修复提交为:
4089fda fix: 修复 Gutenberg 结构签名误判
此前还增加了一次专门用于定位问题的诊断提交:
afdc88e feat: 增强 Gutenberg 结构漂移诊断

这一轮新增和保留的测试包括:
swq-paragraph-run-test.php
swq-plaintext-structure-test.php
swq-token-validation-test.php
swq-adjacent-token-repair-test.php
swq-tail-struct-token-repair-test.php
除了当前真实案例,还专门验证:
- Plaintext Code Block Pro 的
code变化可以接受; codeHTML变化可以接受;highestLineNumber变化可以接受;theme变化仍然失败;language变化仍然失败;lineNumbers变化仍然失败;- 非 Plaintext Code Block Pro 的 attributes 变化仍然失败;
- 纯空白 freeform 不切断普通段落区域;
- 有意义的 freeform 会进入严格哈希校验;
- separator 仍然是严格边界;
- quote 仍然是严格边界。
这比“当前文章终于能翻译”更重要。
因为真正需要解决的不是:
如何让文章 25425 特殊通过。
而是:
如何把这次失败暴露出的通用规则修正到翻译管线中。
十、最终结果:完整原文不做简化,整篇翻译成功
完成这些调整以后,我没有继续删除引用区块,也没有把文章简化成专门用于测试的版本。
而是重新使用:
原始、完整、保留全部引用区块的中文文章。
再次进行整篇翻译。
最终:

WordPress 后台终于返回:
Translation created successfully.
这次成功尤其重要的一点是:
我没有为了提高成功率而:
- 删除 48 个引用区块;
- 批量改写文章结构;
- 拆成几十次 AI 请求;
- 对每个 Gutenberg 区块单独翻译;
- 人工拼接英文正文。
仍然保持:
一篇文章
→ 一次完整 GLM-5.2 请求
→ 自动恢复
→ 自动结构校验
→ 写入英文文章
十一、Gutenberg 编辑器也能够正常打开译文
翻译 API 返回成功还不够。
真正的 WordPress 内容最终还必须能够被 Gutenberg 正常解析。
于是我又打开了生成后的英文文章。

左侧区块导航仍然可以看到大量:
段落
引用
段落
引用
……
没有出现:
尝试恢复
也没有:
区块包含意外或无效内容
说明这次不仅是后端程序认为结构正确。
WordPress Gutenberg 编辑器自己也能够正常识别最终内容。
英文末尾还有一个单独的:
.
这是因为模型在合并普通段落时,把正文语义合并到了前一个段落,却留下了原句末尾的句号。
从翻译质量看,这是一个轻微的排版瑕疵。
但我暂时不准备为了这个问题继续增加自动规则。
因为:
一个孤立句号和 Gutenberg 结构损坏,是完全不同等级的问题。
如果为了自动删除这个 .,再增加“单字符段落自动删除”之类的规则,反而可能误伤真正合法的特殊内容。
必要时手工删除即可。
十二、这次真正改变的是我对“结构保护”的理解
这次排查过程中,最大的收获其实不是多修了几个正则表达式或者增加了几个测试。
而是进一步明确了一件事:
结构保护并不是保护得越多越安全。
如果把所有东西都视为不可变化:
普通段落数量不能变
代码区块所有 attributes 不能变
空白造成的 parse_blocks 差异也不能变
结果就是:
模型明明生成了完全合理的译文,却不断被自动系统判定为失败。
真正合理的自动翻译管线应该区分:
必须严格保持的结构
例如:
- 标题;
- 引用;
- 列表;
- 图片;
- 分隔符;
- 表格;
- 容器;
- 高风险自定义区块;
- 非 Plaintext 代码结构;
- 真正有意义的 HTML / freeform。
可以自然变化的翻译内容
例如:
- 连续普通段落的数量;
- Plaintext Code Block Pro 的文本;
- 根据文本确定性生成的
codeHTML; - 由翻译后行数决定的
highestLineNumber。
不应该影响结构判断的表现差异
例如:
- Gutenberg 区块之间纯粹的空白;
- 不具有实际页面语义的 freeform whitespace。
只有把这三类真正分开,自动翻译才能同时做到:
既允许译文自然表达,又不牺牲 Gutenberg 内容安全。
十三、从“模型必须复制结构”转向“程序负责结构”
这次普通段落区域的改造,也让我进一步确认了一个方向。
以前很多保护机制实际上是在要求模型:
请你一边翻译,一边替程序维护 Gutenberg。
但模型真正擅长的是语言。
它不应该负责机械复制:
1000+
个没有语义的结构标记。
更合理的方向应该是:
程序识别结构
→ 只把真正需要语言模型处理的内容交给模型
→ 模型负责翻译
→ 程序恢复确定性结构
→ 程序负责最终验证
这次将受保护标记数量降到:
506
并不是单纯为了节省 Token。
更重要的是:
减少模型承担的机械结构任务。
让模型负责语言,让程序负责结构。
这应该也是接下来继续完善整篇 WordPress AI 翻译管线时,我会坚持的基本方向。
十四、结语
这篇文章一开始让我怀疑:
是不是 Gutenberg 结构太复杂,整篇 AI 翻译最终还是不可行?
但经过这轮排查以后,结论反而更加明确:
问题并不是“整篇翻译不可行”,而是保护和校验机制必须真正理解哪些变化是合法翻译的一部分。
最终,这篇拥有大量段落、引用、列表、图片和 90 个 Code Block Pro 的超长文章,仍然通过:
一次完整 GLM-5.2 请求
完成了中文到英文的整篇翻译。
而且最终 Gutenberg 编辑器能够正常打开。
这比简单地让某一篇文章“翻译成功”更有价值。
因为这次修复以后,整个翻译管线对于普通段落、Plaintext Code Block Pro、空白 freeform 和 Gutenberg 结构之间的边界,都比以前清楚了一步。
下一阶段,我会继续用真实历史文章验证这套规则。
目标仍然没有改变:
尽可能让完整文章一次进入翻译模型,同时把结构恢复、安全校验和失败判断留给确定性的本地程序完成。
需要长期技术维护或远程问题排查?
我是拥有 15+ 年经验的 PHP / Go 后端工程师,长期关注已有系统维护、Bug 修复、性能优化、服务器排查、WordPress 网站维护和小功能迭代。
如果你的项目遇到以下情况,可以先从一次小问题排查开始合作:
- ✅ PHP / Laravel / Yii2 老项目无人维护
- ✅ Go / Gin 后端接口需要排查或优化
- ✅ WordPress 网站访问慢、报错或插件冲突
- ✅ Nginx / MySQL / Redis / Linux 服务器异常
- ✅ CDN / Cloudflare / DNS / HTTPS 配置问题
- ✅ 需要长期远程技术支持或兼职维护
更多介绍请查看:关于我 & 合作
微信:13980074657
邮箱:shuijingwanwq@gmail.com
Telegram:@shuijingwan
GitHub:https://github.com/shuijingwan


发表回复