本文是「从零理解 Claude Code:20 个 Agent Harness 机制」系列的第 2 篇。
源码仓库:shareAI-lab/learn-claude-code
Claude Code 能读文件、跑命令、改代码,看起来像是一个住在终端里的开发者。
但把它最核心的逻辑抽出来,代码其实不到 30 行。
没有任务编排,没有工作流画布,没有多 Agent,也没有什么神秘提示词。它做的事情很直接:
模型要调用工具 → 程序执行工具 → 把结果交回模型 → 模型决定是否继续把这段过程写成代码,就是一个while True。
这一篇不讨论复杂架构,只把这件事讲明白:模型为什么会调用工具、工具结果为什么还要发回模型、循环又是怎么停下来的。
一、模型会写命令,为什么还不算 Agent
先看一个很普通的需求:
列出当前目录下的 Python 文件,然后运行 demo.py。如果把这句话发给普通聊天模型,它通常知道该怎么做:
find.-name"*.py"但它把命令写出来后,任务就中断了。
因为模型没有终端,也没有文件系统。它不能亲自执行这条命令,更看不到命令输出。
接下来只能由人接手:
- 复制模型给出的命令;
- 打开终端执行;
- 复制输出结果;
- 粘贴回聊天框;
- 等模型给出下一步。
如果模型接着要求运行:
python demo.py人就再做一遍。
这时模型负责思考,终端负责执行,人负责在两者之间传递信息。
最小 Agent Loop 的作用,就是把这段人工操作交给程序。
二、先看最终代码
这一章的核心代码如下:
defagent_loop(messages):whileTrue:response=client.messages.create(model=MODEL,system=SYSTEM,messages=messages,tools=TOOLS,max_tokens=8000,)messages.append({"role":"assistant","content":response.content,})ifresponse.stop_reason!="tool_use":returnresults=[]forblockinresponse.content:ifblock.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,})先不用急着逐行看。
这段代码只有一个判断:
ifresponse.stop_reason!="tool_use":return模型没有调用工具,就结束。
模型调用了工具,就执行工具,把结果追加回消息列表,再重新请求模型。
整个循环的控制权并不在程序手里,而在模型手里。
三、第一步:模型不是直接执行命令,而是请求工具
模型回复时,可能返回普通文本,也可能返回工具调用。
例如,模型希望搜索 Python 文件,它返回的不是终端输出,而是一份工具调用请求:
工具名称:bash 参数: { "command": "find . -name \"*.py\"" }程序从模型响应中取出工具参数:
forblockinresponse.content:ifblock.type=="tool_use":output=run_bash(block.input["command"])这里真正执行命令的是run_bash。
output=run_bash(block.input["command"])模型负责说自己想做什么;程序负责真正调用 Shell。
这个边界很重要。
| 角色 | 工作 |
|---|---|
| 模型 | 决定调用什么工具、传什么参数 |
| 程序 | 执行工具调用 |
| Shell | 在操作系统中执行命令 |
| 工具结果 | 把真实环境的反馈带回模型 |
模型不是直接拥有电脑权限,而是通过工具请求间接影响环境。
四、第二步:工具执行完,为什么还要再发回模型
命令执行以后,程序可以拿到输出。
例如:
find.-name"*.py"返回:
./demo.py ./hello.py这份结果不能只打印在终端里。
模型必须看到它,才能知道下一步应该怎么做。
所以代码会把工具结果包装成tool_result:
results.append({"type":"tool_result","tool_use_id":block.id,"content":output,})然后追加到messages:
messages.append({"role":"user","content":results,})这里的tool_use_id用来把结果和之前那次工具调用对应起来。
模型之前说的是:
我要调用 bash,执行 find . -name "*.py"程序返回时,相当于在说:
你刚才请求执行的那条命令,结果在这里。如果没有这一步,模型就不知道命令到底有没有成功,也不知道输出内容是什么。
五、messages里保存的是整个执行现场
第一次调用模型前,消息列表里只有用户任务:
messages=[{"role":"user","content":"列出当前目录下的 Python 文件,然后运行 demo.py。"}]随着循环继续,messages会不断追加内容。
最终,它大致会记录成这样:
用户:列出当前目录下的 Python 文件,然后运行 demo.py。 助手:调用 bash 命令:find . -name "*.py" 工具结果: ./demo.py ./hello.py 助手:调用 bash 命令:python demo.py 工具结果: Hello, World!下一轮请求模型时,这些内容会一起发送。
因此模型不是凭空知道demo.py存在,也不是凭空知道程序已经运行成功。
它是通过前面工具返回的结果,一步一步获得环境信息。
这也是为什么 Agent 不只是一次 API 调用。
一次 API 调用只会产生一段回答;Agent Loop 会让模型根据新信息持续更新下一步动作。
六、一次完整任务是怎么跑完的
还是看运行demo.py这个任务。
第 1 轮:搜索文件
模型调用:
find.-name"*.py"程序执行命令后,返回:
./demo.py ./hello.py模型看到结果,知道目标文件存在。
第 2 轮:运行程序
模型继续调用:
python demo.py程序执行后,返回:
Hello, World!模型看到输出,知道程序已经正常运行。
第 3 轮:结束任务
这时模型不再需要工具,直接回复:
当前目录中找到了 demo.py,程序已成功运行,输出为 Hello, World!由于这一轮没有工具调用:
response.stop_reason!="tool_use"循环执行return,任务结束。
可以把整个过程看成下面这样:
| 轮次 | 模型做什么 | 程序做什么 |
|---|---|---|
| 第 1 轮 | 请求搜索文件 | 执行find,返回结果 |
| 第 2 轮 | 请求运行文件 | 执行python demo.py,返回结果 |
| 第 3 轮 | 输出最终回答 | 不再执行工具,结束循环 |
程序没有预先写死任务一共三步。
模型根据每一轮返回的结果,自己决定是否继续行动。
七、循环为什么不会无限执行
很多人看到while True的第一反应是:它不会一直跑下去吗?
答案是,不会。
循环的出口就是这段代码:
ifresponse.stop_reason!="tool_use":return只有模型明确请求工具,循环才继续。
| 模型返回状态 | 程序行为 |
|---|---|
tool_use | 执行工具,把结果交回模型 |
| 普通文本回复 | 结束循环 |
因此,程序不需要写死执行顺序。
它不需要规定:
先读文件 再找文件 再运行测试 最后总结不同任务的路径不一样。
有时模型先看日志,有时先读配置,有时根本不需要工具。循环只负责确保:模型需要行动时,有工具可用;工具运行后,结果能回到模型手里。
八、这个最小版本还缺什么
这段循环足够让模型开始行动,但它还只是教学版。
目前它只有一个bash工具,能做的事情比较粗。
而且代码虽然拦截了少量危险命令:
dangerous=["rm -rf /","sudo","shutdown","reboot"]但这显然不是完整的权限系统。
真实的 Agent 还要处理很多问题:
- 文件读写应该用专门工具,还是全部交给 Shell;
- 哪些操作可以直接执行,哪些操作必须让用户确认;
- 命令超时和执行失败怎么处理;
- 上下文太长时,历史消息怎么压缩;
- 多个 Agent 同时工作时,如何避免互相干扰。
这些会在后面的章节中逐步补上。
但它们都建立在同一个基础上:
模型发起工具调用 → 程序执行 → 结果回传 → 模型继续判断九、动手验证
如果想自己跑一遍,可以在临时目录中配置好环境后执行:
python s01_agent_loop/code.py建议先试这几个简单任务:
List all Python files in this directoryCreate a file called hello.py that prints Hello, World!What is the current git branch?运行时不用急着看模型最后说了什么,先观察终端里打印出的命令。
重点看两个问题:
- 模型什么时候开始调用
bash; - 命令返回结果后,模型为什么会继续调用工具,或者为什么会结束。
把这两个问题看明白,Agent Loop 的核心就已经掌握了。
小结
Claude Code 看起来复杂,但最底层的行动方式并不复杂。
模型不会直接读文件,也不会直接执行命令。它只能提出工具调用请求。
程序执行请求,把真实结果交回模型;模型看到结果后,再判断是否继续。
while True不是为了让程序无限运行,而是为了让模型在任务没有结束前,能够持续获得行动和观察的机会。
下一篇会继续在这个循环上增加能力:当 Agent 不再只有一个bash,而是拥有读文件、写文件、编辑文件和搜索文件等工具时,工具应该怎样组织,才不会把主循环写乱?
参考资料:
- 最小 Agent Loop:源码与文档
- learn-claude-code 项目仓库