首页
/ learn-claude-code s01 深入解析:一个 while 循环 + Bash,如何构建最简 Agent Loop

learn-claude-code s01 深入解析:一个 while 循环 + Bash,如何构建最简 Agent Loop

2026-09-03 16:29:18作者:段琳惟

在 Claude Code 式的 coding agent 中,"智能"来自模型本身,而让模型真正"动手"的,是一个不到 30 行的循环。本文基于 learn-claude-code 课程的第一课文档 s01-the-agent-loop.md 及其配套实现 s01_agent_loop/code.py,完整拆解 Agent Loop 的问题背景、控制流设计、逐行实现与安全边界,读完你既能亲手运行一个最小可用的 agent,也能理解后续 16 课所有机制(工具分发、权限、hooks、子代理等)是如何"包裹"在这个不变的循环之上构建的。

Agent Loop:User prompt → LLM → Tool execute,tool_result 回流直至 stop_reason 不再是 tool_use

一、问题:模型能推理,但碰不到真实世界

语言的模型可以阅读代码、推理逻辑,但它无法"触碰"现实——不能读文件、不能跑测试、不能看报错。没有循环的情况下,每次工具调用都需要人把结果手动复制粘贴回对话,用户本人就成了那个"循环":

"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_use block 一一配对,模型据此知道每个结果对应哪次调用;
  • 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}"

从源码结构看,它做了四件教科书式的事:

  1. 危险命令黑名单:对 rm -rf /sudoshutdownreboot> /dev/ 做子串匹配直接拦截,返回错误字符串而不是抛异常——注意这里返回的是 给模型看的字符串,让模型自己感知"被挡住了"并调整策略;
  2. 超时保护timeout=120 防止模型生成一条卡死的命令(如 sleep 9999)拖住整个循环,超时同样以 "Error: Timeout (120s)" 的形式回填;
  3. 输出截断:stdout+stderr 合并后截到 50000 字符,避免一次 cat 大文件就撑爆上下文(这是 s08 上下文压缩机制的前身问题);
  4. 错误即文本:所有失败路径都收敛为字符串,保证 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.txtanthropic>=0.25.0python-dotenv>=1.0.0pyyaml>=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 原文 的完整四条为准):

  1. Create a file called hello.py that prints "Hello, World!" —— 观察模型如何用 echo ... > 写文件;
  2. List all Python files in this directory —— 观察是否用 find / ls
  3. What is the current git branch? —— 单命令、单轮即止的典型场景,验证 stop_reason != "tool_use" 的退出路径;
  4. 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 HooksPreToolUse / 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 课,你会发现它们回答的只是同一个问题:"在这个不变的循环外面,还可以叠加哪些机制?"

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
980
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384