01|Agent 循环:让模型真正动手

@[toc] ## 本篇导读 聊天模型能够告诉你怎样修改代码,但仅仅输出建议,并不会让文件发生变化。要让它完成“创建文件、运行程序、根据报错修复”这样的任务,需要把模型的工具请求执行掉,再把执行结果交给模型。这条反馈链,就是 Agent 循环。 **学完本篇,你将能够:** 理解 messages、tool_use、tool_result 和 stop_reason 如何组成最小执行闭环。 ## 设计思路 每一轮先调用模型,再保存助手响应。如果响应包含工具调用,就逐个执行,并将结果作为下一条 user 消息送回;如果模型不再请求工具,本轮任务结束。模型负责选择动作,程序负责执行与反馈。 例如用户要求创建 hello.py,模型先请求 bash 写文件;工具返回成功后,模型还可以请求运行文件,检查输出,再向用户给出结果。这比一次性生成一段代码多了“观察结果并继续行动”的能力。 ## 核心代码与解释 先通过关键片段理解本篇机制;随后“完整代码”一节给出包含所有依赖定义与交互入口的程序,可整段复制保存并运行。 ### 把 Shell 包装成工具 ```python 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)" ``` 函数捕获标准输出和错误输出,设置超时,并限制返回文本的长度。模型看到的是字符串结果,实际执行发生在 Harness 中。这里把两种输出拼接在一起,便于教学,但没有单独返回退出码。 ### 把工具结果送回模型 ```python def agent_loop(messages: list): while True: response = client.messages.create( model=MODEL, system=SYSTEM, messages=messages, tools=TOOLS, max_tokens=8000, ) # Append assistant turn messages.append({"role": "assistant", "content": response.content}) # If the model didn't call a tool, we're done if response.stop_reason != "tool_use": return # Execute each tool call, collect results results = [] for block in response.content: if block.type == "tool_use": print(f"\033[33m$ {block.input['command']}\033[0m") output = run_bash(block.input["command"]) print(output[:200]) results.append({"type": "tool_result", "tool_use_id": block.id, "content": output}) messages.append({"role": "user", "content": results}) ``` 先追加 assistant 响应,再追加工具结果,才能形成完整的调用记录。tool_use_id 必须对应发起调用的 block.id,模型才能知道每个结果属于哪次请求。stop_reason 判断的是是否继续调用工具,并不是对任务正确性的独立验证。 ## 跟着一次工具调用读懂消息结构 把 messages 理解为本次对话的工作记录。你输入任务后,交互入口先把它追加为 user 消息;agent_loop 再把整份记录发给模型。模型请求运行 hello.py 时,返回的是一个 tool_use 块,其中 name 是 bash,input 保存 command,id 标识这次调用。模型只是提出请求,真正启动进程的是 run_bash。 执行后,程序先保存包含这次调用的 assistant 消息,再追加带有 tool_use_id 的 tool_result。两处 ID 相同,便建立了“请求与结果”的对应关系。工具结果虽然放在 role=user 的消息中,却不是用户重新输入了一句话:这是 Anthropic Messages 协议承载工具反馈的方式。下一轮模型据此判断继续执行还是给出答复。 response.content 是内容块列表,一次响应可能同时包含文本和多个工具调用,因此程序需要遍历 block,而不能直接把响应当作字符串。这里的多个调用按 for 循环逐个执行;它们不会因为同属一次响应就自动并行。 阅读后面的完整程序时,可以先定位这些字段: | 代码位置 | 含义与阅读重点 | | --- | --- | | `TOOLS / input_schema` | 向模型说明工具名称、参数格式与必填字段;这里不执行命令。 | | `SYSTEM / messages` | SYSTEM 提供行为指导,messages 保存本次对话已经发生的事。 | | `block.id / tool_use_id` | 把工具结果关联到发起它的那次调用,多个调用也不会混淆。 | | `max_tokens=8000` | 限制一次模型响应的输出预算,不是整个上下文窗口的大小。 | | `print(output[:200])` | 终端只展示前 200 个字符;送回模型的是 run_bash 返回的输出,最多 50000 个字符。 | run_bash 中的 `shell=True` 让命令字符串交给系统 Shell 解释,cwd 决定命令的工作目录;工具名叫 bash,不代表所有操作系统都会使用 Bash。timeout 约束这次子进程等待时间,不是整个 Agent 任务的总时限。 ## 动手练习 ### 完整代码:复制后独立运行 本篇所需的 Python 程序全部列在下面,包含导入、模型客户端、工具定义、处理函数和交互入口。无需下载教程仓库,也无需复制前一篇的实现。你需要 Python 3.10 或更高版本,以及可用的 Anthropic 兼容接口密钥和模型名称。 #### 1. 创建目录并安装依赖 每篇使用单独目录,避免混入其他课程的任务与团队记录。以下命令用于 macOS / Linux: ```bash mkdir -p ~/agent-harness-s01 cd ~/agent-harness-s01 python3 -m venv .venv source .venv/bin/activate python -m pip install "anthropic>=0.25.0" "python-dotenv>=1.0.0" ``` Windows PowerShell 用户先在用户主目录创建 `agent-harness-s01` 文件夹,再执行 `cd ~/agent-harness-s01`。将创建与激活虚拟环境的两条命令改为 `py -3 -m venv .venv` 和 `.\.venv\Scripts\Activate.ps1`,其他文件内容相同。在该终端设置 `$env:PYTHONUTF8="1"` 后运行,统一使用 UTF-8 读取练习文件。 在当前目录新建 `.env`,复制下面内容,然后把两个占位值替换为你的真实配置: ```dotenv ANTHROPIC_API_KEY=your-api-key MODEL_ID=your-model-id ``` `MODEL_ID` 填写接口实际支持的模型名称。使用默认 Anthropic 服务时,上面两项即可;使用兼容接口时,在 `.env` 增加 `ANTHROPIC_BASE_URL=你的接口地址`。这些程序使用 Anthropic Messages 协议,不能直接填只支持 OpenAI Chat Completions 的接口地址。网络访问、模型调用和实际费用取决于你使用的服务。 程序把启动时的当前目录作为工作区,因此运行前应进入本篇目录。文件工具中的 `hello.py`、`skills/` 和 `.tasks/` 等相对路径都以该工作区为基础;把脚本保存在正确目录,还不等于终端已经进入该目录。 #### 2. 保存完整程序 **文件名:`s01_agent_loop.py`**。在上面创建的 `~/agent-harness-s01/` 目录中新建这个文件,将下面全部代码复制进去。`.env` 和该 Python 文件保存在同一个目录。 ```python import os import subprocess from anthropic import Anthropic from dotenv import load_dotenv 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"] SYSTEM = f"You are a coding agent at {os.getcwd()}. Use bash to solve tasks. Act, don't explain." TOOLS = [{ "name": "bash", "description": "Run a shell command.", "input_schema": { "type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"], }, }] 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)" # -- The core pattern: a while loop that calls tools until the model stops -- def agent_loop(messages: list): while True: response = client.messages.create( model=MODEL, system=SYSTEM, messages=messages, tools=TOOLS, max_tokens=8000, ) # Append assistant turn messages.append({"role": "assistant", "content": response.content}) # If the model didn't call a tool, we're done if response.stop_reason != "tool_use": return # Execute each tool call, collect results results = [] for block in response.content: if block.type == "tool_use": print(f"\033[33m$ {block.input['command']}\033[0m") output = run_bash(block.input["command"]) print(output[:200]) results.append({"type": "tool_result", "tool_use_id": block.id, "content": output}) messages.append({"role": "user", "content": results}) if __name__ == "__main__": 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) response_content = history[-1]["content"] if isinstance(response_content, list): for block in response_content: if hasattr(block, "text"): print(block.text) print() ``` 这段代码已包含本篇所有函数与工具 Schema;下方只需按本篇要求准备练习材料,然后启动程序。 ### 进入代码目录并运行 确认完整代码已保存为 `s01_agent_loop.py`,本篇练习材料已准备,`.env` 的占位值已替换。macOS / Linux 用户在终端进入文件所在目录,激活该目录的虚拟环境,再执行程序: ```bash cd ~/agent-harness-s01 source .venv/bin/activate python s01_agent_loop.py ``` Windows PowerShell 用户进入相同目录,将激活命令换成 `.\.venv\Scripts\Activate.ps1`。如果你把代码保存在其他位置,将 `cd` 后的路径换成实际保存目录;运行命令中的文件名保持一致。 ### 输入任务并检查结果 程序显示交互提示后,输入: > 请创建 hello.py,输出 Hello, Agent Harness;运行它并告诉我实际输出。 **如何检查结果:** 观察终端是否出现写文件和运行文件的两次工具调用,以及最终回复是否引用真实输出。然后故意让程序抛出异常,观察模型是否能根据错误继续修复。 输入 `exit` 退出。出现缺少模块错误时,在当前虚拟环境重新执行安装命令;认证或模型名称错误则检查 `.env`。团队与后台任务需保持主程序运行,结束进程会终止守护线程。 ## 工程边界与常见误区 危险命令字符串黑名单只是教学措施,不能作为可靠的 Shell 沙箱。实际系统还需要目录与进程隔离、明确权限、退出码、轮次及费用预算。循环退出也可能因为达到输出限制或其他停止原因;生产系统需要分别处理。 ## 本篇小结 Agent 的最小闭环是“模型请求动作 → 工具执行 → 结果反馈 → 再次决策”。后续机制都围绕这个闭环展开。 下一篇:**02|工具调用:设计可扩展的工具层**。