
1. 为什么我要把 ReAct 循环单独拎出来跑一遍如果你最近在翻 GitHub 上的 Agent 项目大概率会刷到那个 4.3 万 Star 的 Python 框架——nanobot。它用大约 4000 行代码把「感知-思考-行动」的闭环讲得明明白白很多人把它当成 OpenClaw / Claude Code 的轻量可研究平替。但真把仓库 clone 下来跑的时候第一道坎往往不是代码看不懂而是 LLM 通道怎么接每个 Provider 一套 Key、一套 Base URL换模型就要改环境变量调试 ReAct 循环时注意力全被配置分散了。这篇就聚焦一件事用 TaoToken 统一 Key 和 API 通道在本地把 Python Agent 框架的 ReAct 核心循环跑通。所谓 ReAct就是 Reasoning Acting模型先输出一段 Thought我该干什么再输出 Action调用哪个工具、传什么参数代码执行工具后把 Observation工具返回结果塞回上下文模型继续下一轮 Thought直到给出 Final Answer。这个循环是 Agent 调度与 LLM 调用真正咬合的地方理解了它再看任何框架的 AgentLoop 都不会发怵。适合谁看写过一点 Python、调过 OpenAI 风格接口、想搞懂 Agent 内部到底怎么转起来的开发者。你不需要先精通 nanobot 全部源码只要能把下面这套最小脚本跑起来就能自己往里加工具、换模型、观察每一轮的消息结构。整篇的节奏是先讲清楚问题和场景再配好统一通道然后给可复制的配置和脚本接着验证一次真实工具调用链路最后把常见报错挨个排掉。2. 用 TaoToken 做统一 LLM 通道的前置准备在动手写 Agent 之前先把「模型从哪来」这件事固定下来。ReAct 循环里每一轮 Thought 都要打一次 LLM如果通道不稳定或者换模型要改一堆代码调试体验会很差。我的做法是让所有请求都走同一个 OpenAI 兼容入口Base URL 指向https://taotoken.net/apiKey 用同一个模型名通过参数传进去。这样 Agent 脚本里只认一个 client换模型只改一个字符串。先注册并拿到 Key。打开控制台地址https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentreact_loop在 API Keys 页面创建一个新 Key复制出来先存到本地。注意 Key 只在创建时完整显示一次丢了就重新建一个别硬找。拿到 Key 之后建议先别急着写 Agent用最小请求确认通道是通的。这一步能帮你把「通道问题」和「Agent 逻辑问题」分开后面排错会省很多时间。你可以用 curl 直接打一次对话接口curl https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -d { model: claude-sonnet-4-20250514, messages: [{role: user, content: 只回复两个字通了}], max_tokens: 32 }如果返回的 JSON 里choices[0].message.content是「通了」说明 Key、Base URL、模型名三件套都对。这里有个细节OpenAI 兼容接口的路径是/v1/chat/completions而 Base URL 只写到/apiSDK 会自动补后面的路径别把/v1也塞进 Base URL否则会拼成/api/v1/v1/...直接 404。模型名这块TaoToken 支持多家模型你在控制台的模型列表里能看到当前可用的 ID。写脚本时把它抽成环境变量比如TAOTOKEN_MODEL这样同一份 Agent 代码可以在不同模型间切换对比 ReAct 的表现。我个人调试时会先用一个响应快、指令跟随稳的模型把循环跑通再换更强的模型看复杂任务下的多步推理。环境变量统一放.env里别硬编码进代码。Python 侧用python-dotenv读取或者直接在 shell 里 export。下面这份就是后面脚本要用的全部配置先建好文件# .env TAOTOKEN_API_KEYsk-你的Key TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_MODELclaude-sonnet-4-20250514装依赖只需要两个包openai负责请求python-dotenv负责读环境变量。用你习惯的虚拟环境装python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install openai python-dotenv到这里前置就齐了一个 Key、一个 Base URL、一个模型名、两个依赖。接下来所有 Agent 逻辑都建立在这套统一通道上不再碰任何 Provider 专属配置。3. 可复制的 ReAct 最小 Agent 脚本与配置这一节直接给能跑的代码。核心思路是把 ReAct 循环拆成三块工具注册表、LLM 调用封装、循环调度器。工具先用一个最简单的计算器和一个查时间工具方便你观察 Observation 是怎么回填的。先建tools.py定义工具和它们的 JSON Schema。Agent 靠这份 Schema 告诉模型「有哪些工具可用、参数长什么样」# tools.py import json from datetime import datetime def calculator(expression: str) - str: 只允许数字和四则运算避免 eval 执行任意代码 allowed set(0123456789-*/(). ) if not set(expression) allowed: return 错误表达式包含非法字符 try: return str(eval(expression, {__builtins__: {}}, {})) except Exception as e: return f计算失败{e} def now_time(_: str ) - str: return datetime.now().strftime(%Y-%m-%d %H:%M:%S) TOOL_REGISTRY { calculator: calculator, now_time: now_time, } TOOL_SCHEMAS [ { type: function, function: { name: calculator, description: 计算一个数学表达式例如 12*(34), parameters: { type: object, properties: { expression: {type: string, description: 要计算的表达式} }, required: [expression], }, }, }, { type: function, function: { name: now_time, description: 获取当前本地时间, parameters: {type: object, properties: {}}, }, }, ]再建agent.py这是 ReAct 循环的主体。关键点在于每轮把模型返回的tool_calls解析出来执行对应工具把结果以role: tool的消息追加回messages然后再次请求模型。循环有最大轮数保护避免模型陷入死循环# agent.py import json import os from dotenv import load_dotenv from openai import OpenAI from tools import TOOL_REGISTRY, TOOL_SCHEMAS load_dotenv() client OpenAI( api_keyos.environ[TAOTOKEN_API_KEY], base_urlos.environ[TAOTOKEN_BASE_URL], ) MODEL os.environ[TAOTOKEN_MODEL] SYSTEM_PROMPT ( 你是一个会使用工具的助手。需要计算或查时间时必须调用工具 不要凭记忆回答。拿到工具结果后再给出最终答案。 ) def run_react(user_input: str, max_turns: int 6) - str: messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_input}, ] for turn in range(max_turns): resp client.chat.completions.create( modelMODEL, messagesmessages, toolsTOOL_SCHEMAS, tool_choiceauto, ) msg resp.choices[0].message messages.append(msg) # 没有工具调用说明模型给出了最终答案 if not msg.tool_calls: return msg.content # 有工具调用逐个执行把 Observation 回填 for call in msg.tool_calls: name call.function.name args json.loads(call.function.arguments or {}) print(f[Turn {turn}] Action: {name} Args: {args}) fn TOOL_REGISTRY.get(name) observation fn(**args) if fn else f未知工具{name} print(f[Turn {turn}] Observation: {observation}) messages.append({ role: tool, tool_call_id: call.id, content: str(observation), }) return 达到最大轮数仍未得到最终答案 if __name__ __main__: print(run_react(帮我算一下 (128 72) * 3 等于多少再告诉我现在几点))跑之前确认.env和两个 py 文件在同一目录然后python agent.py预期你会看到类似这样的输出Action 和 Observation 交替出现最后模型给出合并了计算和时间的结果[Turn 0] Action: calculator Args: {expression: (128 72) * 3} [Turn 0] Observation: 600 [Turn 1] Action: now_time Args: {} [Turn 1] Observation: 2026-01-15 14:32:07 (128 72) * 3 600当前时间是 2026-01-15 14:32:07。这里就是 ReAct 的精髓模型不是一次性把答案编出来而是先决定「我要调 calculator」拿到 600 这个 Observation 后再决定「我还要调 now_time」最后才组织语言。你可以在run_react里加一行打印messages的长度会看到每轮都在增长这就是上下文在累积 Thought-Action-Observation 的过程。如果你用的是 Claude Code 这类工具做辅助开发配置里同样填这三件套Base URL 填https://taotoken.net/apiKey 填你的 TaoToken KeyModel ID 填控制台里的模型名。Cline、CC Switch 这类插件的 MCP 或 Provider 配置也是同一个逻辑认准 OpenAI 兼容格式即可。想长期跑编码类 Agent 任务可以了解下 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentreact_loop。4. 验证一次完整工具调用链路与成功结果脚本能跑出答案只是第一步真正要确认的是「调度与 LLM 调用的衔接点」有没有按预期工作。我建议做一次带日志的验证把每一轮的消息角色和工具调用 ID 都打出来这样你能清楚看到框架是怎么把模型的意图翻译成函数执行的。改造一下循环加一个调试开关把每轮的消息摘要打印出来def run_react_debug(user_input: str, max_turns: int 6) - str: messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_input}, ] for turn in range(max_turns): resp client.chat.completions.create( modelMODEL, messagesmessages, toolsTOOL_SCHEMAS, tool_choiceauto, ) msg resp.choices[0].message print(f--- Turn {turn} ---) print(ffinish_reason: {resp.choices[0].finish_reason}) print(fcontent: {msg.content!r}) print(ftool_calls: {[c.function.name for c in (msg.tool_calls or [])]}) messages.append(msg) if not msg.tool_calls: return msg.content for call in msg.tool_calls: name call.function.name args json.loads(call.function.arguments or {}) fn TOOL_REGISTRY.get(name) observation fn(**args) if fn else f未知工具{name} print(ftool_call_id: {call.id} - {observation}) messages.append({ role: tool, tool_call_id: call.id, content: str(observation), }) return 达到最大轮数跑一次run_react_debug(先算 45*8再算 360/9最后告诉我现在时间)你会观察到几个关键信号。第一轮finish_reason是tool_callscontent通常是空的tool_calls里是calculator执行完回填后第二轮模型可能继续调calculator算第二个式子第三轮调now_time直到某一轮finish_reason变成stop、tool_calls为空、content有内容循环才结束。这个finish_reason从tool_calls到stop的切换就是 ReAct 循环的退出条件很多框架内部也是靠它判断的。成功结果应该满足三点工具被真实执行Observation 是算出来的不是模型编的、多步调用按顺序发生、最终答案里包含了工具返回的数据。如果模型跳过工具直接给答案多半是 System Prompt 不够强硬或者tool_choice被设成了none。你可以把tool_choice临时改成强制调用某个工具来验证链路tool_choice{type: function, function: {name: calculator}}这样模型第一轮必定调 calculator用来确认「请求-解析-执行-回填」这条链路本身没问题。确认后再改回auto让模型自己决定。验证通过后你可以把工具换成真实业务里的函数比如查数据库、调内部 HTTP 接口、读文件。ReAct 循环本身不用改只要往TOOL_REGISTRY和TOOL_SCHEMAS里加条目就行。这也是为什么统一通道很重要工具越加越多模型调用越频繁通道稳定和 Key 统一能让你把精力放在工具逻辑上而不是到处找配置。5. 跑 ReAct 循环时常见的报错与排查调试 Agent 时踩的坑基本集中在通道和消息结构两类。下面这几个是我实际遇到过的对照着排会快很多。401 Unauthorized / invalid api key最常见。先确认.env里的 Key 没有多余空格或换行load_dotenv()有没有真的加载到可以print(os.environ.get(TAOTOKEN_API_KEY)[:8])看前几位。如果 Key 是从控制台复制的注意别把前后引号也带进去。还有一种情况是 Key 被删了或过期去控制台重新建一个。local proxy failed / connection error这类报错通常是 Base URL 写错或网络出口有问题。检查TAOTOKEN_BASE_URL是不是https://taotoken.net/api末尾不要带/也不要带/v1。如果你在容器或远程机器里跑确认那台机器能正常访问外网 HTTPS。用前面那条 curl 命令单独测一次能通就说明是代码侧的问题。reading choices of undefined / KeyError: choices说明返回体不是预期的 OpenAI 格式多半是请求打到了错误路径或者返回了一个错误 JSON。把resp整个打印出来看常见原因是 Base URL 拼成了/api/v1/v1/chat/completions或者模型名写错导致服务端返回错误对象。模型名一定要和控制台里列出的 ID 完全一致大小写和日期后缀都别改。tool_calls 为空但模型没给答案有时模型返回finish_reason: stop但content是空字符串。这通常是max_tokens太小或者 System Prompt 让它「必须调工具」但它判断不需要调。把max_tokens调大或者检查tool_choice设置。如果用的是推理型模型还要注意它可能把内容放在别的字段里打印完整resp确认。OAuth / 认证方式不匹配如果你之前用某个 CLI 工具登录过本地可能残留了 OAuth 凭证SDK 优先用了它而不是你的 API Key。检查环境里有没有OPENAI_API_KEY之类的变量被覆盖或者 CLI 的配置文件里存了旧凭证。最稳妥的办法是在脚本里显式传api_key不依赖环境推断。消息结构报错 invalid messages / tool_call_id mismatchReAct 循环里最容易犯的错。每个role: tool的消息必须带tool_call_id且要和对应tool_calls里的id完全一致。如果你手动构造消息漏了tool_call_id或者顺序乱了服务端会拒绝。用 SDK 返回的msg对象直接 append 最省事别自己拼。排错时记住一个原则先用 curl 确认通道再用最小脚本确认消息结构最后才怀疑 Agent 逻辑。把变量一个个固定住问题范围会小很多。接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentreact_loop接口路径和参数以文档为准。6. 把统一 Key 用在更多 Agent 场景ReAct 循环跑通之后你会发现它其实是个通用骨架。nanobot 那类框架的 AgentLoop 再复杂核心也是「拿消息、调模型、执行工具、回填、再调模型」这套。区别只在于它们加了会话锁、记忆压缩、子 Agent 这些工程化能力。你自己写的这版最小循环正好是理解那些机制的起点。接下来可以往几个方向扩展。一是加记忆把每轮对话存到列表或文件下次请求时带上历史就接近了 nanobot 的 Hot Memory 思路。二是加多工具路由工具多了以后Schema 会变长可以按任务类型动态筛选要传给模型的工具减少 token 消耗。三是加并发不同会话用独立的消息列表用asyncio并发跑单会话内保持串行这就是很多框架「Per-session 串行、跨 session 并发」的做法。统一 Key 的价值在这些扩展里会越来越明显。你不需要为每个模型、每个工具链单独维护凭证Base URL 和 Key 固定模型名当参数传切换和对比成本极低。想验证不同模型在 ReAct 多步推理上的表现直接改TAOTOKEN_MODEL重跑就行。需要看模型对话效果可以走https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentreact_loop要管理 Key 和额度去https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentreact_loop。最后留一个实用技巧调试 ReAct 时把每轮的messages完整 dump 成 JSON 存文件出问题时直接看模型到底收到了什么。很多「模型不调工具」的怪现象翻一眼上下文就发现是上一轮的 Observation 没回填对或者 System Prompt 被历史消息挤掉了。这个习惯比任何调试器都好用。