从 Pi 窥见 Loop Engineering 的递归美学

最近一直在研究 Agent 引擎。工作里我们用的是 Eino,典型的 Graph 模式框架。用得越久,一个问题就越挥之不去:Graph 模式和 Claude Code 那种 Agent 模式,差距到底在哪?
Eino 和 Pi 相比的话,Eino 这类的 Graph 框架可以更容易的部署在服务端,同时供批量的用户进行使用。而 Pi 这种 Agent 模式的框架,都往往需要一套独立的执行环境,因此他们的运维成本会高很多。因此,Graph 模式的框架在生产环境里更容易落地。除此之外,它们还有什么差异呢?
光看文档看不出来。这次我干脆把 OpenClaw 背后的引擎 Pi 的源码 clone 下来,从头到尾读了一遍它的 agent-loop。读完之后,我找到了一个词来形容这套东西:递归美学。整个引擎只有一种原语——循环。一切复杂行为,都是循环套循环长出来的。
一、通往 Agent 的三个阶段
我认为,目前我们探索通往智能的道路上,经历了三个阶段。
阶段一:工作流模式。 典型代表 Dify、早期 LangChain。整体是线性的,开发者把步骤一步步串好,模型在每一步里干活。确定性强、灵活性差,本质是"带 LLM 节点的 pipeline"——路径是人预先画好的,模型没有决策权。
阶段二:Graph 模式。 典型代表 LangGraph、Eino。以 ReAct 为基础范式,把"思考 → 调工具 → 拿结果 → 再思考"表达成图上的节点和回边,具备了基础的 loop 能力。今天绝大多数生产环境里的 Agent 产品,都属于这一类。
阶段三:Agent 模式。 典型代表 Claude Code、Manus、Pi。模型拿着一套通用工具(read / bash / edit / write),在真实的执行环境里自己驱动循环。

能力和自由度最强的,一定是最后一档。但代价也很明显:Agent 模式必须配一个真实的代码执行环境,这样的 Agent 才有"做任何事"的可能性,硬件资源的消耗自然也最大。这就是为什么现在的 Agent 产品大多从 Graph 模式起步——对硬件的要求小一点,行为也更可控。
二、为什么选 Pi 来拆
选 Pi 的主要原因是:它的代码很简洁,但它的 Agent 能力又非常高效,很适合学习。
简洁体现在哪?默认工具只有 read、bash、edit、write 四个;系统提示词短到不足一千个 token;核心的循环引擎,认真读一个下午就能读完。
高效就更直观了。OpenClaw 火成那样,一周十几万 star,底下跑的就是 Pi 这套循环,几乎原样扛住了这波流量。作者 Mario Zechner 是写出 libGDX 游戏框架的人,今年 4 月他带着 Pi 加入了 Armin Ronacher 联合创立的 Earendil,仓库从 badlogic/pi-mono 迁到了 earendil-works/pi,核心保持 MIT 协议。
四个工具,撑起一个爆款产品。这件事本身就说明:Agent 的能力上限,看的是循环怎么设计,工具堆得再多也没用。
三、Pi 的分层:循环干净,脏活外移
整体来看,Pi 的架构分四层:
pi-ai
统一不同模型供应商的流式输出
pi-agent-core
agent-loop:真正的循环引擎
Agent:有状态封装,管理消息、工具、队列、事件订阅
pi-coding-agent
AgentSession:CLI/SDK/RPC 的产品层
负责会话保存、扩展、自动压缩、重试、系统提示词、工具注册
TUI / JSON / RPC
只是消费事件,不直接实现 agent loop

读源码时有个对比让我印象很深:核心的 agent-loop 就几百行,而产品层的 AgentSession,体量是它的四倍还多。
循环本身不复杂,把循环包装成能用的产品才复杂。 会话持久化、上下文自动压缩、扩展系统、模型切换、重试策略,这些脏活全部挡在 AgentSession 这一层,往下的 agent-loop 干干净净,只做一件事——驱动循环。
这种分层还带来一个直接的好处:TUI、JSON 输出、RPC 接口都只是事件的消费者。换一个 UI,循环引擎一行不用改。
四、一次 agent-loop 里会发生什么
对现代的 Agent 来说,从用户发出一条消息到拿到最终答案,中间可能发生很多次模型调用和工具执行。下面用 ZCode 的一次真实任务举例:

在这个场景里,模型有思考:

有命令行工具调用:

还有子 Agent 的调用,子 Agent 内部又有更多的命令行调用:

直到整理完所有信息后,输出一个最终答案,这才算一次完整的 agent-loop 走完:

