Site logo
Published on

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

Authors
  • avatar
    Name
    Stone
    Twitter
本篇目录

上一课内容比较多,这一课先把那些类名放到一边。我们只研究一个问题:你只说了一句话,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,但下一轮没有把文件内容交给模型。你觉得问题出在哪?

想过以后,再点开答案

问题不在读取工具,而在结果回传这一步:程序拿到了文件,但后续模型上下文没有得到它。你需要检查工具结果是否被记录,以及是否被带入下一轮模型请求,而不是先怀疑“模型不会读代码”。

能解释清楚“读到文件”与“模型得知文件内容”为什么不是一回事,就抓住了这节课最重要的点。

参考源码

Footnotes

  1. Pi v0.84.1 · packages/agent/README.md ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  2. Pi v0.84.1 · packages/agent/src/agent-loop.ts ↩ ↩2 ↩3 ↩4 ↩5

  3. Pi v0.84.1 · packages/agent/src/agent.ts ↩ ↩2

  4. Pi v0.84.1 · packages/agent/src/types.ts ↩