1. 从一次“跑不完”的任务说起:Agent Loop 到底是什么
如果你正在做自主 Agent,大概率会遇到一个很具体的困惑:模型明明会调工具,但任务稍微长一点就“断片”,要么停在半路等你再问一句,要么反复调同一个工具烧 token。这个问题的根子不在模型能力,而在你有没有把Agent Loop写对。Agent Loop 说白了就是让 LLM 在一个循环里,根据环境反馈持续使用工具,直到任务真正结束。它决定了你的 Agent 是“一问一答的聊天机器人”,还是“能自己跑起来干活的智能体”。
我先把结论放前面:Agent Loop 的核心就是一个 while 循环,循环体里做三件事——调用 LLM、判断是否要调工具、把工具结果塞回消息历史。听起来简单到不像技术难点,但真正落地时,消息历史怎么拼、stop_reason 怎么判、循环怎么刹车,每一步都有坑。这篇就按“能跟做”的标准来:给你可复制的循环控制配置、Tool Use 调用示例,以及验证 Agent 自主迭代是否生效的具体动作。
适合谁看?如果你已经能调通一次 LLM 接口,想让模型自己读文件、跑命令、改代码并持续推进任务,那这篇就是给你写的。如果你还在纠结“Agent 和 workflow 有什么区别”,也可以先看下去,我会用真实场景把差异讲清楚。整篇围绕 Agent Loop、Tool Use、LLM 三个关键词展开,最后给一套可以直接抄的循环骨架。
2. 前置准备:用 TaoToken 统一接入 LLM 与 Tool Use 调用
在写循环之前,得先有一个稳定的模型调用入口。Agent Loop 会高频、连续地调 LLM,如果每次请求的鉴权、Base URL、模型 ID 都散落在代码各处,排障会非常痛苦。我的做法是用 TaoToken 做统一接入层,把模型对话、API Key、接入文档三件事分开管理,循环代码里只关心“发消息、收响应”。
TaoToken 在这里扮演的角色很明确:它是一个兼容主流接口协议的模型接入服务,你拿到 API Key 后,把 Base URL 指向https://taotoken.net/api,就能用统一的请求格式调用模型。对 Agent Loop 来说,这一点很关键——循环里每一轮都要发一次请求,接口格式统一意味着你的消息历史拼接逻辑只需要写一份。
你需要提前准备三样东西:
第一,一个可用的 API Key。到控制台创建,注意 Key 只在创建时完整显示一次,复制好放环境变量里,别硬编码进代码。
第二,确认 Base URL。所有请求走https://taotoken.net/api,不要带多余的路径后缀,具体以接入文档为准。
第三,选定一个 Model ID。Agent Loop 对模型的工具调用能力有要求,选支持 Tool Use 的模型,把模型 ID 记下来,后面配置里要用。
如果你用的是 Claude Code 这类已经封装好循环的工具,接入方式会更省事,本质上还是把 Base URL、Key、Model ID 三件套填进去。但如果你想自己掌控循环逻辑,那就继续往下看,我会给一份手写的循环骨架。
这里提醒一句:Agent Loop 会连续发请求,建议在接入层做一层简单的重试和超时控制,避免某一轮网络抖动直接把整个循环打断。这个后面在排障章节会展开。
3. 可复制的循环控制配置与 Tool Use 调用示例
这一节是全文的核心,我给你一份可以直接改吧改吧就用的循环骨架。先看配置部分,我用 JSON 存模型和循环参数,路径放在项目根目录的agent.config.json,这样循环代码和配置解耦,换模型不用动逻辑。
{ "base_url": "https://taotoken.net/api", "api_key_env": "TAOTOKEN_API_KEY", "model_id": "your-tool-use-model-id", "max_loop_turns": 12, "request_timeout_seconds": 60, "tools": [ { "name": "search", "description": "在指定目录下按关键字搜索文件内容", "input_schema": { "type": "object", "properties": { "pattern": { "type": "string" }, "path": { "type": "string" } }, "required": ["pattern", "path"] } }, { "name": "read_file", "description": "读取指定文件的完整内容", "input_schema": { "type": "object", "properties": { "path": { "type": "string" } }, "required": ["path"] } } ] }注意max_loop_turns这个字段,它是循环的刹车。没有它,模型偶尔会在两个工具之间来回跳,你的 token 账单会很难看。api_key_env指向环境变量名而不是 Key 本身,这样配置可以进版本库,Key 不会泄露。
接下来是循环主体。核心逻辑就是一个 while,每一轮把完整消息历史发给 LLM,看返回里有没有 tool_use,有就执行工具、把结果追加进历史、继续循环;没有就输出最终回答、跳出循环。
import os, json, requests cfg = json.load(open("agent.config.json")) API_KEY = os.environ[cfg["api_key_env"]] HEADERS = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json" } def call_llm(messages): payload = { "model": cfg["model_id"], "messages": messages, "tools": cfg["tools"], "max_tokens": 2048 } resp = requests.post( f"{cfg['base_url']}/v1/messages", headers=HEADERS, json=payload, timeout=cfg["request_timeout_seconds"] ) resp.raise_for_status() return resp.json() def run_tool(name, tool_input): if name == "search": # 这里替换成你真实的搜索实现 return f"searched {tool_input['pattern']} in {tool_input['path']}" if name == "read_file": with open(tool_input["path"], "r", encoding="utf-8") as f: return f.read() return f"unknown tool: {name}" def agent_loop(user_task): messages = [{"role": "user", "content": user_task}] for turn in range(cfg["max_loop_turns"]): response = call_llm(messages) stop_reason = response.get("stop_reason") content = response.get("content", []) # 把模型这一轮的回复原样追加进历史 messages.append({"role": "assistant", "content": content}) if stop_reason == "tool_use": tool_results = [] for block in content: if block.get("type") == "tool_use": result = run_tool(block["name"], block["input"]) tool_results.append({ "type": "tool_result", "tool_use_id": block["id"], "content": result }) messages.append({"role": "user", "content": tool_results}) continue # 关键:继续循环,让模型基于结果再想 # 没有工具调用,说明模型认为任务结束 return content return "达到最大循环轮次,强制停止"这段代码里有三个细节值得单独说。第一,messages.append({"role": "assistant", "content": content})必须把模型返回的原始 content 结构(包含 tool_use 块)完整存进去,不能只存文本,否则下一轮模型看不到自己调过什么工具。第二,工具结果要以tool_result类型、带上tool_use_id回传,这个 ID 是模型把结果和调用对应起来的唯一凭据。第三,continue和return的分支判断完全依赖stop_reason,这是循环走向的唯一信号。
如果你用的是 Claude Code 这类工具,它内部已经实现了这套循环,你只需要在配置里填好 Base URL、Key、Model ID 三件套。但理解上面这段骨架,能让你在它“卡住”的时候知道去哪一层排查。
4. 验证 Agent 自主迭代是否真的生效
写完循环不代表它真的在“自主迭代”。很多人的 Agent 表面上跑通了,实际上是模型一轮就把所有事说完,根本没进循环。所以你需要一组可观察的验证动作,确认循环确实在转。
第一个动作:给一个必须多轮才能完成的任务。比如“在项目里找到所有 TODO 注释并整理成清单”。这个任务模型第一轮通常只会搜一个目录,第二轮才会补搜其他目录,第三轮才输出结果。如果它一轮就给你答案,要么是任务太简单,要么是循环没生效。
第二个动作:打开日志,打印每一轮的stop_reason和工具调用。你期望看到的是类似这样的序列:
turn 1: stop_reason=tool_use, tool=search, input={"pattern":"TODO","path":"./src"} turn 2: stop_reason=tool_use, tool=search, input={"pattern":"TODO","path":"./tests"} turn 3: stop_reason=end_turn, output=最终清单如果 turn 1 就是end_turn,说明模型没调工具,检查你的 tools 定义有没有正确传进去。如果一直是tool_use但工具名重复,说明模型陷入了循环,检查max_loop_turns有没有生效。
第三个动作:故意制造一次工具失败。比如让read_file读一个不存在的路径,看模型下一轮会不会根据错误信息调整策略。一个健康的 Agent Loop 应该能“看到”错误并换一种方式重试,而不是直接崩掉。这一步最能验证“环境反馈驱动”是否真的成立。
第四个动作:数轮次。一个稍微复杂的任务,比如“修复登录接口 token 未返回的 bug”,健康的循环通常要跑 6 到 10 轮,中间会经历读文件、改代码、跑测试、测试失败、再改、再跑。如果你发现轮次总是 1 或 2,基本可以判定循环没真正跑起来。
实测下来,把这四个动作跑一遍,你对 Agent 是否“自己跑起来”就有底了。别只看最终输出对不对,过程日志才是判断循环质量的关键。
5. 常见报错排查:401、local proxy failed、reading choices、OAuth
循环跑不起来,报错往往集中在几个固定位置。这一节按真实报错来对,你遇到哪个直接查哪个。
401 Unauthorized:最常见。先确认环境变量里的 Key 有没有读到,echo $TAOTOKEN_API_KEY看是不是空。再确认请求头格式,Authorization: Bearer <key>中间是一个空格,别写成冒号。如果 Key 是从控制台复制的,注意有没有把首尾空格带进去。还有一种情况是 Key 被禁用或额度耗尽,去控制台确认状态。
local proxy failed / connection refused:这类报错通常出现在你本地配了转发但服务没起来,或者 Base URL 写错了。检查base_url是不是https://taotoken.net/api,不要多加/v1之外的路径。如果你在容器里跑,确认容器网络能出网。这个报错和循环逻辑无关,是接入层问题,先把它解决再谈循环。
reading choices / 响应结构解析失败:这个报错说明你的代码在按某个固定字段解析响应,但实际返回结构不一样。Agent Loop 里最容易踩的是把content当成字符串处理,实际上它是数组,里面混着文本块和 tool_use 块。解析时一定要先判断block["type"],再取对应字段。如果你换了模型或接口版本,字段名可能变,先打印一次原始响应看看结构。
OAuth / 鉴权方式不匹配:有些工具(比如 Claude Code 或某些 CLI)默认走 OAuth 登录流程,你如果直接填 API Key 会报鉴权失败。这时候要确认工具支持的是 Key 模式还是 OAuth 模式,按它的文档切换。用 TaoToken 接入时,走的是 Key 模式,把 Base URL、Key、Model ID 三件套填对即可,不要混用两套鉴权。
循环不报错但提前结束:这个不算报错,但很常见。检查stop_reason的判断逻辑,有些接口返回的是tool_use之外的变体,或者你在追加 assistant 消息时丢了 tool_use 块,导致模型下一轮“忘了”自己调过工具,直接给答案。把每一轮的 messages 长度打印出来,正常应该每轮增加 2(assistant + tool_result)。
排障的核心思路是分层:先确认接入层通不通(401、proxy),再确认响应解析对不对(choices、结构),最后才看循环逻辑(stop_reason、消息历史)。别一上来就怀疑模型,大部分问题在接入和解析这两层。
6. 把循环跑稳之后:接入入口与下一步
循环骨架跑通、验证动作也过了之后,你手里就有了一个能自主迭代的 Agent 内核。接下来无非是两件事:把工具集扩充得更贴近你的真实任务,以及把循环的稳定性再加固一层。
工具集方面,建议从最小可用开始。先只放 search 和 read_file,确认循环能转起来,再逐步加 write_file、bash 这类有副作用的工具。有副作用的工具一定要在run_tool里加确认或沙箱,别让模型在循环里直接改生产环境的东西。
稳定性方面,重点加两个东西:一是每轮请求的重试,网络抖动不该打断整个循环;二是循环轮次和 token 消耗的上限,防止模型陷入死循环把额度烧光。这两个都是几行代码的事,但能省掉很多半夜排障的时间。
如果你还没拿到 Key,或者想先看看模型对话的实际效果,可以从模型对话入口进去试一轮工具调用,确认接口通不通。想把这套循环用到长期编码或 Agent 任务上,Coding Plan 会更合适,它把循环和工具链都封装好了,你专注在任务本身就行。接入过程中遇到鉴权或配置问题,直接翻接入文档,Base URL、Key、Model ID 的填法都写得很清楚。
最后留一个我踩过的坑:别在循环里用同步阻塞的方式跑长命令,模型等不到结果会超时,整个循环就断了。把长任务拆成可轮询的短步骤,让每一轮都有明确的工具结果返回,循环才能稳稳地转下去。