M03. 循环:Agent 如何从一次回答走向多步行动#
普通的模型调用是“一问一答”。Agent 的区别不是模型突然获得了意志,而是宿主程序把模型输出放进了一个可以重复执行的控制流:模型提出动作,工具执行动作,结果再次进入上下文,直到模型给出终止信号。
1. 三种调用方式#
直接生成
程序给模型上下文,拿到一条 assistant message。所有下一步都由程序写死。
Workflow#
程序规定步骤,模型只负责每一步中的判断。优点是稳定,缺点是变化场景需要不断修改流程代码。
Agent Loop#
程序提供工具和终止规则,把下一步选择交给模型。灵活性更高,但必须认真设计工具契约、错误消息、取消和预算。
2. Trace、turn 与 tool call#
一次用户任务可以看成一个 trace;trace 内包含多个 turn。一个 turn 通常从模型响应开始,可能包含零个或多个工具调用,以及对应的工具结果。只要 stop reason 表示还需要工具,Loop 就会继续产生下一轮。
trace
├─ turn 1: user → assistant(tool call)
├─ turn 2: tool result → assistant(tool call)
└─ turn 3: tool result → assistant(final)不要把“assistant 说了一句话”与“turn 结束”混为一谈。流式 delta 只是传输过程;完整 assistant message 和工具结果才是可以被后续逻辑消费的节点。
3. Loop 的最小骨架#
教学版伪代码可以写成:
async function runLoop(context: Context) {
while (true) {
const assistant = await streamAssistantResponse(context);
context.messages.push(assistant);
if (assistant.stopReason !== "toolUse") return assistant;
const results = await executeToolCalls(assistant.toolCalls);
context.messages.push(...results);
}
}Pi 的真实实现还要处理流式事件、多个工具、取消、steering/follow-up 队列、钩子和自动重试,但主线没有变:stopReason 是控制流信号,消息是循环状态。
4. 为什么工具错误不应该让 Loop 崩掉#
如果工具抛异常后整个 Loop 直接退出,模型没有机会知道路径为什么失败,也不能自行修正参数。更适合 Agent 的做法是把执行错误编码成 toolResult 消息,并标记错误状态:模型可以选择换参数、换工具或停止。
当然,不是所有错误都应该交给模型。进程损坏、凭据失效、权限策略拒绝和宿主取消,都可能需要直接终止。关键是把“可恢复的任务错误”和“不可继续的运行错误”分开。
5. steering 与 follow-up 的位置#
Loop 不只是被动等待模型。运行期间,用户可能要求改变当前路线;一个外部系统也可能要求任务完成后继续做下一件事。Pi 将两种输入分别放进 steering 和 follow-up 队列:前者在后续 turn 中影响当前路线,后者在当前工作自然结束后接续。
队列的存在还解决了并发问题:用户输入不必强行打断正在执行的 Promise,而是进入 Loop 已知的消息通道。
6. 退出路径必须可解释#
至少要区分:
- 模型给出最终文本。
- 模型要求工具,但工具调用被拒绝。
- 工具返回错误,模型决定停止。
- 用户或宿主调用 abort。
- 达到预算、超时或安全策略上限。
- 发生不可恢复的模型/协议错误。
UI 和日志不应把这些状态全部显示成“完成”。它们对恢复、计费和用户信任的含义不同。
7. 读源码的办法#
从 runLoop 的外层 while 开始,给每次 emit、getSteeringMessages、工具执行和 follow-up 检查做标记。再回到 Agent.prompt() 看它如何创建运行上下文。最后才看 AgentSession 如何在事件边界上加持久化与扩展。
这样读,比从一堆工具实现反向猜 Loop 更快:先掌握控制流,再填充动作细节。
8. Loop 的核心不变量#
每次继续循环都必须同时满足三个条件:上一轮 assistant message 已经完整进入上下文;它请求的工具结果已经按 call id 配对;运行时仍允许继续执行。stopReason 只回答“模型是否还要求动作”,不替代取消、权限和预算判断。把这些条件分开,才能解释为什么某次运行是正常停止,而不是异常中断。
资料
本文依据 Pi Agent Loop 源码和 Agent 核心源码重新组织。