Skip to content

Latest commit

 

History

History
226 lines (145 loc) · 17.5 KB

File metadata and controls

226 lines (145 loc) · 17.5 KB

English

03 Agent 循环

1️⃣ 什么是 Agent 循环,为什么它如此重要

在理解 Claude Code 的 Agent 循环之前,我们需要先搞清楚一个基本概念:大语言模型本身是无状态的。 你给它一段文字,它返回一段文字,交互就结束了。它不会自己决定下一步做什么,不会主动去读文件,不会自己执行命令。

那么问题来了:Claude Code 是怎么做到接到一个任务后,自己读代码、自己写文件、自己跑测试、发现报错后自己修复,整个过程可能循环十几轮才最终完成的?

答案就是 Agent 循环。它是一段运行在大语言模型之外的程序逻辑,负责不断地向模型提问、解析模型的回答、执行模型要求的操作、把操作结果反馈给模型,然后让模型决定下一步做什么。这个循环一直运转,直到模型认为任务完成为止。

如果把大语言模型比作一个聪明但被关在房间里的大脑,Agent 循环就是那个 帮它搬运信息、执行动作的身体。大脑说"我想看一下这个文件",身体就去把文件内容拿过来;大脑说"把这行代码改成那样",身体就去执行修改操作。

这个概念在学术界有一个正式名称叫 ReAct,即 Reasoning + Acting 的缩写。模型先推理,然后采取行动,观察行动结果,再推理下一步,如此循环。几乎所有现代 Agent 系统都基于这个范式,区别只在于实现的精细程度。

2️⃣ Claude Code 的核心设计选择:AsyncGenerator

AsyncGenerator 设计

Claude Code 的 Agent 循环实现在 src/query.ts 这个文件里,大约 1,700 行代码。整个循环的骨架是用 JavaScript 的 AsyncGenerator 构建的。

这个技术选型值得展开说说,因为它直接影响了整个系统的架构。

在 JavaScript 中,AsyncGenerator 是一种特殊的函数。普通函数被调用后会一口气执行完毕返回结果。而 AsyncGenerator 可以 执行到一半暂停,把中间结果交出来,等外面处理完了再继续执行。这个机制叫做 yield

这对 Agent 循环来说简直是天作之合。想象一下 Agent 的工作流程:模型正在生成回复,每产生一个 token 就需要实时显示在终端上;模型要求执行一个危险操作,需要暂停下来等用户确认;工具执行完毕后,需要把结果送回模型继续生成。这些场景全都需要 暂停、交出控制权、等待、恢复 的能力,而 AsyncGenerator 天然就支持这些。

作为对比,如果用传统的回调函数来实现,代码会陷入深层嵌套的回调地狱。如果用 Promise 链来实现,流式输出的实时渲染会变得很别扭。AsyncGenerator 让 Claude Code 的主循环写出来既像同步代码一样清晰,又具备完整的异步能力。

市面上其他 Agent 框架也做过不同的选择。LangChain 早期版本用的是 回调链模式,每个步骤注册回调函数,灵活但调试困难。AutoGPT 用的是简单的 while 循环加同步调用,清晰但无法做流式输出。Cursor 的内部实现据报道使用了 事件驱动架构,响应速度快但状态管理复杂。Claude Code 选择 AsyncGenerator,在清晰度、流式能力和状态管理之间找到了一个很好的平衡点。

3️⃣ 六阶段循环:Agent 的心跳

六阶段循环

Claude Code 的每一次循环迭代被分成了六个阶段。你可以把它想象成 Agent 的心跳:每跳动一次,就经历一个完整的 思考-行动-观察 周期。

┌──────────────────────────────────────────────────────┐
│                    query loop                         │
│                                                      │
│  ┌──────────────┐                                    │
│  │ 1. 预取 Prefetch │ 记忆文件、Skill 列表(异步)  │
│  └────┬─────────┘                                    │
│       ↓                                              │
│  ┌──────────────┐                                    │
│  │ 2. 构建 Build   │ system prompt + messages + tools │
│  └────┬─────────┘                                    │
│       ↓                                              │
│  ┌──────────────┐                                    │
│  │ 3. 调用 API     │ 流式请求,逐 token yield        │
│  └────┬─────────┘                                    │
│       ↓                                              │
│  ┌──────────────┐                                    │
│  │ 4. 执行工具     │ 权限检查 → 并行执行 → Hook      │
│  └────┬─────────┘                                    │
│       ↓                                              │
│  ┌──────────────┐                                    │
│  │ 5. 压缩检查     │ 超阈值?微压缩/SM/Full Compact  │
│  └────┬─────────┘                                    │
│       ↓                                              │
│  ┌──────────────┐                                    │
│  │ 6. 继续决策     │ tool_use → 循环 | end_turn → 结束│
│  └──────────────┘                                    │
└──────────────────────────────────────────────────────┘

阶段一:预取

在正式调用 API 之前,系统会 异步并行 地加载几类信息:

  • 记忆文件:CLAUDE.md 和 MEMORY.md 中的用户偏好和项目规则
  • Skill 列表:后台自动发现的可用技能
  • Git 状态:当前分支、文件变更、最近提交等信息

为什么要单独设一个预取阶段?因为这些信息需要从文件系统或者子进程读取,涉及 I/O 操作,也就是读写磁盘或者执行外部命令。I/O 操作的特点是速度慢但不占用 CPU。如果把这些读取操作放在主循环里串行执行,每次循环都要白白等待几十毫秒。把它们提前到循环开始前异步发起,等到真正需要用的时候数据已经准备好了。

另一个关键设计是 session 内缓存。这些信息在一次会话内几乎不会变化,所以只需要读取一次。后续循环直接使用缓存数据,跳过 I/O 操作。这个优化看起来简单,但在一个可能循环二三十轮的 Agent 任务中,累积节省的时间是可观的。

阶段二:构建上下文

这个阶段负责组装发送给 API 的完整请求,是整个循环中 信息密度最高 的环节。请求包含四个核心部分:

system prompt 是给模型的基础指令。它由多个来源动态拼接而成:内置的 Agent 行为规则、CLAUDE.md 中的用户记忆、当前 Git 仓库的状态信息、权限模式的描述。这一块的设计非常精细,详见 04-上下文工程

messages 是完整的对话历史。包括用户发送的消息、模型之前的回复、以及之前所有工具调用的结果。这是模型理解当前任务进展的 核心信息来源

tools 是模型可以调用的工具列表。Claude Code 注册了 40 多个工具,通过 feature flag 控制哪些可用。每个工具的定义包括名称、描述和参数格式,模型根据这些信息决定调用哪个工具。

thinking 是 Extended Thinking 的配置参数。启用后,模型会在生成最终回复之前先进行一段内部推理,这段推理过程也会被返回。

阶段三:流式调用 API

通过 Anthropic SDK 发起流式请求。流式 意味着模型不是等全部生成完再一次性返回,而是 每生成一个 token 就立刻返回。Agent 循环用 yield 把每个 token 逐个交给上层的终端 UI,用户就能看到文字一个字一个字地出现,而不是盯着空白屏幕等待。

这里还有一个重要的容错设计:模型 fallback 链。API 客户端内置了重试逻辑,如果主模型调用失败,会依次尝试 Opus、Sonnet、Small。这保证了即使某个模型出现临时故障,Agent 依然能继续工作。

这个阶段在技术上看似简单,但它是用户体验的关键环节。流式输出带来的 即时反馈感 是让用户相信 Agent 正在工作的重要心理因素。如果每次都要等 10 秒才看到完整回复,即使结果完全相同,用户体验也会大打折扣。

阶段四:工具执行

工具执行流程

当模型的回复中包含 tool_use block 时,说明模型决定调用一个或多个工具。这是 Agent 从"思考"转向"行动"的关键转折点。

工具执行的流程经过了精心设计,一共有六个步骤:

tool_use blocks(模型输出的工具调用请求)
  ↓
Pre-Hook(前置钩子:记录日志、校验参数格式)
  ↓
权限检查(根据 default/auto/plan 三种模式决定是否需要用户确认)
  ↓
查找工具(按名称在工具注册表中匹配)
  ↓
并行执行(Promise.all,多个工具同时运行)
  ↓
Post-Hook(后置钩子:清洗结果、写审计日志)
  ↓
tool_result 追加到消息历史

其中 并行执行 是一个重要的性能优化。如果模型同时请求读取三个文件,这三个读取操作会并行进行,而不是一个一个排队等待。对于复杂任务,这可以显著减少总等待时间。

权限检查 是安全的核心保障。根据当前的权限模式和具体的工具类型,系统决定是直接执行、自动放行还是弹出确认对话框。详见 06-权限系统

Pre-Hook 和 Post-Hook 是工具执行前后的拦截点。前置钩子可以校验参数、记录日志;后置钩子可以清洗工具返回的结果、写入审计日志。这种 面向切面 的设计让工具执行的核心逻辑和周边逻辑分离,代码更容易维护。

阶段五:压缩检查

每次工具执行完毕后,系统会检查当前的 token 消耗量 是否已经逼近上下文窗口的上限。如果超过了阈值,就触发压缩操作。

这个阶段解决的是一个现实问题:Agent 在处理复杂任务时,可能会循环几十轮,每一轮都会往消息历史中追加新的内容。如果不做任何清理,消息历史会不断膨胀,最终超过模型的上下文窗口限制,导致 Agent 崩溃。

Claude Code 设计了三层递进的压缩策略:微压缩 清理掉早期工具调用的详细输出,Session Memory 把关键信息沉淀到持久化存储,Full Compact 把整段对话历史压缩成一份摘要。具体细节见 05-消息压缩系统

阶段六:继续决策

最后,系统根据 API 返回的 stop_reason 字段决定接下来做什么:

  • tool_use:模型还需要调用更多工具,回到阶段一继续循环
  • end_turn:模型认为任务已经完成,退出循环
  • max_tokens:模型的单次输出达到了长度上限,可能还有话没说完,需要继续

这个决策机制看似简单,但它赋予了模型 自主判断任务是否完成 的能力。模型不是执行预设的步骤,而是根据每一步的结果动态决定是否继续。这正是 Agent 和传统自动化脚本的根本区别。

4️⃣ 状态管理:不可变更新

不可变状态管理

Agent 循环在运转过程中需要追踪大量状态信息。Claude Code 维护了 7 个核心状态变量

let state = {
  messages,                  // 完整的消息历史
  toolUseContext,           // 工具执行的上下文信息
  maxOutputTokensOverride,  // 输出 token 的动态限制
  autoCompactTracking,      // 压缩状态追踪
  // ... 还有 3 个
}

关键的设计决策是采用 不可变更新模式。每次循环不是直接修改原有的 state 对象,而是创建一个新的拷贝:

state = { ...state, messages: newMessages }

这种模式在前端开发领域已经是最佳实践了,React 和 Redux 都大量使用。但在 Agent 循环中采用它,有一个额外的重要好处:可追溯性。当出现 bug 时,你可以回溯每一轮循环的完整状态快照,精确定位问题发生在哪一轮、哪个字段的变化导致了异常。对于一个可能循环几十轮的复杂 Agent 来说,这种调试能力至关重要。

5️⃣ Extended Thinking 的保留规则

Extended Thinking 是 Claude 模型的一个特殊能力:在生成最终回复之前,先进行一段深度推理。这段推理会以 thinking block 的形式包含在 API 返回中。

代码中有大段注释详细解释了 thinking block 必须遵守的 三条规则

  1. thinking block 只能存在于设置了 max_thinking_length > 0 的请求中
  2. thinking block 不能是一组 block 序列中的最后一个元素
  3. thinking block 必须在整个 assistant 轨迹中 持久化保留,不能丢弃

第三条规则是最关键的,也是最容易出错的。在 Agent 循环中,当模型调用工具时,对话历史中会交替出现 tool_use 和 tool_result。如果在这个过程中把之前的 thinking block 丢弃了,模型的推理链就断了。它会丧失之前思考过程的上下文,后续的决策质量会显著下降。

这是一个典型的 看起来无关紧要但实际影响巨大 的工程细节。很多 Agent 框架在实现工具调用时会简单地清理历史消息来节省 token,不小心就把 thinking block 一起清掉了。Claude Code 专门为此编写了保留逻辑,确保 thinking block 在整个对话生命周期中完整存在。

6️⃣ Agent Fork 机制:分身术

Agent Fork 机制

Claude Code 支持在主 Agent 运行过程中 fork 出子 Agent 来执行特定任务:

主 Agent
  ├── fork → Skill Agent(执行特定技能,拥有独立 token 预算和消息历史)
  ├── fork → Compact Agent(负责生成压缩摘要)
  └── fork → Session Memory Agent(负责记忆沉淀)

fork 可以理解为"分身"。主 Agent 在遇到需要长时间运行的子任务,或者需要隔离上下文的场景时,会创建一个子 Agent 去处理。子 Agent 有自己独立的 token 预算和消息历史,不会污染主 Agent 的上下文。

这里有一个非常精巧的设计叫 CacheSafeParams。Anthropic 的 API 有一个 Prompt Cache 机制:如果两次请求的 system prompt 前缀相同,第二次请求可以复用第一次的缓存,节省大量计算成本。CacheSafeParams 确保子 Agent 的 system prompt 和上下文参数与父 Agent 保持一致,这样子 Agent 的 API 调用就能命中父 Agent 已经建立的 Prompt Cache。

这意味着 fork 子 Agent 在经济上是高效的。你不需要为每个子 Agent 重新付一遍 system prompt 的 token 费用。这个设计对于需要频繁 fork 子 Agent 的场景至关重要。

7️⃣ 与其他 Agent 实现的对比

把 Claude Code 的 Agent 循环和行业内其他知名实现做一个对比,可以更清楚地看出它的设计特点:

LangChain AgentExecutor 是目前使用最广泛的开源 Agent 框架。它的循环结构是同步的 while 循环,通过回调链处理中间事件。优点是生态丰富、入门门槛低。缺点是回调链在复杂场景下难以调试,没有内置的压缩和缓存机制,长对话场景容易 token 溢出。

AutoGPT 是最早引发 Agent 热潮的项目之一。它的循环非常简单直接:调用模型、解析命令、执行命令、把结果放回去。优点是容易理解。缺点是没有流式输出、没有权限系统、没有状态管理,实际生产中很难使用。

OpenAI Assistants API 采用的是服务端管理 Run 的方式。开发者创建一个 Run,服务端负责循环执行直到完成。优点是开发者不需要自己管理循环。缺点是黑盒程度高,自定义空间有限,无法针对特定场景做精细优化。

Claude Code 的 Agent 循环 在这些实现中处于 精细度最高 的位置。六阶段的清晰划分、AsyncGenerator 的流式能力、不可变状态管理、三层压缩、Prompt Cache 感知的 fork 机制,每一个设计点都经过了深度打磨。代价是 复杂度也最高,1,700 行的主循环代码量远超其他实现。

8️⃣ 总结

Claude Code 的 Agent 循环本质上做了一件事:把一个无状态的语言模型,变成了一个有记忆、能行动、会判断的自主智能体。 它是整个系统的心脏,每一次跳动都经历预取、构建、调用、执行、压缩、决策六个阶段。

这个循环的设计揭示了当前 Agent 工程的一个核心现实:模型的能力是基础,但把模型能力转化为可靠产品体验的工程框架同样关键。 同样的底层模型,套上不同质量的 Agent 循环,最终产品的表现可以天差地别。


下一篇:04-上下文工程