Claude Code 实战(一):核心闭环 — 从 0 搭建你的第一个 AI Agent
本系列基于 Learn Claude Code 开源教程整理,带你从零手搓一个结构完整的 AI Agent。本文是第一篇,覆盖 Agent 最核心的 6 个机制。
前言
Claude Code 不只是一个聊天工具,它是一个完整的 Agent 系统。要真正理解它,最好的方式是从零开始搭建。
这个系列分 4 篇文章,对应 4 个阶段:
- 核心闭环(本文)— Agent Loop、工具使用、待办写入、子代理、技能系统、上下文压缩
- 系统加固 — 权限系统、Hook 系统、记忆系统、系统提示词、错误恢复
- 任务运行时 — 任务系统、后台任务、定时调度
- 多 Agent 平台 — Agent 团队、团队协议、自主代理、Worktree 隔离、MCP 与插件
第 1 章:Agent 循环
"一个工具 + 一个循环 = 一个 Agent"
问题
语言模型能推理代码,但碰不到真实世界 — 不能读文件、跑测试、看报错。没有循环,每次工具调用你都得手动把结果粘回去。你自己就是那个循环。
解决方案
+--------+ +-------+ +---------+
| User | ---> | LLM | ---> | Tool |
| prompt | | | | execute |
+--------+ +---+---+ +----+----+
^ |
| tool_result |
+----------------+
(loop until stop_reason != "tool_use")
一个退出条件控制整个流程。循环持续运行,直到模型不再调用工具。
核心代码
def agent_loop(query):
messages = [{"role": "user", "content": query}]
while True:
response = client.messages.create(
model=MODEL, system=SYSTEM, messages=messages,
tools=TOOLS, max_tokens=8000,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
return
results = []
for block in response.content:
if block.type == "tool_use":
output = run_bash(block.input["command"])
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output,
})
messages.append({"role": "user", "content": results})
不到 30 行,这就是整个 Agent。后面 11 个章节都在这个循环上叠加机制 — 循环本身始终不变。
关键理解
- 消息历史不是聊天记录,而是模型下一轮要读的工作上下文
- 工具结果必须重新进入消息历史,否则模型无法基于真实观察继续工作
- 循环退出的唯一条件:
stop_reason != "tool_use"
第 2 章:工具使用
"加一个工具,只加一个 handler"
问题
只有 bash 时,所有操作都走 shell。cat 截断不可预测,sed 遇到特殊字符就崩。专用工具(read_file、write_file)可以在工具层面做路径沙箱。
关键洞察:加工具不需要改循环。
解决方案
用 dispatch map 把工具名映射到处理函数:
TOOL_HANDLERS = {
"bash": lambda **kw: run_bash(kw["command"]),
"read_file": lambda **kw: run_read(kw["path"], kw.get("limit")),
"write_file": lambda **kw: run_write(kw["path"], kw["content"]),
"edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]),
}
循环中按名称查找处理函数,循环体与第 1 章完全一致:
for block in response.content:
if block.type == "tool_use":
handler = TOOL_HANDLERS.get(block.name)
output = handler(**block.input) if handler else f"Unknown tool: {block.name}"
路径安全
def safe_path(p: str) -> Path:
path = (WORKDIR / p).resolve()
if not path.is_relative_to(WORKDIR):
raise ValueError(f"Path escapes workspace: {p}")
return path
对比
| 之前 | 之后 |
|---|---|
| 1 个工具(仅 bash) | 4 个工具(bash, read, write, edit) |
| 硬编码 bash 调用 | TOOL_HANDLERS 字典 |
| 无路径安全 | safe_path() 沙箱 |
| 循环需要改 | 循环永远不变 |
第 3 章:待办写入(TodoWrite)
"可见计划不是装饰,而是防止会话漂移的稳定器"
问题
多步骤任务中,模型容易"忘记"自己在做什么。对话越长,模型越容易跳步或重复。
解决方案
给模型一个 todo_write 工具,让它自己维护待办清单:
- 把大任务拆成小步骤
- 标记每个步骤的状态(pending / in_progress / completed)
- 每次循环都能看到当前进度
todos = []
def run_todo_write(items: list) -> str:
global todos
todos = items
return f"Updated {len(items)} todo items"
关键
- 模型自己写 todo,不是外部系统强加
- todo 列表作为上下文的一部分,每轮都可见
- 防止模型跳步、重复或偏离目标
- 这不是装饰,而是 agent 在长对话中保持方向的稳定器
第 4 章:子代理(Subagent)
"把探索性工作移进干净上下文后,父 agent 才能持续盯住主目标"
问题
主 agent 上下文越来越大。探索性任务(搜索代码、分析文件)会快速消耗上下文窗口,同时污染主对话流。
解决方案
启动子代理 — 一个独立的小型 agent 循环,有自己干净的上下文:
def spawn_subagent(task: str, tools: list = None) -> str:
"""在干净上下文中运行一个子任务"""
sub_messages = [{"role": "user", "content": task}]
sub_tools = tools or TOOLS
while True:
response = client.messages.create(
model=MODEL, messages=sub_messages,
tools=sub_tools, max_tokens=4000,
)
sub_messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
return response.content[0].text
# ... 执行工具,继续循环
为什么需要子代理
- 上下文隔离:子代理有独立的 messages,不污染父上下文
- 资源控制:子代理可以用更小的 max_tokens,节省成本
- 并行执行:父 agent 可以启动多个子代理处理不同子任务
- 结果过滤:子代理只返回最终结果,中间过程不污染主对话
第 5 章:技能系统(Skills)
"专门知识不该一开始全部塞进上下文,而该在需要时被轻量发现"
问题
不同任务需要不同的专业知识(API 文档、编码规范、工具用法)。全部塞进 system prompt 会让上下文爆炸。
解决方案
技能系统 = 按需加载的专业知识模块:
- 用户提出任务
- 系统扫描可用技能,发现相关的
- 只加载相关技能的内容到上下文
- 模型根据技能知识完成任务
SKILLS_DIR = Path("skills")
def discover_skills(task: str) -> list:
"""根据任务描述,找到相关技能"""
skills = []
for skill_dir in SKILLS_DIR.iterdir():
if skill_dir.is_dir():
manifest = json.loads((skill_dir / "manifest.json").read_text())
if is_relevant(task, manifest):
skills.append(manifest)
return skills
技能目录结构
skills/
├── api-docs/
│ ├── manifest.json # 技能元数据
│ └── instruction.md # 技能内容
├── coding-style/
│ ├── manifest.json
│ └── instruction.md
└── ...
关键
- 技能 = 可复用的知识包(markdown + 配置)
- 按需发现,按需加载,不浪费上下文
- 社区可以贡献和共享技能
第 6 章:上下文压缩(Context Compact)
"压缩的目标不是删历史,而是保住连续性和下一步所需的工作记忆"
问题
对话越来越长,上下文窗口有限。直接截断会丢失关键信息,agent 会"失忆"。
解决方案
智能压缩:用模型自己总结已完成的工作,保留关键信息:
def compact_messages(messages: list, max_tokens: int = 4000) -> list:
"""压缩消息历史,保留关键上下文"""
# 1. 保留最近几轮完整对话
recent = messages[-6:]
# 2. 让模型总结早期对话
summary_prompt = "请简洁总结以下对话中完成的工作和关键发现:"
old_messages = messages[:-6]
summary_response = client.messages.create(
model=MODEL,
messages=[{"role": "user", "content": summary_prompt + str(old_messages)}],
max_tokens=max_tokens,
)
# 3. 用摘要替代早期对话
return [
{"role": "user", "content": f"[之前工作的摘要] {summary_response.content[0].text}"},
*recent
]
压缩策略
| 策略 | 说明 |
|---|---|
| 保留 system prompt | 系统指令永不压缩 |
| 保留最近几轮 | 最近的工作最重要 |
| 摘要早期对话 | 关键发现不能丢 |
| 保留 todo 列表 | 任务进度必须保持 |
| 保留关键文件内容 | 当前正在处理的文件 |
关键
- 压缩不等于删除,而是用更少的 token 保留等价信息
- 模型自己知道哪些信息重要,让它做摘要比规则截断好
- 压缩后 agent 必须还能继续工作,不能"失忆"
总结
核心闭环的 6 个机制构成了 AI Agent 的基础骨架:
| 章节 | 机制 | 核心价值 |
|---|---|---|
| s01 | Agent 循环 | 模型能行动的基础 |
| s02 | 工具使用 | 扩展能力的边界 |
| s03 | 待办写入 | 防止任务漂移 |
| s04 | 子代理 | 上下文隔离 |
| s05 | 技能系统 | 按需知识加载 |
| s06 | 上下文压缩 | 长对话可持续 |
下一篇我们将进入"系统加固"阶段,学习如何给这个 Agent 加上权限、Hook、记忆、提示词工程和错误恢复。
常见问题
Q: Agent 循环和普通聊天机器人有什么区别?
普通聊天机器人是一次性问答:用户问一句,模型回一句。Agent 循环是持续工作:模型可以自己决定调用工具、查看结果、继续下一步,直到任务完成。区别就像"问一句答一句"和"给个任务自己干完"。
Q: 子代理和主代理共享上下文吗?
不共享。子代理有自己独立的上下文窗口,只接收任务描述,只返回最终结果。这是设计上的隔离,避免子代理的操作污染主代理的上下文。
Q: 上下文压缩会不会丢重要信息?
会丢一些细节,但压缩的目标是保留"对当前任务仍然有价值"的信息。实现方式通常是让辅助模型提炼摘要,而不是粗暴截断。对于代码文件,agent 可以随时重新读取原文。
Q: 技能系统和普通函数有什么区别?
技能是一组预打包的知识 + 模板 + 脚本。普通函数只做一件事,技能可以包含多步操作流程、注意事项、常见陷阱。技能更像是一本"操作手册"而不只是一个工具。
Q: 我需要用 Claude 才能跑这些代码吗?
代码示例用的是 Anthropic 的 API 格式,但核心概念(Agent 循环、工具使用、权限检查)适用于任何 LLM。你可以替换成 OpenAI、Gemini 或本地模型,只需改 API 调用部分。更多配置方法见 Claude Code 完整配置教程。
本文基于 Learn Claude Code 开源项目(GitHub: shareAI-lab/learn-claude-code)整理改编。
相关阅读:
