跳到主要内容

跨区域 Prompt 版本偏差:你的 CDN 误运行了六小时的 A/B 测试

· 阅读需 12 分钟
Tian Pan
Software Engineer

你在 09:14 发布了一个系统提示词(system-prompt)变更。发布仪表盘在 09:31 变绿。到 11:00 时,你的评估追踪器依然显示正常,成本仪表盘也无异常,但一位客户成功工程师联系了团队:仅在亚太地区,解析端的结构化输出错误上升了约 3%。北美无异常。欧洲无异常。

发布在覆盖 67% 的区域时自动暂停了,因为某个 POP 节点上的一个非核心健康检查在切换期间发生了抖动,而当时没人注意到。在六个小时里,us-easteu-west 运行着提示词 v47,而 ap-southap-northeast 仍停留在 v46。你正在运行一个按地理位置划分的实时 A/B 测试——只不过这个测试不是你设计的,你看不到测试过程,而且那个本应捕捉质量回退的评估套件正巧连接到其中一个区域的新版本,然后若无其事地忽略了问题。

这种失败模式并不是单个工具的 bug。它是将提示词通过为不同类型的工件构建的部署系统进行推送时,所产生的可预见的后果。

提示词不是静态资产,但你的发布系统却这么认为

跨区域分发配置的默认机制——CDN 边缘配置、键值对配置服务、从对象存储读取的 sidecar 轮询器——是为那些接受最终一致性的资产设计的。一个 200ms 的窗口内,某个 POP 节点提供了一个旧的 Logo,这几乎是不可察觉的。一个 200ms 的窗口内,某个 POP 节点提供了一个旧的 CSS 文件,这只是一个缓存未命中。但在一个六个小时的窗口内,某个 POP 节点提供了一个旧的系统提示词,这会给你的产品带来行为分叉。

静态资产与消费它们的模型之间没有语义耦合。但提示词有。一个新的提示词版本通常需要配合解析器预期的新输出架构(schema)、智能体(agent)调用的新工具定义,以及下游过滤器依赖的新安全护栏。当 v47 在一个区域上线而 v46 留在另一个区域时,解析器在 us-east 看到的是一种架构,而在 ap-south 看到的又是另一种。成本仪表盘在不同区域看到不同的 Token 分布。评估套件则只会看到其测试脚本恰好指向的那个区域。

三个强化的特性使这种情况比等效的 CDN 资产偏差更糟糕:

  • 行为分叉是无声的。 模型依然返回 200 状态码。延迟依然在范围内。线路层面的信号完全无法体现“这个区域处于不同的提示词版本”。唯一的证据都在下游——解析失败、评估漂移,以及按地理位置聚集的诡异用户投诉。
  • 质量回退可能需要数小时才会显现。 聊天产品中 Token 级别的 A/B 差异是随着对话累积的,而不是单次请求。一个微妙地降低了多轮对话连贯性的提示词,会表现为次日留存率下降,而不是首日延迟告警。
  • 评估套件假设配置是统一的。 大多数评估脚本通过单一提示词与单一端点通信。它们无法检测到“用户在首尔看到的提示词,并不是我的评估系统在弗吉尼亚测试的那个”。

破坏每用户分配的延迟路由陷阱

这里是情况变得棘手的地方。现代 LLM 服务栈将区域发布与延迟感知路由结合在一起——任播 DNS、GSLB 或根据每个新 TCP 连接选择最近健康 POP 节点的入口(ingress)。路由层针对每个请求的往返时间进行优化,这对于降低延迟是正确的做法。

但对于提示词版本的稳定性来说,这是错误的做法。

在同一个会话中,单个用户发送第一条消息时可能连接到 us-east(握手时最近的健康 POP),十二秒后发送下一条消息时却连接到了 us-central,因为 us-east 的瞬时容量压力让 GSLB 觉得 us-central 更近。如果这两个区域处于提示词发布的不同阶段,该用户就在一次对话中体验了两个提示词版本。任何绑定到 v47 的对话历史现在正被 v46 解释。

user_id 切分回退的评估流水线会将此视为用户行为转变。用户“切换了话题”,用户“说话变得不那么正式”,用户“降低了追问深度”。团队在分析用户细分假设上耗费了两天时间,最后才有人注意到受影响的用户都在会话中途跨 POP 节点跳转了。

解决方法不是放弃延迟路由。解决方法是认识到,提示词版本(而不仅仅是网络距离)也是路由决策的一部分。在会话亲和性的语境下,即使 URL 完全相同,提供 v46 的 POP 也不能被视为与提供 v47 的 POP 是“同一个后端”。

像对待数据库迁移一样对待提示词,而不是静态配置

在这种压力下依然有效的规则,正是数据库社区在十年前为了 Schema 变更而达成共识的那一套——原因也相同。Schema 和提示词一样,与代码有着语义耦合。你不会对 Schema 迁移进行滚动发布,让一些应用服务器看到新列而另一些看不到。你会按顺序操作:Schema 在全球生效,然后代码向前滚动。

对于提示词,类似的规则包含三条。

一个版本全球生效后,再发布下一个版本。 这是 Schema 迁移中“先扩容再缩减(expand-then-contract)”的模拟。新提示词必须在每个区域都达到 100% 覆盖,控制器才会排队处理下一个变更。发布中途暂停是正常的;而病态的状态是 并发 的中途发布,即两个提示词版本同时在同一个集群中部分部署。

