learn-claude-code s01 解析:一个 while 循环 + 单一 Bash 工具如何构成最小 Agent 内核
本文基于 learn-claude-code 仓库的 s01 章节(Agent Loop / Agent 循环),拆解"一个工具 + 一个循环 = 一个 Agent"这一最小 Agent 内核的完整实现:从 stop_reason 驱动的控制流设计,到 messages 累积式消息列表,再到 agents/s01_agent_loop.py 中工具执行的安全护栏细节。读完本文,你将能够独立复现并运行这个不到 30 行的 Agent 循环,并理解后续 11 个章节的机制是如何叠加在它之上的。
问题:没有循环的模型碰不到真实世界
语言模型能推理代码,但它碰不到真实世界——不能读文件、跑测试、看报错。你问它"帮我读取目录下有哪些文件并执行 XXX.py",它能输出一条 bash 命令,但输出完了就停了:它不会自己跑这条命令,也不会看到结果后继续推理。
于是你只能手动执行一遍,把输出粘贴回对话框,让它接着干;下一个命令出来,你再跑一遍、再贴回去。每一个来回,你自己就是那个循环。 s01 章节要做的事情,就是把这层"人肉中间件"自动化。
解决方案:一个退出条件控制整个流程
s01 的答案是一个 while True 循环,整个控制流只依赖一个退出条件:
+--------+ +-------+ +---------+
| User | ---> | LLM | ---> | Tool |
| prompt | | | | execute |
+--------+ +---+---+ +----+----+
^ |
| tool_result |
+----------------+
(loop until stop_reason != "tool_use")
循环持续运行,直到模型不再调用工具。整个过程只由两个信号驱动:
| 信号 | 含义 | 循环动作 |
|---|---|---|
stop_reason == "tool_use" |
模型举手说"我要用工具" | 执行 → 结果喂回去 → 继续循环 |
stop_reason != "tool_use" |
模型说"我做完了" | 退出循环 |
这里体现了 harness 工程的核心分工:模型负责决策(要不要调工具、调哪个),harness 负责执行(调用工具、把结果作为新消息追加回去)。循环本身没有任何智能,它只是把模型的每一次"举手"变成真实世界的动作,并把动作结果送回模型的"视野"。
工作原理:把循环翻译成代码
文档将循环拆成 4 个步骤逐一实现。
第 1 步:用户 prompt 作为第一条消息进入累积式消息列表。
messages.append({"role": "user", "content": query})
第 2 步:将消息和工具定义一起发给 LLM。
response = client.messages.create(
model=MODEL, system=SYSTEM, messages=messages,
tools=TOOLS, max_tokens=8000,
)
第 3 步:追加助手响应,然后检查 stop_reason——如果模型没有调用工具,直接返回。
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
return
第 4 步:执行每个工具调用,收集结果,作为一条 user 消息追加,回到第 2 步。
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})
组装为一个完整函数:
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 个章节(工具扩展、权限、hooks、TodoWrite、子 Agent、技能加载、上下文压缩、任务系统、后台任务、Agent 团队、工作树隔离)全部在这个循环上叠加机制——循环本身始终不变。
源码纵深:agents/s01_agent_loop.py 的实现细节
文档给出的是最小骨架,仓库中的可运行实现补充了让它真正"跑起来"的工程细节。
System prompt:给模型一个工作目录。 源码中 system prompt 直接注入当前工作目录,并要求"行动而非解释":
SYSTEM = f"You are a coding agent at {os.getcwd()}. Use bash to solve tasks. Act, don't explain."
把 os.getcwd() 写进 system prompt,模型才知道"我在哪里";"Act, don't explain"则压缩了模型的闲聊倾向,让它直接产出工具调用。
工具定义:只有一个 bash。 s01 的工具池刻意最小化——读文件用 cat、写文件用 echo ... >、找文件用 find,丑但够用,这正是 s01"单一工具"论断的体现:
TOOLS = [{
"name": "bash",
"description": "Run a shell command.",
"input_schema": {
"type": "object",
"properties": {"command": {"type": "string"}},
"required": ["command"],
},
}]
run_bash:循环中唯一的"手"。 agents/s01_agent_loop.py#L65-L77 中的执行函数包含几道关键护栏,这些在文档骨架里是隐藏变量:
- 危险命令黑名单:
rm -rf /、sudo、shutdown、reboot、> /dev/命中即返回Error: Dangerous command blocked,不真正执行。这是最原始的字符串匹配式防护——仓库在 s03 章节会把它升级为完整的权限系统; - 120 秒超时:
subprocess.run(..., timeout=120),超时返回Error: Timeout (120s),防止一条sleep 9999把循环永久挂起; - 输出截断到 50000 字符:
out[:50000],防止cat一个大文件把消息列表撑爆。空输出统一替换为(no output),让模型拿到明确信号而不是空字符串。
值得强调的是:这些"错误"并没有抛出异常中断循环,而是作为普通字符串返回给模型。从源码结构看,这是有意的——模型读到 Error: Timeout (120s) 后可以自己决定换个方式重试,错误处理被纳入了推理循环本身。
多轮 REPL 入口。 agents/s01_agent_loop.py#L104-L120 的 __main__ 维护一个跨轮次的 history 列表:每条用户输入追加为 user 消息后调用 agent_loop(history),循环结束后从 history[-1] 取出模型最后一段 text 打印出来。这意味着多个问题之间上下文是延续的——agent_loop 每次返回时,messages 里已经沉淀了完整的"提问—工具调用—结果"轨迹。
环境适配细节。 源码通过 load_dotenv(override=True) 加载配置;若设置了 ANTHROPIC_BASE_URL 会主动清除 ANTHROPIC_AUTH_TOKEN 以避免鉴权头冲突(agents/s01_agent_loop.py#L44-L52);同时包含一组 readline 绑定修复 macOS libedit 下 UTF-8 退格问题。仓库当前 track 的对应实现在 s01_agent_loop/code.py,逻辑一致,入口提示语为 s01 >> ,输入 q、exit 或空行退出。
变更内容:s01 到底加入了什么
| 组件 | 之前 | 之后 |
|---|---|---|
| Agent loop | (无) | while True + stop_reason |
| Tools | (无) | bash (单一工具) |
| Messages | (无) | 累积式消息列表 |
| Control flow | (无) | stop_reason != "tool_use" |
试一试:运行 s01
安全提示:代码会执行模型生成的 shell 命令,建议在一个临时测试目录中运行,避免影响你的项目文件。仓库在 s03 章节才加入真正的权限控制。
准备(首次运行):
pip install -r requirements.txt
cp .env.example .env # 编辑 .env,填入 ANTHROPIC_API_KEY 和 MODEL_ID
requirements.txt 只依赖 anthropic、python-dotenv、pyyaml 三个包。从 .env.example 看,ANTHROPIC_API_KEY 与 MODEL_ID 为必填项(默认 MODEL_ID=claude-sonnet-4-6),ANTHROPIC_BASE_URL 为可选项,可指向任意 Anthropic 兼容的提供方端点。
运行(legacy 12 课 track):
cd learn-claude-code
python agents/s01_agent_loop.py
仓库当前主推的 17 课 track 中,同一章节的可运行版本是:
python s01_agent_loop/code.py
试试这些 prompt(英文 prompt 对 LLM 效果更好,也可以用中文):
Create a file called hello.py that prints "Hello, World!"List all Python files in this directoryWhat is the current git branch?Create a directory called test_output and write 3 files in it
观察重点:模型什么时候调用工具(循环继续),什么时候不调用(循环结束)?以 prompt 1 为例,仓库的 Web 模拟场景 web/src/data/scenarios/s01.json 完整记录了预期轨迹:模型先发出 echo 'print("Hello, World!")' > hello.py 创建文件(bash 返回空输出表示成功),看到结果后继续推理,再发一条 cat hello.py 验证内容,最后以一段纯文本回复收尾——此时 stop_reason != 'tool_use',循环终止。这正是"创建—验证—收尾"两段式工具调用在循环内的真实形态。
小结与接下来
s01 交付了一个可运行的最小 harness 内核:while True + stop_reason + 累积式消息列表 + 单一 bash 工具。它不是智能本身,而是让模型持续行动的最小运行框架。局限也很直白——模型手里只有 bash,读文件要 cat、写文件要 echo ... >、找文件要 find,又丑又容易出错,且危险命令防护只是字符串匹配。
下一章 s02 Tool Use 会回答这些问题:给它 5 个真正的工具会发生什么?模型会不会一次调用多个工具?几个工具同时跑会不会互相踩?而无论工具池怎么扩展,s01 的这个循环一行都不会变。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00