LESSON 11 · 进入运行

Agent Loop 是怎样循环工作的?

模型为什么会调用工具,又为什么会带着工具结果继续思考?

Agent 已经拥有模型、工具、提示词和 Session,但这些组件不会自己配合起来。用户发来任务以后,谁来组装上下文、调用模型、执行工具,再把结果交回模型继续判断?

负责把这些环节串起来的,就是 Agent Loop。

它并不是让模型不停运行的“死循环”,而是一套有明确开始、继续和结束条件的任务驱动机制。

1

先看 Agent Loop 的整体路线

一次任务进入 Agent 后,大致沿着下面的路线运行:

Agent Loop 从接收消息、调用模型到执行工具并回到下一轮 Step 的完整流程

图 11-1:一次 Turn 可以包含多个 Step。模型发出 Tool Call 后,工具结果会被记录为 SessionEvent,并回到下一次 Step。

Agent Loop 怎样连接用户、模型、工具与 SessionEvent

图 11-2:Agent Loop 的核心不是“重复调用模型”,而是让模型、工具和 SessionEvent 形成一个可持续推进的闭环。

这张图里最重要的是右侧的回路:模型提出 Tool Call,Harness 执行工具,结果写入 SessionEvent,然后参与下一次模型请求。正是这条回路,让 Agent 能根据真实执行结果继续工作,而不只是一次性生成一段文字。

2

Turn 和 Step 分别是什么

一次用户发言通常开启一个 Turn,可以理解成系统对“一轮用户任务”的完整处理。

一个 Turn 内部可以包含多个 Step。每个 Step 只负责一次模型判断,以及这次判断产生的工具调用。

例如用户说:“运行测试,并修复失败的问题。”

阶段发生的事情
Step 1模型决定运行测试,调用 Shell
Step 2模型看到报错,读取相关文件
Step 3模型修改代码,再次运行测试
Step 4测试通过,模型生成最终回复
Turn 结束当前任务没有待处理的后续工作

如果模型第一次就能直接回答,不需要任何工具,那么这个 Turn 只有一个 Step。如果任务需要不断观察和行动,一个 Turn 里就会出现多个 Step。

因此可以简单记成:

Turn = 一轮完整任务
Step = 这轮任务中的一次“模型判断 + 工具行动”
3

Agent 平时并不会一直运行

Web、CLI 和 API 都把输入送进 Agent 的统一收件箱。Agent 没有任务时处于空闲状态,不会在后台反复请求模型。

新消息进入收件箱后,Loop 才会被唤醒并开启 Turn。Turn 处理完以后,如果收件箱中没有新任务,Agent 就重新回到空闲;如果还有下一条消息,再开启新的 Turn。

把异常处理、取消和重试等细节省略后,整体逻辑可以写成下面这段教学化的伪代码:

只要收件箱还有任务:
    开启一个 Turn

    重复执行:
        开启一个 Step
        组装模型本次需要的内容
        调用模型
        记录模型回复

        如果模型调用了工具:
            执行工具
            记录工具结果
            继续下一个 Step
        否则:
            准备结束当前 Turn

    直到没有待处理的后续输入
    记录 Turn 结束

这段逻辑已经包含 Agent Loop 最核心的运行机制。真实源码更复杂,主要是因为工程系统还必须正确处理流式输出、错误恢复、并行工具、用户中断和进程重启,而不是因为 Loop 的基本思想很复杂。

4

每个 Step 会为模型准备什么

模型请求不是把用户刚输入的一句话直接转发出去。每个 Step 开始时,系统会重新准备四类信息:

  1. Session 历史:此前的用户消息、模型回复和工具结果;
  2. 系统提示词:Persona、项目规则、Plan Mode 等说明;
  3. 工具列表:当前 Preset 让这个 Agent 看见的工具及参数;
  4. 当前输入:用户新消息,以及需要在这一步处理的补充上下文。

这些内容会被组装成统一请求,再交给当前选择的 LLM Provider。Provider 负责把统一格式转换成 DeepSeek、GPT 或其他模型真正需要的 API 格式。

从机制上看,这一步可以压缩成下面几行:

// 简化后骨架:只表达信息流,不对应完整函数签名
const request = assembleRequest({
  history: session.deriveMessages(),
  systemPrompt,
  tools,
  currentInput,
})

const reply = await llm.stream(request)
session.record(reply)

这里值得注意的是:历史并不是从 WebUI 的聊天气泡中读取,而是从 SessionEvent 日志重新推导。界面即使被关闭,进程即使重新启动,只要日志还在,模型上下文就可以重新建立。

5

Tool Call 为什么会产生下一个 Step

模型不能亲自读取文件或运行命令。它只能输出一项结构化意图,例如:

工具:read
参数:{ file_path: "README.md" }

Agent Loop 发现 Tool Call 后,把它交给工具系统。工具系统完成参数检查、权限判断和实际执行,再把结果写入 SessionEvent。

