learn-claude-code s01 精讲:Agent Loop —— 一个循环加 Bash 的最小 Agent Harness 内核
【免费下载链接】learn-claude-codeBash is all you need - A nano claude code–like 「agent harness」, built from 0 to 1项目地址: https://gitcode.com/GitHub_Trending/an/learn-claude-code
本篇围绕 learn-claude-code 仓库第 1 章 s01_agent_loop/README.md 展开:它用不到 30 行 Python 代码构建了一个最小可运行的 Agent Harness 内核,回答“如何把 LLM 从只会输出命令变成能持续执行命令的 Agent”这个问题。读完后你能完整掌握 Agent Loop 的控制信号(stop_reason/tool_use)、消息累积机制、工具结果的回填格式,并能基于 s01_agent_loop/code.py 亲自运行一个带交互式会话的最小编码 Agent。
一、问题:模型只会“说”命令,不会“执行”命令
章节开篇描述的场景非常具体:你让模型“列出目录文件并运行 XXX.py”,模型确实能输出一条 bash 命令,但输出完毕它就停住了——它不会自己执行命令,更不会基于执行结果继续推理。
于是出现了这样一种工作流:你手动执行命令,把输出粘贴回聊天框,模型再产出下一条命令,你再执行、再粘贴。每一轮往返中,你本人充当了模型与世界之间的中间层。s01 要做的事情,就是把这个由人肉充当的“中间层”自动化。
这一点在仓库的另一版文档 docs/en/s01-the-agent-loop.md 中表述得更直白:语言模型能推理代码,但它碰不到真实世界——读不了文件、跑不了测试、看不到报错。“Without a loop, every tool call requires you to manually copy-paste results back. You become the loop.”(没有循环,每次工具调用都要你手动复制粘贴结果回来,你自己就成了那个循环。)
二、解法:一个 while True 循环,只靠两个信号驱动
s01 给出的完整解法是一个while True循环:模型调用工具就继续,不再调用就停。整个过程只依赖两个控制信号,原文明确给出了这张信号表:
| 信号 | 含义 | 循环动作 |
|---|---|---|
stop_reason == "tool_use" | 模型“举手”:我需要工具 | 执行工具 → 把结果喂回去 → 继续循环 |
stop_reason != "tool_use" | 模型说:我做完了 | 退出循环 |
整个流程只有一条出口条件:stop_reason不再是"tool_use"。这意味着“何时停”的决策权完全在模型侧,代码侧只负责执行与回传——这正是仓库 README 中反复强调的分工原则:“The model decides. The harness executes.”(模型决策,Harness 执行。)
三、五步拆解 agent_loop 的实现
README 把实现拆成五步逐步展开,以下按原文骨架完整还原,并与仓库真实源码对齐。
Step 1:以用户问题作为第一条消息
messages = [{"role": "user", "content": query}]Step 2:把消息和工具定义发给 LLM
response = client.messages.create( model=MODEL, system=SYSTEM, messages=messages, tools=TOOLS, max_tokens=8000, )Step 3:追加模型回复,检查是否调用了工具——没调用就结束
messages.append({"role": "assistant", "content": response.content}) if response.stop_reason != "tool_use": returnStep 4:执行模型请求的工具,收集结果
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, })注意这里三个细节:只处理block.type == "tool_use"的内容块(模型回复里可能同时有 text 块);tool_use_id必须原样回传block.id,这是 API 配对工具调用与结果的唯一依据;结果统一包装成tool_result类型。
Step 5:把工具结果作为一条新消息追加,回到 Step 2
messages.append({"role": "user", "content": results})一个容易被忽略但关键的约定:工具结果是以role: "user"的身份回传的,而不是 assistant。这样messages列表的交替结构(user / assistant / user / assistant …)保持不变。
组装成完整函数,就是原文给出的最小内核:
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}) 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 harness 内核。它本身不是智能,而是让模型能够持续行动的最小运行时框架:模型决定(是否调用工具、调用哪个),Harness 执行(调用工具并把结果作为新消息追加)。后续 16 章全部是在这个循环之上叠加机制,而循环本身从未改变。
对照仓库中的 docs/en/s01-the-agent-loop.md,本章前后系统的变化可以总结为:
| 组件 | 之前 | 之后 |
|---|---|---|
| Agent 循环 | (无) | while True+stop_reason判断 |
| 工具 | (无) | bash(一个工具) |
| 消息 | (无) | 不断累积的messages列表 |
| 控制流 | (无) | stop_reason != "tool_use"退出 |
四、深入 code.py:README 之外的真实实现细节
README 展示的是教学骨架,s01_agent_loop/code.py 是可以直接运行的完整版本。两者核心一致,但源码里多出了若干工程细节,值得逐一看清。
4.1 唯一的工具:bash
工具定义只有一个bash,Schema 极简(见 code.py#L57-L65):
TOOLS = [{ "name": "bash", "description": "Run a shell command.", "input_schema": { "type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"], }, }]4.2 工具执行器 run_bash:黑名单、超时与截断
真正的执行器run_bash(code.py#L69-L81)承担了三层防护:
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/五类子串直接拦截,返回错误文本而不抛异常——注意它返回的是字符串,意味着错误会作为tool_result喂回模型,由模型自行决定如何修正,而不是让进程崩溃; - 固定执行边界:
subprocess.run以shell=True、cwd=os.getcwd()执行,即 Agent 的活动范围被限定在启动时的当前目录;timeout=120秒防止命令挂死整个循环; - 输出截断:stdout 与 stderr 合并后截取前 50000 字符,无输出时回退为
"(no output)"。这一层保护了上下文——否则一条cat大文件就可能撑爆下一轮请求。
4.3 系统提示词与工作目录
系统提示词只有一句话(code.py#L54):
SYSTEM = f"You are a coding agent at {os.getcwd()}. Use bash to solve tasks. Act, don't explain."把当前工作目录直接注入提示词,让模型知道自己在哪个目录操作;“Act, don't explain” 则约束模型直接行动而非解释。
4.4 环境配置:.env 与兼容服务商
启动配置集中在文件头部(code.py#L46-L54):
load_dotenv(override=True) if os.getenv("ANTHROPIC_BASE_URL"): os.environ.pop("ANTHROPIC_AUTH_TOKEN", None) client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL")) MODEL = os.environ["MODEL_ID"]- 通过
load_dotenv(override=True)读取.env; - 若设置了
ANTHROPIC_BASE_URL(接入 Anthropic 兼容网关),会主动移除ANTHROPIC_AUTH_TOKEN,避免两套鉴权凭证冲突; MODEL_ID是必填环境变量,缺失时脚本会直接抛KeyError——这是运行时的第一个硬性前提。
仓库根目录的 .env.example 给出了完整模板:必填的ANTHROPIC_API_KEY与MODEL_ID(模板默认claude-sonnet-4-6),可选的ANTHROPIC_BASE_URL,并附有一张兼容服务商对照注释表(MiniMax、GLM/智谱、Kimi/月之暗面、DeepSeek 的对应MODEL_ID与 Base URL,区分国际与大陆端点)。依赖清单 requirements.txt 只有三项:anthropic>=0.25.0、python-dotenv>=1.0.0、pyyaml>=6.0。
4.5 入口:一个支持多轮会话的 REPL
README 的agent_loop(messages)接收一个消息列表,而 code.py 的入口把它包进了一个交互式 REPL:
if __name__ == "__main__": print("s01: Agent Loop") print("Enter a question, press Enter to send. Type q to quit.\n") history = [] while True: try: query = input("\033[36ms01 >> \033[0m") except (EOFError, KeyboardInterrupt): break if query.strip().lower() in ("q", "exit", ""): break history.append({"role": "user", "content": query}) agent_loop(history) # Print the model's final text response response_content = history[-1]["content"] if isinstance(response_content, list): for block in response_content: if getattr(block, "type", None) == "text": print(block.text) print()两个值得注意的点:
- 跨轮上下文保持:
history在 REPL 外定义,每条新指令以 user 消息追加进同一个列表后再调用agent_loop(history)。也就是说,第二句话依然能看到第一句话的完整工具调用与结果历史——多轮会话能力不是额外开发的,它是“消息列表只增不减”这一设计的自然产物; - 最终文本的打印策略:
agent_loop返回时,history[-1]必然是最后一条 assistant 消息(因为循环以“追加 assistant 轮后return”结束),入口从其中取出text类型的块打印到终端。循环执行期间,每条命令会以黄色高亮、其输出前 200 字符会实时打印,方便观察“模型何时调用工具、何时停下”。
另外,文件头部有一段readline配置(code.py#L33-L41),是为 macOS 上 libedit 的 UTF-8 退格问题做的修复,属于纯终端体验优化。
五、运行与验证
5.1 环境准备
安全提示(原文强调):该代码会执行模型生成的 shell 命令。请在临时测试目录中运行,避免影响项目文件。完整的权限控制留到 s03 章节引入。
首次运行:
pip install -r requirements.txt cp .env.example .env # 编辑 .env,填入 ANTHROPIC_API_KEY 和 MODEL_ID5.2 启动
python s01_agent_loop/code.py进入s01 >>提示符后,原文建议尝试这三条指令来观察循环行为:
Create a file called hello.py that prints "Hello, World!"List all Python files in this directoryWhat is the current git branch?
观察要点原文也写明了:注意模型什么时候调用了工具(循环继续),什么时候没调用(循环结束)。前一条会触发echo ... > hello.py之类的写入命令,第三条会触发git branch——它们会分别让你看到“bash 是唯一工具时,读写文件、查仓库状态都要借道 shell”这一事实。
5.3 仓库中如何保证这一章代码可用
- tests/test_chapter_readmes.py 会对所有 17 个章节做三重检查:英文/中文/日文三语 README 齐备、语言导航行一致,并且用
py_compile验证每个章节的code.py均可在 Python 3.11 下编译通过(见 test_every_chapter_script_compiles_on_python_311); - 仓库同时保留了一条旧版 12 课轨道,其中 agents/s01_agent_loop.py 是本章的遗留可运行副本,与新版 s01_agent_loop/code.py 核心逻辑一致(同样只有 bash 工具、同样的
run_bash黑名单与 120 秒超时),由 tests/test_agents_smoke.py 参数化地校验其可编译性。两条轨道的章节编号不完全对应,按 README.md 的说明,新读者应以根目录s01_agent_loop/到s17_goal_loop/的 17 课轨道为准。
六、边界与下一步:为什么必须只有 Bash 起步
s01 的刻意“残缺”本身就是教学设计:此刻模型只有 bash 一件工具——读文件要靠cat,写文件要靠echo ... >,找文件要靠find。丑且易错,但足以验证“一个循环 + 一个工具 = 一个 Agent”这个命题。
README 的 “What's Next” 明确指向下一站:
- s02 Tool Use(s02_tool_use/):给模型 5 个正规工具后会发生什么?模型会一次调用多个工具吗?并行执行的工具会互相踩踏吗?从 s02_tool_use/code.py 可以看到演化的第一步:把 s01 中硬编码的
run_bash调用替换为TOOL_HANDLERS分派映射("bash": run_bash, "read_file": run_read, ...),循环本体原封不动——这正印证了本章的结论:后续所有机制都叠加在循环之上,循环本身从不改变; - s03 Permission(s03_permission/):s01 的
run_bash只有五条子串黑名单,属于演示级别防护;s03 将引入正式的权限规则与审批管线,回答“哪些命令可以直接跑、哪些必须停、哪些需要人工批准”。
七、小结
s01 给出的结论可以压缩成三句话:
- Agent 的核心是一个
while True循环,唯一的出口条件是stop_reason != "tool_use",停与不停由模型决定,Harness 只负责执行与回传; - 消息列表只增不减:assistant 轮追加
response.content,工具结果以tool_result结构(携带tool_use_id配对)作为 user 消息回传,多轮会话能力由此免费获得; - 内核不到 30 行,但工程细节决定可用性:code.py 中的危险命令拦截、120 秒超时、50000 字符输出截断、
.env配置与兼容服务商接入,构成了这个内核在真实环境里可运行的底线。
这就是整个课程的地基:先有一个能转起来的循环,再谈工具、权限、规划、记忆与协作。
【免费下载链接】learn-claude-codeBash is all you need - A nano claude code–like 「agent harness」, built from 0 to 1项目地址: https://gitcode.com/GitHub_Trending/an/learn-claude-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考