learn-claude-code s01 深入解析:一个 while 循环 + Bash,如何构建最简 Agent Loop
在 Claude Code 式的 coding agent 中,"智能"来自模型本身,而让模型真正"动手"的,是一个不到 30 行的循环。本文基于 learn-claude-code 课程的第一课文档 s01-the-agent-loop.md 及其配套实现 s01_agent_loop/code.py,完整拆解 Agent Loop 的问题背景、控制流设计、逐行实现与安全边界,读完你既能亲手运行一个最小可用的 agent,也能理解后续 16 课所有机制(工具分发、权限、hooks、子代理等)是如何"包裹"在这个不变的循环之上构建的。
一、问题:模型能推理,但碰不到真实世界
语言的模型可以阅读代码、推理逻辑,但它无法"触碰"现实——不能读文件、不能跑测试、不能看报错。没有循环的情况下,每次工具调用都需要人把结果手动复制粘贴回对话,用户本人就成了那个"循环":
"One loop & Bash is all you need" —— one tool + one loop = an agent.
这一课的定位是整个课程 harness 层的第一块基石:the loop —— the model's first connection to the real world(循环是模型与真实世界的第一座桥)。原始文档给出的场景很直接:你让模型"列出目录文件并运行 XXX.py",模型输出了一条 bash 命令,但输出完就停了——它不会自己执行,也不会基于结果继续推理。每来一条命令你跑一次、贴回一次,这个"人肉中间层"正是本章节要自动化掉的对象。
二、解决方案:一个退出条件控制整个流程
整个 agent 的控制流只依赖一个信号:stop_reason。
+--------+ +-------+ +---------+
| User | ---> | LLM | ---> | Tool |
| prompt | | | | execute |
+--------+ +---+---+ +----+----+
^ |
| tool_result |
+----------------+
(loop until stop_reason != "tool_use")
两个信号决定了循环的全部行为:
| 信号 | 含义 | 循环动作 |
|---|---|---|
stop_reason == "tool_use" |
模型举手:"我需要一个工具" | 执行工具 → 把结果回填 → 继续循环 |
stop_reason != "tool_use" |
模型说:"我做完了" | 退出循环 |
One exit condition controls the entire flow. The loop runs until the model stops calling tools. 退出条件只有一个:模型不再调用工具。注意决策权在模型侧——什么时候调工具、调哪个、什么时候停,都由模型决定;代码(harness)只负责执行模型的要求并如实回填结果。这正是仓库主文档 README.md 中 "The model decides. The harness executes." 的分工原则。
三、How It Works:四步机制逐段拆解
原始文档把循环拆成四个可独立理解的小步骤,以下每段代码都可在 s01_agent_loop/code.py 中找到对应实现。
Step 1:用户 prompt 成为第一条消息。
messages.append({"role": "user", "content": query})
Step 2:把消息列表和工具定义一起发给 LLM。
response = client.messages.create(
model=MODEL, system=SYSTEM, messages=messages,
tools=TOOLS, max_tokens=8000,
)
Step 3:追加 assistant 回复,检查 stop_reason——模型没调工具就是结束。
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
return
Step 4:执行每个工具调用,收集结果,作为一条 user 消息回填,回到 Step 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})
两个值得注意的 API 语义细节:
- tool_result 必须以 user 角色消息回填,且通过
tool_use_id与 assistant 消息中的tool_useblock 一一配对,模型据此知道每个结果对应哪次调用; - assistant 消息在检查 stop_reason 之前就已入列,这保证了即使本轮就结束,对话历史也是完整、可直接续聊的状态。
组装成完整函数,就是整个 agent 的全部:
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})
That's the entire agent in under 30 lines. Everything else in this course layers on top -- without changing the loop. 课程其余部分(工具、权限、hooks、压缩、任务、团队……)全部是在这个循环"外面"叠加机制,循环本身从未被改动。
四、源码纵深:run_bash 的安全边界与工程细节
文档中抽象的 run_bash(block.input["command"]),在 s01_agent_loop/code.py 中是一个带多重防护的执行器,这是把"教学伪码"变成"可跑的最小 harness"的关键:
def run_bash(command: str) -> str:
dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
if any(d in command for d in dangerous):
return "Error: Dangerous command blocked"
try:
r = subprocess.run(command, shell=True, cwd=os.getcwd(),
capture_output=True, text=True, timeout=120)
out = (r.stdout + r.stderr).strip()
return out[:50000] if out else "(no output)"
except subprocess.TimeoutExpired:
return "Error: Timeout (120s)"
except (FileNotFoundError, OSError) as e:
return f"Error: {e}"
从源码结构看,它做了四件教科书式的事:
- 危险命令黑名单:对
rm -rf /、sudo、shutdown、reboot、> /dev/做子串匹配直接拦截,返回错误字符串而不是抛异常——注意这里返回的是 给模型看的字符串,让模型自己感知"被挡住了"并调整策略; - 超时保护:
timeout=120防止模型生成一条卡死的命令(如sleep 9999)拖住整个循环,超时同样以"Error: Timeout (120s)"的形式回填; - 输出截断:stdout+stderr 合并后截到 50000 字符,避免一次
cat大文件就撑爆上下文(这是 s08 上下文压缩机制的前身问题); - 错误即文本:所有失败路径都收敛为字符串,保证 tool_result 的 content 恒为 str,模型总能基于失败继续推理。
另外几处实现细节决定了它的"生产可用性":
- 工具定义极简:TOOLS 只注册一个名为
bash的工具,input_schema仅要求一个必填的command字符串字段——"Bash is all you need" 名副其实; - System prompt 注入工作目录:
SYSTEM = f"You are a coding agent at {os.getcwd()}. Use bash to solve tasks. Act, don't explain.",让模型知道自己在哪个目录,并被要求"行动而非解释"(见 code.py); - 模型与端点全部来自环境:
MODEL = os.environ["MODEL_ID"],client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL")),兼容任何 Anthropic 协议端点。
五、环境配置与运行:从 0 到跑起来
5.1 依赖与配置
依赖见 requirements.txt:anthropic>=0.25.0、python-dotenv>=1.0.0、pyyaml>=6.0。
pip install -r requirements.txt
cp .env.example .env
# 编辑 .env,填入 ANTHROPIC_API_KEY 与 MODEL_ID
.env.example 中需要关注的变量:
| 变量 | 必填 | 说明 |
|---|---|---|
ANTHROPIC_API_KEY |
是 | Anthropic API Key(示例占位为 sk-ant-xxx) |
MODEL_ID |
是 | 模型 ID,示例默认 claude-sonnet-4-6 |
ANTHROPIC_BASE_URL |
否 | 可选,指向任意 Anthropic 兼容端点(MiniMax / GLM / Kimi / DeepSeek 等) |
需要说明的版本前提:该仓库存在两条教程轨道——当前轨道为根级 s01_agent_loop/ … s17_goal_loop/,而本文所基于的 docs/en/s01-the-agent-loop.md 属于遗留 12 课轨道(对应 agents/ 目录),两条轨道的 s01 主题一致,详见 README.md 的 "Legacy-to-Current Mapping"。
5.2 运行
遗留轨道(与本文档严格对应):
cd learn-claude-code
python agents/s01_agent_loop.py
当前轨道(功能相同的独立可运行版本):
python s01_agent_loop/code.py
安全提示:代码会执行模型生成的 shell 命令,建议在临时测试目录中运行,避免影响项目文件。权限控制(permission)由 s03 引入,本版本仅有黑名单级别的防护。
5.3 交互式 REPL 与多轮历史
入口部分 提供了一个 REPL:s01 >> 提示符下输入问题即执行,输入 q / exit / 空行退出。从源码结构看,history 列表在多条 query 之间持续累积并传入 agent_loop,也就是说该实现天然是多轮会话的——第二次提问时,模型能看到第一次任务的全部 tool 调用与结果。执行过程中每条命令会以黄色 $ <command> 打印、并附带前 200 字符的输出预览,便于人类观察循环进行到哪一步;结束后从最后一条消息中提取 text block 打印模型的最终答复。
六、Try It:四个验证性任务与观察点
原始文档给出的四个试跑 prompt(当前轨道 README 为前三条加观察项,此处以 docs 原文 的完整四条为准):
Create a file called hello.py that prints "Hello, World!"—— 观察模型如何用echo ... >写文件;List all Python files in this directory—— 观察是否用find/ls;What is the current git branch?—— 单命令、单轮即止的典型场景,验证stop_reason != "tool_use"的退出路径;Create a directory called test_output and write 3 files in it—— 多命令串联,验证循环能连续执行多次工具调用。
观察重点(来自章节 README 的提示):什么时候模型调用了工具(循环继续),什么时候没有调用(循环结束)。这正是理解 "agency 来自模型、harness 只负责执行" 的最直接窗口。
七、What Changed:本课引入的四个组件
完整继承原文档的变更对照表:
| Component | Before | After |
|---|---|---|
| Agent loop | (none) | while True + stop_reason |
| Tools | (none) | bash(one tool) |
| Messages | (none) | Accumulating list |
| Control flow | (none) | stop_reason != "tool_use" |
一句话概括:本课从"零"引入了一套完整的消息累积 + 工具执行 + 退出判定机制,构成 harness 的内核。
八、在课程全景中的位置:循环不变,机制叠加
主文档 README.md 给出的 agent 模式伪码与本课实现逐行同构(只是把 run_bash 泛化为 TOOL_HANDLERSblock.name 分发表):
def agent_loop(messages):
while True:
response = client.messages.create(
model=MODEL, system=SYSTEM,
messages=messages, tools=TOOLS,
)
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 = TOOL_HANDLERSblock.name
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output,
})
messages.append({"role": "user", "content": results})
"The loop belongs to the agent. The mechanisms belong to the harness." 后续每一课都是在这个循环外围加一层机制:
- s02 Tool Use:本课后模型只有 bash——读文件要
cat、写文件要echo ... >、找文件要find,丑陋且易错。s02 的回答是"加一个工具 = 加一个 handler":新增 5 个正经工具并注册进分派表,循环本身一行不改(章节文档见 s02_tool_use/README.md); - s03 Permission:把本课的黑名单升级成完整的审批管线("Set boundaries first, then grant freedom");
- s04 Hooks:
PreToolUse/PostToolUse扩展点,"hook around the loop, never rewrite the loop"。
学习路径上,s01 是"让 Agent 行动"阶段的第一步;建议按 s01 → s17 顺序阅读,复杂章节用各目录下的 README 与 code.py 对照学习,章节间关系可参考 s01_agent_loop/README.md 末尾的 "What's Next" 指引。
九、小结
s01 用不到 30 行代码证明了 agent harness 的核心事实:一个累积的 messages 列表、一个 while True、一个 stop_reason != "tool_use" 的退出判定,加上一个把 tool_result 以 user 消息回填的机制,就构成了模型与真实世界之间的完整闭环。 实现层面的防护(命令黑名单、120 秒超时、5 万字符输出截断、错误即文本)则展示了把教学循环变成可运行最小 harness 所需的最低成本工程实践。掌握这个循环后,再去看后续 16 课,你会发现它们回答的只是同一个问题:"在这个不变的循环外面,还可以叠加哪些机制?"
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