跳到主要内容

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

· 阅读需 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 调度器。关键在于依赖信息必须存在于框架读取的地方,而不是提示词建议的地方。两个核心属性:

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates