跳到主要内容

两个模型厂商,一个功能:用冗余换来的连贯性噩梦

· 阅读需 12 分钟
Tian Pan
Software Engineer

你接入了第二个模型供应商,因为第一个挂了。那是一个星期二,主供应商的状态页在几个小时内一片通红,你的功能也随之陷入瘫痪。所以你做了件负责任的事:添加了一个备选方案。现在,如果 OpenAI 无法访问,你就路由到 Anthropic。如果 Anthropic 对你进行了频率限制,你就回退到 Gemini。架构图看起来干净且成熟。可靠性问题,解决了。

可惜你添加的不是一个副本。你添加的是第二种意见。而操作“第二种意见”与操作“第二个副本”是完全不同的两回事。

你引入的心理模型来自“无状态服务”的策略:在负载均衡器后运行三个相同的实例,如果一个挂了,另外两个会提供完全相同的响应。这之所以有效,是因为副本之间在字节级别是完全等效的。但来自两个供应商的两个语言模型并非如此。它们是在不同的数据上训练的,针对不同的目标进行了微调,而且它们在用户最容易察觉的输入点上——会产生系统性的、而非随机的——分歧。

你所防范的故障是供应商宕机。而你刚刚引入的故障则更隐蔽且持久:同一个用户,问同一个问题,根据请求恰好由哪个“大脑”处理,会得到截然不同的答案。你用一个罕见的、响亮的问题,换取了一个持续的、隐蔽的问题。

两个模型不等于两个副本

微服务类比中最具诱惑力的词是“故障转移”(failover)。在无状态系统中,故障转移是免费且隐形的:第二个副本不知道也不关心第一个副本是否挂了,你的用户也是如此。多供应商 LLM 方案的推销语完全借用了这些词汇,但这具有误导性。

首先,即使基准测试分数完全相同的模型,对单个问题的回答也会有所不同。最近对跨模型分歧的分析发现,两个整体准确率几乎相同的模型,在特定项目上仍可能存在实质性的差异,而且这种差异是系统性的而非噪声——它反映了每个模型在处理、推理问题时的真实差异,即使在固定提示词和确定性解码(deterministic decoding)的情况下也是如此。综合准确率告诉你这些模型“同样好”,但它无法告诉你它们是否对你眼前的这个请求达成一致。对于运行在某个功能背后的摘要生成器或分类器来说,单项一致性(per-item agreement)才是真正重要的,而这恰恰是被基准测试所掩盖的。

其次是格式。一位工程师将相同的 50 个生产环境提示词输入到 6 个模型中,并记录了其中的差异。最显著的差异不在于正确性,而在于形状。当被要求“仅返回 JSON,不要包含 markdown”时,一个模型大约有三分之一的时间会用代码块包裹输出;而另一个模型即便有明确指令,比例也接近 40%。一个模型添加了没人要求的架构字段。另一个模型在预期接收单个对象时返回了一个数组。当达到 token 限制时,有些模型会干净利落地停止,而另一些则会输出缺失闭合括号的截断 JSON。如果你的解析器是针对主模型的干净输出编写的,那么备选模型的输出就会导致崩溃——不是每次都崩,但足以在凌晨 2 点把你叫起来处理报警。

这就是核心问题所在。你的故障转移路径是你系统中“最缺乏测试的代码路径”,而且它只有在主路径起火时才会执行。在那个最糟糕的时刻,你才会发现备用模型的日期格式不同、拒绝了主模型本会回答的边缘请求,或者默默地执行了主模型本会标记出来的矛盾指令。

你调优的提示词并不可移植

有一个假设在悄悄地破坏多供应商架构:即提示词(prompt)是一种可移植的人造物,写一次就可以指向任何模型。事实并非如此。提示词是针对特定运行时(runtime)编译的程序,而每个供应商的运行时都有不同的调用规范。

Anthropic 的模型经过训练,会密切关注 XML 风格的标签作为结构分隔符;依赖这些标签的提示词在 Claude 上表现出色,但在将其视为装饰的模型上则表现平平。OpenAI 的模型则响应另一套线索。让提示词在某个平台上大放异彩的技术,在另一个平台上可能毫无意义甚至适得其反。研究也证实了这一民间智慧:即使是最强的前沿模型,对提示词措辞的鲁棒性也明显不足,而且这种敏感性是特定于模型的。关于提示词稳定性的研究测量到,仅仅通过重新排序相同信息或重新表述语义相同的指令,输出质量就会出现两位数的波动。

实际结果是,一旦你有了两个供应商,就不再存在“这套功能的提示词”了。而是存在两个提示词——有时是两个提示词“家族”——它们恰好指向相同的行为。行业内甚至给从一个模型移植提示词到另一个模型的工作起了一个名字:LLM 迁移(LLM migration),即“为不同的 LLM 重新设计提示词以实现相同任务的过程”。这是一项工程任务,而不是一次配置更改。

这意味着每当产品经理要求微调时——比如“让语气更热情一点”、“在答案不确定时总是包含一条免责声明”——你不能只做一次修改。你必须在两种方言中分别修改,然后分别验证两次。这就涉及到了成本问题。

没人预料到的“双重评估税”

备用模型的成本并不在于备用模型本身,而在于保持其正确性所需的严苛纪律。

