6 min read教程

M03. 循环:Agent 如何从一次回答走向多步行动

拆解 Agent Loop 中的 turn、tool call、stop reason 与多步行动控制流。

M03. 循环:Agent 如何从一次回答走向多步行动#

普通的模型调用是“一问一答”。Agent 的区别不是模型突然获得了意志,而是宿主程序把模型输出放进了一个可以重复执行的控制流:模型提出动作,工具执行动作,结果再次进入上下文,直到模型给出终止信号。

1. 三种调用方式#

直接生成

程序给模型上下文,拿到一条 assistant message。所有下一步都由程序写死。

Workflow#

程序规定步骤,模型只负责每一步中的判断。优点是稳定,缺点是变化场景需要不断修改流程代码。

Agent Loop#

程序提供工具和终止规则,把下一步选择交给模型。灵活性更高,但必须认真设计工具契约、错误消息、取消和预算。

2. Trace、turn 与 tool call#

一次用户任务可以看成一个 trace;trace 内包含多个 turn。一个 turn 通常从模型响应开始,可能包含零个或多个工具调用,以及对应的工具结果。只要 stop reason 表示还需要工具,Loop 就会继续产生下一轮。

text
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 的最小骨架#

教学版伪代码可以写成:

ts
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 开始,给每次 emitgetSteeringMessages、工具执行和 follow-up 检查做标记。再回到 Agent.prompt() 看它如何创建运行上下文。最后才看 AgentSession 如何在事件边界上加持久化与扩展。

这样读,比从一堆工具实现反向猜 Loop 更快:先掌握控制流,再填充动作细节。

8. Loop 的核心不变量#

每次继续循环都必须同时满足三个条件:上一轮 assistant message 已经完整进入上下文;它请求的工具结果已经按 call id 配对;运行时仍允许继续执行。stopReason 只回答“模型是否还要求动作”,不替代取消、权限和预算判断。把这些条件分开,才能解释为什么某次运行是正常停止,而不是异常中断。

资料

本文依据 Pi Agent Loop 源码Agent 核心源码重新组织。

相关文章