注意子 Agent 那一步:子 Agent 内部跑的,还是同一个 agent-loop。循环里套循环,每一层都是完整的"模型决策 → 工具执行 → 结果回流"。开头说的递归美学,指的就是这个——只有一种原语,靠自嵌套长出复杂行为。
那这样一个过程,引擎侧要怎么组织?
五、拆解循环:agent run 与 turn 的两层结构
agent-loop 的整体目标,是把一次不可靠、不可预测的模型输出,变成一个可控的任务执行过程。
先看模型一次输出可能出现的情况:
- 它直接回答了用户
- 它要求调用工具
- 它调用完工具后还需要继续判断
- 工具失败了
- 用户中途又插了一句话
- 上下文太长,需要压缩
- UI 需要知道每一步发生了什么
所以 agent-loop 要解决的核心问题是:模型每次只决定"下一步",引擎要负责把这些"下一步"串成完整任务。
Pi 的答案是两层结构。
上层是 agent-run loop 它表示"用户这次请求整体还在处理",从 agent_start 开始,到 agent_end 结束。它回答一个问题:这次用户请求是否已经真正结束?
下层是 turn loop 一个 turn 是:一次 assistant 响应 + 它触发的工具调用 + 工具结果。从 turn_start 到 turn_end。一个 agent run 里可以有很多个 turn。
用一个修测试的任务串起来看:
用户:帮我修测试
↓
agent run 开始
turn 1:模型说要看错误日志
turn 2:模型看完日志,说要读源码
turn 3:模型读完源码,说要改文件
turn 4:模型改完文件,说要跑测试
turn 5:模型看到测试通过,给用户总结
agent run 结束
落到源码上,这两层结构就是 runLoop 函数里的两个嵌套 while 循环,逻辑提炼出来是这样:
外层循环(agent run 级):
内层循环(turn 级):
发出 turn_start
把排队的用户插话(steering 消息)注入上下文
请求模型,流式接收 assistant 响应
有工具调用 → 执行,结果写回上下文
发出 turn_end
模型不再要工具、也没有新插话 → 退出内层
// agent 打算停了,最后再看一眼
follow-up 队列里有排队消息 → 回到内层继续
没有 → 发出 agent_end,真正结束
两个队列的设计很有意思。Agent 类维护了 steeringQueue 和 followUpQueue 两个待处理消息队列:
- steering(转向):用户在任务执行中途插话。消息先排队,等当前 turn 结束,在下一次请求模型之前注入上下文。模型看到新指令,自然把方向转过来。
- follow-up(追问):用户在模型忙的时候发的后续消息。agent 本来要停了,退出前检查一下这个队列,有货就再跑一轮,省得用户干等一个新会话。
也就是说,中途插话靠的是在两个 turn 的缝隙里注入消息,全程不用打断任何东西。"用户随时可以说话"这个交互需求,最后落成了循环里一个普通的状态检查,没有任何特殊路径。
围绕这两层结构,循环对外发出一套稳定的事件流:agent_start、turn_start、message_start / message_update / message_end、tool_execution_start / update / end、turn_end、agent_end。UI 渲染、会话保存、扩展系统、自动压缩,全都挂在这些事件上工作,谁也不需要伸手进循环内部。
因此 Pi 的状态机模型就是:
用户输入
→ 模型决定下一步
→ Pi 执行现实动作
→ 结果回到模型
→ 模型重新决定下一步
→ 直到不需要动作

顺带说一个读源码时让我停下来的细节。当模型的输出被 token 上限截断时,Pi 会把这条消息里的所有工具调用整批标记为失败,一个都不执行。原因写在注释里:流式解析器会对截断的 JSON 参数做"抢救",抢救出来的参数能通过校验,但内容是残缺的——执行一个参数残缺的 write,可能直接毁掉用户的文件。fail 掉之后把错误信息喂回模型,让它重新发起调用就好。
工具执行抛异常也是同样的思路:循环不中断,把错误包装成结果喂回模型,让模型自己决定下一步。简洁不等于简单,这些边界处理才是一个 loop 引擎在生产环境里站得住的原因。
六、能从 Pi 学到什么
回到开头的问题:Graph 模式和 Agent 模式,差距到底在哪?
读完源码,我的答案是:Graph 把控制流写在图的结构里,Agent 模式把控制流交给模型,把确定性做进运行时。 Eino 和 LangGraph 里,每一条边都是开发者的预判;Pi 里没有边,只有一个循环加两个队列,模型说要干什么就干什么,引擎负责的是让这个过程可观测、可插话、可持久化、出错不崩。
具体到设计层面,有两条我准备直接搬回自己项目里。
第一,loop 可以设计成多层级的。
最早的 ReAct 基本是单层 loop 的结构,而 Pi 拆成了 run 和 turn 两级,子 Agent 再嵌套一层完整的 loop。分层的原因很实际:不同粒度的问题,处理时机不一样——插话要在 turn 之间处理,追问要在 run 结束前处理,压缩要在 turn 开始前处理。单层 loop 里,这些逻辑会搅成一团。
第二,每一层 loop 的职责要单一。
Pi 的两层分工非常干净:
- 第一层(agent run)只提供完整的任务边界。它回答:这次用户请求是否已经真正结束?
- 第二层(turn)只提供稳定的阶段事件。让 UI、会话保存、扩展系统、自动压缩都能围绕稳定事件工作。
一层管边界,一层管节奏。职责切干净了,复杂度就不会跨层扩散。
最后说点感受。这次读 Pi 的源码,比起学到什么新技术,更大的收获是把一个老道理又确认了一遍:方向对了,代码可以很少。
几百行的循环,四个工具,一个状态机,两个队列。没有编排 DSL,没有节点抽象,没有插件市场。但它把"不确定的模型输出"和"确定性的任务执行"之间的那条缝,缝得严严实实。
以上是我最近学习 Agent 引擎的一个小结,希望对你有启发。