1. 从一次“手动粘贴”说起:Agent Loop 到底解决什么问题
如果你用过大模型写代码,大概率经历过这个场景:让模型帮你看看当前目录有什么文件,它说“我无法访问你的文件系统”,于是你手动把ls的结果复制粘贴回去,它再基于这段文本继续推理。整个过程里,你本人就是那个循环——模型负责想,你负责跑命令、贴结果、再喂回去。
Agent Loop(智能体循环)要干的事,就是把这个“人肉循环”交给程序。语言模型本身只能推理文本,碰不到真实世界:读不了文件、跑不了测试、看不到报错。但只要给它一个工具定义,再套一个while循环,它就能自己决定“我要调用 shell 跑 dir”,程序执行完把结果塞回消息列表,模型看到结果再决定下一步。循环持续到模型不再请求工具为止。
一句话概括:一个工具 + 一个循环 = 一个最小智能体。这也是 Claude Code 这类编码智能体的骨架,后面所有花哨能力——多工具、子智能体、权限控制——都是在这个循环上叠加的机制,循环本身始终不变。
这篇就带你从零跑通这个循环:用 TaoToken 的统一 Key 接入模型,写一份可复制的配置骨架,然后完成一次真实的工具调用往返,亲眼看到stop_reason从tool_use变成end_turn。
2. 前置准备:用 TaoToken 统一 Key 接入模型
在写循环之前,先把“模型从哪来”这件事解决掉。Claude Code 的 Agent Loop 依赖 Anthropic 风格的 Messages API,你需要一个兼容该协议的接入点和一个可用的 Key。TaoToken 在这里的作用是提供统一的 API 入口,让你不用为每个模型单独折腾一套鉴权和 base_url。
你需要准备三样东西:
第一,一个 API Key。登录 TaoToken 控制台,在 API Keys 页面创建一个,复制出来形如sk-xxxx的字符串。这个 Key 就是后面所有请求的凭证。
第二,确认接入地址。Anthropic SDK 的base_url填https://taotoken.net/api,注意这里不带任何查询参数,SDK 会自己在后面拼接/v1/messages。
第三,选一个支持工具调用的模型 ID。工具调用(tool use)不是所有模型都支持,选之前可以在模型对话页面先手动试一句“帮我列一下目录”,看它会不会返回工具调用块。实测下来,带 function calling 能力的模型都能正常走通这个循环。
注意:不要把 Key 硬编码进代码提交到仓库。用
.env文件管理,并把它加进.gitignore。这是踩过的坑里最常见的一个。
3. 可复制配置:settings.json 与 .env 骨架
Claude Code 本身支持通过settings.json配置模型接入,但这一课我们聚焦 Agent Loop 的代码实现,所以配置分两层:一层是给 Claude Code CLI 用的settings.json,一层是给我们的 Python 脚本用的.env。
先看settings.json的骨架。放在项目根目录的.claude/settings.json,或者用户级的~/.claude/settings.json:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "sk-你的Key", "ANTHROPIC_MODEL": "你的模型ID" } }这个配置的作用是让 Claude Code CLI 启动时读取环境变量,把请求打到 TaoToken 的接入点。ANTHROPIC_AUTH_TOKEN就是你的统一 Key,ANTHROPIC_MODEL填你在控制台选好的模型 ID。
再看 Python 脚本用的.env:
ANTHROPIC_BASE_URL=https://taotoken.net/api ANTHROPIC_AUTH_TOKEN=sk-你的Key MODEL_ID=你的模型ID然后在代码里用python-dotenv加载。这里有个细节值得单独说:如果你的环境里同时存在ANTHROPIC_AUTH_TOKEN和ANTHROPIC_BASE_URL,Anthropic SDK 在某些版本下会优先用 token 走官方端点,导致请求打错地方。稳妥做法是加载完.env后,如果检测到ANTHROPIC_BASE_URL存在,就主动清掉ANTHROPIC_AUTH_TOKEN,只保留 base_url 让 SDK 用 Key 鉴权:
import os from dotenv import load_dotenv load_dotenv(override=True) if os.getenv("ANTHROPIC_BASE_URL"): os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)依赖安装很简单:
pip install anthropic python-dotenv到这里,模型接入就通了。接下来写循环本体。
4. 拆解 Agent Loop:从一条消息到一次工具往返
4.1 工具定义与系统提示
工具定义是一个 JSON Schema,告诉模型“你有 shell 这个工具,它接受一个 command 字符串参数”。系统提示则交代运行环境,让模型知道自己在什么系统上、工作目录在哪:
import platform SYSTEM = f"""You are a coding agent running on {platform.system()} ({platform.platform()}). Working directory: {os.getcwd()}. Use tools to solve tasks. Act, don't explain.""" TOOLS = [ { "name": "shell", "description": "Run a shell command.", "input_schema": { "type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"], }, }, ]这里有个 Windows 环境的坑:工具名用shell而不是bash。CMD 属于 Shell 的一种,是 Windows 的命令行 Shell,和 Unix 的 Bash 有显著差异。如果你把工具描述写成 bash,模型可能生成ls -la这类命令,在 CMD 里直接报错。用中性的shell命名,配合系统提示里的平台信息,模型会自己选dir这类正确命令。
4.2 循环骨架:五步走
整个 Agent Loop 就是一个while True,核心逻辑五步:
第一步,把用户 prompt 作为第一条 user 消息放进messages。第二步,把messages和tools一起发给模型。第三步,把模型的响应追加为 assistant 消息。第四步,检查stop_reason——如果不是tool_use,说明模型不再需要工具,循环结束。第五步,遍历响应里的每个tool_use块,执行工具,把结果以tool_result的形式作为新的 user 消息追加回去,然后回到第二步。
def agent_loop(messages: list): 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": print(f"$ {block.input['command']}") output = run_shell(block.input["command"]) results.append({ "type": "tool_result", "tool_use_id": block.id, "content": output, }) messages.append({"role": "user", "content": results})注意tool_use_id这个字段:它把工具结果和模型发出的那次调用一一对应起来。如果对不上,模型会认为工具没执行,可能重复调用。这是协议层面的硬要求,不能省。
4.3 工具执行函数与安全边界
run_shell负责真正执行命令。它需要处理三件事:危险命令拦截、超时控制、输出截断。
import subprocess def run_shell(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)"危险命令拦截是必须的,因为模型可能生成破坏性命令。超时设 120 秒,避免某个卡住的命令把整个循环挂死。输出截断到 50000 字符,防止一次dir /s把上下文撑爆。
4.4 主入口:交互式 REPL
最后加一个简单的交互入口,让你能连续输入指令:
if __name__ == "__main__": history = [] while True: try: query = input("s01 >> ") 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()把history放在循环外面,是为了让多轮对话共享上下文。每次agent_loop结束后,history里已经包含了完整的 user、assistant、tool_result 消息链,下一轮直接接着用。
5. 验证一次工具调用往返
配置和代码都齐了,跑起来验证。启动脚本后输入:
s01 >> 列出当前目录的所有文件名预期看到的过程是这样的:脚本先打印一行黄色的$ dir,这是模型决定调用的命令;然后run_shell执行它,把结果作为tool_result追加;循环回到顶部,再次请求模型;这次模型拿到目录列表,stop_reason变成end_turn,循环退出,最后打印出模型基于目录内容生成的回答。
如果你在调试时打印完整的response,会看到第一次响应的结构大致是:
Message( id='msg_...', content=[ToolUseBlock(id='call_..._0', input={'command': 'dir'}, name='shell', type='tool_use')], model='你的模型ID', role='assistant', stop_reason='tool_use', ... )关键看两个字段:content里是ToolUseBlock而不是TextBlock,stop_reason是tool_use。如果模型返回的是TextBlock且stop_reason是end_turn,说明它没走工具调用,直接回答了——这通常意味着工具定义没传对,或者模型不支持工具调用。
第二次请求的响应里,content会变成TextBlock,stop_reason是end_turn,循环正常退出。这一进一出,就是一次完整的工具调用往返。
6. 常见报错排查
报错一:stop_reason一直是end_turn,模型不调工具。先检查tools参数有没有传进messages.create。再检查模型是否支持工具调用,可以在模型对话页面手动发一句带工具定义的请求测试。如果模型支持但就是不调,把系统提示里的 “Act, don't explain” 保留,这句话对触发工具调用有明显作用。
报错二:tool_result报 400,提示tool_use_id不匹配。检查你追加tool_result时用的tool_use_id是不是来自同一个响应里的block.id。跨轮次复用 ID 会直接报错。
报错三:Windows 下命令执行失败,提示“不是内部或外部命令”。模型可能生成了 Unix 风格命令。确认工具名是shell而非bash,系统提示里带上platform.system()的信息。如果还是不行,在工具描述里补一句“On Windows, use CMD commands like dir, type, findstr”。
报错四:请求打到官方端点而非 TaoToken。检查.env里ANTHROPIC_BASE_URL是否生效,以及代码里有没有清掉ANTHROPIC_AUTH_TOKEN。SDK 的 base_url 优先级在部分版本里会被环境变量覆盖,显式传base_url参数更稳:
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))报错五:循环停不下来,一直调工具。给循环加一个最大轮次保护,比如for _ in range(20)替代while True,超过就强制退出并打印当前消息链,方便定位是哪个工具结果让模型误判了。
7. 下一步:把循环跑稳,再叠加能力
Agent Loop 本身就这么点东西,难的是让它稳定跑起来。建议你先用这个最小版本跑通三五个真实任务——列目录、读文件、跑测试——确认工具调用往返没问题,再去叠加多工具、权限控制、子智能体这些机制。
如果你想把 Claude Code 的完整能力接进来长期用,可以在 TaoToken 控制台创建一个 Coding Plan,把模型、Key、额度统一管理,省得每次换项目都重新配一遍。接入文档里有 Anthropic SDK 和 Claude Code CLI 两种方式的详细说明,遇到鉴权或端点问题可以直接对照排查。想先验证模型对工具调用的支持情况,用模型对话页面手动发一条带工具定义的请求最快。