Site logo
Published on

Pi 源码解读(八):多个工具并发执行,异常如何处理?

Authors
  • avatar
    Name
    Stone
    Twitter
本篇目录

前几课的任务都很顺利:模型提出请求,工具成功执行,结果交回模型。但真实情况可能是:两个工具同时完成、参数只生成了一半、服务临时出错,或者你突然按下停止。

这一课不重新讲循环,而是看 Pi 在这些地方作了什么具体选择。

NOTE

工具可以并行,结果不能对错号;请求不完整,不能冒险执行;任务失败,也不能不分原因地重试。

源码基准:Pi v0.84.1。例子为教学推演,不是实测记录;Codex 对照单独标明来源。

一、两个工具能不能一起执行?

假设模型为了检查前端项目,在同一条响应里提出两个请求:A 读取 package.json,B 读取 src/App.vue。我们先假设两次读取相互独立,也没有其他操作正在修改它们。

等待 A 读完再读 B,不一定有必要。Pi 的默认并行模式会先按顺序完成这批请求的执行前检查,再并发执行通过检查的工具。检查和执行不是随意混在一起的。1

但它也不是无条件并行。executeToolCalls 会检查全局配置与每个工具的执行模式:全局要求顺序执行,或者这批请求中有工具声明必须顺序执行,整批就走顺序分支。2

这里没有自动证明业务依赖的魔法。比如“先修改文件,再测试修改后的结果”,本来就有先后关系,不能因为两个操作都叫工具,就认为可以一起跑。

补充:Pi 是否完全不考虑同一个文件被同时修改?

不是。内置 edit 的执行会进入 withFileMutationQueue,在同一文件的修改过程中排队。扩展文档也要求自定义文件修改工具接入同样的队列,避免多个操作基于同一旧内容写回,互相覆盖。34

不过,这种文件级协调不等于自动识别所有业务依赖,更不等于管理所有外部进程的文件写入。第一遍记住“运行层并发与资源层协调是两件事”即可。

二、B 先完成,结果会不会被当成 A 的?

每个请求都有自己的 toolCallId,结果带着相同编号,所以不能只靠返回顺序识别它属于谁。

Pi 还把两种顺序分开处理:实时工具结束事件,可以按完成顺序出现;最终的工具结果消息,仍按原请求顺序组织。5

例如模型先提出 A、再提出 B,但 B 更快:界面可以先看到“B 已处理完”,不用假装 A 已完成;等结果收齐后,后续模型使用的记录仍按 A、B 的顺序组织。

源码中的 executeToolCallsParallel 就体现了这个安排:执行与结束通知可以交错,汇总后再按原顺序产生工具结果消息。稳定的记录顺序,不代表工具实际按这个顺序执行。6

默认并行路径可以压缩成下面这段伪代码:

收到带工具请求的模型响应:
    若输出被长度限制截断:返回错误,不执行这些请求
    否则,如果允许并行:
        按顺序做执行前检查
        并发执行通过检查的工具
        哪个先处理完,先发哪个的结束事件
        收齐后,按原请求顺序生成结果消息
    否则:按顺序逐个检查、执行和记录

这里省略了取消与扩展结果修改,接下来解释第一条分支为什么重要。

三、参数能解析,为什么 Pi 仍然不执行?

假设模型正在生成一个修改请求,完整意图需要提供一段替换文本,但输出额度用完了,只传来了前半段。

危险点是:某些不完整输出经过尽力解析,仍可能得到一个结构上合法、内容却不完整的参数对象。 参数校验通过,不能证明模型把要写的内容全部传完了。

Pi 因此增加了一道判断:如果模型响应以 length 停止,而且包含工具请求,就调用 failToolCallsFromTruncatedMessage,将这条响应中的全部工具请求转为错误结果,不执行它们,并提示重新提出完整请求。哪怕某个请求看起来完整,也不会冒险单独放行。78

这是一个很具体的设计取舍:宁可多一次完整请求,也不根据可能缺失的参数修改环境。 后面是否继续调用模型,仍受运行中的停止策略控制。

别把这里与第五课混淆:这里是“模型输出被截断”;第五课讨论的则是“输入上下文太多”。处理位置和手段不同。

四、失败之后,先判断失败发生在哪一层

同样显示“失败”,后续动作不应该完全一样。

发生了什么Pi 中对应的主要处理
工具读取的路径不存在正常工具异常路径会生成错误工具结果,让模型知道原因,而不是无条件重复读取。
模型服务暂时限流或出现可重试故障会话层按配置决定是否延迟重试,并限制重试次数。
输入上下文超出模型容量不归入普通瞬时错误重试,而是进入上下文压缩相关恢复判断。

