
并发工具调用
编写于 2026-08-16
审查这三个文件,再对比两份配置。这类任务里,Peri 经常在一个回合内并发执行多个工具。终端里的工具事件是一条条冒出来的,快工具先显示结束,与慢工具交错;同一个回合的会话记录里,固定的却是一套整齐的配对,一条 assistant 消息声明了这批调用,之后每条调用各对应一条结果,失败的也包含在内。
这个差异来自两种机制的分工,事件流实时配送,transcript 成组提交。
事件流与记录采用不同顺序
一个回合里模型发出多个工具调用,界面感知到的顺序由完成先后决定,哪个工具先结束,它的结束事件就先到。三个工具之间在事件层乱序是正常的——A 先发起、C 先完成都可能出现。而回合结束时 transcript 里的顺序是确定的,同批结果按声明顺序排列,不随完成先后变化。
第二种现象是文本先于记录。assistant 的文本块在工具执行前就带事件发射出去了,工具结果还没提交,文字已经显示出来。
第三种现象是失败也被记进去。某个工具报错、超时或被打断,该回合结束后仍能看到它对应的一条错误结果,而不是缺失。
以下演示示例,不是真实会话。
用户:审查 src/auth.rs、src/router.ts 和 src/db.ts,并对照 config 目录下的两份配置给出结论。
Peri 思考后一次发起三个 Read 工具调用: [事件] Read src/auth.rs → 先完成,结束事件先出现 [事件] Read src/router.ts → 后完成 [事件] Read src/db.ts → 报错(文件不存在)
会话记录(该回合结束后): assistant:审查这三个文件…(声明了三个工具调用) tool_result:src/auth.rs 内容 tool_result:src/router.ts 内容 tool_error:src/db.ts 不存在用身份与配对核对记录
核对一个回合的工具调用是否一致,看三点,
- 配对完整,每个声明过的调用都要有一条结果。有调用没有结果,才是真正的半套历史。
- 顺序确定,同批工具结果按声明顺序排列,不随完成先后变动。终端里交错、乱序都是预期,不是数据损坏。
- 身份可追,一次回合的文本块共享同一个
message_id,对应模型那轮输出的 assistant 消息;每条结果按tool_call_id配回对应调用。批内 id 必须唯一,空 id 或重复 id 不会触发猜测配对,而是把该调用直接结算成错误结果。
想核对这一轮实际发生了什么,以回合结束事件携带的 transcript 快照为准,别从事件流反推重建。快照是当时 transcript 的可见消息镜像,不会因为渲染缓存或事件乱序而失真。
两阶段提交会话记录
transcript 写入是两阶段的。Peri 先解析并审批整批调用,然后并发执行、把全部结果收齐,最后一次性提交,assistant 消息先进入暂存区,各条结果按声明顺序追加到同一个暂存批次,提交动作把它们成组移入主历史。收集期间主历史不加任何东西;暂存期间读取会话可见长度,也看不到这批消息。
并发执行期间,每个工具拿到的是同一份已提交可见消息 + 本轮 AI 消息的快照,而不是其他工具正在产生的半成品。一个工具的结果对同批另一个工具的输入不可见——这是隔离保证,也是协作约束,依赖前一个结果的任务需要显式分步,不能指望并发工具互相看到对方的结果。
事件的乱和记录的齐由身份字段衔接。工具事件带 tool_call_id,文本块带 message_id,客户端据此把交错的事件归位到正确的调用与消息上。子代理并行执行时是另一个来源,子代理有自己的事件总线,事件经转发器注入 source_agent_id 后路由回父代理,避免父子事件混淆(多代理场景见多代理编排)。
失败与中断仍然成组
失败也成对。工具未找到、参数非法、执行超时、运行报错,都会被转成带调用标识的错误结果,与成功结果一起提交。工具后处理钩子(after_tool)的错误只保留首个,其余工具照常结算;已经提交的结果不会被下游错误解释成这些工具从未运行。
中断成组。按 Esc 或主动终止时,正在运行的工具结算为 interrupted by user 错误结果,与已完成的结果一起提交,回合随后以 Interrupted 结束。下一轮模型读到的是一套完整配对,不需要从残缺历史猜测上一轮发生了什么。
暂存区有显式清理。新的 assistant 消息覆盖旧暂存时,旧配对随之丢弃;Rewind 截断会话、Compact 重建主历史时清空暂存,避免旧轮次数据落到新历史上。
用户侧不需要专门规避并发。多工具并行是正常路径,与其一次只让 Peri 跑一件工具,不如在存在先后依赖时明确说先完成 A,再处理 B。
限制与边界
这里的原子性只指进程内 transcript 的可见性,提交前对主历史不可见,提交后成组可见。它不等同于数据库事务——提交会异步触发批量落库,进程在已提交、未落库的窗口内崩溃,恢复边界由持久化层决定。
终端抢先显示的内容也不算已提交。模型下一轮读取的是提交后的 transcript,不是渲染缓存。
并发调用的量化收益这里没有实测数据,不估算加速比;需要数字的场合应以实际工作负载为准。
相关实现
继续阅读 理解 Agent Loop 了解回合如何收尾并回喂给模型,Hook 中间件统一入口 查看 before_tool、after_tool 的拦截点,中断长任务的恢复边界 处理中断后的恢复动作。