LEARN CLAUDE CODE
S01: Agent Loop — 30 行代码构成整个 Agent
这一课解决什么问题
大语言模型能推理、能写代码,但它碰不到真实世界。你问它「帮我创建一个文件」,它只会输出文本告诉你怎么做,却没法真的执行 touch file.txt。模型被关在一个只能说话的牢笼里。
Agent Loop 要做的就是打破这个牢笼:让模型能持续跟外部世界交互,而不是一问一答就结束。
核心机制
整个 Agent 的骨架只有一个 while True 循环和一个退出条件:
┌─────────────────────────────────────────────────────┐
│ AGENT LOOP │
│ │
│ messages = [system_prompt, user_message] │
│ │
│ while True: │
│ ┌───────────────────────────────────────────┐ │
│ │ response = claude.send(messages) │ │
│ │ │ │
│ │ if response.stop_reason == "end_turn": │ │
│ │ break ← 模型主动说「我做完了」 │ │
│ │ │ │
│ │ if response.stop_reason == "tool_use": │ │
│ │ result = execute_tool(response.tool) │ │
│ │ messages.append(response) ← 累积 │ │
│ │ messages.append(tool_result) ← 累积 │ │
│ │ continue ← 回到循环顶部 │ │
│ └───────────────────────────────────────────┘ │
│ │
│ messages[] 是不断增长的完整对话历史 │
│ 每次 API 调用都带上全部历史 ← 这很贵但很关键 │
└─────────────────────────────────────────────────────┘
关键概念解释
- stop_reasonClaude API 返回的字段,告诉你模型为什么停止输出。"end_turn" 表示模型认为对话结束;"tool_use" 表示模型想调用一个工具。 — 循环的唯一退出信号。不是你决定何时停,是模型自己决定。
- messages[]一个数组,存放整个对话的完整历史——包括系统提示、用户消息、模型回复、工具调用请求、工具执行结果。每次 API 调用都把这个数组完整发给模型。 — 模型的「记忆」。每次循环都会往里追加新消息,模型下一轮就能看到上一轮做了什么。
- tool_result工具执行完毕后返回的结果。它作为一条新消息追加到 messages[] 中,这样模型就能看到工具执行的输出,并据此决定下一步。 — 工具的执行结果,回灌给模型让它做下一步判断。
为什么是 while True 而不是 for 循环?因为你无法预知模型需要几步才能完成任务。一个「帮我重构这个模块」的请求可能需要 3 步,也可能需要 30 步。循环次数由模型的 stop_reason 决定,不由人决定。
messages[] 累积机制详解
这是最容易被低估的设计。来看一个具体例子:
| 循环轮次 | messages[] 内容 | token 数(估算) |
|---|---|---|
| 初始 | system + user_message | ~500 |
| 第 1 轮 | + assistant(tool_call) + tool_result | ~1,200 |
| 第 2 轮 | + assistant(tool_call) + tool_result | ~2,000 |
| 第 3 轮 | + assistant(end_turn) | ~2,300 |
注意:messages[] 只增不减。每一轮的完整 assistant 回复和 tool_result 都保留在数组里。这确保了模型在第 N 轮能看到之前所有轮的上下文——但代价是 token 成本线性增长。这也是 S06(Context Compact)存在的原因。
对应到 Claude Code 官方的什么
QueryEngine.submitMessage()
Claude Code 源码中的核心循环在 QueryEngine 类里。它做的事情和我们的 while True 完全一样,只是多了错误处理、重试、流式输出、权限检查等生产级逻辑。
submitMessage()— 发送消息并进入循环processToolCalls()— 执行工具调用appendToMessages()— 累积消息- 退出条件同样是
stop_reason === "end_turn"
变更对比表
| 维度 | S01 之前(无 Agent) | S01 之后(有 Agent Loop) |
|---|---|---|
| 交互模式 | 一问一答,单次调用 | 持续循环,多步执行 |
| 模型能力 | 只能输出文本 | 可以调用工具并获取结果 |
| 退出控制 | 调用者决定 | 模型自己通过 stop_reason 决定 |
| 上下文 | 单次,无记忆 | messages[] 累积完整历史 |
| 代码量 | — | ~30 行 Python |
Agent Loop 交互模拟器
点击「播放」观察 Agent Loop 的 7 个步骤如何依次执行,消息如何在 messages[] 中累积:
等待开始...
1
初始化 messages = [system_prompt, user_message]
2
发送 messages[] 给 Claude API
3
收到 response: stop_reason = "tool_use" (调用 Bash)
4
执行工具 → 结果追加到 messages[]
5
第二轮:发送更新后的 messages[] 给 API
6
收到 response: stop_reason = "end_turn"
7
break — 循环结束,返回最终结果给用户
messages[] 实时状态
// 等待开始...