- Published on
Pi 源码解读(二):Agent Loop 怎样一步步完成任务?
- Authors

- Name
- Stone
本篇目录
上一课内容比较多,这一课先把那些类名放到一边。我们只研究一个问题:你只说了一句话,Pi 为什么能接着读文件、改文件,最后再回来回答你?
NOTE
先记住一句话:模型提出下一步,Pi 执行这一步,把结果交回模型,然后再决定下一步。
本课对照 Pi v0.84.1 的官方源码。例子是教学用的可能执行过程,不是实际运行记录;唯一一段代码是帮助理解的伪代码,不需要运行。
一、跟着一个改标题的任务走一遍
假设你在一个练习用的 Vue 项目里,对 Pi 说:
把 src/App.vue 里的页面标题从 Hello 改成你好,其他地方不要动。
为方便讲解,假设文件内容还没有提供给模型。先不考虑测试和额外检查,只看“读取、修改、回复”这条路径。
第一轮:不知道文件内容,所以先读
模型收到任务后,可以返回一个请求:读取 src/App.vue。
这个请求叫 Tool Call(工具调用请求)。它不是文件内容,也不是已经完成的操作。
Pi 收到请求,找到读取工具并执行。工具返回文件内容,比如其中确实存在标题 Hello。Pi 再把这个结果记进对话记录。这个结果叫 Tool Result(工具结果)。
这一轮结束时,发生的事情是:模型提出读取请求,程序读到了文件,但标题还没改。
第二轮:看到了文件,才提出修改
Pi 再次调用模型。这一次提供的材料里,多了刚刚读取到的内容。
模型可以基于实际内容提出:把标题里的 Hello 替换成你好。
Pi 执行修改工具,并把“修改成功”或具体错误记回对话记录。
注意,不是循环代码提前写好了“第二步必须改文件”。这一步是模型结合需求和上一轮结果提出的。
第三轮:看到执行结果,再告诉用户
如果修改工具返回成功,下一次模型调用就可以据此回答:
已把页面标题改为你好。没有运行测试。
在这条没有额外任务和停止策略的成功路径里,模型没有继续请求工具,本次运行就可以结束。
所以,这个例子是一句用户需求,三次模型调用,两次工具执行。 三次只是这里选择的执行路径,不是 Pi 固定要调用三次。官方事件说明也展示了“工具结果返回后,开启下一轮模型调用”的过程。1
把整个故事缩成一行就是:
模型请求读取 → Pi 返回文件内容 → 模型请求修改 → Pi 返回修改结果 → 模型回复用户。
二、Agent Loop,其实就这一小段思路
Loop 就是“循环”。它之所以要循环,是因为下一步往往取决于上一步的结果。
下面只表示普通主流程,暂时省略新消息排队和特殊停止策略:
记下用户需求
重复:
把当前记录交给模型,拿到这一轮输出
记下模型的输出
如果出错或需要停止:结束
如果模型请求工具:
检查请求,执行工具
把工具结果记入记录
继续下一轮
否则:结束
这段伪代码里,最重要的不是“重复”,而是 “把工具结果记入记录”。Pi 的循环确实会把工具结果加入上下文,让后续模型调用使用。2
你可以把这份记录想成一张工作便签:最开始只有“改标题”;后来补上“文件实际内容”;再后来补上“修改是否成功”。下一轮模型拿到的是更新后的便签,而不是每次都只看到最初那句话。
假如不记下工具结果呢? 模型就没有通过这条链路得知文件读到了什么、修改是否成功。程序在终端里打印了“成功”,不代表这个消息已经传给模型。
这也解释了一个区别:固定脚本是你提前规定每一步;这里的 Agent 循环是你规定执行规则,模型根据新的结果提出下一步。 它不是没有规则,而是任务的具体路径不必提前完全写死。
三、打开源码,只找三个地方
这节课不需要来回跳十几个文件。先打开这一份:
packages/agent/src/agent-loop.ts(v0.84.1)
在文件里搜索下面三个名字。把它们当路标,不用背。
| 源码中的名字 | 翻译成大白话 |
|---|---|
runLoop | 管流程:这一轮做完后,继续还是结束? |
streamAssistantResponse | 问模型:下一步要做什么?接收它的输出。 |
executeToolCalls | 办事情:处理模型提出的工具请求,拿到结果。 |
它们对应的关系就是:runLoop 调用“问模型”,需要动手时调用“办事情”,拿到结果后再决定是否继续。2
第一遍看源码,只要找到这三者如何连起来就够了。遇到复杂类型、回调参数或并发细节,先不展开;不要因为一个局部看不懂,就丢掉已经理解的主流程。
四、为什么屏幕已经有字了,它还没结束?
因为“正在输出”“一轮回答完成”“整个任务完成”是三件事。
在改标题的例子里,模型可能先输出“我先看看文件”,然后在同一条助手消息里提出读取请求。界面上出现这句话,并不表示已经完成修改。
所谓 流式输出,你可以先理解为:一次模型回答不是整段送来,而是分批送来。Pi 接收这些片段,同时发出更新事件,让界面逐步显示;收到这一轮完整回答后,才继续处理其中的工具请求。1
这里记住两个判断就够了:文字不断出现,不代表一直在发起新的模型请求;一轮模型回答结束,也不代表整个 Agent 任务结束。
五、做事途中,你改主意了怎么办?
还是改标题这个例子。Pi 正在处理任务时,你可能提出三种不同要求。
“标题改成欢迎回来,不要改成你好了”——调整方向
这类中途修正叫 Steering。可以把它理解成:“接下来按我的新要求办。”
在本课版本中,运行期间到达的这类消息会排队,在当前一轮的工具处理完后,被加入下一轮模型输入。它不是把一句话直接塞进已经发出的模型请求里。1
因此,中途改要求,不保证之前的修改还没发生。模型后续需要根据实际状态继续处理。
“标题改完后,再告诉我改了哪个文件”——追加任务
这叫 Follow-up,可以理解成:“手头的事做完,再做这件。”
Pi 在本来可以收工、没有工具调用和待处理 Steering 时,检查是否还有这类排队消息。有的话就把它加入上下文,再继续一轮。1
现在再看源码里的“两层循环”就不神秘了:里面那层处理当前任务的工具和插话;外面那层在准备收工时,再检查有没有排队的后续任务。 不是两个模型,也不是两个 Agent。2
“不改了,停止”——取消当前运行
取消对应 Abort。Pi 的 Agent 会通过取消控制器发出信号;模型调用和工具执行接口可以接收这个信号。34
但取消不是时间倒流。某个操作能否及时停下,取决于执行方是否响应取消;已经成功修改的文件,也不会因为发出停止信号就自动恢复。
一句话区分:Steering 是改方向,Follow-up 是排后续任务,Abort 是发出停止请求。
六、读文件失败了,整个任务就失败了吗?
不一定。
假设读取 src/App.vue 时,工具发现文件不存在。在 Pi 的正常工具错误处理路径里,这个错误会被转换成带有错误标记的工具结果,交给模型,而不是只在终端里打印一下。1
模型接下来可能请求查看目录,寻找正确文件;也可能询问用户路径。循环提供了“根据失败调整下一步”的机会,但不保证模型一定能解决问题。
这和模型服务本身出错也不一样:工具出错时,可以把错误作为信息继续交给模型;模型调用若以错误或取消状态结束,循环中有对应的退出分支。2
另外,Pi 也允许配置额外的停止条件。第一遍先不研究那些接口,只记住:正常结束、报错停止、主动取消,是不同情况。
WARNING
循环结束,不等于任务一定做对了。
例如工具返回“修改成功”,可以证明修改操作成功返回,但不能凭空证明页面显示正常、测试通过。没有做过的验证,不应该写进最终结论。
七、面试时,把这一段讲顺就够了
下面这段适合在你确实对照源码看过这条链路之后使用,不需要声称自己研究完了整个 Pi:
我主要看了 Pi 的 Agent Loop。它不是调用一次模型就结束,而是模型先提出工具请求,运行时执行后,把结果放回上下文,再调用模型决定下一步。比如改 Vue 页面的标题,可以先读文件,再根据实际内容修改,最后依据工具结果回复用户。源码里我主要跟了“控制循环、接收模型输出、执行工具”这三个环节;在这个基础上,它还处理了中途改要求、追加任务和取消。
面试官问“这不就是一个循环吗”,可以接一句:
最小思路确实是循环,但工程上还得处理什么时候继续、什么时候停止,以及工具失败、用户插话这些情况。难点不是写出 while,而是让这些情况都能被正确处理。
以上表达对应本课阅读的循环实现、Agent 控制接口和官方运行说明。231
最后,留一个不用写代码的小练习
Pi 已经成功读到了 App.vue,但下一轮没有把文件内容交给模型。你觉得问题出在哪?
想过以后,再点开答案
问题不在读取工具,而在结果回传这一步:程序拿到了文件,但后续模型上下文没有得到它。你需要检查工具结果是否被记录,以及是否被带入下一轮模型请求,而不是先怀疑“模型不会读代码”。
能解释清楚“读到文件”与“模型得知文件内容”为什么不是一回事,就抓住了这节课最重要的点。
