---
title: "并发工具调用"
description: "说明同一回合的多个工具如何并发执行并成组提交记录，以及事件乱序、失败、中断和持久化窗口的边界。"
---

> Documentation Index
> Fetch the complete documentation index at: https://konghayao.github.io/peri-cool/llms.txt
> Use this file to discover all available pages before exploring further.

# 并发工具调用

审查这三个文件，再对比两份配置。这类任务里，Peri 经常在**一个回合**内并发执行多个工具。终端里的工具事件是一条条冒出来的，快工具先显示结束，与慢工具交错；同一个回合的会话记录里，固定的却是一套整齐的配对，一条 assistant 消息声明了这批调用，之后每条调用各对应一条结果，失败的也包含在内。

这个差异来自两种机制的分工，事件流实时配送，transcript 成组提交。

## 事件流与记录采用不同顺序

一个回合里模型发出多个工具调用，界面感知到的顺序由**完成先后**决定，哪个工具先结束，它的结束事件就先到。三个工具之间在事件层乱序是正常的——A 先发起、C 先完成都可能出现。而回合结束时 transcript 里的顺序是**确定**的，同批结果按声明顺序排列，不随完成先后变化。

第二种现象是**文本先于记录**。assistant 的文本块在工具执行前就带事件发射出去了，工具结果还没提交，文字已经显示出来。

第三种现象是**失败也被记进去**。某个工具报错、超时或被打断，该回合结束后仍能看到它对应的一条错误结果，而不是缺失。

以下演示示例，不是真实会话。

```text
用户：审查 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` 后路由回父代理，避免父子事件混淆（多代理场景见[多代理编排](/peri-cool/posts/engineering/multi-agent/)）。

## 失败与中断仍然成组

**失败也成对**。工具未找到、参数非法、执行超时、运行报错，都会被转成带调用标识的错误结果，与成功结果一起提交。工具后处理钩子（after_tool）的错误只保留首个，其余工具照常结算；已经提交的结果不会被下游错误解释成这些工具从未运行。

**中断成组**。按 Esc 或主动终止时，正在运行的工具结算为 interrupted by user 错误结果，与已完成的结果一起提交，回合随后以 Interrupted 结束。下一轮模型读到的是一套完整配对，不需要从残缺历史猜测上一轮发生了什么。

**暂存区有显式清理**。新的 assistant 消息覆盖旧暂存时，旧配对随之丢弃；Rewind 截断会话、Compact 重建主历史时清空暂存，避免旧轮次数据落到新历史上。

用户侧不需要专门规避并发。多工具并行是正常路径，与其一次只让 Peri 跑一件工具，不如在存在先后依赖时明确说先完成 A，再处理 B。

## 限制与边界

这里的原子性只指**进程内 transcript 的可见性**，提交前对主历史不可见，提交后成组可见。它不等同于数据库事务——提交会异步触发批量落库，进程在已提交、未落库的窗口内崩溃，恢复边界由持久化层决定。

> **原子性是可见性，不是回滚**
>
> 这里讨论的原子只保证 transcript 的成组可见。工具写入的文件、发出的请求和已删除的数据，需要按各自系统的事实源核对。

终端抢先显示的内容也不算已提交。模型下一轮读取的是提交后的 transcript，不是渲染缓存。

并发调用的量化收益这里没有实测数据，不估算加速比；需要数字的场合应以实际工作负载为准。

## 相关实现

继续阅读 [理解 Agent Loop](/peri-cool/posts/engineering/agent-loop/) 了解回合如何收尾并回喂给模型，[Hook 中间件统一入口](/peri-cool/posts/engineering/hook-middleware-overview/) 查看 before_tool、after_tool 的拦截点，[中断长任务的恢复边界](/peri-cool/posts/engineering/rcra-interruption-recovery/) 处理中断后的恢复动作。

Source: https://konghayao.github.io/peri-cool/posts/engineering/concurrent-tool-calls/index.mdx
