跳到主要内容

多维 Agent 二分查找:当回归出现在交互中时

· 阅读需 12 分钟
Tian Pan
Software Engineer

质量在一夜之间下降了。值班工程师打开仪表盘,追踪了几个异常会话,并开始进行显而易见的二分定位:模型提供商在 UTC 时间 02:00 切换到了新的快照,于是将模型回退到固定的旧别名。评估套件仍然显示红色。回滚昨天的提示词更改。仍然是红色。将检索索引固定回上周的版本。仍然是红色。每个负责团队都在孤立地回滚自己的维度,并报告“不是我们的问题”。三个小时过去了,没有人负责诊断,因为没有人负责回归真正存在的交互面(interaction surface)——新模型以一种旧模型绝不会采取的方式,解释了新的工具描述。

这就是单轴工具无法解决的失败模式。git bisect 之所以有效,是因为搜索空间是一维的:提交记录的线性序列。而 Agent 没有单一的时间线。它有四到五个并行运行的时间线——模型快照、系统提示词、工具目录、检索索引、采样配置——每个都有自己的负责人、自己的部署节奏,以及自己的“回滚”按钮,只能将其自身的轴恢复到已知状态。你正在追踪的回归通常是一个双因素交互作用,沿着任何单一轴进行二分都会返回假阴性结果,因为该 bug 仅在“新模型遇上新工具描述”的交叉乘积单元格中触发。

这个故事的丧气版本是,团队最终通过偶然发现了这个单元格——通常是因为某种无关的原因同时回滚了两个轴,看到评估变绿,然后反向推理。而严谨的版本要求从第一天起就将 Agent 视为一个多维发布产物(release artifact),而不是一堆凑巧共享运行时的独立发布组件。

为什么单轴二分法会失效

现代 Agent 在任何给定时刻至少有四个版本化的层在运行,而大多数团队拥有的更多:

  • 模型快照(Model snapshot)——claude-opus-4-7gpt-5.5-turbo 别名背后的实际权重,提供商会按照客户无法控制的计划进行切换。
  • 系统提示词(System prompt)——由负责 Agent 行为的人员持续编辑,有时通过提示词注册表,有时通过应用仓库的提交。
  • 工具目录(Tool catalog)——Agent 可以调用的工具集,加上它们的描述、Schema,以及每个工具背后的实现。在 MCP 世界中,这包括上游 MCP 服务器在最新版本中决定发布的任何内容。
  • 检索索引(Retrieval index)——语料库、嵌入模型、分块策略、重排序器和索引版本。其中任何一个都可以按独立的节奏重新构建。
  • 采样配置(Sampling config)——温度(temperature)、top-p、最大 token 数、结构化输出 Schema、停止序列。看起来无伤大雅,但将温度从 0.7 调低到 0.3 会改变规划器选择的分支。

当回归不在这些层中的任何一层,而是在组合中时,单轴回滚会在每个轴上都给你一个干净的假阴性。模型团队回滚了模型:仍然是红色。平台团队回滚了提示词:仍然是红色。检索团队回滚了索引:仍然是红色。每个团队都正确地得出结论:“bug 不在我的轴上”。Bug 存在于两个轴之间的边缘——一个没有任何团队负责的地方。

这正是析因实验设计(factorial experimental design)中双因素交互作用的结构。将模型 A 更改为 B 的边缘效应很小。将工具描述 X 更改为 Y 的边缘效应也很小。同时进行这两项更改的交互效应却很大。对析因数据集进行方差分析(ANOVA)会立即显现这一点。而一系列单轴回滚永远不会,因为每一次回滚都将另一个已更改的轴固定在其新状态。

版本信封:每次 Agent 运行必须记录的内容