工具错误的包装在 Agent Loop;模型请求的重试分类与预算在 AgentSession。_prepareRetry 会检查是否启用重试、是否超过次数,并按指数增加等待间隔;_isRetryableError 则明确将上下文溢出排除在普通重试之外。91011

所以,“模型收到错误后换个方案”与“程序按照策略重发一次模型请求”是两种机制。 不应该统称为“Agent 失败了会自动重试”,更不能理解成所有工具都会被自动再执行一遍。

五、按下停止,能否保证文件没有变化?

不能这样保证。Pi 的 Agent.abort() 会通过取消控制器发出信号,工具执行接口也能收到这个信号;能否及时停下,还取决于执行方是否检查并响应它。1213

edit.ts 里有一个很好的源码例子:它会在异步文件操作之后再次检查取消信号,而不是一听到取消就提前释放文件修改队列,因为已经发起的文件操作可能仍会完成。14

由这个执行顺序可以推导:如果写入已经完成,紧接着检测到取消,最后报告了错误,也不能据此认定文件一定没变。后续处理应先确认当前文件状态,不要盲目再执行一次修改。

WARNING

报错不等于没有副作用,取消不等于回滚。

运行时能控制后续流程,但不能让已经发生的操作自动倒退。

对照 Codex:它把哪些执行问题放到一起?

Codex 的公开源码 tools/orchestrator.rs 将工具审批、沙箱选择、执行尝试,以及部分沙箱拒绝后的后续尝试组织在一个协调流程中。具体是否允许继续,仍受审批和执行策略约束,不是任何失败都自动换更大权限重跑。15

这适合用来对比“工具执行周围还需要哪些控制”,但不能反过来声称 Pi 也采用这套沙箱协调代码。本段来源是 Codex 官方仓库 main,查阅于 2026-09-25;与 Pi v0.84.1 是两个独立的观察对象。

六、回到源码和面试,只挑几个关键分支

需要定位时,再展开源码入口

agent-loop.ts 的 executeToolCalls、executeToolCallsParallel:看执行模式如何选择,结果如何排序。

同一文件的 failToolCallsFromTruncatedMessage:看参数可能被截断时为什么不执行。

agent-session.ts 的 _isRetryableError、_prepareRetry:看哪些错误可重试,次数和等待怎么控制。

edit.ts 的执行函数:看取消检查与文件修改队列之间的关系。

面试时,可以这样讲

我在 Pi 源码里重点看了几个异常分支。工具可以并行完成,但最终结果消息仍按请求顺序整理;模型输出因长度限制被截断时,Pi 会拒绝执行这条响应里的工具请求,避免参数看着合法却缺内容。工具错误与模型服务错误也分层处理,后者才按会话层策略作有限重试。另外,取消只是发出停止信号,不能保证已经发生的文件修改被回滚。

一个不用写代码的小练习

假设 A 修改文件、B 测试项目,两者被允许并行。最终记录按 A、B 排好了,能否证明测试针对的是修改后的代码?

想过之后再看答案

不能。记录顺序不等于执行依赖。测试可能在修改完成前已经读取了旧文件。有先后关系的操作,需要明确安排串行,或者等修改结果返回后,再发起测试。


这一课记住:看源码时,别只看成功路径;更要看不完整、失败、并发和取消分别被怎样处理。

参考源码

Footnotes

  1. Pi v0.84.1 · packages/coding-agent/docs/extensions.md ↩

  2. Pi v0.84.1 · packages/agent/src/agent-loop.ts,第 384–399 行 ↩

  3. Pi v0.84.1 · packages/coding-agent/src/core/tools/edit.ts,第 290–338 行 ↩

  4. Pi v0.84.1 · packages/coding-agent/docs/extensions.md ↩

  5. Pi v0.84.1 · packages/coding-agent/docs/extensions.md ↩

  6. Pi v0.84.1 · packages/agent/src/agent-loop.ts,第 457–519 行 ↩

  7. Pi v0.84.1 · packages/agent/src/agent-loop.ts,第 188–205 行 ↩

  8. Pi v0.84.1 · packages/agent/src/agent-loop.ts,第 348–380 行 ↩

  9. Pi v0.84.1 · packages/agent/src/agent-loop.ts,第 629–668 行 ↩

  10. Pi v0.84.1 · packages/coding-agent/src/core/agent-session.ts,第 2466–2474 行 ↩

  11. Pi v0.84.1 · packages/coding-agent/src/core/agent-session.ts,第 2505–2529 行 ↩

  12. Pi v0.84.1 · packages/agent/src/agent.ts,第 301–313 行 ↩

  13. Pi v0.84.1 · packages/agent/src/types.ts,第 365–371 行 ↩

  14. Pi v0.84.1 · packages/coding-agent/src/core/tools/edit.ts,第 290–326 行 ↩

  15. Codex 官方源码 · tools/orchestrator.rs(原笔记查阅于 2026-09-25) ↩