跳到主要内容

工具调用顺序是偏序,而非集合

· 阅读需 12 分钟
Tian Pan
Software Engineer

“先创建后通知”的序列在开发阶段运行良好。而“先通知后创建”的序列则会为一个尚不存在的实体触发 webhook,导致消费者返回 404,接着你的团队会花上一周时间来调试这个看起来像是不稳定的集成测试。这种不稳定并非随机。它是确定性的,源于你的工具集拥有而你的规划器(planner)却不知晓的隐藏排序不变性。

这就是生产环境中 Agent 工具调用排序 bug 的常见形态:工具集在底层以偏序(partial order)方式组合——某些操作必须先于其他操作执行,而另一些则可以按任意顺序运行——但在规划器看来,它们只是一个无序的能力集合。模型选择了一个昨天行之有效的顺序。而明天,一次提示词修改、模型升级,甚至只是不同的 temperature 采样,都会选出另一个顺序。对于阅读追踪记录(trace)的人来说,这两种顺序看起来都很合理。但其中只有一个是正确的。

如果不声明顺序,团队交付的就是一个最终会被模型的提示词敏感性(prompt sensitivity)触发的 bug 隐患。

为什么顺序起初会发生漂移

LLM 的自回归特性意味着输入的小幅改变会产生非局部的输出变化。系统提示词中工具定义的顺序重排、新增的 few-shot 示例、系统消息措辞的清理——所有这些都会改变模型在特定轮次中优先选择哪个工具。模型并不是从一个稳定的分布中选择顺序;它是在对一个序列进行重新采样,而该序列的首个元素会强烈制约后续内容。

这在实践中表现为三个方面:

  • 在相同提示词的不同运行中:temperature > 0,甚至在模型版本变更时 temperature = 0,都会产生不同的排序。那些看起来“几乎总是 A→B→C”的调用,实际上在 92% 的情况下是 A→B→C,6% 是 B→A→C,2% 是 A→C→B。那 8% 的情况从未在评估(eval)中出现,因为评估只使用了三个固定的追踪记录。
  • 在提示词编辑中:一次为了“精简系统提示词”的清理,重排了注册表中的工具列表,这会改变规划器在整个层面的偏好顺序。这种差异看起来只是措辞调整,但行为变化会在一周后出现在生产环境中。
  • 在模型升级中:针对新模型版本使用相同的提示词会选出不同的顺序。发布说明可能会说“改进了工具调用能力”。对于你的特定工具集而言,这意味着排序分布发生了偏移,而长尾效应也随之改变。

最近关于 LLM 顺序敏感性的研究准确地衡量了这一点:打乱输入序列几乎总是会降低多步任务的准确率,其降低幅度取决于任务结构有多少是隐含在提示词顺序中,而非模型推理中。Few-shot 提示可以在一定程度上缓解这一问题,但无法根治。

将工具视为集合的陷阱

大多数团队在交付时持有的心理模型是:“规划器是智能的;工具是可交换的(commutative);规划器会搞清楚正确的顺序。”这两点都是错误的。

规划器并不具备你所需要的那种智能。 它是一个对常见模式具有强先验(priors)的下一个 Token 预测器。如果你的工具名称与公开数据集中的工具名称相似,规划器就会继承公开数据集的排序偏好——这与你的依赖结构毫无关系。像 internal_create_workspaceinternal_attach_member 这样具有自定义名称的工具,对模型来说比较陌生,因此先验较弱,方差也更高。

工具不是可交换的。 这是团队容易低估的部分,因为在他们考虑过的情况下,工具几乎是可交换的。不可交换性存在于他们未曾考虑的情况中:

  • 身份验证刷新工具和获取(fetch)工具在令牌(token)尚新鲜时是可交换的;在过期时则是不可交换的。
  • 创建资源工具和通知协作人员工具在个人工作区中是可交换的;但在共享工作区中则是不可交换的,因为通知需要渲染资源标题。
  • 搜索工具和写入工具在只读查询中是可交换的;但在搜索结果也用于构建 Agent 写入决策的上下文时,则是不可交换的。

每一个这样的偏序边缘都存在于团队成员的脑海中,而不是工具定义中。规划器无法访问团队的脑海。因此,原则必须是:在规划器消耗的产物中明确偏序关系,或者通过幂等性(idempotency)使工具真正可交换。

在工具定义处声明顺序

最干净的修复方法是扩展工具定义本身,加入 depends_on 声明,并由框架(harness)在结构上强制执行。基于图的框架设计通过将整个控制结构提升到静态 DAG 中进一步实现了这一点:每个节点的就绪集(ready-set)根据图计算得出,框架仅在工具的前提条件满足时才分发该工具,规划器的偏好顺序会根据声明的依赖关系进行重排或拒绝。LLM 编译器(LLM Compiler)模式也采用了同样的思路,在每个任务中包含 DEPENDS_ON: [node_id, ...] 字段,空列表表示“无前提条件——可立即调度”。

关键点并不在于你需要为每个 Agent 都配备一个完整的 DAG 调度器。关键在于依赖信息必须存在于框架读取的地方,而不是提示词建议的地方。两个核心属性:

  • 局部性(Locality):依赖关系在工具旁边声明,因此开发者在添加工具时可以查看并更新它。以系统提示词中的段落形式表达的依赖关系会立即且无声地发生偏移。
  • 强制执行(Enforcement):框架会拒绝(或重排)违反声明的计划,因此规划器无法将提示词诱发的 bug 交付到生产环境。表达为“系统提示词要求模型请先调用 A 再调用 B”的依赖关系只是一种建议,而不是不变性。

