Pi Agent Loop 源码解析:Context、Streaming、Tool Calling、Steering 与停止条件 Pi Agent Loop 源码解析Context、Streaming、Tool Calling、Steering 与停止条件Meta DescriptionPi 的 Agent Loop 不只是一个 while 循环。它需要处理 AgentMessage 与 LLM Message 转换、流式 AssistantMessage、Tool 参数验证、串并行执行、Steering、Follow-up、Abort、错误结果和生命周期事件。本文基于 v0.82.1 固定源码重建一次用户输入的完整运行链。版本与证据说明Pi 源码事实基线v0.82.1短提交b4f2936发布日期 2026-07-25。固定实现事实使用版本 Tag滚动文档、竞品和 Provider 事实的统一访问日期为 2026-07-29。动态事实风险等级低。高风险文章发布前必须再次核对官方来源。命令、Demo 或兼容性若未明确标为PASS-RUNTIME不得理解为已在真实 Pi Runtime 中执行通过。本篇定位系列编号PI-05。前置文章PI-04。核心问题用户调用prompt()之后Pi 如何持续调用模型、执行工具、接收结果并决定下一轮直到 Agent 真正结束预计阅读时间35—45 分钟。研究基线仓库earendil-works/pi版本v0.82.1Release 短提交b4f2936查询日期2026-07-28主要源码packages/agent/src/agent.ts与packages/agent/src/agent-loop.ts本篇使用的是源码重建与伪代码没有声称在本轮环境中完成实际运行追踪。固定源码agent.tshttps://github.com/earendil-works/pi/blob/v0.82.1/packages/agent/src/agent.tsagent-loop.tshttps://github.com/earendil-works/pi/blob/v0.82.1/packages/agent/src/agent-loop.tsAgent Core READMEhttps://github.com/earendil-works/pi/blob/v0.82.1/packages/agent/README.md一、Agent Loop 不是“模型没回答完就继续调用”最简单的 Tool Calling 示例通常写成while(true){constresponseawaitcallModel(messages);if(response.toolCalls.length0)break;constresultsawaitexecuteTools(response.toolCalls);messages.push(response,...results);}这段代码只展示了骨架。真实 Runtime 必须解决用户在 Agent 运行时又发送消息怎么办AssistantMessage 还在流式生成时如何展示Tool Call 参数被截断后能不能执行多个 Tool Call 串行还是并行Tool 执行过程中如何上报进度Tool Hook 如何阻止或改写结果Abort 后如何保存部分内容模型返回错误时是否抛异常当前 Turn 结束后是否立即停止Follow-up Message 何时进入下一轮Context Transform 何时发生UI 和持久化层如何观察一致的事件顺序。Pi 的实现可以看成两个协作层Agent 类状态、队列、订阅、生命周期 ├─ agent-loop模型流与工具循环 │ ├─ pi-ai模型流式协议 │ └─ Tool Runtime └─ Application / UIAgent类管理长期状态和调用入口agent-loop管理一次执行过程的细粒度循环。二、先区分四种消息理解 Agent Loop 前需要先区分不同层的消息。1. 用户消息表示用户当前任务或后续方向。2. AssistantMessage模型生成的消息可以包含普通文本Thinking/ReasoningTool CallUsageStop ReasonError 或 Aborted 状态。3. ToolResultMessage工具执行后返回给模型的观察结果。4. 自定义 AgentMessage应用可以定义模型原生协议不认识的消息例如UI 通知外部任务状态领域事件需要经过转换才能进入模型的业务消息。因此Pi 不直接把整个应用状态当成模型消息。它使用以下边界AgentMessage[] → transformContext() → AgentMessage[] → convertToLlm() → Message[] → pi-aitransformContext()适合裁剪历史压缩上下文动态注入资料按当前模型调整消息。convertToLlm()适合过滤 UI-only Message将自定义消息转换成标准用户消息将多个业务事件合并成模型可读文本。这一边界保证Agent 保存的状态可以比模型真正看到的上下文更丰富。三、Agent类管理什么状态固定版本的Agent类维护一组可变状态核心包括System PromptModelThinking LevelToolsMessages当前是否 Streaming当前 Partial AssistantMessagePending Tool CallsError。它还持有Steering QueueFollow-up QueueEvent Subscribers当前运行 PromiseAbortControllerContext TransformMessage ConverterTool Hooks 与执行配置。这说明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()时发生什么prompt()是外部应用最容易接触的入口。它首先要防止同一个 Agent 同时启动两个独立主循环。如果 Agent 正在运行新的输入不应该再次调用prompt()。Pi 会要求调用者使用steer()改变当前执行方向followUp()排到当前任务之后。这避免两条主循环同时修改同一个messages和工具环境。一次新的prompt()可以概括为检查 Agent 是否空闲 → 标准化用户输入 → 写入 Agent Message → 创建 AbortController → 生成上下文快照与 Loop Config → 启动 Agent Loop → 消费事件并更新 State → 等待结束Agent Loop 启动时会发出agent_start turn_start message_start(user) message_end(user)随后才进入模型调用。事件顺序不是装饰。应用可能在事件上执行Session 持久化UI 更新审计成本统计日志Extension Hook。五、上下文快照为什么重要Agent 在运行时允许外部修改某些配置例如模型、工具或 System Prompt。如果模型调用过程中直接读取一组不断变化的引用会产生不稳定行为一次 Turn 开始时使用模型 ATool 执行后外部切换到模型 B同一轮事件却无法判断由哪个配置产生。Pi 会在启动 Loop 时建立 Context Snapshot并通过配置与后续准备函数控制何时允许模型、Thinking 或 Context 发生变化。这是一项通用 Runtime 原则一次不可分割的执行单元应当拥有可解释的配置快照动态更新应发生在明确边界而不是任意时刻。在 Agent 系统里这个边界通常是 Turn。六、Pi 实际上有内外两层循环runLoop的核心不是一个单层while。它需要区分两种“继续”模型调用工具后必须继续当前任务当前任务已经没有 Tool Call但队列里还有 Follow-up必须开始新任务。可以重建成下面的伪代码emit(agent_start);while(true){// 外层Follow-up 生命周期while(true){// 内层Tool Steering 生命周期constassistantawaitstreamAssistant(context);append(assistant);if(assistant.error||assistant.aborted)break;constcallsextractToolCalls(assistant);constresultsawaitexecuteTools(calls);append(results);emit(turn_end);contextawaitprepareNextTurn(context);conststeeringdrainSteeringQueue();append(steering);if(calls.length0steering.length0)break;}constfollowUpsdrainFollowUpQueue();if(followUps.length0)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 创建时执行因为上下文可能随执行变化Tool Result 增加Steering Message 到达历史接近窗口上限当前模型改变外部资源更新自定义消息需要按当前状态重写。Context 是运行时产物不只是静态 Prompt。八、流式 AssistantMessage 不是最后才出现模型响应通常通过 Event Stream 增量返回。Pi 不会等待完整响应后一次性创建 AssistantMessage而是维护一个 Partial Messagemessage_start → message_update(text delta) → message_update(thinking delta) → message_update(tool call delta) → message_end这样 UI 可以实时展示文字、Reasoning 和 Tool Call 参数生成过程。但这也带来状态一致性问题。Agent 必须区分已经写入永久历史的消息当前仍在 Streaming 的临时消息流结束后的完整消息。如果请求被 Abort部分内容也可能有价值用户可以看到模型已经分析到哪里Session 可以记录中断调试时可以判断模型为何准备调用某个工具。因此错误与中断不应该简单抛弃全部流式状态。九、Stop Reason 决定后续能否安全执行统一的 AssistantMessage 会包含 Stop Reason例如stoplengthtoolUseerroraborted。其中最危险的情况之一是length。模型可能在生成 Tool Call JSON 时达到输出上限留下一个语法上勉强可恢复、语义上却不完整的调用。例如{path:src/auth.ts,oldText:...,newText:尚未生成完整即便 Partial JSON Parser 能把它修成结构执行也可能破坏文件。Pi 在输出因长度终止时不会继续执行这些可能被截断的 Tool Call而是把它们失败化处理。这是一个重要安全原则“能够解析”不等于“足够完整可以产生副作用”。十、Tool Call 的完整执行管线一个 Tool Call 从模型输出到 Tool Result需要经过多个阶段。1. Assistant Tool Call 2. 查找 Tool 3. 准备与规范化参数 4. Schema Validation 5. beforeToolCall Hook ├─ 阻止 → Error Tool Result └─ 允许 → Tool.execute 6. Progress Updates 7. Raw Result / Error 8. afterToolCall Hook 9. ToolResultMessage 10. 加入 Agent Context1. 查找工具模型可能调用不存在的工具。Runtime 不能崩溃而应生成模型能够理解的错误结果让模型有机会修正。2. 参数准备工具可以拥有参数预处理逻辑例如路径规范化默认值兼容旧字段将用户友好参数转换成内部表示。3. Schema Validation参数必须经过 Tool Schema 验证。这可以阻止缺少必填字段错误类型非法枚举结构错误。但 Schema 不能判断所有语义风险。例如一个字符串路径类型正确却可能指向不应访问的位置。4.beforeToolCallHook 可以检查路径实现权限门屏蔽危险命令记录审计拒绝执行。5. Tool 执行Tool 接收参数、Abort Signal 与 Update Callback。6. Progress Update长工具可以持续发送当前阶段增量日志进度中间状态。Runtime 将其转成tool_execution_update事件而不是立即当成最终 Tool Result 送回模型。7. 错误捕获工具异常会被转换成isError: true的 Tool Result而不是直接让整个 Agent Loop 丢失上下文。8.afterToolCallHook 可以改写ContentDetailsUsageError 标记是否为当前工具结果设置terminate。固定版本只有当同一批次每一个最终 Tool Result 都设置terminatetrue时才跳过该批工具之后的自动模型调用它不会直接结束整个 Agent Run之后仍会检查 Steering 与 Follow-up。9. ToolResultMessage最终结果带着对应 Tool Call ID 回到消息历史模型才能知道这是谁的执行结果。十一、多个工具为什么有时串行、有时并行一个 AssistantMessage 可能同时返回多个 Tool Call。例如read file A read file B read file C这些只读操作通常可以并行。但下面这些操作可能存在顺序依赖edit package.json npm install npm test如果并行执行后两项可能使用旧文件或未完成的依赖。Pi 的执行策略允许全局要求串行某个 Tool 标记自己必须串行否则并行执行。Tool Calls → sequential config 或任一 Tool 标记 sequential ├─ Yes → 按顺序执行 ─┐ └─ No → 并行执行 ─┴→ 按调用顺序产生结果即使并行最终 Tool Result 仍需要维持可预测的对应关系。并行不是默认越多越好。它必须考虑文件写冲突共享进程数据库事务API 限流结果依赖日志顺序。十二、Steering 与 Follow-up 为什么必须分开假设 Agent 正在执行重构认证模块并运行测试。用户中途说不要改登录接口保持公开 API 不变。这是一条 Steering Message。它需要尽快进入当前任务。另一个输入完成以后再补一份迁移文档。这是一条 Follow-up Message。它不应该干扰当前重构。Pi 将两者放入不同队列并允许配置队列取出策略例如一次取全部一次取一条。执行顺序大致是当前模型响应 → 当前 Tool Calls 完成 → Turn End → 注入 Steering → 继续当前任务 → 当前任务无 Tool、无 Steering → 注入 Follow-up → 开始后续任务这里有一个重要边界Steering 通常不会强行回滚正在执行的 Tool。若 Tool 运行时间很长立即停止依赖Tool 是否监听 Abort Signal子进程是否能被终止终止后是否有清理写入是否原子化。“消息已进入 Steering Queue”不等于“当前系统副作用已经立即停止”。十三、prepareNextTurn是运行时扩展点工具完成后Runtime 并不一定直接用原配置开始下一轮。prepareNextTurn可以在 Turn 边界调整ContextModelThinking Level其他下一轮配置。这使得上层可以实现根据任务阶段切换模型Tool 执行后加载新上下文进入低成本总结模型达到阈值后触发 Compaction根据错误类型提高 Reasoning动态路由。Turn 边界是安全修改运行策略的位置因为上一轮的 Assistant 与 Tool Result 已经形成完整记录。十四、什么时候 Agent Loop 停止不能只用“模型没有 Tool Call”作为唯一条件。Pi 的控制流还需要考虑Assistant 返回 ErrorAssistant 被 Abort当前工具批是否全部请求terminate这只会跳过工具后的自动模型调用不等于结束整个 Agent RunshouldStopAfterTurnHook当前没有 Tool CallSteering Queue 为空Follow-up Queue 为空。可以写成一组概念条件若发生 fatal error / abort 停止 否则若整批 Tool Result 都 terminatetrue 跳过本批工具后的自动模型调用但继续检查 steering / follow-up 否则若 shouldStopAfterTurn 返回 true 直接发出 agent_end 并结束 Agent Run不再轮询 steering / follow-up 否则若还有 tool calls 继续当前任务 否则若还有 steering 继续当前任务 否则若还有 follow-up 开始后续任务 否则 agent_end对于自研 Harness还应加入最大 Turn最大 Token最大成本最大运行时间连续相同错误次数人工审核节点。否则模型和工具可能进入无界循环。十五、事件系统如何保证外部观察Pi 的 Agent Runtime 会发出类似以下事件agent_start turn_start message_start message_update message_end tool_execution_start tool_execution_update tool_execution_end turn_end agent_endUser → Agentprompt Agent → Subscriberagent_start → turn_start Agent → Modelstream Model → Agentpartial message Agent → Subscribermessage_update Model → Agenttool call complete Agent → Subscribermessage_end → tool_execution_start Agent → Toolexecute Tool → Agentprogress Agent → Subscribertool_execution_update Tool → Agentresult Agent → Subscribertool_execution_end → turn_end事件订阅的用途包括TUI 渲染Session 保存追踪 Token记录 Tool Timeline建立 OpenTelemetry Span审计测试事件顺序外部控制面。高级Agent类会串行等待异步 Listener这意味着 Listener 可能成为执行屏障。收益是状态一致性更强。风险是某个缓慢 Listener 会拖慢 Runtime。Listener 必须区分必须在下一步前完成的关键逻辑可以异步投递的日志或遥测。十六、错误为什么要回到模型而不是只抛给程序员Agent 的工具错误通常有两类消费者外部应用需要记录异常模型需要理解失败并修正策略。如果 Tool 直接抛异常并终止循环模型无法知道命令不存在文件路径错误测试失败参数不合法权限被拒绝。Pi 将可恢复工具错误转换为 Tool Result{isError:true,content:File not found: src/auth.ts}模型下一轮可以搜索正确路径修正参数改用其他工具向用户说明阻塞。这不意味着所有错误都应该继续。以下情况更适合终止Runtime 内部状态损坏Context 无法序列化认证失效且无法恢复用户主动 Abort安全策略要求停止达到资源上限。十七、从一次“修复测试”重建完整链路用户输入修复认证模块中失败的刷新 Token 测试不要修改公开 API。完整链路可以表示为1. User → Agentprompt(task) 2. Agent → Session/UIagent_start / user message 3. Agent → Context Transform → Modelsystem messages tools 4. Model → Agentread(test file) → 校验并执行 → test source 5. Agent → Modelassistant tool result 6. Model → Agentread(implementation) → implementation source 7. Model → Agentedit → 校验 before hook 执行 → edit result 8. Model → Agentbash(test command) → 携带 abort signal 执行 9. Bash → Agentone test still fails → 失败结果回注模型 10. Model → Agentsecond edit call → execute → result 11. Model → Agentbash(full test suite) → all tests pass 12. Agent → Modelfinal observation 13. Model → Agentfinal summaryno tool calls 14. Agent → Session/UIturn_end / agent_end 15. Agent → Userresult这张图中模型从未直接读取文件或执行命令。它只能生成 Tool Call读取 Harness 构造的 Tool Result根据 Context 决定下一步。现实世界始终由 Harness 中介。十八、Agent Loop 的几个关键安全门1. 工具白名单只有当前注册的 Tool 才能执行。2. 参数 Schema阻止结构错误但不能替代语义权限。3. Length Stop Guard防止截断 Tool Call 产生副作用。4.beforeToolCall实现路径、命令、网络和人工审批策略。5. Abort Signal允许模型调用与工具执行响应取消。6. Turn Stop Hook允许应用根据成本、安全或业务规则提前结束。7. Tool Result Normalization确保错误与结果以模型可理解的方式返回。这些安全门仍然不能代替容器、权限隔离和 Secret 管理。Runtime 只能控制它知道的工具入口一旦 Bash 在宿主机拥有广泛权限Tool 内部可能产生任意副作用。十九、自研 Agent Loop 最容易犯的错误1. 同时运行两个主 Loop会导致消息历史、Tool Result 和文件修改交错。2. 在流结束前执行 Tool Call参数可能仍未完整生成。3. 把所有错误都抛出模型失去自我修正机会。4. 把所有错误都返回模型严重 Runtime 错误可能导致无限重试。5. 不区分 Steering 与 Follow-up用户的后续任务会污染当前执行。6. 没有明确 Turn 边界模型切换、Compaction 和状态持久化变得难以解释。7. 并行执行有副作用的 Tool产生竞态、覆盖和不可复现状态。8. 没有资源限制Agent 可能无限调用模型或工具。9. UI 直接修改内部消息破坏 Runtime 的事件和状态一致性。10. 忽视 Partial Message中断、错误和调试信息会丢失。二十、常见误解误解一Agent Loop 就是 ReAct PromptReAct 是一种推理与行动组织方式。Agent Loop 是实际运行模型、工具、状态和事件的软件控制流两者不在同一层。误解二模型返回 Tool Call 后可以立即执行必须等待完整消息检查 Stop Reason并进行 Tool 查找、参数验证和 Hook 检查。误解三多个 Tool Call 应该全部并行存在写入、进程和结果依赖时必须串行。误解四Steering 等于立即中断当前命令Steering 是消息调度机制。立即停止还依赖 Abort Signal 和 Tool 的取消实现。误解五Tool 执行失败就是 Agent 失败可恢复错误应成为 Tool Result让模型修正。不可恢复 Runtime 错误才应该终止。误解六Session 只需保存最终 AssistantMessage要重建行为必须保存 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关键字而是这些边界谁可以启动执行Context 何时转换消息何时算完整工具何时允许产生副作用错误何时可恢复用户输入何时进入动态配置何时改变Loop 何时停止。理解这一层之后下一步自然是研究它调用的模型层Pi 如何让同一个 Agent Loop 面向不同 Provider 工作并在会话中切换模型。下一篇PI-06让不同大模型共享一个 AgentPi 如何统一 Provider 与 Context Handoff下一篇将分析Provider、Model 与 API Implementation 的区别Models Collection 如何路由Auth 与 Header 如何合并Reasoning、Stop Reason 和 Usage 如何统一为什么跨 Provider 切换不能只把原始 JSON 原样发送Thinking Block、Tool Call 和 Tool Result 如何完成 Handoff。参考资料Agent Core READMEhttps://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.tsAgent Loophttps://github.com/earendil-works/pi/blob/v0.82.1/packages/agent/src/agent-loop.tsAgent 类型定义https://github.com/earendil-works/pi/tree/v0.82.1/packages/agent/srcPi AI 消息与流式接口https://github.com/earendil-works/pi/blob/v0.82.1/packages/ai/README.md