news 2026/10/1 20:49:47

零基础复现Claude Code(三):灵魂篇——用TaoToken打通ReAct循环的Thought骨架

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
零基础复现Claude Code(三):灵魂篇——用TaoToken打通ReAct循环的Thought骨架

1. 为什么你的 Agent 只会“说”不会“做”

很多人第一次写 Agent,卡在同一个地方:模型能输出一段看起来很聪明的分析,但代码跑完就结束了,文件没动、命令没跑、任务没闭环。问题不在模型,而在你少了一个循环骨架。ReAct 循环(Reasoning + Acting)就是让 LLM 从“顾问”变成“执行者”的那根脊椎,而 Thought 是这根脊椎里最先要立起来的一节。

我先把这一篇要交付的东西说清楚:你会拿到一个可复制的 ReAct 循环伪代码骨架,一套用 TaoToken 统一 Key 接入 LLM 的配置,以及一次完整的 Thought 链路跑通验证。目标不是让你背概念,而是让你在本地看到“思考→行动→观察→再思考”真的转起来。

先对齐一个最小认知。ReAct 循环里,Thought 是模型对当前状态的推理,Action 是它决定调用的工具,Observation 是工具返回的真实结果。三者按顺序进入对话历史,形成状态更新:

S_{t+1} = 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_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,注册后在控制台创建 API Key。控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,Key 管理页在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。创建完先复制保存,页面刷新后不再完整显示。

接入信息三件套,写死在配置里:

配置项值
Base URLhttps://taotoken.net/api
API Key你在控制台创建的 Key
Model ID例如 claude-sonnet-4-5、gpt-4o 等,按控制台可用列表填

注意 Base URL 不要加 UTM,API 调用地址就是https://taotoken.net/api,OpenAI 兼容路径是/v1/chat/completions,SDK 会自动拼。如果你用 requests 手写,完整地址是https://taotoken.net/api/v1/chat/completions。

为什么 ReAct 循环特别需要统一入口?因为循环里模型会被调用 N 次,每次都要带完整 messages 历史。如果调用层有厂商差异,你会在“为什么第 3 轮开始报错”这种问题上浪费大量时间。统一之后,循环逻辑和模型解耦,调试边界清晰。

环境变量方式最省事,写进.env:

TAOTOKEN_API_KEY=sk-你的key TAOTOKEN_BASE_URL=https://taotoken.net/api TAOTOKEN_MODEL=claude-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, model=None): 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, temperature=0.2): url = f"{self.base_url}/v1/chat/completions" headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", } payload = { "model": self.model, "messages": messages, "temperature": temperature, } resp = requests.post(url, headers=headers, json=payload, timeout=60) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"]

这段代码的关键点:base_url指向 TaoToken,model从环境变量读,循环里换模型不用改代码。temperature=0.2是为了让 Thought 输出更稳定,ReAct 循环对格式一致性要求高,温度太高模型容易自由发挥。

接下来是循环骨架。我把 Thought 解析、Action 执行、Observation 回填三件事拆成独立方法,方便你逐段调试。

# react_agent.py import re from llm_client import LLMClient class ReActAgent: def __init__(self, max_iterations=10): 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(r"Thought:\s*(.+?)(?=\n(?:Action|Answer):|$)", response, re.DOTALL) if thought: result["thought"] = thought.group(1).strip() action = re.search(r"Action:\s*(.+?)(?=\n|$)", response) if action: result["action"] = action.group(1).strip() answer = re.search(r"Answer:\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[第 {i+1} 轮]") response = self.client.chat(messages) parsed = self.parse_response(response) if parsed["thought"]: print(f"Thought: {parsed['thought']}") if parsed["answer"]: print(f"Answer: {parsed['answer']}") return parsed["answer"] if parsed["action"]: print(f"Action: {parsed['action']}") observation = self.execute_action(parsed["action"]) print(f"Observation: {observation}") messages.append({"role": "assistant", "content": response}) messages.append({"role": "user", "content": f"Observation: {observation}"}) else: print("格式错误:没有 Action 或 Answer") break return "达到最大轮数,任务未完成"

这里有一个必须讲透的设计点:Observation 为什么用role="user"回填。从模型视角看,它只能看到 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_iterations=5) 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指向 TaoToken,Cline 的 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_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。模型对话调试入口在 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite ,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,Key 管理在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。

Thought 骨架是整个 Agent 里最值得先写扎实的部分。它不依赖复杂框架,却能让你看清 LLM 推理步骤如何变成可调试的代码。把这一节跑通,后面接真实工具、加终端执行、做上下文管理,都是在稳定骨架上加肌肉。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 20:49:30

办公效率神器 OpenClaw:用 TaoToken 统一 Key 打通文件与浏览器自动化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 20:45:10

uni-app Android原生插件开发全攻略:从环境搭建到打包上架

做 uni-app 项目的朋友应该都有过这种体验:JS 层逻辑写得好好的,一碰到蓝牙、NFC、身份证读卡器这类硬件能力,或者要接入某个只有原生 SDK 的厂商服务,瞬间就抓瞎了。我自己第一次在 uni-app 项目里对接一脸谱人脸识别 SDK 时&…

作者头像 李华
网站建设 2026/10/1 20:44:07

Streama私人家用视频站搭建指南:Ubuntu+Java8+systemd全栈部署

1. 项目概述:为什么一个“私人家用视频网站”值得花三小时认真搭一次Streama 是我过去三年里反复重装、迁移、优化过至少七次的个人媒体服务。它不是 Plex 那种开箱即用的商业方案,也不是 Jellyfin 那样功能堆叠到需要查文档才能调出字幕设置的全栈平台—…

作者头像 李华