首页
/ learn-claude-code s01 解析:一个 while 循环 + 单一 Bash 工具如何构成最小 Agent 内核

learn-claude-code s01 解析:一个 while 循环 + 单一 Bash 工具如何构成最小 Agent 内核

2026-09-04 12:12:22作者:胡唯隽

本文基于 learn-claude-code 仓库的 s01 章节(Agent Loop / Agent 循环),拆解"一个工具 + 一个循环 = 一个 Agent"这一最小 Agent 内核的完整实现:从 stop_reason 驱动的控制流设计,到 messages 累积式消息列表,再到 agents/s01_agent_loop.py 中工具执行的安全护栏细节。读完本文,你将能够独立复现并运行这个不到 30 行的 Agent 循环,并理解后续 11 个章节的机制是如何叠加在它之上的。

Agent Loop 循环结构图:User prompt 发给 LLM,LLM 触发 Tool 执行,tool_result 回流到 LLM,循环持续到 stop_reason 不再是 tool_use

问题:没有循环的模型碰不到真实世界

语言模型能推理代码,但它碰不到真实世界——不能读文件、跑测试、看报错。你问它"帮我读取目录下有哪些文件并执行 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 /sudoshutdownreboot> /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 >> ,输入 qexit 或空行退出。

变更内容: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 只依赖 anthropicpython-dotenvpyyaml 三个包。从 .env.example 看,ANTHROPIC_API_KEYMODEL_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 效果更好,也可以用中文):

  1. Create a file called hello.py that prints "Hello, World!"
  2. List all Python files in this directory
  3. What is the current git branch?
  4. 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 的这个循环一行都不会变。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
docsdocs
暂无描述
Markdown
889
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341