对于尚未准备好采用完整图框架的团队,有一种轻量级版本:在 Agent 定义阶段进行静态检查,读取工具注册表,构建隐含的依赖图,如果评估套件中由规划器生成的追踪记录出现了拓扑排序冲突,则 CI 失败。这能在 PR 阶段而非生产环境中捕获回归问题。


## 幂等性是另一半

声明式依赖处理顺序至关重要的情况。幂等性处理顺序不应产生影响的情况。这两者不是替代方案 —— 你两者都需要,而它们之间的界限就是设计决策。

[幂等智能体模式](https://www.buildmvpfast.com/blog/idempotent-ai-agent-retry-safe-patterns-production-workflow-2026) 将每次工具调用视为确定性的副作用:将 `(workflow_id, tool, args)` 哈希为幂等键(idempotency key),将结果存储在账本(ledger)中,在回放时返回缓存的结果而不是重新执行。[agent-ledger 库](https://news.ycombinator.com/item?id=46933954) 专门位于工具调用边界,以防止“重试时不同的推理导致双重副作用”的故障模式。

具体到排序,幂等性让你在说“这些工具是可交换的”时名副其实。如果 `create_workspace` 是幂等的,规划器(planner)可以调用它两次 —— 一次在 `attach_member` 之前,一次在之后 —— 而不会产生危害。如果 `send_email` 基于 `(template_id, recipient, idempotency_key)` 是幂等的,那么即使排序改变导致邮件发送早于预期,当规划器恢复并重试时,也不会导致重复发送。

陷阱在于只断言可交换性而不强制执行它。团队在文档注释中写道“此工具是幂等的”,但该工具在存储层实际上并非幂等(它仅在干净的数据库上幂等),而每季度发生一次的重新排序会产生一个重复行,需要花两天时间才能排查出来。原则是:幂等性是存储和副作用路径的属性,而不是工具签名的属性,且测试框架(harness)应将未声明的幂等性视为“非幂等”,而不是“可能没问题”。

## 进行对抗性评估,否则你无法捕获错误

基于“顺境路径”(happy-path)轨迹构建的评估套件会锁定生成这些轨迹的模型的排序偏差。它们无法捕获排序 bug,因为它们针对每个任务只采样一种排序。

行之有效的对抗模式:对于任何工具集包含两个以上有副作用工具的任务,构建一个具有相同任务描述且包含 **至少三种合理排序** 的评估案例 —— 两种正确,一种错误(这里的“错误”指违反了真实的偏序边界)。根据智能体无论选择哪种排列组合是否都能达到正确的最终状态来评分。两个正确的排序可以捕获智能体只知道一条路径的情况;错误的排序可以捕获智能体投机取巧,只会遵循提示词恰好建议的任何顺序的情况。

第二个原则:当你添加一个新工具时,评估套件应该对现在包含该新工具的现有任务运行一次排列模糊测试(permutation-fuzz pass),生成 N 个随机排序,并验证框架要么将它们重新排序为有效的拓扑结构,要么以明确的错误拒绝它们。这就是“团队添加了第六个工具,引入了新的排序依赖,导致 0.3% 的生产流量开始产生不一致状态”这种故障被捕获的地方。0.3% 是提示词敏感度分布的长尾,除非金丝雀发布(canary)专门采样对抗性排序,否则它不会出现在金丝雀发布中。

与准确率并列绘图的指标:**排序稳定性(ordering-stability)** —— 在生产环境温度下,给定相同任务的 N 个样本,有多少比例的运行选择了相同的顺序,以及有多少比例的 *不同* 排序达到了正确的最终状态?稳定的智能体在顺序上具有高度一致性。而 *鲁棒* 的智能体在顺序上一致性较低,但在结果上一致性很高。你想要的是鲁棒性。“稳定且脆弱”是最糟糕的象限:它在评估中看起来不错,但一旦模型升级导致分布偏移,它就会崩溃。

## 这在架构中如何体现

这一切背后的架构实现是:工具组合是一个偏序(partial order),而不是一个集合。集合将 `{A, B, C}` 视为可互换的;偏序则具有像 `A → C` 和 `B → C` 这样的边,同时保持 `A` 和 `B` 无序。大多数生产工具集都是团队从未记录下来的偏序。

具体来说,这意味着有四个设计决策值得明确制定,而不是顺其自然:

- **在工具层声明依赖。** 在工具定义中使用 `depends_on` 字段,由框架强制执行,并在注册构建时进行静态检查以确保图是一个 DAG(有向无环图)。
- **使可交换工具真正可交换。** 使用幂等键、内容寻址存储或数据库层的天然幂等性 —— 而不是文档注释断言。
- **评估偏序,而不仅仅是顺境路径。** 将对抗性排序作为一等公民的评估案例。在添加工具时进行排列模糊测试。
- **接受规划器的首选顺序是一个承重的人造产物。** 它是随着提示词或模型变化而偏移的分布中的一个采样。将其视为配置值:固定它、对其进行版本化,并在升级时重新基准化。

这样做的团队最终得到的智能体,其排序行为是“乏味”的 —— 框架强制执行必须为真的约束,规划器在这些约束内自由优化,模型升级会改变顺序但不会改变正确性。而不这样做的团队最终会花一周时间调试一个看起来不稳定的集成测试,最后发现这从头到尾都是一个隐藏的拓扑冲突。

Bug 的暴露面是真实存在的。模型的提示词敏感性最终会发现它。唯一的问题是你的评估是否能先发制人。
References:Let's stay in touch and Follow me for more thoughts and updates