这里最容易产生误解的一点是:工具函数不会在内部直接再次调用模型。工具执行结束后,当前 Step 先正常收尾;下一个 Step 再从 Session 日志中读到 Tool Result,并把它交给模型。

这段判断可以简化成:

// 简化后骨架
if (reply.toolCalls.length === 0) {
  return "当前 Turn 可以结束"
}

const results = await tools.run(reply.toolCalls)
session.record(results)

return "进入下一个 Step"

所以真正推动 Loop 继续的不是一句抽象的“再思考一次”,而是系统中出现了新的事实:工具已经返回结果,模型需要基于这个结果做下一次判断。

6

为什么所有过程都先写入 SessionEvent

Agent Loop 不直接维护一份只存在于内存中的对话数组。用户消息、模型回答、工具调用和工具结果都会先成为 SessionEvent。

这样做带来三个直接好处:

  • 下一次 Step 可以从日志重建模型历史;
  • WebUI 可以从同一条日志恢复消息和工具卡片;
  • 进程中断后,系统仍然知道任务进行到了哪里。

例如一次文件读取会留下这样的主线:

user/message
  → assistant/message(包含 Tool Call)
  → tool/call
  → tool/result
  → assistant/message(最终回答)

其中 Turn、Step、流式 chunk 和请求配置也会被记录,只是它们不一定进入模型的消息历史。

可以把 SessionEvent 理解成 Loop 的“传送带”。模型、工具和 UI 都不会私下交换一份无法恢复的状态,而是把已经发生的事实放到传送带上,再由下一环节读取。

7

Loop 什么时候继续,什么时候停止

一个 Step 结束后,系统会检查当前 Turn 是否还有未完成的工作。

常见的继续原因包括:

  • 刚刚得到 Tool Result,需要交回模型;
  • 用户在 Agent 工作时补充了新要求;
  • 后台任务或子 Agent 返回了结果;
  • 某个扩展插件注入了下一步需要处理的上下文。

在正常完成的情况下,只有同时满足下面两个条件,当前 Turn 才会结束:

  1. 模型已经给出可以结束这轮工作的回答;
  2. 收件箱里没有等待处理的下一步输入。

如果用户主动取消、模型请求失败或系统被中断,Turn 也会关闭,但会记录成相应的 aborted 或 error 原因,而不是伪装成正常完成。

在正式结束前,Loop 还会给扩展插件一次检查机会。例如长期目标插件可能发现目标尚未完成,于是补入下一轮上下文;如果没有插件增加后续工作,系统才写入 turn/end,让 Agent 回到空闲。

因此停止条件不是简单的“模型没有调用工具”。更准确地说,是“模型已经完成当前判断,并且系统确认没有新的事实需要它继续处理”。

8

插件怎样在不修改 Loop 的情况下参与

Agent Loop 只负责稳定的主流程,具体业务行为交给插件。插件通常在三个位置参与:

位置插件可以做什么
Step 开始前补充上下文、调整提示词,或阻止当前 Step
工具执行前后审批调用、执行安全检查、处理工具结果
Turn 准备结束时判断是否还有目标、后台结果或后续任务

源码中这些位置有对应的事件扩展点,例如 agent/pre-step、工具执行流水线和 agent/turn-stopping。第一次阅读不必记住这些名称,只要知道:插件可以在稳定节点上参与,而不需要复制或改写整套 Agent Loop。

这也是 Harness 采用插件架构的一个重要价值。Loop 保持简单稳定,权限、计划、目标、工具和工作流则围绕它扩展。

9

想继续读源码,可以从这三个位置开始

理解上面的运行机制后,如果还想对照源码深入阅读,只需要先关注三个文件:

源码位置先看什么
packages/core/agent-loop/src/agent.tsTurn、Step、模型请求与停止判断
packages/core/agent-loop/src/tool-calls.tsTool Call 怎样调度并写回结果
packages/core/session/src/index.tsSessionEvent 怎样投影成模型消息

不必从文件第一行开始逐句阅读。先沿着“组装请求 → 调用模型 → 执行工具 → 判断继续”这条主线找对应代码,遇到取消、并发、重试等分支时暂时跳过,整体结构会更容易看清。

10

用一句话记住 Agent Loop

Agent Loop 做的事情可以归纳成:

从收件箱认领任务
  → 从 Session 重建上下文
  → 调用模型
  → 执行模型提出的工具
  → 把结果写回 Session
  → 判断继续还是结束

下一课,我们专门拆开 SessionEvent:为什么 DSH 不只保存一组聊天消息,而要把整个 Agent 工作过程设计成追加式事件日志?

下一课 · 12

SessionEvent:Agent 的记忆从哪里来?

对话、工具结果、界面恢复和持久化,为什么都能来自同一条事件流?

0 人点赞,0 人看过