跳到主要内容

提供商故障转移:在对话中途替换了你安全策略的隐忧

· 阅读需 12 分钟
Tian Pan
Software Engineer

用户正与你的助手进行一场关于受控物质处方模式的谨慎对话,已经进行了十二轮。模型表现得很有分寸,提出澄清性问题,引用指南,并拒绝进行文献之外的推演。在第十三轮,用户提出了一个后续问题,按理说应该得到与前十二轮相同的回应。然而,他们得到的却是一个生硬的拒绝:“我无法提供相关帮助。”对话结束了。他们怒气冲冲地给支持团队写信——他们并没有问任何不同的内容,助手刚才还在帮助他们,到底发生了什么变化。

你的日志解释了变化的原因。在第十三轮进行到一半时,你的主供应商在流式传输过程中返回了 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) 的情况。在这种情况下,单个面向用户的请求会扇出到多个模型调用中的数十个使用工具的子对话,其中任何子集都可以路由到不同的供应商。子智能体并不是亲和性意义上的“会话”。它们是一群小对话,其策略差异会在扇出过程中不断累积。

位于供应商之上的统一策略层

架构上的答案是安全工具厂商一直在定价中体现的那样:一个运行在网关中、位于两个供应商之上的统一策略层,它独立于执行该轮对话的模型,强制执行面向用户的契约。这一层的形态正在全行业范围内趋于一致。LiteLLM 相关的网关 Bifrost 集成了 AWS Bedrock Guardrails、Azure Content Safety 和 Patronus AI,将其作为可路由的护栏步骤。研究小组发布了统一的审核框架 —— Legilimens、StreamGuard、AEGIS —— 它们的明确动力在于“相同的提示词可能在 OpenAI API 中执行,但在 Google 的 GenAI 界面中被过滤或拒绝”,应用所有者需要一个统一的策略面,让两个供应商的输出都流经此处。

这一层所实施的约束是供应商本身无法提供的,因为他们谁都不能代表对方:单一的拒绝策略、单一的语气校准、单一的内容边界,在网关边界处应用。这样无论哪个供应商执行对话,面向用户的行为都是你的行为。供应商变成了生成器;策略变成了你的。笛卡尔积折叠回了一行。

这与成熟平台在十年前针对身份认证所做的架构迁移如出一辙。身份验证曾经是特定服务恰好集成的任何 IdP;用户的体验因处理其会话的后端而异。成熟的平台将身份验证提升到了网关层,这样无论运行哪个后端,用户的身份都是网关的身份,是标准化后的。安全校准现在也处于同样的进化阶段。供应商各有一套身份模型;网关必须主张用户的那一套。

两个操作原语让这一切在实践中行之有效。第一个是安全回归评估 (safety-regression eval),在任何新供应商加入池之前,对故障转移池中的每个供应商运行一组固定的针对拒绝、语气和内容边界的探测。如果新供应商的校准超出了容差范围,则它不具备入池资格。评估是准入条件;为了可用性收益而增加供应商是一个安全审查过程,而不是一个 SRE 工单。第二个是针对每个供应商的拒绝率的可观测性,按查询类别和会话亲和性状态细分,并对变化率进行告警。当某天供应商的基础模型更新导致其拒绝率偏移 5 个点时,你会从仪表盘上发现,而不是通过工单投诉。

架构层面的领悟

将故障转移层设计为可用性决策的团队是基于正确的假设进行推理的:供应商确实会宕机,用户确实需要响应,能够吸收单点供应商故障的网关确实很有价值。但他们没有审视的假设是:LLM 并不是一个无状态函数。它是一种经过微调的姿态,具有拒绝面和语气校准,这些是每个供应商签署的特定契约的属性。多供应商路由将这些属性变成了笛卡尔积,而没有对该乘积进行建模的团队无论如何都要承担其全部后果。

前进的道路是平台层在过去 18 个月中一直在悄悄趋同的道路:固定会话,对资源池进行回归测试,并将策略契约提升到网关层,使其不再是每个供应商特有的属性。供应商将继续调整他们自己的阈值;但用户不会将这些调整视为你产品的瑕疵。故障转移决策将不再是无意间的策略决策,而是开始回归其本意 —— 在稳定的契约背后进行生成器的更换。

将多供应商路由定性为可靠性投资的团队,并未意识到他们同时也在进行一项信任与安全投资。要么他们现在就为统一策略层付费,要么每当错误的对话落在错误的模型上时,支持工单就会不断涌现 —— 而事后复盘报告却一直在责怪用户察觉到了这些差异。

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