Agent 的工具调用什么时候可以并行?如何处理依赖、写冲突和部分失败?
简化版
工具调用只有在输入已确定、没有结果依赖且副作用互不冲突时才适合并行。运行时应按依赖图调度就绪任务,并显式处理失败状态、并发上限和取消语义;模型一次给出多个调用不代表它们天然独立。
详细版
- 区分数据依赖、业务顺序和共享资源冲突,不能只看工具名称是否不同。
- 用依赖图表示前置条件,只有必要上游成功且输入校验通过的任务才能进入执行队列。
- 独立读取通常容易并行,写操作还需要业务幂等键、版本检查或资源级串行化。
- 每个调用保存唯一 ID、状态和结果,汇总时按 ID 对齐,不能按完成顺序猜对应关系。
- 部分失败要根据依赖决定阻断、降级或补偿;成功分支不应因为另一分支失败而被盲目重放。
- 限制并发和重试预算,测量关键路径、排队时间和失败率,避免把并行变成限流风暴。
完整版教学
一、工具不同不等于任务独立
考虑一个生成订单草稿的 Agent:先查询客户,再查询客户折扣,最后计算报价。 折扣查询依赖客户 ID,计算报价又依赖折扣结果,因此不能同时提交并期待自动形成正确输入。 与此同时,查询商品基础价格可能与客户查询独立,可以先并行读取。 判断独立性的对象应是具体调用的输入与业务资源,而不是工具的名字。
| 依赖类型 | 示例 | 调度要求 |
|---|---|---|
| 数据依赖 | 根据客户查询结果查折扣 | 等上游结果并校验输入 |
| 业务顺序 | 审核通过后提交订单 | 确认前置状态才执行 |
| 写冲突 | 两个调用更新同一份草稿 | 使用版本控制或串行化 |
| 无依赖读取 | 查询不同商品的公开参数 | 可在并发预算内执行 |
并行拆分的适用原则可参考 Anthropic 的 Agent 工作流说明。 即使两个请求都是只读,也可能要求一致快照,例如同一时间点的库存与预留量。 因此“可并行”与“结果可直接组合”仍是两个需要分别确认的条件。
二、把调用计划转换为依赖图
设 A 查询客户,B 查询基础价格,C 根据客户查折扣,D 生成报价草稿。 A 与 B 可以同时开始,C 必须等 A 成功,D 则必须拿到 B 和 C 的有效结果。 运行时可以维护每个节点的前置节点集合,只调度依赖已满足的节点。 模型提出的图还应检查引用是否存在、是否有环,以及参数占位能否解析。
A:查客户 -> C:查折扣 --+
+-> D:生成报价草稿
B:查价格 --------------+
ready(node) = 必要前置节点全部成功
且输入完整且通过校验
且资源与并发配额可用
若 C 失败,D 不能自动把缺失折扣当作零折扣。 是否允许使用原价报价必须由业务规则明确,并在结果中标注降级条件。 一个包含循环依赖的计划也不能直接交给队列等待,否则所有节点都可能一直不就绪。 应拒绝该计划或重新规划,而不是无限等待所谓“模型继续思考”。
三、并行速度由关键路径限制
假设 A 用 200 毫秒,B 用 500 毫秒,C 用 300 毫秒,D 用 100 毫秒。 全部串行需要 1100 毫秒,但依赖图允许 A 与 B 同时执行。 A 完成后开始 C,A+C 这条路径恰好也是 500 毫秒,之后 D 再用 100 毫秒。 因此理想完成时间是 600 毫秒,而不是把总时间简单除以调用数。
串行:200 + 500 + 300 + 100 = 1100 ms
并行:max(200 + 300, 500) + 100 = 600 ms
理想加速比:1100 / 600 ≈ 1.83
这个计算忽略了模型规划、网络排队和结果合并开销。 如果 B 的服务开始限流,B 可能延长为 900 毫秒,整个任务至少需要 1000 毫秒。 因此要测依赖链上的耗时和外部资源瓶颈,而不能只看本地同时启动了多少个 Promise。 同一服务并发上限过高时,增加排队与重试甚至会让总体速度更慢。
四、写操作需要额外的冲突控制
两个调用都读取草稿版本 7,一个改收货地址,另一个改商品数量。 如果都提交整份草稿且没有版本校验,后写入者可能覆盖先写入者的修改。 即使它们在语言上描述的是不同字段,也可能通过接口操作同一个资源。 可使用带版本条件的更新,或按资源键串行执行关键写入。
初始版本:7
调用 X:update(expected_version=7, address=新地址) -> 成功,版本 8
调用 Y:update(expected_version=7, quantity=3) -> 版本冲突
处理 Y:重读版本 8 -> 核查原修改意图 -> 重新构造更新
版本控制处理的是并发覆盖,幂等键处理的是同一业务动作重复提交,不能互相替代。 生成一次下单意图后,重试应复用该意图的幂等键,而不是每次重新生成一个键。 工具超时也不代表写入失败,应先查询原操作状态,避免把“结果未知”当成“可以再做一次”。 重读后的状态若改变了原审批条件,则还需要按业务规则重新确认执行资格。
五、部分失败和取消如何传给下游
并行任务应分别记录 pending、running、succeeded、failed、cancelled 或结果未知等状态。 一个分支失败时,先判断它是否是最终结果的必要依赖,再决定是否阻断其他分支。 例如可选图片搜索失败不一定阻断文字报告,但必要价格查询失败应阻止生成确定报价。 汇总结果必须保留缺失原因,不能把异常转换成空字符串后继续让模型猜。
call_id: price_lookup_01
status: failed
error_type: timeout
required_by: quote_draft_01
call_id: customer_lookup_01
status: succeeded
result_ref: customer_snapshot_42
调用 ID 应在调度前确定,响应按 ID 归档,因为完成顺序可能与发出顺序不同。 停止等待通常只表示客户端不再接收结果,不保证服务端已中止处理。 对已经发出的写操作,取消后仍要核对真实状态;对成功分支也不要因整体失败而重复执行。 只有明确完成状态、补偿能力和业务规则后,才能决定是否重试或补偿。
六、常见误区与追问
- 误区:模型一次返回多个工具调用就能全部并发。 运行时仍需验证参数依赖、业务顺序和资源冲突,模型输出不是调度正确性的证明。
- 追问:两个只读请求为什么可能不能随便合并? 它们可能读取不同版本的关联数据,应明确是否要求快照一致性或版本一致性。
- 误区:用了幂等键就不会发生并发覆盖。 幂等键防重复业务动作,版本检查或锁才处理不同动作争用同一资源的问题。
- 追问:一个分支失败后其他成功结果怎么办? 保存已确认结果,阻断必要下游;只重试可安全重试的失败分支,避免重放成功写入。
- 误区:取消请求意味着外部副作用已撤销。 客户端取消不保证服务端停止,必须核查操作状态,必要时执行受控补偿。
- 追问:并发数应该设多大? 结合服务配额、尾延迟、排队时间与失败率压测,并设置全局和单工具限制,不能按模型可生成多少调用来决定。
七、加强记忆
把并行工具调用记成“先画箭头,再开工”:箭头描述必须等待的结果和业务条件,没有箭头也要检查是否争用同一资源。速度看最长依赖路径,可靠性看每个调用的独立状态、幂等与版本控制。任务结束时核实外部动作是否真正完成,才能既缩短等待,又避免把局部失败放大成重复写入。
记忆钩子:输入不依赖、资源不冲突才并行;取消不等于撤销,失败不等于没执行。