区域内原子切换,而非区域内滚动更新。 在一个区域内部,所有副本同时切换。这就是团队有时称之为“排空并切换(drain-and-cutover)”的关卡:处理中的流量按旧版本完成,然后整个区域的资源池瞬时迁移到新版本。另一种做法——逐渐替换 Pod,每个 Pod 独立获取新提示词——会在更小的规模上产生同样的偏移问题(区域内偏移而非区域间偏移),并且由于大多数监控仪表盘是按区域聚合的,这种问题更难被发现。

按区域逐个切换,并设有区域漂移熔断机制。 控制器按顺序推进区域:一个区域完成切换,经过观察期,检查该区域的评估增量与生产基线的对比,然后排队进入下一个区域。如果观察期检测到质量回退,发布停止,且已切换的区域共同回滚。发布的单位是“一个完整的区域”,而不是“全集群 1% 的机器”。

这比你现在的发布方式要慢。而这正是重点所在。数据库迁移比代码部署慢的原因并不是工程师懒惰,而是因为意识到“半套用”的 Schema 所带来的代价远高于多等一个小时的代价。

将 Prompt 版本作为一等可观测性维度

那六小时的偏差之所以未被发现,是因为没有任何仪表盘将其显现出来。成本图表按模型和区域进行了切分,但没有按 prompt 修订版本切分。评估追踪器显示了按主题排列的通过率,但没有注明每个 trace 是由哪个 prompt 版本生成的。SLO 面板报告了按服务分类的错误率,但无法询问:“在过去一小时内,ap-south 区域的错误中,有多大比例是在 v46 与 v47 版本下产生的?”

填补这一鸿沟的插桩(Instrumentation)手段其实很平凡。每一条 trace、每一个日志行、每一个指标标签都携带一个 prompt_version 字段,该字段从网关向下透传到模型调用、工具调用、解析器和评估打分器。一旦这个维度存在,三件事就变得可能:

  • 成本仪表盘可以按 prompt 版本进行拆分。 在某一版本下,某个区域的每个请求 token 数(tokens-per-request)出现激增是一个真实的信号,而不是噪声。如果没有这个维度,同样的数据看起来就像是区域性的流量异常。
  • 评估追踪器可以将评估运行与生产版本的分布关联起来。 如果过去一小时内 12% 的生产流量运行在 v46 上,但评估打分器只看到了 v47,仪表盘就会标明这一点。团队就会知道评估结果不具有代表性。
  • SLO 面板可以将漂移表达为告警。 “生产环境中存在多个活跃的 prompt 版本”是一个需要触发告警(Paged)的条件,而不是在“本周 Slack 频道”里的闲聊话题。检测它的系统与检测生产中多个模型版本的系统是同一个,大多数团队为了灰度发布(Canary)的安全已经搭建好了这套系统。

这所实现的更深层次的组织资产,是为那个在复盘中永远不该难以回答的问题提供了单一事实来源:对于这个用户,在这个会话中,在这个区域,在这个时间戳,他们看到的是哪个 prompt 修订版本? 当答案集中在一个地方,并按 trace ID 排序时,关于回滚的争论就结束了。当答案需要从三个半信半疑的日志流中重构时,回滚所花费的时间将超过事故本身的时间。

偏差带来的回滚难题

偏差使得回滚比原始部署更难。在干净的全局滚动发布中,回滚就是“将每个区域锁定在 v46”。而在部分发布中,回滚首先必须回答:哪些区域实际上接收了 v47? 如果发布控制器关于“谁接收了什么”的记录与运行时现实不符(由于抖动的健康检查、在切换中途重启并拉取了过期值的 pod、激进的 sidecar 缓存),回滚可能会在一个已经运行 v46 的区域锁定 v46 —— 而遗漏了那个悄悄漂移到新版本的区域。

缓解措施是将运行时(Runtime)—— 而非发布控制器的意图 —— 作为权威。控制器的职责是 提议 一个状态。运行时的职责是 报告 它实际拥有的状态。回滚查询的是运行时,而不是控制器。一个运行时报告“我正在运行 v47”的区域会被回滚。一个运行时报告“我正在运行 v46”的区域则保持不动,无论控制器认为它之前推行了什么。

同样的原则也适用于更令人头疼的情况:v47 prompt 本身没问题,但它与一个仅在两个区域上线的解析器(Parser)更新配套。现在,回滚目标取决于哪些区域也同步更新了解析器。如果没有运行时权威的版本追踪 —— 涵盖 prompt、解析器、工具定义以及模型本身这四个维度 —— 回滚就是一种猜测。有了它,回滚就是一个查询。

经得起考验的选择

那些没有在这些问题上踩坑的团队,都有一套共同的选择。他们拒绝将 prompt 视为配置的观点;他们将 prompt 视为碰巧是字符串的代码。他们将 prompt 的发布与模型和解析器的发布耦合在单个部署单元中,因此这四个维度要么一起移动,要么都不动。他们投入精力在区域切换纪律上,这在表面上看起来成本很高,但在部分发布本会产生六小时“幽灵 A/B 测试”时,它就体现了价值。他们将 prompt_version 作为每个仪表盘、每条日志、每个 trace、每个评估计分板中的顶级维度。

并且,他们对待“发布仪表盘显示绿色”的态度,就像数据库团队对待“迁移脚本退出状态码为 0”一样谨慎。脚本退出是成功的先决条件,但并不等同于成功。成功的标准是运行时 —— 跨越每个区域、每个副本、每个活跃会话 —— 对其运行的版本达成一致。

实现这一点的基础设施已经存在。其中大部分与你的团队已经为代码部署运行的基础设施相同,只是应用在了一类被社区长期视为“只是配置文件中的一个字符串”的产物上。它不只是一个字符串。它是你产品的行为,分布在全球集群中,你所使用的发布系统应该反映出这一点。

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