作者:互联网 时间: 2026-08-24 09:57:55
处理Pi Agent Loop 源码解析:Context、Streaming、Tool Calling、Steering 与停止条件这类问题时,先确认目标场景,再按步骤核对配置或玩法细节。
Pi 的 Agent Loop 不只是一个 while 循环。它需要处理 AgentMessage 与 LLM Message 转换、流式 AssistantMessage、Tool 参数验证、串并行执行、Steering、Follow-up、Abort、错误结果和生命周期事件。这篇内容以 v0.82.1 固定源码重建一次用户输入的完整运行链。
v0.82.1(短提交 b4f2936,发布日期 2026-07-25)。低。高风险文章发布前必须再次核对官方来源。PASS-RUNTIME,不得理解为已在真实 Pi Runtime 中执行通过。系列编号:PI-05。
前置文章:PI-04。
核心问题:
预计阅读时间:35—45 分钟。
研究基线:
earendil-works/pi;v0.82.1;b4f2936;packages/agent/src/agent.ts 与 packages/agent/src/agent-loop.ts;固定源码:
agent.ts:https://github.com/earendil-works/pi/blob/v0.82.1/packages/agent/src/agent.tsagent-loop.ts:https://github.com/earendil-works/pi/blob/v0.82.1/packages/agent/src/agent-loop.tshttps://github.com/earendil-works/pi/blob/v0.82.1/packages/agent/README.md最简单的 Tool Calling 示例通常写成:
while (true) {const response = awaitcallModel(messages);if (response.toolCalls.length === 0) break;const results = awaitexecuteTools(response.toolCalls);messages.push(response, ...results);}
这段代码只展示了骨架。
真实 Runtime 必须解决:
Pi 的实现可以看成两个协作层:
Agent 类(状态、队列、订阅、生命周期)├─ agent-loop(模型流与工具循环)│├─ pi-ai(模型流式协议)│└─ Tool Runtime└─ Application / UI
Agent 类管理长期状态和调用入口;agent-loop 管理一次执行过程的细粒度循环。
理解 Agent Loop 前,需要先区分不同层的消息。
表示用户当前任务或后续方向。
模型生成的消息,可以包含:
工具执行后返回给模型的观察结果。
应用可以定义模型原生协议不认识的消息,例如:
因此,Pi 不直接把整个应用状态当成模型消息。
它使用以下边界:
AgentMessage[]→ transformContext()→ AgentMessage[]→ convertToLlm()→ Message[]→ pi-ai
transformContext() 适合:
convertToLlm() 适合:
这一边界保证:
固定版本的 Agent 类维护一组可变状态,核心包括:
它还持有:
这说明 Agent 不是无状态函数。
它更像一个受控状态机:
Idle├─ prompt() → Streaming│├─ assistant tool calls → ExecutingTools → next model turn → Streaming│├─ final response → Idle│└─ abort() → Aborting → Idle└─ ExecutingTools ├─ no queued work / aborted / error → Idle └─ abort() → Aborting → Idle
真实实现没有必要严格使用这个枚举,但行为上存在这些状态边界。
prompt() 是外部应用最容易接触的入口。
它首先要防止同一个 Agent 同时启动两个独立主循环。
如果 Agent 正在运行,新的输入不应该再次调用 prompt()。Pi 会要求调用者使用:
steer():改变当前执行方向;followUp():排到当前任务之后。这避免两条主循环同时修改同一个 messages 和工具环境。
一次新的 prompt() 可以概括为:
检查 Agent 是否空闲→ 标准化用户输入→ 写入 Agent Message→ 创建 AbortController→ 生成上下文快照与 Loop Config→ 启动 Agent Loop→ 消费事件并更新 State→ 等待结束
Agent Loop 启动时会发出:
agent_startturn_startmessage_start(user)message_end(user)
随后才进入模型调用。
事件顺序不是装饰。应用可能在事件上执行:
Agent 在运行时允许外部修改某些配置,例如模型、工具或 System Prompt。
如果模型调用过程中直接读取一组不断变化的引用,会产生不稳定行为:
Pi 会在启动 Loop 时建立 Context Snapshot,并通过配置与后续准备函数控制何时允许模型、Thinking 或 Context 发生变化。
这是一项通用 Runtime 原则:
在 Agent 系统里,这个边界通常是 Turn。
runLoop 的核心不是一个单层 while。
它需要区分两种“继续”:
可以重建成下面的伪代码:
emit("agent_start");while (true) {// 外层:Follow-up 生命周期while (true) {// 内层:Tool + Steering 生命周期const assistant = awaitstreamAssistant(context);append(assistant);if (assistant.error || assistant.aborted) break;const calls = extractToolCalls(assistant);const results = awaitexecuteTools(calls);append(results);emit("turn_end");context = awaitprepareNextTurn(context);const steering = drainSteeringQueue();append(steering);if (calls.length === 0 && steering.length === 0) break;}const followUps = drainFollowUpQueue();if (followUps.length === 0) break;append(followUps);}emit("agent_end");
这不是源码逐字复制,而是按固定版本控制流整理的结构。
内层循环处理“同一个任务还没结束”。
外层循环处理“当前任务结束后还有下一项输入”。
每次调用模型前,streamAssistantResponse 会完成以下工作:
当前 Agent Context→ transformContext→ convertToLlm→ 组装 systemPrompt + messages + tools→ 解析 API Key→ 调用 stream function
这一步才真正从 Agent 世界进入 LLM 世界。
Agent Messages→ transformContext→ convertToLlm→ LLM Context→ Model Provider Stream
为什么每一轮都执行转换,而不是只在 Session 创建时执行?
因为上下文可能随执行变化:
Context 是运行时产物,不只是静态 Prompt。
模型响应通常通过 Event Stream 增量返回。
Pi 不会等待完整响应后一次性创建 AssistantMessage,而是维护一个 Partial Message:
message_start→ message_update(text delta)→ message_update(thinking delta)→ message_update(tool call delta)→ message_end
这样 UI 可以实时展示文字、Reasoning 和 Tool Call 参数生成过程。
但这也带来状态一致性问题。
Agent 必须区分:
如果请求被 Abort,部分内容也可能有价值:
因此,错误与中断不应该简单抛弃全部流式状态。
统一的 AssistantMessage 会包含 Stop Reason,例如:
stop;length;toolUse;error;aborted。其中最危险的情况之一是 length。
模型可能在生成 Tool Call JSON 时达到输出上限,留下一个语法上勉强可恢复、语义上却不完整的调用。例如:
{"path":"src/auth.ts","oldText":"...","newText":"尚未生成完整
即便 Partial JSON Parser 能把它修成结构,执行也可能破坏文件。
Pi 在输出因长度终止时,不会继续执行这些可能被截断的 Tool Call,而是把它们失败化处理。
这是一个重要安全原则:
一个 Tool Call 从模型输出到 Tool Result,需要经过多个阶段。
1. Assistant Tool Call2. 查找 Tool3. 准备与规范化参数4. Schema Validation5. beforeToolCall Hook ├─ 阻止 → Error Tool Result └─ 允许 → Tool.execute6. Progress Updates7. Raw Result / Error8. afterToolCall Hook9. ToolResultMessage10. 加入 Agent Context
模型可能调用不存在的工具。
Runtime 不能崩溃,而应生成模型能够理解的错误结果,让模型有机会修正。
工具可以拥有参数预处理逻辑,例如:
参数必须经过 Tool Schema 验证。
这可以阻止:
但 Schema 不能判断所有语义风险。例如,一个字符串路径类型正确,却可能指向不应访问的位置。
Hook 可以:
Tool 接收参数、Abort Signal 与 Update Callback。
长工具可以持续发送:
Runtime 将其转成 tool_execution_update 事件,而不是立即当成最终 Tool Result 送回模型。
工具异常会被转换成 isError: true 的 Tool Result,而不是直接让整个 Agent Loop 丢失上下文。
Hook 可以改写:
terminate。固定版本只有当同一批次每一个最终 Tool Result 都设置 terminate=true 时,才跳过该批工具之后的自动模型调用;它不会直接结束整个 Agent Run,之后仍会检查 Steering 与 Follow-up。最终结果带着对应 Tool Call ID 回到消息历史,模型才能知道这是谁的执行结果。
一个 AssistantMessage 可能同时返回多个 Tool Call。
例如:
read file Aread file Bread file C
这些只读操作通常可以并行。
但下面这些操作可能存在顺序依赖:
edit package.jsonnpm installnpm test
如果并行执行,后两项可能使用旧文件或未完成的依赖。
Pi 的执行策略允许:
Tool Calls→ sequential config 或任一 Tool 标记 sequential?├─ Yes → 按顺序执行 ─┐└─ No→ 并行执行 ─┴→ 按调用顺序产生结果
即使并行,最终 Tool Result 仍需要维持可预测的对应关系。
并行不是默认越多越好。它必须考虑:
假设 Agent 正在执行:
用户中途说:
这是一条 Steering Message。它需要尽快进入当前任务。
另一个输入:
这是一条 Follow-up Message。它不应该干扰当前重构。
Pi 将两者放入不同队列,并允许配置队列取出策略,例如:
执行顺序大致是:
当前模型响应→ 当前 Tool Calls 完成→ Turn End→ 注入 Steering→ 继续当前任务→ 当前任务无 Tool、无 Steering→ 注入 Follow-up→ 开始后续任务
这里有一个重要边界:
Steering 通常不会强行回滚正在执行的 Tool。
若 Tool 运行时间很长,立即停止依赖:
“消息已进入 Steering Queue”不等于“当前系统副作用已经立即停止”。
工具完成后,Runtime 并不一定直接用原配置开始下一轮。
prepareNextTurn 可以在 Turn 边界调整:
这使得上层可以实现:
Turn 边界是安全修改运行策略的位置,因为上一轮的 Assistant 与 Tool Result 已经形成完整记录。
不能只用“模型没有 Tool Call”作为唯一条件。
Pi 的控制流还需要考虑:
terminate;这只会跳过工具后的自动模型调用,不等于结束整个 Agent Run;shouldStopAfterTurn Hook;可以写成一组概念条件:
若发生 fatal error / abort停止否则若整批 Tool Result 都 terminate=true跳过本批工具后的自动模型调用,但继续检查 steering / follow-up否则若 shouldStopAfterTurn 返回 true直接发出 agent_end 并结束 Agent Run;不再轮询 steering / follow-up否则若还有 tool calls继续当前任务否则若还有 steering继续当前任务否则若还有 follow-up开始后续任务否则agent_end
对于自研 Harness,还应加入:
否则模型和工具可能进入无界循环。
Pi 的 Agent Runtime 会发出类似以下事件:
agent_startturn_startmessage_startmessage_updatemessage_endtool_execution_starttool_execution_updatetool_execution_endturn_endagent_end
User → Agent:promptAgent → Subscriber:agent_start → turn_startAgent → Model:streamModel → Agent:partial messageAgent → Subscriber:message_updateModel → Agent:tool call completeAgent → Subscriber:message_end → tool_execution_startAgent → Tool:executeTool → Agent:progressAgent → Subscriber:tool_execution_updateTool → Agent:resultAgent → Subscriber:tool_execution_end → turn_end
事件订阅的用途包括:
高级 Agent 类会串行等待异步 Listener,这意味着 Listener 可能成为执行屏障。
收益是状态一致性更强。
风险是某个缓慢 Listener 会拖慢 Runtime。Listener 必须区分:
Agent 的工具错误通常有两类消费者:
如果 Tool 直接抛异常并终止循环,模型无法知道:
Pi 将可恢复工具错误转换为 Tool Result:
{"isError":true,"content":"File not found: src/auth.ts"}
模型下一轮可以:
这不意味着所有错误都应该继续。
以下情况更适合终止:
用户输入:
完整链路可以表示为:
1. User → Agent:prompt(task)2. Agent → Session/UI:agent_start / user message3. Agent → Context Transform → Model:system + messages + tools4. Model → Agent:read(test file) → 校验并执行 → test source5. Agent → Model:assistant + tool result6. Model → Agent:read(implementation) → implementation source7. Model → Agent:edit → 校验 + before hook + 执行 → edit result8. Model → Agent:bash(test command) → 携带 abort signal 执行9. Bash → Agent:one test still fails → 失败结果回注模型10. Model → Agent:second edit call → execute → result11. Model → Agent:bash(full test suite) → all tests pass12. Agent → Model:final observation13. Model → Agent:final summary,no tool calls14. Agent → Session/UI:turn_end / agent_end15. Agent → User:result
这张图中,模型从未直接读取文件或执行命令。
它只能:
现实世界始终由 Harness 中介。
只有当前注册的 Tool 才能执行。
阻止结构错误,但不能替代语义权限。
防止截断 Tool Call 产生副作用。
实现路径、命令、网络和人工审批策略。
允许模型调用与工具执行响应取消。
允许应用根据成本、安全或业务规则提前结束。
确保错误与结果以模型可理解的方式返回。
这些安全门仍然不能代替容器、权限隔离和 Secret 管理。Runtime 只能控制它知道的工具入口;一旦 Bash 在宿主机拥有广泛权限,Tool 内部可能产生任意副作用。
会导致消息历史、Tool Result 和文件修改交错。
参数可能仍未完整生成。
模型失去自我修正机会。
严重 Runtime 错误可能导致无限重试。
用户的后续任务会污染当前执行。
模型切换、Compaction 和状态持久化变得难以解释。
产生竞态、覆盖和不可复现状态。
Agent 可能无限调用模型或工具。
破坏 Runtime 的事件和状态一致性。
中断、错误和调试信息会丢失。
ReAct 是一种推理与行动组织方式。Agent Loop 是实际运行模型、工具、状态和事件的软件控制流,两者不在同一层。
必须等待完整消息,检查 Stop Reason,并进行 Tool 查找、参数验证和 Hook 检查。
存在写入、进程和结果依赖时必须串行。
Steering 是消息调度机制。立即停止还依赖 Abort Signal 和 Tool 的取消实现。
可恢复错误应成为 Tool Result,让模型修正。不可恢复 Runtime 错误才应该终止。
要重建行为,必须保存 Tool Call、Tool Result、错误、中断和必要的事件关系。
Pi 的 Agent Loop 可以概括为五个连续闭环:
上下文闭环AgentMessage → LLM Message生成闭环Model Stream → Partial AssistantMessage行动闭环Tool Call → Validation → Execution → Tool Result交互闭环Steering / Follow-up → 下一轮输入状态闭环Events → Agent State → Session / UI
真正使模型成为 Agent 的,不是一个 while 关键字,而是这些边界:
理解这一层之后,下一步自然是研究它调用的模型层:Pi 如何让同一个 Agent Loop 面向不同 Provider 工作,并在会话中切换模型。
下一篇将分析:
https://github.com/earendil-works/pi/blob/v0.82.1/packages/agent/README.mdAgent 类:https://github.com/earendil-works/pi/blob/v0.82.1/packages/agent/src/agent.tshttps://github.com/earendil-works/pi/blob/v0.82.1/packages/agent/src/agent-loop.tshttps://github.com/earendil-works/pi/tree/v0.82.1/packages/agent/srchttps://github.com/earendil-works/pi/blob/v0.82.1/packages/ai/README.md