从一个循环开始:用 Learn Claude Code 入门 Agent 开发
面向第一次开发 Agent 的工程师,从 Learn Claude Code 的 s01 到 s03 读懂 Agent Loop、工具协议、ReAct、权限边界与完成条件,再给出一条可动手的进阶路线。
第一次接触 Agent 开发,很容易从框架名词开始:ReAct、Memory、MCP、Multi-Agent、Context Engineering。每个词都能找到一套库,装完以后也能跑出 Demo,但模型为什么会连续行动、工具结果怎样回到下一轮、循环为什么会停,仍然藏在框架里面。
Learn Claude Code适合反过来学。它用 17 个递进章节重新搭建一个 Claude Code 风格的 Coding Agent。s01 只有 Agent Loop 和 Bash,s02 加工具注册与分发,s03 把权限判断插到执行之前。后面的规划、子 Agent、Skills、上下文压缩和 Memory 都继续挂在同一个循环上。
本文基于仓库提交 ce8f9f1。它是教学项目,不是 Claude Code 官方源码,也不能代表 Anthropic 产品内部的全部实现。我们用它回答一个更适合入门的问题:一个模型怎样从“能回答”变成“能观察环境、采取动作并根据结果继续工作”的程序?
一、先确认你在开发哪一层
一个最小 Agent 产品可以写成:
Agent 产品 = Model + Harness
Model 根据当前输入生成文本或工具请求。Harness 保存消息、声明工具、执行动作、回传结果,并控制权限和生命周期。大多数应用工程师所说的“开发 Agent”,实际工作集中在 Harness。
这和训练模型是两件事。微调、偏好优化会更新模型参数;给模型增加 read_file、数据库查询或浏览器工具,则是在改变模型能够观察和操作的环境。当前会话的历史、工具结果和项目规则也由 Harness 组织,它们不会因为一次调用自动写进模型参数。
Anthropic 在《Building effective agents》中还区分了 Workflow 与 Agent:Workflow 的代码路径由程序预先决定,Agent 则由模型动态决定过程和工具使用。两者可以组合。发布审批适合固定 Workflow,排查一个陌生代码库则需要模型根据刚读到的文件决定下一步。
如果模型只回答一次文本,程序没有把它的动作落到环境里,也没有把新观察送回来,那仍然是一次普通的模型调用。Agent Loop 补上的就是这段往返。
二、最小 Agent Loop 只有六个动作
先忽略 Memory、规划和多 Agent,一个能够使用客户端工具的循环只做六件事:
- 把任务和历史消息发给模型;
- 同时告诉模型有哪些工具及其参数;
- 从响应中读取
tool_use; - 执行工具;
- 用同一个
tool_use_id回传tool_result; - 没有工具请求时退出。
图里的环路发生在 Harness 内。模型不会自己运行 Shell,也不会在下一次 API 请求中自动记得工具输出。程序必须保留消息,并把模型刚才的响应和真实执行结果按协议追加进去。
Learn Claude Code 的 s01_agent_loop/code.py有 151 行,其中核心形状可以缩成下面这段:
def agent_loop(messages):
while True:
response = client.messages.create(
model=MODEL,
system=SYSTEM,
messages=messages,
tools=TOOLS,
max_tokens=8000,
)
messages.append({
"role": "assistant",
"content": response.content,
})
tool_calls = [
block for block in response.content
if block.type == "tool_use"
]
if not tool_calls:
return
results = []
for block in tool_calls:
output = run_bash(block.input["command"])
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output,
})
messages.append({"role": "user", "content": results})
代码短,是因为 Messages API 已经处理了模型推理与结构化工具请求。Harness 仍然承担了四类状态:messages 保存运行轨迹,TOOLS 定义动作空间,run_bash 把请求变成副作用,循环条件决定这一轮是否继续。
messages 保存完整运行状态
第一轮输入可能只有用户任务:
messages = [
{"role": "user", "content": "找出测试失败的原因并修复"}
]
模型请求执行测试后,轨迹会变成:
user: 找出测试失败的原因并修复
assistant: tool_use(bash, "pytest -q", id="toolu_01")
user: tool_result(id="toolu_01", "1 failed, 8 passed ...")
下一次模型调用会重新接收这三段内容。模型能根据报错继续行动,是因为 Harness 把观察结果放回了 Context。删掉 tool_result,模型就只能猜测试发生了什么;删掉前面的 tool_use,结果又失去了对应动作。
Anthropic 的工具使用文档要求客户端工具结果紧跟对应的工具请求,并用 tool_use_id 关联。并行返回多个工具请求时,也要为每个请求提供结果。这个 ID 是协议关联键,不是业务操作的幂等键,不能用它防止重复付款、重复发消息或重复部署。
Tool Schema 定义模型能采取哪些动作
s01 只向模型暴露一个 Bash 工具:
TOOLS = [{
"name": "bash",
"description": "Run a shell command.",
"input_schema": {
"type": "object",
"properties": {
"command": {"type": "string"}
},
"required": ["command"],
},
}]
名称、描述和 Schema 一起构成模型看到的动作说明。模型先生成一段满足协议的结构化请求,Harness 再把其中的 name 映射到本地实现。
工具描述太宽,模型不知道什么时候该用;参数语义模糊,模型会把路径、ID 和自然语言混在一起;返回值只写 failed,下一轮又没有足够信息修正。工具 API 既服务普通程序,也服务一个根据自然语言描述选择动作的调用者。Anthropic 在工具设计经验中建议从少量、边界清楚、可组合的高影响工具开始,再用评测轨迹迭代。
“没有工具调用”只是循环退出条件
s01 在响应里找不到 tool_use 就返回。这是一条很适合教学的规则,却不能自动证明任务完成。
模型可能因为以下原因停止调用工具:
- 它真的完成了任务;
- 它认为自己完成了,但没有运行测试;
- 输入条件不足,需要用户补充信息;
- API 输出被
max_tokens截断; - 模型拒绝继续;
- 它误判下一步不需要工具。
Messages API 的 stop_reason能够区分 end_turn、tool_use、max_tokens、refusal 等情况。官方停止原因说明要求调用方针对不同原因处理。教学循环只检查内容块,是为了把主干暴露出来;实际系统还应检查停止原因、步数、总耗时和预算,并保留 completed、needs_input、failed、budget_exhausted 等不同状态。
三、这条循环和 ReAct 是什么关系
ReAct 论文研究的是把推理轨迹与任务动作交错,让模型通过外部环境取得新信息,再更新后续判断。经典表达常写成:
Thought → Action → Observation → Thought → ...
Learn Claude Code 没有在普通文本里解析 Action: bash。现代模型 API 直接输出结构化 tool_use,Harness 执行后返回 tool_result。从系统行为看,仍然存在行动与观察的往返:
模型根据当前 Context 选择动作
→ Harness 执行动作
→ 环境产生可观察结果
→ Harness 把结果加入下一轮 Context
→ 模型重新选择动作
这里不需要程序读取或保存模型的私有思维链。可观察、可审计的对象是工具请求、工具参数、工具结果、外部状态变化和最终回答。模型可以在内部完成推理,Harness 只消费协议允许它看到的输出。
ReAct 解释了循环为什么有效:动作让模型接触训练参数之外的当前事实,观察结果又能纠正上一轮判断。但它没有替系统解决权限、幂等、超时、上下文上限和验收。那些都属于 Harness 的责任。
四、先在临时目录运行 s01
仓库当前推荐从根目录的 s01 到 s17 学习,不要把旧版 agents/ 下的 12 章编号混进来。准备步骤以项目的当前 README为准:
git clone https://github.com/shareAI-lab/learn-claude-code.git
cd learn-claude-code
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
# 在 .env 中填写 ANTHROPIC_API_KEY 和 MODEL_ID
s01 会执行模型生成的 Shell 命令。第一次运行时,不要把工作目录放在真实项目、家目录或装有凭据的目录中。可以单独准备一个练习目录:
mkdir -p /tmp/agent-loop-lab
cd /tmp/agent-loop-lab
python /path/to/learn-claude-code/s01_agent_loop/code.py
然后给它一个只读任务:
List the files in this directory, inspect any Python files,
and explain what the project currently does. Do not modify files.
运行时先观察四件事:
- 模型返回文本还是
tool_use; - 每次工具请求的参数是什么;
- Shell 输出怎样成为下一条
tool_result; - 模型在哪一轮停止调用工具。
先别急着做网页、数据库或多 Agent。只要能把一条完整轨迹解释清楚,就已经理解了 Agent 的最小运行机制。
151 行里,循环之外还有什么
s01 文件共有 151 行,但循环主体只占其中一小段。其余代码处理环境变量、终端输入、Windows 与 Unix 差异、超时、输出截断和异常。这些代码没有偏离主题,反而说明 Harness 必须接住模型之外的现实条件。
例如 run_bash 设置 120 秒超时,并把输出截到 50,000 个字符。超时防止单条命令永远占住循环,截断避免一个巨型日志立刻吞掉 Context。代价也很清楚:定位问题所需的报错可能位于被截掉的后半段。更完整的实现会把大结果落盘,返回摘要和文件位置,让模型按需读取。
文件里的危险命令字符串检查只能减少练习时的明显误操作。它不是 Shell 安全解析器,也没有操作系统级隔离。只要 Agent 拿到了通用 Shell,字符串变体、脚本文件、子进程和间接调用都可能绕开简单列表。这个边界要在学习 s03 时继续处理。
五、s02 把工具做成可扩展的动作空间
只给 Bash 的好处是代码少,问题是模型必须把“读文件”“修改精确片段”“查找路径”翻译成 Shell 语法。不同操作系统的命令不同,转义和路径也容易出错。模型还可能为了一次简单读取,生成拥有更大权限的命令。
s02_tool_use/code.py保留原来的循环,新增四个文件工具和一张分发表:
TOOL_HANDLERS = {
"bash": run_bash,
"read_file": run_read,
"write_file": run_write,
"edit_file": run_edit,
"glob": run_glob,
}
for block in tool_calls:
handler = TOOL_HANDLERS.get(block.name)
output = (
handler(**block.input)
if handler
else f"Unknown: {block.name}"
)
增加新工具时,开发者做两件事:把工具说明加入 TOOLS,再把本地函数注册进 TOOL_HANDLERS。Agent Loop 不需要知道 read_file 怎样读文件,也不用为每个工具新增分支。
这形成了三个不同层次:
| 层 | 面向谁 | 负责什么 |
|---|---|---|
| Tool Schema | 模型 | 说明动作名称、用途和参数 |
| Dispatcher | Harness | 把工具名路由到实现 |
| Handler | 外部环境 | 执行真实操作并返回结果 |
分层以后,工具实现可以单独测试,权限也能按工具类型处理。只读工具默认放行,精确编辑要求路径位于工作区,删除或外部写入则需要审批。
safe_path 展示了边界应该放在哪里
s02 的文件工具先把用户参数解析到工作目录,再检查最终路径是否仍在工作区:
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
检查 resolve() 后的路径很重要。只检查字符串是否以工作目录开头,挡不住 ../../secret 和一部分符号链接路径。权限规则应约束最终资源,而不是相信模型提供的原始字符串。
不过 safe_path 只保护四个专用文件工具,Bash 仍然可以访问更广的路径。这里能得到一条实用经验:专用工具的权限更容易描述和验证,通用 Shell 则需要进程级沙箱、文件系统限制和网络策略。给模型一个“万能工具”会降低工具数量,也会放大权限面。
多个工具请求不等于可以安全并行
模型可能在一次响应中生成多个 tool_use。s02 按内容块顺序逐个执行,行为简单且可预测。生产系统可以并行执行互不依赖的只读调用,例如同时读取几个文件;写文件、运行迁移或依赖前一步输出的动作仍要串行。
是否并行不能只看“模型一次返回了几个调用”。Harness 需要知道工具是否只读、是否访问同一资源、有没有顺序依赖,以及失败后是否能重试。否则一次响应里的两个编辑可能互相覆盖。
六、用一条修复轨迹看懂数据怎样流动
假设练习仓库里有一个 duration.py,parse_duration("1h30m") 错误地返回 30,测试要求它返回 90。下面是一条示意轨迹,格式与 s02 的工具协议一致,但具体动作仍由所选模型和 Context 决定。
初始目标和验收条件是:修复组合时长解析,并让现有测试通过,不修改测试文件。
user
修复 parse_duration 对 "1h30m" 的处理。
不要修改测试,完成前运行测试。
assistant → tool_use
glob({"pattern": "**/*.py"})
tool_result
duration.py
test_duration.py
assistant → tool_use
read_file({"path": "duration.py"})
read_file({"path": "test_duration.py"})
tool_result
返回两个文件的内容
assistant → tool_use
edit_file({
"path": "duration.py",
"old_text": "...",
"new_text": "..."
})
tool_result
Edited duration.py
assistant → tool_use
bash({"command": "python -m unittest -q"})
tool_result
Ran 4 tests in 0.01s\nOK
assistant
说明修改位置和测试结果
这条轨迹里有三种证据。源文件说明当前实现,测试文件给出约束,测试进程的退出与输出支持“修复完成”。模型生成的最终总结不是独立证据,它只是引用前面已经发生的环境事实。
如果 edit_file 返回 text not found,下一轮应重新读取文件或缩小匹配范围;继续提交同一个编辑只会重复失败。如果测试命令超时,Harness 应返回超时状态,而不是空输出。错误结果要保留动作、原因和可修正信息,模型才有条件换一条路径。
七、s03 在副作用之前插入权限闸门
工具 Schema 告诉模型“能请求什么”,不代表所有请求都应该执行。s03 在 Dispatcher 前增加权限管线:
s03_permission/code.py把决策拆成三段:
- 硬拒绝列表处理任何时候都不允许的操作;
- 规则匹配识别工作区外访问和潜在破坏性命令;
- 命中规则后暂停,由用户批准或拒绝。
循环只多了一个检查:
if not check_permission(block):
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": "Permission denied.",
})
continue
拒绝结果仍然回到模型。这样模型知道动作没有发生,可以选择只读方案、修改目标路径或请求用户解释。若 Harness 直接丢掉被拒绝的调用,消息协议会缺失对应的 tool_result,模型也无法区分“还在执行”和“明确拒绝”。
教学权限规则不能直接当安全产品
仓库文档明确说明,简单字符串拒绝表只是为了展示闸门位置。真实 Shell 命令可以通过变量、脚本、解释器、编码和子进程表达同一副作用。rm -rf / 没出现,不等于命令安全。
一套更可信的边界通常分成几层:
| 层 | 例子 | 能解决什么 |
|---|---|---|
| 工具层 | 只提供 read_file、search、apply_patch |
缩小动作空间 |
| 参数层 | 规范化路径、Schema 校验、资源白名单 | 拒绝越界输入 |
| 策略层 | allow / ask / deny、风险分级 |
表达用户授权 |
| 运行层 | 容器、工作树、文件系统和网络沙箱 | 限制实际影响范围 |
| 审计层 | 保存请求、批准者、结果与变更 | 支持追踪和恢复 |
Prompt 可以提醒模型不要删除文件,但不能替代这些执行检查。模型负责提出动作,Harness 决定动作是否具备授权和运行条件。
八、从 s04 到 s17,增加的是哪类责任
前三章已经形成最小骨架:循环负责推进,工具连接环境,权限控制副作用。后续章节没有替换这条骨架,而是把长任务暴露出的责任逐步挂上去。
可以把 17 章压缩成五段学习路线:
| 阶段 | 章节 | 新增责任 | 先观察什么 |
|---|---|---|---|
| 能安全行动 | s01-s04 | Loop、工具、权限、Hooks | 一次动作怎样请求、执行和回传 |
| 能处理长任务 | s05-s09 | Todo、子 Agent、Skills、压缩、Memory | 状态放在哪里,哪些信息进入 Context |
| 能跨时间运行 | s10-s12 | 任务依赖、后台命令、Cron | 进程和会话结束后什么仍然存在 |
| 能协作与扩展 | s13-s15 | Agent Teams、MCP、集成 Harness | 隔离、通信、认领和工具命名空间 |
| 能编排并收口 | s16-s17 | Workflow Journal、Goal Loop | 谁判断完成,失败后从哪里恢复 |
Hooks:给扩展逻辑稳定插口
权限、日志和自动格式化如果都直接写进 agent_loop,循环会很快变成大量条件分支。s04 引入工具执行前后的 Hook,让审计、阻止或结果加工成为可注册扩展。主循环仍然只负责状态推进。
Hook 的价值在于调用时机和契约固定,不在于名字。一个 PreToolUse Hook 必须能看到工具名与参数,并且有明确的允许、拒绝或改写语义;PostToolUse 要区分成功、失败和结果未知。否则它只是另一个散落的回调列表。
Todo、子 Agent 与 Skills:管理任务和 Context
s05 把计划做成工具状态,模型可以创建、更新和完成 Todo。计划不应只存在于一段自然语言里,否则上下文压缩后很难继续维护。s06 给子任务一份新的 messages[],主 Agent 只接收最后结果,从而隔离探索噪声。
s07 的 Skills 采用渐进加载。常驻 Context 只放技能名称和简介,选中后再读取完整说明与资源。它解决的是“有哪些知识可用”和“本轮真正需要哪些知识”的分离,不是把更多 Markdown 一次性塞进系统提示。
Compact 与 Memory:一个管当前会话,一个管跨会话知识
s08 处理 Context 预算:先控制巨型工具结果,再裁剪可重建内容,最后才用摘要替换较早历史。压缩会损失细节,因此关键目标、文件状态和未完成事项不应只依赖一段自由摘要。
s09 的 Memory 面向跨会话复用,拆成选择、提取和整理。聊天历史的每句话都保存下来不等于有效记忆。可复用事实需要来源、范围和更新机制,过期结论也要能被合并或删除。
Task、Teams、Workflow 与 Goal:让“结束”成为系统决策
s10 以后,任务状态开始持久化,后台进程和定时触发拥有独立生命周期;s13 增加队友通信、原子认领和任务绑定工作目录;s14 把 MCP 工具接进同一工具池;s15 展示多种机制如何回到一个 Harness。
s16 适合路径相对固定的 Workflow,把编排写进代码并用 Journal 恢复。s17 则在 Agent 准备结束时增加独立目标判断。两章正好说明 Workflow 与 Agent 可以协作:开放探索交给模型,固定流程与完成闸门交给程序。
九、入门项目:先做一个只读 Repo Scout
第一次自己写 Agent,不建议复制 s15 的完整 Harness。做一个只读代码库侦察器更容易看清机制,也降低误操作风险。
它只需要三个工具:
glob(pattern) 查找文件
read_file(path, start, n) 分段读取文本
search(query, path) 搜索符号或关键字
任务可以定义为:读取陌生 Python 项目,找出入口、核心模块、测试命令和一个潜在风险,最后给出每条结论对应的文件位置。这个任务没有写操作,但需要模型多轮选择信息,已经足够体现 Agent 与一次性问答的差别。
第一版只实现四条不变量
- 每个
tool_use都产生对应的tool_result; - 所有路径解析后必须位于工作区;
- 循环有最大步数、总耗时和输出长度限制;
- 最终结论必须引用本轮实际读取过的文件。
前三条可以由代码确定检查。第四条可以先做简单验证:记录成功读取的路径,最终回答里出现的证据路径必须属于这个集合。它不保证分析正确,但能拦住一部分凭空引用。
第二版再增加一个受控编辑工具
只读版本稳定后,再增加 edit_file(path, old_text, new_text)。执行前检查:
- 路径是否在工作区;
old_text是否恰好出现一次;- 文件是否在允许类型中;
- 这次任务是否获得写权限;
- 修改后 diff 是否超过范围限制。
编辑成功不等于任务完成。再提供一个有限的测试工具,或允许特定命令前缀,例如 python -m unittest。让 Agent 用测试结果支持完成声明。
用固定任务集判断改动有没有变好
准备十个很小的练习仓库,每个仓库有明确答案:入口文件在哪里、哪个测试失败、允许修改哪些文件。每次调整工具描述、模型或 Prompt 后重复运行,记录:
| 指标 | 要回答的问题 |
|---|---|
| 任务成功率 | 最终状态是否满足验收条件 |
| 无效工具率 | 有多少调用没有带来新信息 |
| 越界请求数 | 模型提出了多少未授权动作 |
| 平均轮数 | 完成同类任务需要几次模型调用 |
| Context 消耗 | 工具定义和结果占用了多少 Token |
| 假完成率 | 模型声称完成但外部检查失败多少次 |
Agent 开发很难只靠“这次看起来挺聪明”判断。固定任务和完整轨迹能把变化落到具体故障上:是工具没选对、结果不可用、Context 被污染,还是完成条件太松。
十、初学者最常遇到的六类故障
模型始终不调用工具
先检查任务是否明确要求行动,以及工具名称、描述和参数是否足够具体。用户说“你觉得这个文件怎么改”时,模型给建议可能完全符合请求;要它执行,应明确“读取文件并完成修改”。不要一开始就靠强硬 Prompt 强制所有请求调用工具,普通问答并不需要副作用。
模型传出的参数不合法
Schema 应该尽量把约束前移:必填字段、枚举、数字范围和清晰描述都要写明。Handler 仍需校验,因为模型输出和外部输入都不可信。错误结果要指出字段、收到的值和允许范围,方便下一轮修正。
工具结果迅速撑满 Context
给读取与搜索工具增加分页、行数和结果数限制。大输出落盘后返回摘要、总量和位置。静默截断最危险,因为模型会把不完整结果当成完整事实。必须在返回值里明确说明还有多少内容未展示。
Agent 重复同一个失败动作
记录规范化后的工具名和参数。如果连续调用完全相同,且上一次失败结果没有变化,Harness 可以返回“重复调用未产生新证据”,达到阈值后停止。更重要的是让第一次错误包含可行动信息,避免模型只能盲试。
Agent 说完成了,但没有证据
把完成条件写成环境可检查的状态。例如测试退出码为 0、目标文件存在、API 返回指定字段。模型自评可以补充解释,不能取代这些检查。s17 的 Goal Loop 是这类机制的后续教材。
权限提示太多,用户开始无脑批准
权限粒度过粗时,读操作和高风险写操作都会弹窗。按工具和资源范围分类,安全的常见操作直接允许,明确危险的操作直接拒绝,只把少量上下文相关决策交给用户。审批界面要展示将执行的具体动作和影响范围。
十一、Learn Claude Code 和 Hello-Agents 怎么选
两个项目都能作为入门材料,教学角度不同。
Hello-Agents是一套更广的系统教程,覆盖 Agent 基础、经典范式、框架、多 Agent 和应用案例。适合先建立概念地图,再按章节学习不同范式。
Learn Claude Code 更像一组连续的源码实验。每章只增加一项 Harness 机制,并保留独立可运行的 code.py。如果你的目标是亲手看见 messages、tool_use、Dispatcher、权限和压缩怎样进入循环,它更适合作为第一条实作主线。
一种省力的组合方式是:先读本文并动手完成 s01-s04,建立运行时直觉;再用 Hello-Agents 补 ReAct、Planning、Reflection 和多 Agent 的概念背景;最后回到 Learn Claude Code 的 s05-s17,观察这些概念怎样落实为状态和协议。
十二、读完以后应该能回答什么
入门 Agent 开发不要求先掌握所有框架。你至少应该能沿着一条真实轨迹回答这些问题:
- 当前任务和历史存放在哪里;
- 模型从哪里知道有哪些工具;
- 工具请求怎样映射到真实函数;
- 执行结果怎样与请求配对并进入下一轮;
- 哪一层决定允许、询问或拒绝;
- 为什么没有继续调用工具不等于已经完成;
- 哪些状态只属于当前 Context,哪些需要跨会话持久化;
- 如何用外部证据判断一次运行是否成功。
能回答这些问题后,再学习 Memory、MCP 和 Multi-Agent,新增机制就有了落点。它们是在解决上下文、扩展和协作问题,没有改变最里面那条循环:模型选择动作,Harness 执行动作,环境返回观察,下一轮基于新事实继续。
参考资料
如果这篇文章对你有帮助