Claude Code 01:Agent Loop——一个循环就是一个 Agent

Claude Code 01:Agent Loop——一个循环就是一个 Agent
Asaakii在探讨复杂的自主智能体架构时,我们很容易被多角色对话、规划图、记忆网络等概念裹挟。但如果剥去所有外层的花哨包装,Agent 的物理内核其实就是一个受控的循环语句。
大语言模型本身具备强大的代码推理能力,但它没有双手,碰不到现实世界——它既不能读取磁盘文件,也不能运行单测,更看不到标准错误输出(stderr)。在没有 Agent 循环时,开发者在网页端使用大模型写代码:模型输出一段命令,你复制到终端执行,再把报错信息粘回输入框。在这个过程中,你自己就是那个手动循环。
所谓的自主 Coding Agent,本质就是用一段代码把“模型决策 $\rightarrow$ 工具执行 $\rightarrow$ 结果捕获 $\rightarrow$ 回传模型”的链路闭环起来。
本文将实现一个不到 30 行的最小可运行 Agent Loop,并深入剖析 Claude Code 生产源码中多达 1,729 行的状态机控制流。
30 行最小实现:One Loop & Bash
下面是用 Python 编写的最小闭环实现。它只注册了一个 bash 工具,但已经具备了自主完成真实工程任务的核心能力:
1 | import subprocess |
核心设计要点剖析
上述 30 行代码虽然简短,却包含了 Agent 状态机的三个黄金法则:
1. messages 上下文是追加而非覆盖
每一轮循环中,模型的工具请求响应(包含 tool_use 块)和本地执行的结果(tool_result 块)都必须按序累加追加到消息数组中。模型之所以能感知自己的执行历史、基于上一轮的报错做出纠偏,完全依赖这个累积的消息栈。
2. stop_reason 是唯一的出口
从聊天机器人(Chatbot)跃迁到智能体(Agent),最核心的技术分水岭就在于退出的判断逻辑:
- 普通 Chatbot:模型吐完一句话(
stop_reason == "end_turn")就将控制权抛回给终端用户; - 自主 Agent:只要模型返回了
stop_reason == "tool_use",系统就会剥夺用户的介入权,自动执行工具并进入下一轮循环,直到模型自主判断任务彻底完成。
3. tool_use_id 物理强对齐
Anthropic Messages API 规定:回传的每个 tool_result 对象中,必须显式携带对应的 tool_use_id。如果一个响应里模型并行发起了多个工具调用,返回给模型的 tool_result 必须一一对应,否则 API 会抛出 invalid_request_error 导致会话中断。
生产源码穿透:Claude Code 的 1,729 行状态机
上面的 30 行代码覆盖了核心的 Happy Path。但在工业级实践中,进程会遭遇网络抖动、Token 超限、文件死锁或输出截断。
在 Claude Code 的开源源码架构中,Agent 核心循环被拆分为两层:
src/QueryEngine.ts(会话生命周期层):
持有会话实例的mutableMessages[]、全局取消信号abortController、文件读取内存缓存readFileState以及 Token 用量追踪器totalUsage。src/query.ts(单轮查询循环层):
一个长达 1,729 行的while(true)状态机,负责模型流式调用、工具分发、异常捕获与深层次自动压缩。
queryLoop 的六大状态转移
flowchart TD
START([进入 while true 循环]) --> API[请求 Claude Messages API]
API --> CHECK{评估响应结果}
CHECK -->|收到 tool_use| TOOLS[执行本地工具]
TOOLS -->|next_turn| API
CHECK -->|API 报 prompt-too-long| C1[轻量折叠 context collapse]
C1 -->|collapse_drain_retry| API
C1 -->|仍超限| C2[激进压缩 reactive compact]
C2 -->|reactive_compact_retry| API
CHECK -->|模型输出被截断| E1[escalate: 提升上限至 64k]
E1 --> API
E1 -->|仍截断| E2[recovery: 注入多轮续接指令]
E2 --> API
CHECK -->|代码检查 Hook 报错| HOOK[stop_hook_blocking: 注入错误]
HOOK --> API
CHECK -->|end_turn 且无工具| DONE([Terminal: 任务顺利完成])
在 src/query.ts 中,每轮迭代都会计算 state.transition,指导循环的走向:
转移类型(transition.reason) |
触发边界 | 生产级恢复策略 |
|---|---|---|
next_turn |
正常返回 tool_use |
执行工具并回填结果,继续进入下一轮推理 |
collapse_drain_retry |
上下文触发 prompt-too-long |
第一级防御:轻量折叠已归档的历史摘要缓存,然后静默重试 |
reactive_compact_retry |
折叠后依然超长 | 第二级防御:对整个会话历史发起一次紧急 LLM 深度摘要,压缩后重试 |
max_output_tokens_escalate |
生成内容触碰 8k 输出上限截断 | 自动将 max_tokens 参数从 8,000 升级到 64,000 后重试 |
max_output_tokens_recovery |
升级到 64k 后依然生成超限 | 在下一轮中注入 [system: 请从刚才截断处继续输出] 提示词引导多轮拼接 |
stop_hook_blocking |
提交前执行代码 Lint/单测失败 | 拦截退出请求,将错误栈注入为用户消息,强制模型修复坏味道 |
completed |
任务完成且 Hook 校验通过 | 正常结束并返回结果 |
恢复链设计亮点:输出截断与超长重试
在生产编码中,最容易让初学者编写的 Agent 挂死的问题是长文件输出被截断:
flowchart TD
A["API 返回 stop_reason == 'max_tokens'"] --> B["检查重试计数器 maxOutputTokensRecoveryCount"]
B -->|首次超限| C["临时提高 max_tokens 至 64,000 并重试"]
B -->|仍旧超限| D["生成恢复消息: 请紧接上一次输出继续编写"]
D --> E{"重试是否超过 3 次?"}
E -->|否| B
E -->|是| F["抛出保护性错误,避免无限消耗 Token"]
这种自愈机制让 Claude Code 即使在重构几百行的复杂文件时,也不会因为单次请求上限而留下半截残缺代码。
架构演进视角:从 REPL 到 Agent
回看软件工程的交互演化脉络:
$$\text{Lisp REPL (1960)} \longrightarrow \text{Unix Shell (1971)} \longrightarrow \text{Jupyter Notebook (2014)} \longrightarrow \text{Claude Code (2025)}$$
所有这些系统的核心架构完全一脉相承:Read $\rightarrow$ Eval $\rightarrow$ Print $\rightarrow$ Loop。
- Shell 的
Eval执行的是系统命令; - Jupyter 的
Eval执行的是 Python 解释器; - 而 Coding Agent 的
Eval,执行的是 “大语言模型推理 + 物理工具调度”。
这印证了一个底层事实:while True 循环不是一个简陋的临时代码,它就是智能体系统的本质骨架。
下一篇我们将探讨如何摆脱单调的 Bash 执行:构建安全可扩展的 Tool Use 与 Dispatch Map 机制。











