1. 为什么你的 Agent 跑三圈就断片:从 ReAct 事件循环说起
如果你正在本地搭 LLM 智能体,大概率遇到过这种场景:模型第一轮说要读文件,第二轮说要搜索符号,第三轮突然开始胡言乱语,或者干脆把前面读到的内容忘得一干二净。这不是模型不行,而是你缺了一个真正的 Agent Runtime。
Agent Runtime 是什么?一句话说,它是智能体的运行时系统,负责把「LLM 思考 → 工具执行 → 结果回灌 → 再思考」这条链路持续驱动下去。LLM 本身不会调用工具,不会管理上下文,不会判断任务是否结束,它只会在给定输入下吐出下一段 token。真正让智能体「活起来」的,是 Runtime 里那个不断转动的 Event Loop。
它适合谁?适合所有在本地用 Python/Node 搭过 ReAct 循环、被上下文爆炸和工具调用格式折磨过的开发者。你可以把它理解成智能体的大脑皮层:LLM 是神经元,Runtime 是让神经元按顺序放电的节律器。
我试过最原始的写法——一个 while 循环里反复调 OpenAI 接口,手动拼 messages,手动解析 JSON action。跑简单任务还行,一旦涉及多轮工具调用,代码里全是 if-else 和字符串截取,维护成本极高。更麻烦的是,每换一个模型供应商,Key 和 Base URL 都要改一遍,调试时根本分不清是 Runtime 逻辑错了还是接入层挂了。
这篇要解决的就是这件事:用 TaoToken 统一 Key 接入,把模型调用层收敛成一个稳定入口,然后把精力全部放在 ReAct 事件循环和 Event Loop 调度上。下面会给出可复制的配置片段、完整的 ReAct 任务事件流验证动作,以及真实会撞上的报错排查。目标很明确——让你本地那个智能体,能连续跑完一个多轮工具调用任务而不崩。
2. TaoToken 前置:统一 Key 接入与 Agent Runtime 的接入层收敛
在写 Event Loop 之前,先把模型调用层固定下来。Agent Runtime 最怕的就是接入层不稳定:今天用这家 API,明天换那家,Base URL、Key、Model ID 三件套一变,整个 ReAct 循环的调试基线就没了。TaoToken 在这里扮演的角色,是给 Runtime 提供一个统一的 OpenAI 兼容入口,让call_llm()这个函数永远只认一套参数。
先说清楚它是什么:TaoToken 提供 OpenAI 兼容的 API 网关,你可以用同一套 Key 和 Base URL 调用不同模型。对 Agent Runtime 来说,这意味着 Event Loop 里的模型调用节点不需要关心底层是哪家模型,只需要按标准格式发请求、收响应。
接入前你需要准备三样东西,我把它叫做「三件套」:
| 项目 | 值 | 说明 |
|---|---|---|
| Base URL | https://taotoken.net/api | OpenAI 兼容入口,不加 UTM |
| API Key | 在控制台生成 | 形如sk-...,只显示一次 |
| Model ID | 按需选择 | 例如gpt-4o-mini、claude-3-5-sonnet等 |
获取 Key 的路径:访问官网https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=,进入控制台后找到 API Keys 页面创建。注意 Key 只在创建时完整显示,务必当场复制保存。
这里有个关键认知:Agent Runtime 的接入层应该只暴露一个函数,比如call_llm(messages, tools),内部用统一的 Base URL 和 Key。这样 Event Loop 里所有模型调用都走同一个出口,出问题时排查范围立刻缩小到「是 Runtime 逻辑问题还是接入层问题」。
如果你用的是 Claude Code 这类工具做辅助开发,它的配置也是同样的三件套逻辑。在~/.claude/settings.json或项目级配置里,Base URL 指向https://taotoken.net/api,Key 填控制台生成的,Model ID 按需指定。这样你在本地调试 Agent Runtime 时,辅助编码工具和运行时用的是同一套接入,环境一致性有保障。
对于长期跑 Agent 任务的场景,可以考虑 Coding Plan,它在多轮调用下有更稳定的配额策略。但无论用哪种方式,核心原则不变:接入层收敛成一个入口,Runtime 只依赖这个入口。
3. 可复制配置:Agent Runtime 的 settings 与 ReAct 循环骨架
这一节直接给可复制的配置和代码骨架。先给配置文件,再给 Event Loop 的核心逻辑。
3.1 统一接入配置片段
如果你用 Claude Code 辅助开发,配置文件路径是~/.claude/settings.json,内容如下:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的Key", "ANTHROPIC_MODEL": "claude-3-5-sonnet-20241022" } }如果你用 Codex 类工具,配置文件在~/.codex/auth.json:
{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的Key", "model": "gpt-4o-mini" }注意三件套必须齐全:Base URL、Key、Model ID。少任何一个,Runtime 在 Event Loop 里调用模型时都会失败。
3.2 ReAct 事件循环骨架
下面是一个最小可运行的 ReAct Runtime,用 Python 写,依赖openai库。核心是 Event Loop:不断调用模型、解析 action、执行工具、把 observation 塞回 messages,直到模型输出 finish。
import json from openai import OpenAI client = OpenAI( base_url="https://taotoken.net/api", api_key="sk-你的Key" ) MODEL = "gpt-4o-mini" MAX_STEPS = 20 TOOLS = { "read_file": lambda path: open(path, encoding="utf-8").read()[:2000], "list_files": lambda path: str(__import__("os").listdir(path)), } SYSTEM_PROMPT = """你是一个 ReAct 智能体。每轮只输出一个 JSON: {"thought": "...", "action": "工具名", "args": {...}} 或 {"thought": "...", "finish": true, "answer": "最终答案"} 可用工具:read_file(path), list_files(path)""" def call_llm(messages): resp = client.chat.completions.create( model=MODEL, messages=messages, temperature=0 ) return resp.choices[0].message.content def parse_action(text): text = text.strip().removeprefix("```json").removesuffix("```").strip() return json.loads(text) def run_react(task): messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": task} ] for step in range(MAX_STEPS): raw = call_llm(messages) print(f"[step {step}] raw: {raw[:120]}") try: decision = parse_action(raw) except json.JSONDecodeError: messages.append({"role": "assistant", "content": raw}) messages.append({"role": "user", "content": "输出不是合法 JSON,请重新输出。"}) continue if decision.get("finish"): return decision["answer"] action = decision["action"] args = decision.get("args", {}) try: observation = TOOLS[action](**args) except Exception as e: observation = f"工具执行失败: {e}" messages.append({"role": "assistant", "content": raw}) messages.append({"role": "user", "content": f"Observation: {observation}"}) return "达到最大步数,任务未完成" if __name__ == "__main__": print(run_react("列出当前目录文件,然后读取 README.md 的前 500 字"))这段代码就是 Event Loop 的骨架:for step in range(MAX_STEPS)是循环边界,call_llm是模型调用节点,parse_action是输出解析节点,TOOLS[action](**args)是工具执行节点,messages.append是上下文更新节点。ReAct 的 Thought-Action-Observation 闭环,全在这二十行里。
3.3 关键参数说明
MAX_STEPS是 Runtime 的终止保护,防止模型陷入死循环。temperature=0让 ReAct 决策更稳定,减少格式漂移。SYSTEM_PROMPT里强制 JSON 输出,是让parse_action能稳定工作的前提。工具返回值截断到 2000 字符,是 Context Management 的第一道防线——否则读一个大文件就把上下文撑爆了。
4. 验证请求:一次完整 ReAct 任务的事件流与成功结果
配置写完了,现在跑一次完整任务,确认推理与工具调用链路正常。验证动作要能观察到每一轮的事件流,而不是只看最终答案。
4.1 准备测试环境
在当前目录建一个README.md,随便写点内容,比如:
# 测试项目 这是一个用于验证 Agent Runtime 的示例项目。 包含 ReAct 循环、Event Loop 调度和工具调用链路。然后运行上面的脚本:
python react_runtime.py4.2 预期事件流
正常运转时,你会看到类似这样的输出:
[step 0] raw: {"thought": "需要先了解目录结构", "action": "list_files", "args": {"path": "."}} [step 1] raw: {"thought": "看到 README.md,需要读取内容", "action": "read_file", "args": {"path": "README.md"}} [step 2] raw: {"thought": "已获取 README 内容,可以总结", "finish": true, "answer": "当前目录包含 README.md,内容是..."}这三步就是一次完整的 ReAct 事件流:第一步 Thought 判断需要列目录,Action 是list_files,Observation 是目录列表;第二步 Thought 判断需要读文件,Action 是read_file,Observation 是文件内容;第三步 Thought 判断信息足够,输出 finish。
4.3 成功结果的判定标准
链路正常的标志有三个:每一轮 raw 输出都是合法 JSON;action 字段对应的工具被真实执行且返回了 observation;最终 finish=true 且 answer 包含了工具返回的信息。如果 answer 里出现了 README 的实际内容,说明 Observation 成功回灌到了 Context,Event Loop 闭环成立。
4.4 观察 Event Loop 的调度行为
你可以在run_react里加一行日志,打印每轮 messages 的长度:
print(f"[step {step}] context_len={len(messages)}")正常情况每轮增加 2(一条 assistant,一条 user observation)。如果某轮没增加,说明解析失败走了 continue 分支。这个日志是排查 Event Loop 卡死的第一手证据。
跑通这一步,你的 Agent Runtime 就已经具备了 ReAct 推理-行动循环的基本能力。接下来是排错,因为真实环境里几乎不可能一次就顺。
5. 本篇常见错排查:401、local proxy failed 与 reading choices
这一节对照真实报错,逐个拆解。这些错我都踩过,按顺序排查能省很多时间。
5.1 401 Unauthorized
报错长这样:
openai.AuthenticationError: Error code: 401 - {'error': {'message': 'Invalid API key'}}原因通常是三件套里的 Key 不对。检查顺序:第一,api_key是不是控制台生成的完整 Key,有没有多余空格;第二,Key 是否已过期或被删除;第三,Base URL 是否写成了https://taotoken.net/api,注意结尾没有斜杠,也不要加 UTM 参数到 API 地址上。如果 Key 确认没问题,去控制台看该 Key 的配额是否耗尽。
5.2 local proxy failed / connection error
报错长这样:
openai.APIConnectionError: Connection error.或者在某些工具里显示local proxy failed。这类错误是网络层没通。检查:Base URL 是否可达,可以用curl https://taotoken.net/api/models -H "Authorization: Bearer sk-你的Key"测试;本地是否有环境变量HTTP_PROXY/HTTPS_PROXY干扰,如果有,临时 unset 再试;DNS 解析是否正常。注意不要配置任何非官方的网络转发工具,直接用标准 HTTPS 访问即可。
5.3 reading 'choices' 报错
报错长这样:
TypeError: Cannot read properties of undefined (reading 'choices')这是响应结构不符合预期。原因通常是 Base URL 写错了,请求打到了非 OpenAI 兼容的端点,返回的不是标准 chat completion 结构。检查 Base URL 是否为https://taotoken.net/api,以及请求路径是否正确拼接为/chat/completions。如果你用的是 SDK,确认base_url参数只填到/api,SDK 会自动补全路径。
5.4 OAuth 相关报错
如果你用 Claude Code 类工具,可能遇到:
OAuth error: invalid_grant这是工具侧的认证配置和 API Key 模式冲突。解决方式是在settings.json里显式配置ANTHROPIC_BASE_URL和ANTHROPIC_API_KEY,走 Key 模式而不是 OAuth 模式。三件套齐全后,工具会优先使用 Key 认证。
5.5 JSON 解析失败导致 Event Loop 空转
报错长这样:
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)这不是接入层问题,是模型输出格式漂移。排查:temperature是否设成了 0;SYSTEM_PROMPT是否明确要求只输出 JSON;模型是否支持稳定的结构化输出。如果频繁出现,在解析失败时把原始输出回灌并追加一条「请只输出 JSON」的 user 消息,让 Runtime 自我纠正,而不是直接崩溃。
5.6 工具调用链路断裂
现象是模型一直输出 action,但 observation 永远是「工具执行失败」。检查TOOLS字典里的函数签名和模型输出的 args 是否匹配。比如模型输出{"action": "read_file", "args": {"file": "README.md"}},但你的函数参数是path,就会报unexpected keyword argument。解决办法是在 SYSTEM_PROMPT 里把每个工具的参数名写清楚,或者在 Runtime 里做参数名映射。
排查完这些,你的 Event Loop 基本能稳定跑多轮了。记住一个原则:接入层错误看三件套,Runtime 错误看事件流日志,模型错误看输出格式。
6. 把 Runtime 跑稳之后:统一 Key 与 Event Loop 的长期配合
走到这里,你已经有了一个能跑通 ReAct 多轮任务的 Agent Runtime,接入层用 TaoToken 统一 Key 收敛成了一个入口。接下来真正影响体验的,是 Event Loop 的调度细节和 Context 管理策略。
几个实战建议。第一,给 Event Loop 加步数上限和超时,MAX_STEPS之外再加一个timeout,防止某个工具卡死拖垮整个循环。第二,Observation 一定要截断,读文件、跑命令的输出都可能很长,不截断上下文很快爆炸。第三,每轮把 assistant 的原始输出和 observation 都存进 messages,不要只存解析后的结构,否则模型看不到自己的历史决策,容易重复动作。
如果你要让 Agent 长期跑编码或 Agent 类任务,Coding Plan 在多轮调用下有更稳的配额,适合把 Runtime 挂在后台持续执行。验证模型行为时,可以用模型对话页面快速对比不同 Model ID 在同一个 ReAct 任务下的输出稳定性,省得每次改代码。
接入文档里有完整的 Base URL、Key 和 Model ID 说明,配置卡住时对照一遍三件套。API Keys 页面用来管理你的 Key,建议给 Runtime 单独建一个 Key,方便按项目排查用量。
最后说一个我踩过的坑:不要把所有工具都塞进 SYSTEM_PROMPT。工具一多,模型选择困难,action 命中率下降。正确做法是像 Skill 的渐进式披露那样,先给工具清单的元信息,需要时再展开详细参数。Runtime 的 Skill Router 逻辑,本质上和 Event Loop 是同一套调度思想——按需加载,按需执行,跑完即止。