第 44 章:并行工具执行

"只读工具可以并行"——只读和并发安全不是同一个属性,而事故就住在这两者的缝里。


⏱️ 快速通道(5 分钟掌握核心)

  1. 并发安全性分批,不是按只读性——这是两个不同的问题
  2. 安全性属于这一次调用,不属于工具名字
  3. bash 是难点:分类命令字符串本身,遇到任何不认识的就 fail closed
  4. 任何 shell 元字符——包括换行——都取消资格
  5. 并发结果需要 tool_use_id 配对,否则 UI 会把输出挂到错误的调用上

10 分钟路径:44.1-44.3 → 44.5 → Kocoro OSS Lab


公开源码快照:实现细节取自 Kocoro 公开仓库 origin/main4ec6772 提交,复核日期为 2026-07-27。章节讲的是不变量;具体常量应以当前源码为准。

44.1 从一个多出来的词说起

模型在一轮里发出四个 bash 调用。四个都是读。而分类器实际是这么处理的:

bash("git log --oneline -10")              并发批次
bash("cat config.yaml")                    并发批次
bash("git stash list")                     并发批次
bash("git config --global core.editor")    串行,大小为 1 的批次

第四个确实是读。git config --global core.editor 不带值就是查询这个设置,什么都不改。那它为什么被拎出来了?

因为分类器证明不了它是读。git config 只要后面多接一个值就变成写——git config --global user.email me@example.com 和上面那条查询之间只差一个 token——而分类器只认显式的只读形式(--get--list 及其变体)。config 子命令下的其他一切,一律拒绝。

这是一个假阴性。它花了你一点延迟,仅此而已。现在反过来想:如果分类器对 config 家族大方一点,git config --global user.email x 就会和一个 git stash pop 一起溜进并发批次,两个 git 操作并发地改同一份共享状态。这个不对称就是全部设计。 错向「串行」,代价是几毫秒;错向「并行」,代价是你的仓库。

这底下是一个很少被拆开的问题:「只读」回答的是错的问题。只读问的是这会改变状态吗;并发安全问的是它能和它的兄弟们同时跑而互不干扰吗。这两者在两个方向上都会分叉:一个要拿排他锁的读是只读但不并发安全;两个写在完全不同资源上的操作不是只读,但一起跑毫无问题。

所以分发器应该按它真正关心的那个问题来分批。

44.2 并发安全是「这一次调用」的属性

公开 Runtime 用并发安全检查而不是只读标志来划分工具调用。没实现并发 Trait 的工具,回退到它的只读值——所以已有工具行为不变,给某一个工具加上这个 Trait,对其他任何工具都没有影响。

这个回退设计本身值得注意。一个新的分类维度应该默认落回旧行为,否则引入它就从「逐个工具的改进」变成了「全系统的风险」。

关键词是调用file_read 对任何参数都并发安全。bash 对某些参数字符串安全,对另一些是灾难。按名字的白名单表达不了这件事——这正是为什么这个检查要接收参数,而不只是工具名。

44.3 Bash,以及 fail closed 的纪律

给任意 shell 分类是难点,公开实现直接写明了自己的姿态:

It is intentionally conservative: any shell metacharacter, any unknown leading token, or any unrecognized subcommand pattern returns false.

三道过滤,必须全过:

没有 shell 元字符。 管道、重定向、&&、命令替换——以及很重要的,换行和回车。换行是命令分隔符,所以一个含换行的「单条命令」其实是两条命令,而第二条你从来没分类过。这种缺口只在模型把命令排版成多行时才现形。

已知的起始 token。 一份严格的只读白名单。git pushnpm installcurlrm——不在名单上的一律串行,没得商量。不认识就等于不安全。

认得出的子命令形状。git 这种多用途二进制,起始 token 不够。要跳过全局选项,把子命令对照安全集合检查,并且让「暗示输出或外部修改」的参数模式取消资格。

其余的一律进大小为 1 的批次。不是被拒绝——只是串行。这个区别很重要:保守的分类器从不阻塞工作,它只是拒绝把工作并行化。假阴性的代价是延迟,假阳性的代价是数据损坏。 按这个来调。

44.4 并发结果需要身份

并行一旦真的跑起来,立刻会咬人的是一个二阶问题。

串行执行时结果按调用顺序到达,UI 可以按位置配对。四个调用并发跑,它们完成顺序就乱了——git log 结束了,cat 还在跑。如果你的事件流只说「有个工具完成了」,UI 根本不知道该更新哪张卡片。

