Pi Agent 学习笔记:一次提问背后的工具调用循环

hello 我是逆境不可逃

我最近在学习 PiAgent ok废话不说,开始

让 Pi"读取 package.json 并解释 workspace"时,模型不会自己打开本地文件。第一次请求通常只会返回一个 toolCall:调用什么工具、参数是什么。Pi 执行工具后,把结果包装成 ToolResult 放回上下文,再请求一次模型,最终得到基于真实文件内容的回答。

这就是 Pi Agent Loop 处理工具调用的基本过程。一次完整运行称为 Agent Run,其中可能包含多个 Turn,也可能请求模型多次。

为什么一次 Prompt 会调用模型两次

先看最常见的一条路径:

text 复制代码
用户输入 Prompt
  -> 模型返回 toolCall
  -> Pi 执行工具
  -> ToolResult 写入上下文
  -> 模型读取工具结果并生成最终回答

这里可以顺便区分两个概念:

  • Agent Run:启动或继续 Agent 后的一次完整运行,从运行开始到运行结束。
  • Turn:一次 Assistant 响应,以及紧随其后的工具调用和工具结果。

在没有追加消息和额外停止条件的情况下,如果模型第一轮直接回答,整个 Run 只有一个 Turn。如果第一轮要求读取文件,第二轮给出最终回答,整个 Run 就包含两个 Turn。

模型和工具在同一段循环中交替工作。每次拿到工具结果,模型都能据此决定下一步动作。

输入如何进入 Agent Loop

从终端输入到循环核心,主要经过四层:

text 复制代码
交互层:接收用户输入
  -> 会话层:整理输入,处理模型和会话配置
  -> Agent 状态层:维护消息和运行状态
  -> 循环层:请求模型、执行工具、判断是否继续

这种分层让每部分只承担一类职责。例如,把终端输入换成网页输入,模型如何请求工具、如何接收结果的规则仍然可以复用;修改聊天记录的保存方式,也不必改动工具循环。

核心循环做了什么

循环开始前,Pi 会把用户输入加入上下文。这里的"上下文"可以理解为下一次请求模型时携带的对话记录,包含用户消息、模型回复和工具结果。

先只看普通工具调用这条路径,省略追加消息和额外停止条件,流程可以写成下面的伪代码:

text 复制代码
把用户输入加入对话记录

重复执行:
    携带对话记录,请求模型
    把模型回复加入对话记录

    如果回复中没有工具调用:
        结束循环

    执行模型请求的工具
    把工具结果加入对话记录
    进入下一轮

这里必须保存两部分内容:模型发出的工具请求,以及工具返回的结果。下一次请求时,模型才能知道"之前准备做什么"和"实际发生了什么":

text 复制代码
用户:读取 package.json
模型:请求读取工具,路径为 package.json
工具:返回文件内容

模型拿到这段历史后,才有条件解释文件内容、修正参数,或者决定继续调用另一个工具。

Pi 还需要处理运行过程中到来的新消息,因此实际使用了两层循环:

  • 内层推进当前任务,同时处理 steering 消息,也就是运行中的补充指示,例如"先别修改文件,只分析原因"。
  • 外层在当前任务准备结束时检查 follow-up 消息,也就是排队等待后续处理的请求,例如"做完以后再解释测试结果"。

这也解释了为什么"模型没有请求工具"不一定意味着整个 Run 立刻结束:还要看有没有等待处理的新消息。

工具失败后为什么还能继续

执行工具前,Pi 要确认工具存在、参数合法,还会调用执行前后的钩子。钩子就是留给其他逻辑介入的检查或处理位置,例如在执行前阻止某次调用。处理完成后,结果会统一包装成 ToolResult

工具不存在、参数错误或执行抛出异常时,Pi 通常不会让整个 Agent Run 直接崩溃,而是生成一个带 isError=trueToolResult。这个结果同样会进入上下文,模型可以看到错误原因,再决定修改参数还是换一种做法。

例如,读取文件失败后,模型可能根据"文件不存在"的错误重新检查路径,再发起一次读取。错误信息因此也能成为下一步决策的依据,不过模型能否修正成功并没有保证。

isError 回答"工具执行是否失败",terminate 回答"工具执行之后是否停止循环"。Pi 只有在非空工具批次中的所有结果都要求终止时,才通过这个条件停止后续模型请求。

假设模型同时请求两个工具,只有一个结果要求终止,另一个没有要求终止,就不会触发这个批次终止条件。是否最终继续,还要检查其他停止条件。

