提供商故障转移:在对话中途替换了你安全策略的隐忧
用户正与你的助手进行一场关于受控物质处方模式的谨慎对话,已经进行了十二轮。模型表现得很有分寸,提出澄清性问题,引用指南,并拒绝进行文献之外的推演。在第十三轮,用户提出了一个后续问题,按理说应该得到与前十二轮相同的回应。然而,他们得到的却是一个生硬的拒绝:“我无法提供相关帮助。”对话结束了。他们怒气冲冲地给支持团队写信——他们并没有问任何不同的内容,助手刚才还在帮助他们,到底发生了什么变化。
你的日志解释了变化的原因。在第十三轮进行到一半时,你的主供应商在流式传输过程中返回了 503 错误。你的网关按照配置执行了操作:在请求的剩余部分故障转移(failover)到了备用供应商。备用供应商对该类查询的拒绝阈值校准得比主供应商更保守。用户并没有问任何不同的问题——他们在同一个品牌下对不同的模型提出了相同的问题,而新模型说了“不”。
构建故障转移机制的团队将其视为一个可用性(availability)决策。他们根据在线率目标衡量网关,并展示了证 明维持了 SLO 的事后分析图表。他们没有考虑到的是,他们刚刚发布了一个信任与安全团队从未审查、从未批准,且无法从其拥有的任何仪表盘中检测到的安全策略差异。故障转移池中供应商的笛卡尔积才是真正的策略表现面,而目前没有人对其负责。
为什么“可互换的 LLM 端点”是一个支撑性的虚构概念
故障转移层建立在一个在 API 层面基本成立的前提下:供应商暴露了大致兼容的聊天补全(chat-completions)接口,接受类似的系统提示词,返回类似的流式 Token 序列。你可以用适配器代码和路由器来交换它们。从可用性的角度来看,它们是可互换的,就像同一个数据库的两个可用区(AZ)是可互换的一样。
但在安全层面,它们完全不可互换。已发布的拒绝率基准测试使这种差异量化了。在 OR-BENCH 和类似的过度拒绝探测中,Claude 系列模型在相同提示词下的拒绝频率明显高于 GPT 系列模型——一个被广泛引用的结果显示,Claude-3.5-Sonnet 对受控物质查询的拒绝率为 73%,而 GPT 在同一组测试中的安全完成率高达 90% 以上。Gemini 则处于另一个位置,其特征是在代理循环(agentic loops)上更倾向于“完成工作”,而在社会偏见问题上姿态更严厉。供应商的拒绝策略面在任何轴线上都不一致。
这不是任何供应商的 bug。它们都根据各自的威胁模型、客户群体以及对谁在进行审核的假设,调整到了一个合 理的运行点。Bug 出在你的网关上,它将这些运行点视为“前沿 LLM”的属性,而不是每个供应商签署的具体合同的属性。一旦你的故障转移池中有多个条目,面向用户的策略就是每次路由决策中所有条目策略的并集,无论你是否对其建模,你都承担了这个并集。
情况比拒绝率更糟糕。类似的差异还出现在语调、推测意愿、模型处理不确定性的方式、措辞提醒的方式以及对相同系统提示词的解释上。用户感知为连贯助手的对话,在底层实际上是几个不同“个性”之间的接力,你将它们的台词拼接在了一起。大多数时候没人注意到。当拼接点恰好落在一个敏感回合时,你就会收到一个无法从任何单一追踪中查明根本原因的支持工单。
流式传输中途是进行切换的最糟糕时机
生产网关中的故障转移路径是不对称的。非流式传输的情况表现相对良好:返回 503 错误,路由器针对池中的下一个供应商进行重试,用户感受到的只是轻微的延迟增加,答案保持不变。流式传输才是破绽百出的地方。
在响应中途失败的流式请求已经向客户端发送了一些 Token。网关现在必须决定是丢弃这些 Token 并从原始提示词重新开始,还是从第一个供应商停止的地方继续响应。这两个选项都很糟糕。丢弃已发送的 Token 意味着从头开始重新渲染 UI,从而让用户感到困惑。继续响应意味着要构造一个回退请求,其中包括对部分输出的助手预填充(assistant prefill),以便备用 供应商可以接续句子。
第二条路径是各种记录在案的事故发生地。LiteLLM 自己的问题追踪器中有多个报告:中途回退路径构造的请求被回退目标拒绝;流式回退配置被与非流式路径不同的代码路径读取;或者即使配置中明确列出了回退,MidStreamFallbackError 仍显示为 Available Model Group Fallbacks=None。每一个都是正在被修复的代码级 bug;架构上的要点是,流式传输中途的故障转移本质上是两个模型各自带着对齐方式去完成同一个话语,其结果是一个策略为两者之和的混合体。
病态的情况是:第一个供应商发出了一个谨慎且有条件的开头——“我想在这里保持谨慎,让我解释一下我们已知的情况”——而第二个供应商从该预填充接手后,根据自己的立场决定拒绝,并在同一个句子中发出拒绝信息。用户读到的是:“我想在这里保持谨慎,让我解释一下我们已知的情况——实际上,我无法处理该请求。”这不是一个假设出来的破绽。当两个供应商都严格执行它们接受过的训练时,缝隙就是这个样子的。
“会话亲和性” 究竟为你带来了什么
第一阶段的修复方案是目前大多数生产级网关支持的功能,其名称包括粘性路由 (sticky routing)、会话亲和性 (session affinity) 或会话固定 (session pinning):一旦某个会话落到某个供应商上,就在该会话的剩余时间内将其固定在那里。Byteplus 的 API 网关文档中提到了一个显式的 LLM 会话亲和性路由原语。Kgateway 暴露了 会话持久化标头 (session-persistence headers)。Truefoundry 提供 30 分钟的供应商亲和性窗口。LiteLLM 为其 Responses API 合并了路由亲和性逻辑。vLLM 的 semantic-router 项目有一个正在跟进的 issue,名为“对话路由惯性” (Conversational Routing Momentum) —— 这是一种针对路由决策的低通滤波器,用于防止网关在复杂度峰值时切换模型。
亲和性同时起到了两个作用。首先,它将策略面限制在每个会话一个供应商,这意味着用户对“这个助手有一种特定的调性”的感知在对话中途不会被破坏。其次,它保留了供应商为该会话建立的任何前缀缓存 (prefix-cache) 状态,这在长上下文场景下是一个显著的延迟收益。这两个理由都很充分;但它们并非同一个理由,那些为了延迟收益而接入亲和性的团队,通常没有意识到他们也无意中恢复了策略的一致性。
但亲和性并非最终答案。它只能在一个会话内进行固定;对于新会话的第一条请求,它无能为力,请求仍会落在路由器当天偏好的任何供应商上。它是一种尽力而为的固定;如果被固定的供应商在下一轮对话中不可用,网关无论如何都必须进行故障转移,而你延迟了 12 轮的策略差异,现在会在第 13 轮出现。用户对连贯调性的感知在大多数会话中得以保留,但在那些经历过真实停机的会话中会被彻底粉碎 —— 也就是说,恰恰是在用户已经有理由感到沮丧的那些会话中。
亲和性也无法解决智能体 (agentic) 的情况。在这种情况下,单个面向用户的请求会扇出到多个模型调用中的数十个使用工具的子对话,其中任何子集都可以路由到不同的供应商。子智能体并不是亲和性意义上的“会话”。它们是一群小对话,其策略差异会在扇出过程中不断累积。
