多语言功能在换供应商后劣化,通常不是模型质量而是三个工程耦合点:分词器、语言选择机制和输出格式(分隔符、stop 序列、JSON 结构)。
在讨论"哪个提供商日语更好"之前,需要先澄清一个事实,这个问题本身无法回答,也不是真正的问题。你的国际化产品会在三个特定的地方因迁移而出现故障,而这三点与模型质量毫无关系。
三个耦合点
当多语言功能在提供商切换后出现降级时,条件反射般地会将其归咎于新模型在该语言上表现更差。这种情况偶尔确实存在,但从来都不是可以采取行动的方向。真正可以采取行动的三个方面,是你代码与旧提供商之间的耦合:
Token 计数。你计算的每一个预算——包含多少上下文、何时截断、一次请求的成本——都是用不再适用的分词器计算的。
语言选择。你的 prompt 告诉模型用什么语言回答的机制,这是 prose 形式,因此与模型相关。
输出格式。分隔符、stop sequences、JSON 字段值和长度限制,一旦文本不是拉丁字母体系,这一切都表现不同。
按这个顺序来处理它们,因为第一个是可测量的,第二个是可测试的,第三个是可见 bug 出现的地方。
分词器,以及为什么你的计数是错的
分词器是训练出来的产物,每个提供商都自带。同一个字符串在两个分词器下不会产生相同的计数,而对于那些在分词器训练语料库中覆盖稀疏的语言文字体系来说,这个差距要大得多。英文 prose 倾向于每个短词接近一个 token;而在词汇覆盖不佳的文字体系中,文本可能趋向于每个字符一个 token,这是一个倍数关系,而不是差值。这种机制及其计费后果是分词器语言税的主题;迁移相关的后果范围更窄,也更容易说明。
如果你用分词器库在本地计算预算——这是常见做法,因为本地计数免费,而 API 调用不是——那个库编码了一个提供商的词汇。在不改变计数方式的情况下将客户端指向不同的提供商,每一个衍生出来的数字都是错的,错误方向因语言而异。常见的问题表现是:检索步骤打包了太多 chunk,在覆盖最差的 locales 上溢出窗口;以及成本估算与同一部分流量的发票不符。
在提供商不发布本地分词器的地方,它通常会提供一个计数端点。Anthropic 文档记录了 POST /v1/messages/count_tokens,它接受相同的 messages 数组——连同 tools、images 和 documents——并返回一个对象,其唯一字段是 input_tokens。这是一次额外的往返,这就是为什么实际的做法是:在评估期间调用它来校准,从结果中导出每种语言的 characters-per-token 比率,然后在生产环境中用这个比率并留出余量。在每次迁移时重新导出这些比率;它们才是真正变动的数字。
命名语言的指令
几乎每个国际化的 prompt 都会有一行类似 "Respond in {{locale_name}}" 的内容,被插入到一个英文 system prompt 中。它能工作,直到不能工作为止,而失败表现为模型无论如何都用英文回答。这是一个众所周知的独立失败场景——参见"当 prompt 的输出语言指令被忽略时"——迁移带来的新问题是,对于这一特定指令的指令遵循能力在不同模型之间并不统一,所以一个从未需要强化的 prompt 突然就需要强化了。
三个细节可以让这个指令更具可移植性,而且都很廉价:
用该语言本身来命名语言。"Respond in 日本語" 比写在英文海洋中的 "Respond in Japanese" 锚定得更强,因为目标文字体系出现在上下文中,而不是仅仅被描述。
把指令放在最后,同时也放在最前。在长 system prompt 顶部的一次提及要与后面的所有内容竞争;紧接着用户 turn 之前的一个简短重述,是最能经受模型变化的 position。
也把 locale 作为结构化值传递。如果你使用 schema 约束的输出,包含一个携带 BCP 47 标签的语言字段,并将其约束为期望值。模型然后必须将语言作为数据来承诺,而你的验证器可以在没有任何语言检测的情况下拒绝不匹配。
警惕与 few-shot 示例的交互。如果你的示例是英文的,而指令说用另一种语言回答,这些示例就是反对该指令的证据。这种冲突被不同模型以不同方式解决,而这正是迁移所暴露的那种依赖。
输出格式契约
第三个耦合点是用户可见 bug 的来源,因为针对英文文本编写的格式假设往往是偶然成立的。
用字符表达的 length budgets 无法延续:一百个拉丁文本字符和一百个表意文字脚本字符携带的信息量非常不同,为一种情况 sized 的 UI 卡片在另一种情况下要么看起来为空,要么溢出。用 surface 实际约束的单位来做预算——渲染宽度或行数——并在生成后验证,而不是信任 prompt。
Stop sequences 和分隔符是另一个陷阱。像 triple-hyphen 或 bracketed marker 这样的 sentinel 是一个模型必须逐字输出的字符串;当周围文本是另一种文字体系时,模型更可能输出标点字符的全角变体,这是一个不同的码点,无法匹配。在英文中始终有效的分隔符在另一种文字体系中默默地不再终止生成,你得到的是一个达到上限的响应——这是输出长度页面上描述的故障。在提供商支持的地方,优先使用结构化输出而不是 sentinel 解析,而在必须使用 sentinel 的地方,在匹配前规范化全角标点。
从右到左的内容增加了第三个问题:作为字符串正确的文本在与拉丁标识符连接后可能会被错误渲染,而模型引入的方向标记可能无法在你的 sanitizer 中存活。测试渲染后的结果,而不是字符串。
一个你可以实际运行的 per-locale gate
这个 gate 很小,它是让上述所有内容在用户发现之前被你发现的唯一途径。
从生产环境获取每个支持的语言环境十到二十个真实输入,经过脱敏处理。真实输入,因为英文输入的合成翻译共享英文句子结构,无法暴露这个失败。
对于每个输入,记录三个断言而不是预期输出:响应使用的是请求的语言,它按照你的格式契约可以解析,并且它符合你的渲染长度预算。这三个都可以在不人工判断质量的情况下检查。
从提供商自己的计数器记录每个输入的 token 计数,并导出每个语言环境的 characters-per-token 比率。存储它;这是你的生产估算器使用的数据。
在切换前用候选提供商运行这个集合,在任何语言环境出现断言退化时使迁移失败。每个语言环境的通过率是进入 go/no-go 决策的数字,它比总体数字更可辩护。
之后按计划继续运行,作为多语言一致性检查。出现故障的始终是用户最少的语言环境,这意味着没有人报告它们。
Tokenizer 行为、计数端点和结构化输出支持都是会变化的提供商 surface。重新导出比率而不是复用任何在别处发布的数字,包括本库中的任何数字。