循环 —— Pi 怎么让模型转起来
30 秒版本: Agent 就是一个有教养的
while循环。调模型 → 它要工具就执行并把结果喂回去 → 再调模型 → 重复,直到它不再开口。Pi 的其他一切,都是这副骨架上的装饰。
为什么需要循环?
普通的大模型调用是一锤子买卖:问题进去,答案出来,结束。翻译、问答,这样够了。
但真实工作是迭代的。"修好这个失败的测试"意味着:读测试 → 读被测代码 → 形成假设 → 改代码 → 跑测试 → 看新的报错 → 再改。没有任何一个提示词能提前规划完所有这些步骤,因为每一步都取决于上一步发现了什么。
所以 Agent 把安排反了过来:不是你的代码决定步骤,而是模型决定——你的代码只负责让轮子转下去。
你的代码负责: 模型负责:
────────────── ──────────────
跑循环 决定下一步做什么
执行工具调用 决定用哪个工具、传什么参数
把结果喂回去 决定什么时候算干完了
这个分工就是所有 Agent 框架的全部哲学。Pi 的实现之所以让人眼前一亮,是因为它几乎没替你做别的决定。
一个"轮次"长什么样
先来两个 Pi 的词汇,因为它们承担了太多工作:
- Trace(一次运行):从你发出请求到 Agent 彻底停下,整个过程。
- Turn(一个轮次):一次模型调用,加上这次调用请求的所有工具执行。
看看你输入"读一下 config.json 并解释它"时发生了什么:
你按下回车
│
├── Turn 1
│ ├── 调模型 → "我需要读一下文件" + toolCall(read, "config.json")
│ ├── 执行工具 → 文件内容返回
│ └── (结果追加进对话)
│
├── Turn 2
│ ├── 调模型 → "这个配置设置了 X、Y、Z……"(这次没要工具)
│ └── 没东西可执行
│
└── 结束
两个 Turn,一个 Trace。节奏永远是这个:思考 → 行动 → 观察 → 再思考……
如果你想亲眼看到这些节拍:Pi 的核心为每一步都发事件——agent_start、turn_start、message_start、message_update(这就是你看到的文字一个字一个字蹦出来的原因)、tool_execution_start/update/end、turn_end、agent_end。你盯着的那个终端界面,只是这场事件广播的一个订阅者而已。事件的话题后面还会回来;现在只需记住:Pi 做的每件事都是可观察的,因为每件事都是一个事件。
最关键的问题:什么时候停?
让人意外的部分来了:模型从不会宣布"我干完了"。 它做不到——它是文本预测器,不是项目经理。
Pi 用的规则是人类定义的一条约定,简单得漂亮:
如果一次模型回复里没有任何工具调用,本轮结束。
就这样。"完成"是从沉默里推断出来的。模型要工具?继续转。模型只是说了段话?收工。
这也是为什么"模型失控不停调工具"是真实存在的故障模式,以及为什么 Pi 给了你逃生通道:
- 你手动打断——Ctrl+C 立刻中止(循环把中止和硬错误当作"立刻停,队列都不用查")。
shouldStopAfterTurn——Pi 自己用的安全阀钩子:每个轮次结束后可以问一句"上下文窗口快满了吗?是不是跑了两百轮了?",然后体面收场。- 工具也能说"够了"——工具结果可以设置
terminate: true。有个值得知道的细节:只有当这一批工具全部同意时循环才停。一个反对票就让循环继续。保守,但可预期。
"等等,它干活的时候我能说话吗?"
能——这是 Pi 最讨人喜欢的交互设计之一。两个队列,两种气质:
steer()——拍它肩膀。 Agent 干活到一半,你可以插一条消息,在下一个轮次边界切入。"对了,顺便看看 tests 目录。"它会在两个轮次之间落地,赶在下一次调模型之前。
followUp()——留个便条。 这个会等当前运行完全结束,再带着你的消息开启新一轮。"忙完之后跑一下 linter。"
在 SDK 里它们就是 session 上的两个方法:session.steer(text) 和 session.followUp(text)。在终端里,你只是边干活边打字而已。同一个机制,更友好的皮肤。
执行工具:并行,但有礼貌
模型经常一口气要好几个工具——"把这三个文件都读了"。Pi 本可以一个个跑,但那太慢,所以默认并行执行一批工具……外加三条礼仪规则:
- 预检串行。 参数校验和
beforeToolCall权限检查一个个来——因为检查可能拦截调用、可能碰共享状态,让这些东西赛跑是 bug 的温床。 - 执行并行。 真正的
execute()并发跑。省时间靠的就是这一段。 - 结果按序汇报。 哪个工具先完成哪个先喊(
tool_execution_end按完成顺序),但交回给模型的结果消息按模型请求的顺序排。模型对叙事顺序很挑剔,Pi 尊重它。
覆盖规则只有一行:工具可以声明 executionMode: "sequential",只要一批里有任何一个这么说,整批排队走。改文件这种活就该这样——没人希望两个编辑在同一个文件上赛跑。
错误不会炸掉循环——它们喂给循环
这里先预告一个工具章才展开的设计思想,因为它决定了循环的手感:
当工具失败——文件不存在、命令非零退出、网络抖动——Pi 不会抛个异常弄死整个运行。它把错误包装成一条正常的工具结果消息(标记 isError: true)交给模型。模型随后自己决定:重试、换条路、或者跟你解释出了什么问题。
一个能从自己的失败里活下来的循环,是"演示程序"和"你敢信任的工具"之间的区别。
循环,浓缩版
请求 ──▶ 调模型 ──▶ 有工具调用?──没有──▶ 结束(agent_end)
▲ │有
│ ▼
│ 执行工具(并行,有礼貌)
│ │
└──── 结果追加进对话
(steering 消息可在此切入;
followUp 之后重启循环)
十几行伪代码就能描述这副骨架。Pi 的真实实现加上了流式输出、钩子、事件发射、队列处理——但骨架从不变,而且你总能一眼看穿它。
带走一句话: 下次再被某个 Agent 框架的功能清单唬住,问那个真正重要的问题——它的循环是什么?谁来决定什么时候停? 在 Pi 这里,答案写得下一张明信片。这是故意的。
资料来源: pi-agent-core README——事件序列、钩子语义、执行模式,项目自己写得很清楚。