
本文是「从零理解 Claude Code20 个 Agent Harness 机制」系列的第 2 篇。源码仓库shareAI-lab/learn-claude-codeClaude Code 能读文件、跑命令、改代码看起来像是一个住在终端里的开发者。但把它最核心的逻辑抽出来代码其实不到 30 行。没有任务编排没有工作流画布没有多 Agent也没有什么神秘提示词。它做的事情很直接模型要调用工具 → 程序执行工具 → 把结果交回模型 → 模型决定是否继续把这段过程写成代码就是一个while True。这一篇不讨论复杂架构只把这件事讲明白模型为什么会调用工具、工具结果为什么还要发回模型、循环又是怎么停下来的。一、模型会写命令为什么还不算 Agent先看一个很普通的需求列出当前目录下的 Python 文件然后运行 demo.py。如果把这句话发给普通聊天模型它通常知道该怎么做find.-name*.py但它把命令写出来后任务就中断了。因为模型没有终端也没有文件系统。它不能亲自执行这条命令更看不到命令输出。接下来只能由人接手复制模型给出的命令打开终端执行复制输出结果粘贴回聊天框等模型给出下一步。如果模型接着要求运行python demo.py人就再做一遍。这时模型负责思考终端负责执行人负责在两者之间传递信息。最小 Agent Loop 的作用就是把这段人工操作交给程序。二、先看最终代码这一章的核心代码如下defagent_loop(messages):whileTrue:responseclient.messages.create(modelMODEL,systemSYSTEM,messagesmessages,toolsTOOLS,max_tokens8000,)messages.append({role:assistant,content:response.content,})ifresponse.stop_reason!tool_use:returnresults[]forblockinresponse.content:ifblock.typetool_use:outputrun_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.typetool_use:outputrun_bash(block.input[command])这里真正执行命令的是run_bash。outputrun_bash(block.input[command])模型负责说自己想做什么程序负责真正调用 Shell。这个边界很重要。角色工作模型决定调用什么工具、传什么参数程序执行工具调用Shell在操作系统中执行命令工具结果把真实环境的反馈带回模型模型不是直接拥有电脑权限而是通过工具请求间接影响环境。四、第二步工具执行完为什么还要再发回模型命令执行以后程序可以拿到输出。例如find.-name*.py返回./demo.py ./hello.py这份结果不能只打印在终端里。模型必须看到它才能知道下一步应该怎么做。所以代码会把工具结果包装成tool_resultresults.append({type:tool_result,tool_use_id:block.id,content:output,})然后追加到messagesmessages.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 项目仓库