Agentpath
推演6 / 31 节 · 预计 35 分钟

Pi 的 loop:事件流、中断与 steering

真实 harness 的循环比教学版复杂的地方,几乎都在「人要能插话」这件事上。

学完这节你能做到

  • 按步走完 Pi 的一轮循环,指出每一步的产物与失败表现
  • 解释 steering(Enter)与排队(Alt+Enter)在循环里的落点差异
  • 判断某个现象该去循环的哪一段找原因

你自己的循环里,一轮只有「请求 → 执行 → 回灌」三步。真实 harness 里是五步 —— 多出来的两步一个管上下文,一个管人和保险。

先把这一轮完整走一遍,然后挨个把环节打挂,看现象。

推演while (!done) { ... }
1 / 5
点环节可以把它「打挂」,看这一轮断在哪
assembleContext

组装上下文

系统提示词、AGENTS.md、工具声明、历史消息按固定顺序拼成一次请求。顺序不是随便定的 —— 稳定的内容放前面才能命中 prompt 缓存。

这一步之后:messages 数组就绪,输入 token 数已确定

五个环节各自的责任

环节谁的代码唯一职责
assembleContextharness按固定顺序拼请求,顺序稳定才能命中缓存
callModel提供方产出文本或工具调用请求
readStopReasonharness判断这一轮结束还是继续 —— 决策在代码,不在模型
runTools本机 / 沙箱执行并把结果(含报错)原样回灌
checkLimitsharness轮次、预算、超时、人是否插话

看这张表最有用的地方是责任归属:五个环节里有四个是你的代码。 所以「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 说「方向不对」。会发生什么?

这一节的结论

  1. 一轮五步,其中四步是你的代码 —— 责任在 harness 这边。
  2. 事件流是唯一的对外接口,UI / 日志 / 计费 / 评测都只订阅它。
  3. steering 打断当前工具之后的调用,但已完成的动作必须如实回灌。
  4. 记住那张「现象 → 环节」对照表,排障能省一半时间。

下一节讲工具设计 —— 五个环节里,runTools 是唯一你能大幅提升 agent 能力上限的地方。

延伸资料