所以工具生命周期事件要携带 tool_use_id,客户端按它配对,而不是按到达顺序。Daemon 在握手时以能力令牌的形式声明这一点——这正是第 33 章那个模式:任何客户端必须能探测到的线协议变更,都要铸一个令牌,绝不靠嗅探版本号。

注意公开配置里「默认开启并发 bash」那条注释的时序:

Phase C: Desktop now consumes tool_use_id on tool_status events, safe to enable concurrent bash batches by default.

功能先做出来,客户端先学会按 id 配对,然后默认值才翻转。在消费方还不能归因结果时就开并行,产出的是一个「把正确数据显示在错误调用上」的 UI——那比不并行更糟,因为它是错的,而不只是慢。

44.5 快照证据

观察4ec6772 源码位置
按并发安全而非只读性分批partition.go L41
只读回退保持既有行为partition.go L28
分区与批次执行partition.go L51
保守的 bash 分类器bash_concurrency.go L61
git 子命令安全检查bash_concurrency.go L166
输出/修改类参数模式取消资格bash_concurrency.go L115
客户端消费 tool_use_id 之后才默认开启config.go L449

以上描述的是一个有日期的实现快照,不是普遍契约。

44.6 并行的代价

并发不是免费的,其中三项代价很容易被忽略。

审批顺序。 如果一个批次里两个调用都需要审批,用户会看到两个弹窗,而且顺序没有保证。审批应该在批次执行之前解决,不是在批次里面竞速。

结果体积。 四个并发工具可以同时返回四个大结果,第 36 章那个每轮聚合上限现在被撞穿的速度快了四倍。并行会加速上下文压力。

可调试性。 串行执行给你一条干净的因果轨迹。并发执行会把四个调用的日志交错在一起,而你要追的那个故障就藏在交错里。这才是「批次要小、分类要保守」的真正理由——不只是安全,还有事后能不能看懂发生了什么。

也说清楚本章没有主张什么。第 32 章描述的是更早的「准入后并行执行」模型;当前的分类粒度更细,且按调用判定。请把那一章当作它自己声明的历史快照来读。

44.7 常见的坑

拿只读当安全的代理。 这两个属性在两个方向上都会分叉。

按工具名分类。 bashcat 安全,跑 git push 不安全。名字层面的决策表达不了这件事。

忘了换行是元字符。 一条多行的「单命令」是多条命令,而你只检查了第一条。

整体放行一个多用途二进制。 git 不是一条命令。npmdockerkubectl 也都不是。

在结果身份就绪之前上并行。 UI 把输出挂到错误的调用上,用户会对整个界面失去信任。

以为测试通过就证明安全。 并发 bug 依赖时序;测试变绿只意味着这次没发生。保守分类才是真正的防线。

Kocoro OSS Lab(10 分钟上手)

  1. IsCommandConcurrencySafe,列出一条命令被拒绝的所有途径。数一数其中有多少是「我们不认识它」,多少是「我们知道它不安全」。
  2. gitSubcommandSafe,想想给 dockerkubectl 写一个同类函数需要覆盖什么。
  3. 拿你自己的分发器:它是按只读分批,还是按并发安全分批?如果是只读,去找一个「两个答案不一致」的工具——每个代码库都有一个。

划重点

  1. 按并发安全分批,不是按只读。 它们回答不同的问题,而且在两个方向上都会分叉。
  2. 安全性属于这一次调用。 同一个工具,有些参数安全,有些不安全。
  3. 不认识的一律 fail closed。 假阴性花的是延迟,假阳性花的是数据。
  4. 换行是元字符。 多行命令就是多条命令。
  5. 先上结果身份,再上并行。 没有 tool_use_id 配对,并发输出会落在错误的卡片上。

下一章:第 45 章把同一套预算纪律用到最贵的一类结果上——截图。

引用本文 / Cite
Zhang, Wayland (2026). 第 44 章:并行工具执行. In AI Agent 架构:从单体到企业级多智能体. https://waylandz.com/ai-agent-book/%E7%AC%AC44%E7%AB%A0-%E5%B9%B6%E8%A1%8C%E5%B7%A5%E5%85%B7%E6%89%A7%E8%A1%8C/
@incollection{zhang2026aiagent_44_-,
  author = {Zhang, Wayland},
  title = {第 44 章:并行工具执行},
  booktitle = {AI Agent 架构:从单体到企业级多智能体},
  year = {2026},
  url = {https://waylandz.com/ai-agent-book/%E7%AC%AC44%E7%AB%A0-%E5%B9%B6%E8%A1%8C%E5%B7%A5%E5%85%B7%E6%89%A7%E8%A1%8C/}
}