我的判断很直接:能并行的一定要并行。用户不会因为你编排得优雅就多等十秒。但并行的前提是无依赖,而「这两个调用有没有依赖」交给模型自己判断,它经常判错。所以别问模型,在工具定义里写清楚——只有显式声明过无副作用的工具,才进并行候选池。
串行的代价是用户的耐心,不是你的 token
三个互不相干的搜索工具串着调,就是三轮 decode 加三轮网络往返。用户盯着转圈,心里已经在想「这玩意儿是不是卡了」。工具调用这件事上,模型有个惯性:一轮只发一个调用,哪怕它明明知道另外两个结果也需要。这不是能力问题,是训练数据里工具调用就长这样。
所以并行不该被当成优化项,它是默认该有的样子。真正需要论证的是「这两个为什么不能并行」。
但「无依赖」这件事,模型判不准
反面意见我一直觉得有道理。模型对自己干了什么的描述都不太可靠——那篇量化 overclaiming 的论文说的就是这件事,agent 会声称自己完成了实际没完成的任务。它对自身完成度的报告都靠不住,你凭什么信它说「这两个调用互相独立」?
真实场景里的依赖经常是隐式的。查用户资料和查订单列表,看着独立,但订单接口要 user_id,模型可能上一轮刚好拿到,也可能在编。它不会犹豫,它会自信地并行发出去,然后其中一个 400。
靠 schema,不靠 prompt
我的做法是在工具定义里加字段,side_effect_free、idempotent 之类。只有标了的才允许进并行批次,没标的一律串行,模型主动请求并行也拒绝。这是声明式,不是祈祷式——你没法用一段系统提示保证模型每次都判对依赖。
写操作并行,基本等于掷骰子
顺序就是语义。append 到日志、转账、发消息、git commit,这些并行之后,最终状态取决于谁先返回,两次跑出来不一样。这不是并发,是随机。
物理世界更狠。那篇做机械臂的 coding agent 论文把障碍物当成硬约束来评估 agent 写的控制器——推错一个箱子没有 ctrl+z。数字世界的写操作其实同理,只是代价小一点、暴露得慢一点。
还有一种更阴的:你以为只读的工具在写。ZCode 悄悄上传 git history就是个例子。工具定义里那句「只读」如果是错的,白名单就是漏的。所以声明得跟实现对齐,写工具的人要负责。
并行之后,调试会难一个量级
Chronicle 那篇讲 cut-point replay说得挺实在:LLM 推理不是逐位可复现的,工具读的是会变的状态,多步轨迹重跑几乎不会一样。并行把这三个问题各放大一点。所以要给并行结果一个确定的合并顺序——按工具名加参数 hash 排,不然同样输入两次跑出来的 trace 都对不上。
还有限流。一口气发八个请求撞上 rate limit,触发重试风暴,最后比串行还慢。并发上限是必须的,我现在设四到六,超了排队。
我现在的配置
默认并行池:搜索、读文件、读网页、查缓存。白名单外一律串行。并发上限四到六,结果确定性排序,trace 里标出哪些属于同一批并行。
Harnesstax 那个 harness 拆解提醒了一件事:harness 的每个组件单独看效果都可能不显著,组合起来差很多。并行策略也是这种组件,别指望换个 prompt 就解决。
也许过半年回头看,这套白名单会显得很土,直接让模型自己决定就够了。按我现在看,还不到时候。