STORY 01
LLM 输出错位事故与重建校验——「宁缺勿错」不变量
问题
解析管线里每行歌词有两套分词:本地 Intl.Segmenter(快、免费、边界保守)和 LLM 返回的 tokens(附带读音与语法角色)。上线初期的实现按下标把 LLM 的读音、语法角色逐位贴到本地分词上。
真实线上事故:Intl.Segmenter 把「薄く」切成「薄|く」(把形容词连用形拆开了),而 LLM 按语言学上正确的方式切成一个词「薄く」。边界差一个位置,下标 zip 之后整句左移一位——「く」被标成「とうめい」(透明的读音)、「な」被标成「くちざわり」(口触り的读音)。错位读音混进 furigana,TTS 又把「透明」「口触り」各读了两遍。用户看到的是:注音是错的、朗读是重复的——对教日语的产品,这是致命错误。
根因:两个独立系统对同一文本的切分之间没有任何一致性保证,而旧代码假设它们逐位对齐。这不是实现层面的 bug,而是设计错误——假设本身就不成立。
方案
重建校验(reconstruction check):
- 把 LLM tokens 的 text 拼接后与原句比对(忽略空白差异):拼得回去 → 整体采用 LLM 的边界、读音、角色;拼不回去 → 整句退回纯本地切分,并且不贴任何 LLM 数据。这是一条「全有或全无」的不变量。
- 缺读音的内容词,用该词自身的文本推假名(
wanakana,只对纯假名词有效)兜底——关键在于兜底数据来自 token 自己的 text,不再依赖位置对齐。 - 客户端再加一道防线:TTS 发送前检测假名轨是否含汉字(含汉字说明某词读音丢失、已退回原文),检出即弃用整条假名轨,退回原文朗读。
结果
真实事故句「薄く透明な口触りで」被固化为回归测试,另补充空白差异、缺 token、空 text 等降级用例。修复部署后,新解析的错位问题归零。
设计权衡 Q&A
Q为什么不用编辑距离/LCS 做部分对齐,尽量多保留 LLM 数据?
部分对齐会产生「看起来对了、实际错位」的中间态,而在语言学习产品里,错误的读音比缺失的读音危害更大——用户会把错的学进去。「全有或全无」的不变量简单、可证明、可测试。教育产品的取舍是正确性优先于覆盖率。
Q为什么不在 prompt 里强制 LLM 按本地分词输出,从源头消除分歧?
prompt 里确实提供了本地分词作参考,但 LLM 不提供硬保证;而且这次事故里 LLM 的切分在语言学上反而是对的(「薄く」本来就是一个词)。正确做法不是强迫 LLM 迁就本地的错误切分,而是让校验器决定信谁。
Q服务端修好了,为什么客户端还要再防一道?
两个原因。其一是存量数据:已解析的歌存在用户设备上,服务端修复不追溯,重新导入之前老数据仍是错位的。其二是防御纵深:假名轨的生成路径未来还会演进,客户端检查的是最终不变量——送给 TTS 的读音轨不应含汉字——与上游实现解耦。