Pi 的 loop:事件流、中断与 steering
真实 harness 的循环比教学版复杂的地方,几乎都在「人要能插话」这件事上。
学完这节你能做到
- 按步走完 Pi 的一轮循环,指出每一步的产物与失败表现
- 解释 steering(Enter)与排队(Alt+Enter)在循环里的落点差异
- 判断某个现象该去循环的哪一段找原因
你自己的循环里,一轮只有「请求 → 执行 → 回灌」三步。真实 harness 里是五步 —— 多出来的两步一个管上下文,一个管人和保险。
先把这一轮完整走一遍,然后挨个把环节打挂,看现象。
组装上下文
系统提示词、AGENTS.md、工具声明、历史消息按固定顺序拼成一次请求。顺序不是随便定的 —— 稳定的内容放前面才能命中 prompt 缓存。
五个环节各自的责任
| 环节 | 谁的代码 | 唯一职责 |
|---|---|---|
assembleContext | harness | 按固定顺序拼请求,顺序稳定才能命中缓存 |
callModel | 提供方 | 产出文本或工具调用请求 |
readStopReason | harness | 判断这一轮结束还是继续 —— 决策在代码,不在模型 |
runTools | 本机 / 沙箱 | 执行并把结果(含报错)原样回灌 |
checkLimits | harness | 轮次、预算、超时、人是否插话 |
看这张表最有用的地方是责任归属:五个环节里有四个是你的代码。 所以「agent 跑飞了」这句话,八成是在说 harness 写漏了什么,而不是模型不听话。
事件流:为什么界面只订阅事件
上面的推演是同步视角,真实实现是异步的。Pi(以及所有像样的 harness)把一轮循环拆成事件流:
turn_start
message_start
text_delta ×N ← 流式吐字
tool_use_start (read_file)
tool_progress ×N ← 长工具的进度
tool_use_end (ok / error)
usage {input, output, cache_read}
turn_end {stop_reason}
好处是 UI、日志、计费、评测全都只订阅这条流,谁都不需要知道循环内部长什么样。
终端 TUI、Web 前端、CI 里的 JSON 输出(pi --mode json)用的是同一条流 ——
这也是为什么 Pi 能有四种运行形态而核心只有一份。
--mode json 把这条流原样打出来。任何「它为什么这么干」的问题,
先把这段流拉出来看:调用序列、每次的用量、每个工具的返回长度。比读日志快得多。
steering:人插话落在哪一步
这是教学版循环和真实 harness 差别最大的地方。你的 while 里没有「人」这个角色 ——
它一旦跑起来,你只能 Ctrl+C。Pi 给了三档不同力度的介入:
| 操作 | 落点 | 用在 |
|---|---|---|
| Enter(steering) | 排队,等这一轮的工具调用全部跑完、下一次模型调用之前送达 | 「方向不对,改成这样」 |
| Alt+Enter(follow-up) | 等 agent 彻底停下(agent_settled)再送达 | 「顺手把那个也做了」 |
| Escape | 直接中止,排队的消息退回编辑器 | 「停,我重新想想」 |
关键区别在送达时机,不在「能不能取消」。steering 不会掐掉正在跑的工具批次 ——
那批工具的结果照样会回灌,只是模型看到结果之后紧接着还会看到你那句话,
于是下一轮就换了方向。真正的中止是 Escape,它对应循环里的 abort()。
送达节奏还能调:steeringMode / followUpMode 取 "all" 或 "one-at-a-time"(默认后者)——
一次插三句话,是三句一起进去,还是一轮吃一句。
写文件、跑命令这些动作没有撤销键。Escape 能做到的只是「不再往下做」, 以及「把已经做了什么如实留在会话里」。设计工具时就要考虑这一点 —— 这也是为什么大改动要拆成可随时停下的小步。
现象 → 该去哪一段找
把上面的推演器挨个打挂一遍,你会得到这张对照表。它比任何排障文档都好记:
| 现象 | 先看哪个环节 |
|---|---|
| 跑到第十几轮突然全线报错 | assembleContext —— 上下文超窗,压缩没生效 |
| 一直在重试,用量涨得很慢 | callModel —— 限流退避中 |
| 明明答完了还在继续 / 刚开始就收尾 | readStopReason —— 结束判定写错了 |
| 同一个工具反复调,参数几乎不变 | runTools —— 返回值里没有可行动的信息 |
| 半夜跑完发现账单爆了 | checkLimits —— 压根没有上限 |
agent 正在执行这一轮的 4 个工具调用,你在第 2 个跑完时按了 Enter 说「方向不对」。会发生什么?
这一节的结论
- 一轮五步,其中四步是你的代码 —— 责任在 harness 这边。
- 事件流是唯一的对外接口,UI / 日志 / 计费 / 评测都只订阅它。
- steering 打断当前工具之后的调用,但已完成的动作必须如实回灌。
- 记住那张「现象 → 环节」对照表,排障能省一半时间。
下一节讲工具设计 —— 五个环节里,runTools 是唯一你能大幅提升 agent 能力上限的地方。