最小可行化的仪表化方案是:为每次 Agent 运行记录完整的版本信封(full version envelope)作为结构化字段。不是记录“Agent”——而是 Agent 执行瞬间命名的、固定的完整依赖闭包:

  • model.alias —— 调用者请求的内容(例如 claude-opus-4-7
  • model.snapshot —— 请求时别名解析到的内容(例如 claude-opus-4-7-20260415
  • prompt.commit —— 系统提示词源码的 SHA,或提示词注册表中的版本号
  • tool_catalog.manifest_hash —— Agent 看到的完整工具名称、描述和 Schema 的内容哈希
  • retrieval.index_version —— 提供检索调用的索引版本(理想情况下还应分别记录嵌入模型版本)
  • sampling.config_hash —— 温度、top-p、最大 token 数、response_format、停止序列

这里不可妥协的细节是模型快照(snapshot),而不是别名。像 claude-opus-4-7 这样的别名会根据提供商的计划更新,昨天 23:59 运行的会话和今天 00:01 运行的会话,可能在相同的别名字符串下命中不同的权重。在请求时将别名解析为快照并记录解析后的值,是让“回归在模型切换后开始”成为一个可证伪的断言而非假设的唯一方法。

一旦开始记录版本信封,两种新能力就成为可能。首先,你可以进行过滤——给我所有 tool_catalog.manifest_hash = X 且 model.snapshot = Y 的会话,并专门查看该单元格的质量。其次,你可以进行冻结——在排查期间将任何轴子集固定为已知值,保持除可疑轴之外的所有内容恒定。没有版本信封,这两者都不可能实现,因为你甚至不知道每个历史会话属于交叉乘积的哪个单元格。

对交叉乘积进行二分,而非针对时间线

一旦你有了包络(envelope),二分算法的形式就会发生变化。它不再是“沿着提交历史进行二分查找”,而是“沿着各轴的交叉乘积进行搜索,并使用评测集(eval suite)作为断言(predicate)”。

一个合理的方案:

  1. 定义你要搜索的轴(通常来自上述包络中的四到五个)。
  2. 为每个轴确定“最后已知良好”(last known good)的值和“当前”值。如果你做不到这一点,那你已经陷入麻烦了——这意味着缺少快照存储,请先解决这个问题。
  3. 在交叉乘积的所有 2k2^k 个顶点(如果 kk 很大,则取其子集)运行评测集。四个轴就是 16 个单元格;五个轴就是 32 个。运行成本确实存在,但它是有限的。
  4. 评测分数骤降的单元格就是交互单元格(interaction cells)。在红色单元格中同时发生数值变化的轴对,就是该交互作用(interaction)。
  5. 一旦交互作用被命名(例如“模型 B + 工具描述 Y 是坏单元格”),拥有其中任一轴的团队就可以进行修复——通过回滚其中一方,通过调整工具描述使其在新模型下没有歧义,或者通过添加一个评测用例来锁定该交互。

对于许多调查来说,完整的 2k2^k 扫描是过头的——通常从 trace 中就能明显看出单个可疑的交互,你只需要通过 2×2 矩阵来确认它。但重点在于搜索的形式:一个以评测为断言的交叉乘积,并以包络版本作为变动单位。单轴二分只是 k=1k = 1 时的退化情况。

经典的 git bisect run 形式完美适用于这种断言。“此构建是好是坏”的脚本变成了“在此包络顶点运行评测集;如果分数高于阈值则以 0 退出,否则以 1 退出”。LWN 在十多年前就记录了自动化 git bisect,这种规范可以无缝迁移——唯一的转变是搜索空间不再是提交记录的单链表。

归因预算(Blame Budget):让搜索空间变得可控

交叉乘积的增长速度极快。五个轴是 32 个单元格;六个轴是 64 个;七个轴是 128 个。如果你的团队正在轮换模型快照、提示词、工具描述、检索索引、采样配置,以及一个根据用户层级填充模板的系统消息——你将面临指数级的搜索空间和有限的评测预算。

使这一过程变得可控的规范是每个轴的归因预算(blame budget):即限制在两次连续评测运行之间允许偏移的轴的数量。具体来说,一条发布工程规则规定:“在预定的评测运行之间,变动的轴不得超过两个,且评测每 24 小时运行一次。”现在,值班工程师必须搜索的交叉乘积最多只有 22=42^2 = 4 个单元格,而不是 25=322^5 = 32 个。Bug 如果存在,在构建时就被限制在一个很小的范围内。

这是 Agent 领域的“小提交,频繁合并”——原因相同:二分法只有在变更单位足够小,使得搜索空间小到实际可搜索时才有效。如果一个团队在同一天发布了四个提示词修改、一次模型轮换、一个新的 MCP 服务器以及一个重新索引的检索语料库,他们不是在发布,而是在赌博。

推论是,命名的、固定的包络——而不是任何单个轴上的独立提交——才是正确的回滚单位。“回滚到包络 v47”意味着将所有四或五个轴恢复到该命名版本下的值。如果在不恢复与之配套测试的模型快照的情况下“回滚提示词”,就等于回滚到了一个从未经过联合评估的状态,而这本身可能就是一个新的坏单元格。

掌控交互面

组织层面的失效模式是最深层的。每个轴都有一个负责人。模型由平台团队固定;提示词由构建该功能的 Agent 团队负责;工具目录来自内部和第三方 MCP 服务器的混合;检索索引由搜索团队负责;采样配置由 Agent 团队的运行环境控制。没有人对交叉乘积负责,这意味着当交互单元格中出现回归时,诊断工作会掉入团队之间的真空地带。

解决办法是组织上的:指定一个人或一个团队负责包络,而不是任何特定的轴。该角色的工作是维护版本包络架构(schema)、运行定期的交叉乘积评测、把控归因预算规则,并在发生交互类回归时负责二分操作指南(runbook)。如果没有包络的单一负责人,在任何人开始查看交互作用之前,每一次回归都至少会经历一轮“不是我的轴”的推诿。

模型团队仍将负责模型轮换。Agent 团队仍将负责提示词。但必须有人负责耦合——即模型快照的怪癖、工具描述的歧义与检索重排序的分布偏移相遇的那个层面——因为这正是生产环境回归真正发生的地方,而且它是任何单轴仪表盘都无法察觉的。

这在实践中意味着什么

本季度要做三件事:

  1. 将版本包络添加到你的 trace 架构中。 每次 Agent 运行都要记录模型快照(解析后的确切版本,而非别名)、提示词提交、工具目录清单哈希、检索索引版本和采样配置哈希。如果你今天无法通过这些字段过滤会话,这就是首要解决的问题。

  2. 将包络作为回滚单位。 标记并存储命名的包络。“将包络 v47 推向生产环境”取代“部署此提示词更改”。回滚应恢复最后已知良好的联合状态,而不是恢复每个轴上最近发布的任何内容。

  3. 限制归因预算。 每个评测周期最多变动两个轴,绝无例外。如果需要更改三个轴,请运行两次评测,并在两个周期内发布更改。指数级的搜索空间是敌人;限制偏移量才能让二分搜索保持在“一个下午”的工作量,而不是“一个冲刺周期(sprint)”。

更深层的论点是,Agent 系统继承了传统分布式系统在二十年前解决的多版本噩梦——只不过现在的版本化组件包含了团队既没写过也读不懂的模型权重。那些能够可靠发布的团队,是像 SRE 对待第三方 API 一样对待这种不透明、轮换且外部拥有的依赖项的团队:固定它、版本化它、记录解析后的快照,并且永远不要让“我们一次性升级了所有东西”成为部署策略。

回归并不存在于任何单一层中。它存在于交互中——如果一个团队没有工具来搜索交互面,那么在下一次事故发生一小时后,他们将面对四个绿色的仪表盘和一个红色的评测,却毫无头绪。

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