事件流如何连接执行、界面和会话

Agent Loop 在运行时会发出几组事件:

text 复制代码
运行开始 / 运行结束
本轮开始 / 本轮结束
消息开始 / 消息更新 / 消息完成
工具开始 / 工具进度更新 / 工具结束

事件是一种通知:循环每推进一步,就告诉订阅者发生了什么。界面可以据此显示"工具正在执行",状态层可以记录哪些工具还没结束,会话层可以保存已经完成的消息。

以流式输出为例,模型可能先产生"你",再更新成"你好"。Pi 会暂存当前正在生成的消息,供界面持续刷新;收到消息完成事件后,才把完整回复加入正式历史。

如果每次更新都向历史中追加一条消息,下一轮模型就可能同时看到"你"和"你好"两条回复。把生成中的内容与已完成的消息分开,可以避免这类重复。

这种事件设计让循环层只负责报告进度。界面如何展示、消息如何保存,都由各自的订阅者处理。

用最小实验观察两轮请求

为了确认工具结果确实进入下一轮模型请求,本地实验用一个按预设返回结果的模拟模型,以及一个把输入原样返回的工具:

  1. 第一次模型调用固定返回工具请求;
  2. 工具执行后返回一条 ToolResult
  3. 第二次模型调用读取上下文并返回最终文本。

把实验记录按消息含义展开,结果如下:

text 复制代码
模型请求次数:2
第一次请求:用户消息
第二次请求:用户消息 -> 模型的工具请求 -> 工具结果
最终对话记录:用户消息 -> 模型的工具请求 -> 工具结果 -> 模型最终回答

事件顺序可以压缩成一行:

text 复制代码
运行开始 -> 第 1 轮(工具调用) -> 第 2 轮(最终回答) -> 运行结束

这次运行只有一个 Agent Run,却包含两个 Turn 和两次模型请求。第二次请求中出现 toolResult,说明工具输出已经成为模型可见的正式上下文,而不是只在本地执行完就丢弃。

这套设计的代价

工具回填让模型能够根据外部结果继续处理任务,但它也带来几项实际成本:

  • 一次 Prompt 可能触发多次模型请求,延迟和 Token 消耗随 Turn 数增加;
  • 循环必须有清晰的退出条件,否则模型可能在失败工具和重复计划之间来回尝试;
  • 并行工具调用可以减少等待时间,但两个工具同时修改同一个文件时,还需要处理冲突;
  • 异步事件监听器位于运行链路上,监听器过慢也会拖长整个 Run。

例如,同一个文件路径反复读取失败时,继续调用模型会消耗时间和费用,却不一定产生新信息。因此,错误反馈之外还需要停止机制,限制无效尝试。

总结

Pi 的一次工具调用闭环可以概括为:模型提出工具请求,运行时校验并执行工具,把结果写回上下文,然后再次请求模型。当没有新工具调用和待处理消息,或者命中停止条件时,运行结束。

理解这条链路后,几个容易混淆的问题也就清楚了:一次 Prompt 不等于一次模型请求;一个 Agent Run 可以包含多个 Turn;工具错误既可以反馈给模型继续处理,也可以通过独立的终止语义结束循环。

这套思路与具体编程语言无关。无论用 Java、Python 还是 TypeScript 实现,都需要回答同样的问题:谁执行模型提出的动作,结果怎样交回模型,失败后怎么办,以及整个过程什么时候结束。


相关推荐
落魄大学生之流水线上谋生计1 小时前
Java并发核心机制详解:线程池、CAS、AQS、锁升级
java·开发语言·数据库
aosky1 小时前
Linux 写内容到文件的几种方式
java·linux·服务器
Ztt6666666661 小时前
紫釉高筒壶向上收紧,修长器身更显精神
经验分享·笔记·其他·百度·微信公众平台
疯狂打码的少年1 小时前
【计算机网络】UDP协议与TCP/UDP对比(面向连接vs无连接)
笔记·tcp/ip·计算机网络·udp
Wang's Blog1 小时前
Java框架快速入门: Spring Security+OAuth2之邮件验证码发送(SMTP与API方式)
java·数据库·spring
巧克力味的桃子2 小时前
模拟大厂大模型方向面试题
笔记
砚底藏山河2 小时前
容错重试与指数退避:网络抖动手抖不再丢数据(魔码量化实战 #04)
java·数据库·python·金融
Peng7112 小时前
浙大数据可视化课程学习笔记
笔记·学习·信息可视化
今儿敲了吗3 小时前
02词云生成器
笔记·python