如果你认真对待评估(如果你运行的功能重要到需要故障转移,你就应该认真对待),那么每次提示词的更改在发布前都必须通过你的评估套件。如果只有一个供应商,那只需要一套套件,一个维度。如果有两个,每一次更改都必须通过两次,针对两套模型特有的怪癖进行测试,否则你就在自己为紧急情况修筑的道路上盲目飞行。一个执行这种纪律的工程团队报告称,跨供应商评估提示词让他们的开发时间增加了 20–30%,因为每次提示词更改在发布前都必须至少针对前两个供应商进行评估。

这个数字是“热备”的真实代价。它不是一次性的集成成本,而是对该功能未来每一次更改征收的永久税收。而且它还会随着提示词漂移(prompt drift)而产生复合影响:为了让主模型表现良好而进行的一系列微小、合理的修复,可能会悄无声息地降低次要模型的性能,因为你在进行修复时只关注了主模型。每一次更改看起来都是无害的,但累积的偏差却并非如此。

成本也会体现在你的实际账单中,而这在所谓的“韧性故事”中从未被提及。“等效”的模型层级在价格上并不等效——特别是输出 token,不同供应商之间的价格差异可能非常大。在极端情况下,针对同一工作负载,预算型模型与高端模型之间的差价可达一个数量级或更多。这意味着你的单次请求成本现在变成了非确定性的:同一个查询花费的金额取决于由哪个供应商提供服务。你财务团队的预测现在继承了与你模型输出一样的方差。

流式传输之墙:故障转移在何处悄悄失效

还有一个细节戳破了“无缝故障转移”的幻想,这值得深入理解,因为它是结构性的,而不是你可以修复的 bug。

故障转移只有在“第一个 token 产生前”才能干净利落地运行。一旦你开始向用户屏幕流式传输 token(对于任何聊天类功能,这几乎是瞬间发生的),你就无法透明地在不同模型上进行重试。正如一个构建此类功能的团队所言:一旦他们开始向用户发送 token,就无法轻易地使用另一个模型重试。在中途重新开始意味着用户会看到一半的答案消失,取而代之的是另一个答案。因此,大多数“自动故障转移”的真实行为比架构图暗示的要窄:它能捕获连接错误和生成前的失败,但对于一个开头强劲随后表现退化,或者接受了请求但在生成中途卡死的供应商,它无能为力。

换句话说,你的韧性机制在其最常见的执行路径中内置了一个一致性漏洞。这并不是反对建立韧性机制,而是为了让你确切地知道它能给你带来什么,不能给你带来什么,从而不再把它当成某种神奇的可用性倍增器。

构建接缝,否则干脆别做

这并不意味着多供应商冗余是错误的。它意味着这是一项真正的工程投入,具有真实的抽象成本,你应该有意识地支付这一成本,否则干脆不要承担。

要避免的失败模式是:供应商特定的逻辑泄露到你的领域代码中——if (provider === 'openai') { ... } else if (provider === 'gemini') { ... } 像癌细胞一样在你的业务逻辑中扩散,直到每个功能都了解每个供应商的怪癖。行之有效的纪律是经典的适配器接缝(adapter seam):为你自己的模型调用定义内部契约——请求形状、响应形状、token 计量——并强迫每个供应商的 SDK 在适配器后转换成该契约。你的功能代码只与你的契约对话。添加或更换供应商变成了注册表中的配置更改,而不是对整个代码库的手术。将外部系统视为外部系统;你的平台应该始终只与其内部契约交互。

但要诚实面对接缝无法隐藏的一件事:结构化输出。“给我符合这个 schema 的 JSON”在不同供应商之间并不是同一个功能,而是披着相同名字的不同机制。一些供应商提供真正的服务器端严格 schema 强制执行。Anthropic 的模型根本没有松散的 JSON 模式,而是通过工具调用(tool-calling)路由结构化输出,因此你定义一个工具,强制使用它,并从工具使用块中提取参数。一些供应商仅保证输出在语法上是有效的 JSON,而将 schema 验证留给客户端。一个干净的适配器可以掩盖调用惯例,但仍然必须有人编写并测试每个供应商的路径,将“我想要这个形状”变为现实,而这个路径正是“同一个功能”表现出细微差异的地方。

因此,决策框架比工具市场想让你相信的要简单。如果你规模较小——只有少量服务,支出有限——那么选择一个具有良好错误处理和文档化手动故障转移手册的单一供应商,可能是合适的工程量。当你有了特定的、明确的驱动因素时,再增加第二个供应商:单一供应商的实际可靠性无法满足的硬性可用性协议(SLA)、模型之间真实的性能差距、在达到一定规模后能显著影响盈亏的成本套利,或者数据驻留的合规性要求。 “感觉更安全”和“微服务团队也在这样做”不是驱动因素。它们只会让你为了规避每年几小时的停机时间,而付出了永久性的 20–30% 的额外税收。

无状态系统中的冗余几乎是免费的,因为副本是完全相同的。跨模型供应商的冗余是昂贵的,因为模型各不相同——而语言模型的全部价值恰恰在于它与其他模型的不同之处。当故障概率计算要求这样做时,再增加第二个供应商。只是在进入之前要明白,你买的不是一个备胎,而是雇佣了第二个走不同路线去往同一目的地的司机,而你现在必须确保他们两个都认路。

References:Let's stay in touch and Follow me for more thoughts and updates