:灵魂篇——用TaoToken打通ReAct循环的Thought骨架)
1. 为什么你的 Agent 只会“说”不会“做”很多人第一次写 Agent卡在同一个地方模型能输出一段看起来很聪明的分析但代码跑完就结束了文件没动、命令没跑、任务没闭环。问题不在模型而在你少了一个循环骨架。ReAct 循环Reasoning Acting就是让 LLM 从“顾问”变成“执行者”的那根脊椎而 Thought 是这根脊椎里最先要立起来的一节。我先把这一篇要交付的东西说清楚你会拿到一个可复制的 ReAct 循环伪代码骨架一套用 TaoToken 统一 Key 接入 LLM 的配置以及一次完整的 Thought 链路跑通验证。目标不是让你背概念而是让你在本地看到“思考→行动→观察→再思考”真的转起来。先对齐一个最小认知。ReAct 循环里Thought 是模型对当前状态的推理Action 是它决定调用的工具Observation 是工具返回的真实结果。三者按顺序进入对话历史形成状态更新S_{t1} S_t (Thought_t, Action_t, Observation_t)普通对话模型只做一次LLM(S_0)就结束Agent 则要反复执行Think → Act → Observe直到模型输出 Answer 或达到最大轮数。Thought 骨架的意义在于它把模型的“内心独白”变成可解析、可调试、可回放的结构化文本。没有 Thought你只能看到最终答案出了问题根本不知道模型在哪一步跑偏。适合谁读写过 Python、调过任意一家大模型 API、想手写 Agent 循环但被“怎么把推理步骤落成代码”卡住的开发者。你不需要懂 Function Calling也不需要框架这一篇只用最朴素的字符串解析把骨架搭出来。我试过用纯正则解析 Thought一开始觉得土后来发现它反而是最好的教学工具——因为你能亲眼看到模型输出什么、你的代码怎么切、Observation 怎么塞回去。等你理解了这条链路再换 JSON 或 Function Calling 就是换皮的事。下面按“先接模型、再写循环、最后验证”的顺序推进。前置接入部分我会用 TaoToken 做统一入口这样你不用在多个厂商 Key 之间来回切一个 Key 就能把循环跑通。2. TaoToken 前置一个 Key 打通 ReAct 循环的模型调用写 Agent 循环最烦的不是循环本身是模型调用层。你可能会遇到这家要改 base_url那家要换 SDK换模型还得改参数名。ReAct 循环每轮都要调一次模型调用层不稳定循环调试就是灾难。所以第二步先把模型入口统一掉。TaoToken 在这里的角色是统一 API 网关你拿一个 Key通过一个兼容 OpenAI 协议的 Base URL 调用多家模型。对 ReAct 循环来说这意味着LLMClient只需要写一次换模型只改一个 model 字符串。先拿 Key。打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后在控制台创建 API Key。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite Key 管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。创建完先复制保存页面刷新后不再完整显示。接入信息三件套写死在配置里配置项值Base URLhttps://taotoken.net/apiAPI Key你在控制台创建的 KeyModel ID例如 claude-sonnet-4-5、gpt-4o 等按控制台可用列表填注意 Base URL 不要加 UTMAPI 调用地址就是https://taotoken.net/apiOpenAI 兼容路径是/v1/chat/completionsSDK 会自动拼。如果你用 requests 手写完整地址是https://taotoken.net/api/v1/chat/completions。为什么 ReAct 循环特别需要统一入口因为循环里模型会被调用 N 次每次都要带完整 messages 历史。如果调用层有厂商差异你会在“为什么第 3 轮开始报错”这种问题上浪费大量时间。统一之后循环逻辑和模型解耦调试边界清晰。环境变量方式最省事写进.envTAOTOKEN_API_KEYsk-你的key TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_MODELclaude-sonnet-4-5然后LLMClient只读这三个值。这样你的react_agent.py里不会出现任何硬编码 Key也不会因为换模型改循环代码。如果你更习惯用 Claude Code 或 Cline 这类工具做辅助调试也可以在对应配置里填同一套 Base URL Key Model ID。比如 Claude Code 的 settings 里配置 Anthropic 兼容入口Cline 的 MCP 配置里填 OpenAI 兼容地址Codex 的auth.json里填 base_url 和 api_key。三件套一致工具和你的手写循环就能共用同一个 Key。这一步做完你手里应该有一个能调通的模型入口。下一节直接把它塞进 ReAct 循环。3. 可复制配置ReAct 循环骨架与 LLMClient 落地这一节给你可以直接复制的代码。分两个文件llm_client.py负责模型调用react_agent.py负责循环骨架。先看调用层。# llm_client.py import os import requests from dotenv import load_dotenv load_dotenv() class LLMClient: def __init__(self, modelNone): self.api_key os.getenv(TAOTOKEN_API_KEY) self.base_url os.getenv(TAOTOKEN_BASE_URL, https://taotoken.net/api) self.model model or os.getenv(TAOTOKEN_MODEL, claude-sonnet-4-5) def chat(self, messages, temperature0.2): url f{self.base_url}/v1/chat/completions headers { Authorization: fBearer {self.api_key}, Content-Type: application/json, } payload { model: self.model, messages: messages, temperature: temperature, } resp requests.post(url, headersheaders, jsonpayload, timeout60) resp.raise_for_status() data resp.json() return data[choices][0][message][content]这段代码的关键点base_url指向 TaoTokenmodel从环境变量读循环里换模型不用改代码。temperature0.2是为了让 Thought 输出更稳定ReAct 循环对格式一致性要求高温度太高模型容易自由发挥。接下来是循环骨架。我把 Thought 解析、Action 执行、Observation 回填三件事拆成独立方法方便你逐段调试。# react_agent.py import re from llm_client import LLMClient class ReActAgent: def __init__(self, max_iterations10): self.client LLMClient() self.max_iterations max_iterations self.system_prompt 你是一个Python工程师Agent。 工作方式 1. 先思考Thought分析当前情况决定下一步 2. 再行动Action调用工具 3. 观察结果Observation系统会返回执行结果 4. 根据结果继续思考直到任务完成 可用工具 - read_file(path): 读取文件 - write_file(path, content): 写入文件 - run_cmd(command): 执行命令 输出格式严格遵守 Thought: [你的思考] Action: [工具名]([参数]) 任务完成时输出 Thought: [总结] Answer: [最终回答] def parse_response(self, response): result {thought: None, action: None, answer: None} thought re.search(rThought:\s*(.?)(?\n(?:Action|Answer):|$), response, re.DOTALL) if thought: result[thought] thought.group(1).strip() action re.search(rAction:\s*(.?)(?\n|$), response) if action: result[action] action.group(1).strip() answer re.search(rAnswer:\s*(.?)$, response, re.DOTALL) if answer: result[answer] answer.group(1).strip() return result def execute_action(self, action): if read_file in action: return 文件内容\ndef main():\n print(Hello)\n user_id get_user() if write_file in action: return 文件已保存 if run_cmd in action: return 测试通过 return f未知工具{action} def run(self, user_input): messages [ {role: system, content: self.system_prompt}, {role: user, content: user_input}, ] for i in range(self.max_iterations): print(f\n[第 {i1} 轮]) response self.client.chat(messages) parsed self.parse_response(response) if parsed[thought]: print(fThought: {parsed[thought]}) if parsed[answer]: print(fAnswer: {parsed[answer]}) return parsed[answer] if parsed[action]: print(fAction: {parsed[action]}) observation self.execute_action(parsed[action]) print(fObservation: {observation}) messages.append({role: assistant, content: response}) messages.append({role: user, content: fObservation: {observation}}) else: print(格式错误没有 Action 或 Answer) break return 达到最大轮数任务未完成这里有一个必须讲透的设计点Observation 为什么用roleuser回填。从模型视角看它只能看到 messages 列表。assistant 是它自己说过的话user 是外部输入。Observation 是工具返回的“外部世界信息”语义上等同于用户告诉它一个事实所以用 user 角色。如果你用 assistant 回填模型会以为那是自己编的容易忽略。另一个点是messages.append的顺序先 append assistant 的原始 response再 append user 的 Observation。这样下一轮模型能看到“我上一步想了什么、做了什么、结果是什么”状态才完整。配置层面如果你用 Cline 或 Claude Code 做辅助把三件套填进去即可{ base_url: https://taotoken.net/api, api_key: sk-你的key, model: claude-sonnet-4-5 }Codex 的auth.json同理填base_url和api_key模型 ID 按控制台可用列表选。三件套一致你的手写循环和工具链共用同一个入口。4. 验证请求一次完整 Thought 链路跑通代码写完跑一次。测试脚本# test_react.py from react_agent import ReActAgent agent ReActAgent(max_iterations5) result agent.run(帮我检查 main.py 有没有 Bug有就修复) print(\n最终结果, result)运行python test_react.py你会看到类似下面的输出[第 1 轮] Thought: 用户想检查 main.py我应该先读取文件内容 Action: read_file(main.py) Observation: 文件内容def main(): print(Hello) user_id get_user() [第 2 轮] Thought: 我看到 user_id 可能是拼写问题应该写入修正后的内容 Action: write_file(main.py, ...) Observation: 文件已保存 [第 3 轮] Thought: 文件已修改应该运行测试验证 Action: run_cmd(pytest) Observation: 测试通过 [第 4 轮] Thought: 测试通过任务完成 Answer: Bug 已修复并通过测试看到这条链路说明四件事都对了模型按格式输出了 Thought 和 Action正则解析成功提取Observation 正确回填进 messages循环在 Answer 出现时终止。如果你想单独验证模型入口是否通先用 curl 打一发curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-5, messages: [{role: user, content: 回复ok}] }返回里有choices[0].message.content就说明 Key 和 Base URL 没问题。这一步能帮你把“模型调用失败”和“循环逻辑失败”分开定位。验证时重点看三个信号Thought 是否每轮都有、Action 是否被解析出来、Observation 是否出现在下一轮模型输入里。如果 Thought 有但 Action 没有多半是模型输出格式漂了如果 Action 有但 Observation 没进下一轮检查 append 顺序。跑通之后你可以把execute_action里的模拟返回换成真实文件读写循环骨架不用动。这就是骨架的价值Thought 链路稳定后工具是插拔的。5. 常见报错排查401、local proxy failed、reading choices这一节按真实报错来。ReAct 循环调试时错误通常出现在调用层和解析层分清楚能省很多时间。401 Unauthorized。最常见原因是 Key 没读到或格式不对。检查.env里TAOTOKEN_API_KEY是否以sk-开头load_dotenv()是否在LLMClient初始化前执行。如果你把 Key 写进 shell 变量确认echo $TAOTOKEN_API_KEY有输出。还有一种情况是 Key 复制时带了空格或换行strip 一下。local proxy failed / connection refused。这类报错说明请求没到 TaoToken。先确认base_url是https://taotoken.net/api没有多余斜杠或路径。如果你本地有网络工具改了系统代理requests 可能走了错误出口临时unset HTTP_PROXY HTTPS_PROXY再试。注意不要用任何非正规网络手段正常直连即可。reading choices / KeyError: choices。这通常不是网络问题是返回体结构和你预期不一致。先打印resp.text看原始返回。常见原因模型 ID 写错导致返回错误对象请求体缺messages或者resp.raise_for_status()没触发但返回了错误结构。加一行print(resp.status_code, resp.text[:200])能快速定位。OAuth / authentication_error。如果你在 Claude Code 或 Cline 里看到 OAuth 相关报错说明工具在走它自己的登录流程而不是你填的 Key。检查配置里是否同时存在 OAuth token 和 API Key优先用 API Key 模式。Claude Code 的 settings 里确认base_url指向 TaoTokenCline 的 MCP 配置里确认api_key字段生效。Thought 解析为空。模型输出格式漂了。先看原始 response如果 Thought 后面直接跟换行再 Action正则里的re.DOTALL和前瞻断言要能覆盖。如果模型用了中文冒号“Thought”正则要兼容。最稳的做法是在 System Prompt 里强调“必须用英文冒号”并在解析前做一次response.replace(, :)。循环不终止。模型一直输出 Action 不输出 Answer。检查max_iterations是否设置通常 10 到 15 轮够用。如果模型反复执行同一个 Action说明 Observation 没让它获得新信息检查execute_action返回是否为空或重复。排查顺序建议先 curl 验证模型入口再单独测parse_response最后跑完整循环。分层定位比盯着循环日志猜快得多。6. 把 Thought 骨架接进你的工作流骨架跑通后下一步是让它稳定服务于真实任务。几个实用建议。第一把 System Prompt 里的工具描述和execute_action的分支保持一一对应。模型只能根据 Prompt 里的工具名输出 Action你执行层多一个少一个都会导致“未知工具”。改工具时两边同时改。第二Thought 建议保留完整历史不要每轮截断。ReAct 的推理链是累积的截断会让模型丢失上下文。如果 messages 太长优先压缩早期 Observation而不是删 Thought。第三调试阶段把每轮的response原始文本落盘方便回放。你可以加一个debug_log列表把(iteration, response, parsed, observation)存下来出问题时直接看哪一轮格式漂了。第四模型选择上ReAct 循环对指令遵循要求高。如果你发现某模型经常不按格式输出换一个指令遵循更强的 Model ID。TaoToken 的好处是换模型只改环境变量循环代码不动。如果你要把这套骨架用于长期编码或 Agent 任务可以了解 Coding Plan 相关入口https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。模型对话调试入口在 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite Key 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。Thought 骨架是整个 Agent 里最值得先写扎实的部分。它不依赖复杂框架却能让你看清 LLM 推理步骤如何变成可调试的代码。把这一节跑通后面接真实工具、加终端执行、做上下文管理都是在稳定骨架上加肌肉。