这是本项目的核心学习文档。先读懂这篇,再去看 roadmap 里的里程碑,你会知道每一步在复现什么。
Agent = LLM + 工具 + 循环。
Claude Code、Codex CLI、Cursor Agent 这些产品,剥掉外壳后核心都是同一个循环:
用户输入
│
▼
┌─────────────────────────────────────────┐
│ while True: │
│ response = LLM(messages, tools) │◄─── system prompt(性格+操作手册)
│ if 模型没有要调工具: │
│ break # 说完了,还话筒给用户 │
│ for 每个工具调用请求: │
│ 权限检查(allow/ask/deny) │
│ result = 本地执行工具 │
│ messages += 工具结果 │
└─────────────────────────────────────────┘
│
▼
显示回复,等下一次用户输入
关键洞察(这是初学者最容易想复杂的地方):
- 模型从不"执行"任何东西。 模型只输出"我想调用 read_file,参数是 {...}"这样的结构化意图,真正执行的是你写的本地 Python 代码,执行结果再作为消息喂回去。
- 消息列表是唯一的状态。 没有状态机、没有工作流图。对话历史(用户消息、模型回复、工具调用、工具结果)就是 agent 的全部记忆。
- 智能在模型里,不在框架里。 循环代码不到 100 行;agent 显得聪明,是因为模型本身会规划。工程代码的职责是把工具、权限、上下文伺候好。
┌────────────────────────────────────────────────┐
│ 交互层 终端 REPL / 流式渲染 / slash 命令 │ → cli.py (M0, M8)
├────────────────────────────────────────────────┤
│ 上下文层 system prompt / CLAUDE.md 记忆 / │ → prompts.py (M4)
│ token 预算 / compaction 压缩 │ → context.py (M6)
├────────────────────────────────────────────────┤
│ 核心层 agent 循环(唯一的调度中枢) │ → agent.py (M1) ⭐
├────────────────────────────────────────────────┤
│ 权限层 工具执行前的统一守门人 │ → permissions.py (M3)
├────────────────────────────────────────────────┤
│ 工具层 Read/Edit/Write/Bash/Glob/Grep/Task │ → tools/ (M1,M2,M5,M7)
└────────────────────────────────────────────────┘
一个工具由四部分组成:name、description、input_schema(JSON Schema)、本地执行函数。前三个会随每次 API 请求发给模型——模型选不选、会不会用对一个工具,几乎完全取决于 description 写得好不好。Claude Code 的每个工具 description 都是几百词的精心之作,写明了何时该用、何时不该用、和其他工具怎么配合。
同理,工具的错误信息也是 prompt:edit_file 匹配失败时返回「old_str 匹配到 3 处,请提供更多上下文使其唯一」,模型下一轮就会自我纠正。错误信息写得好,agent 就显得"会自愈"。
工具自己不做权限判断(机制),统一由 permissions 模块决策(策略):每个工具声明危险级别(read/write/execute),权限模式(default / accept-edits / yolo)+ 会话白名单决定 allow / ask / deny。用户拒绝时,拒绝理由作为 tool_result 返回模型,模型会换个做法——拒绝也是对话的一部分。
这是 coding agent 最核心的工程问题。三个手段:
- 入口截断:工具输出限长(bash 截 2000 字符、read 分页)——脏东西不进上下文
- compaction:接近窗口上限时,用模型把旧历史总结成摘要替换掉,保留任务目标和最近几轮
- 子 agent 隔离:见 3.4
搜索类任务("全项目找 X")会产生大量中间垃圾(几十次 grep/read 的输出)。Task 工具把这种任务派给一个全新消息列表的子 agent,它在自己的上下文里翻箱倒柜,只把最终结论一段话带回主上下文。两条铁律:子 agent 不能再派子 agent(防递归爆炸);子 agent 通常只给只读工具。
| 层 | Claude Code | 我们 | 生命周期 |
|---|---|---|---|
| 系统提示词 | 内置 | prompts.py | 每次请求 |
| 项目记忆 | CLAUDE.md | AGENT.md | 跨会话,随项目 |
| 会话历史 | messages | messages | 单会话(可持久化) |
| Claude Code 有 | 我们的取舍 |
|---|---|
| 20+ 个工具、每个 description 数百词 | 8 个左右核心工具 |
| 并行工具调用、后台任务 | 串行执行,够学习用 |
| MCP、hooks、插件生态 | M9 可选 |
| 精细的 allowlist 规则语法 | 三档模式 + 会话白名单 |
| 多模型路由(Haiku 干杂活) | 单模型 |
| IDE 集成、GitHub Actions | 无 |
简化的原则:保留每一层的"思想",砍掉每一层的"规模"。
- 读完本文 → 做 M1,亲手跑通循环(这一步价值最大)
- 每完成一个里程碑,回来重读对应小节,对比理解
- M6 做完后,重点复盘:没有上下文管理时 agent 是怎么"变笨"的
- 全部做完后,读一遍 Claude Code 官方文档的 agent loop 章节,看看真实实现多了什么