1. 为什么单步 ReAct 撑不起复杂任务:从「边走边看」到「先谋后动」的 Agent 演进
如果你最近在写 LLM Agent,大概率绕不开一个尴尬:Demo 里 ReAct 跑得挺顺,一换到真实任务就开始翻车。问它「帮我分析这份销售数据并生成周报」,它会先查一下数据、再算个平均值、然后突然忘了自己要干嘛,最后给你一段看起来像周报但数字对不上的文字。这不是模型不够聪明,而是 ReAct 这种「思考—行动—观察」的单步循环,天生缺少全局视角。
ReAct 的核心价值在于把推理和工具调用交织在一起:模型先输出 Thought 分解当前该做什么,再输出 Action 调用搜索或计算器,拿到 Observation 后继续下一轮。它解决了 Chain-of-Thought 只能空想、Act-Only 只会蛮干的问题,在知识密集型问答里表现很稳。但它的局限也很明显——每一步都只看眼前,没有一份「任务地图」,步骤一多就容易跑偏,而且它不会记住上一轮为什么失败。
Plan-and-Execute 换了个思路:先让 Planner 把目标拆成结构化步骤序列,再由 Executor 逐步执行,Replanner 根据执行结果决定继续、调整还是终止。这就像出门前先看地图规划路线,而不是走到路口才想要往哪拐。再往上,Reflection(Reflexion)给 Agent 加了「复盘」能力:Actor 产生轨迹,Evaluator 打分,Self-Reflection 生成语言反馈存进记忆,下次别再犯同样的错。
这篇会带你用一套统一的 Key,把 ReAct、Plan-and-Execute、Reflection 三种范式串成一个可运行 Demo,跑通「规划—执行—反思」全流程。适合已经会调 LLM API、想往 Agent 方向深入的同学。下面先从接入配置讲起,再给代码骨架,最后用三类任务做对比验证。
2. TaoToken 统一 Key 前置准备:Base URL、API Key 与模型 ID 三件套怎么配
不管你最后选哪种 Agent 范式,第一步都是让代码能稳定调到大模型。我试过在多个项目里各配一套 Key,结果环境变量互相覆盖,排查半天。后来统一用 TaoToken 做接入层,一个 Key 走通对话、代码、Agent 循环,省掉不少切换成本。
你需要准备的三件套是:Base URL、API Key、Model ID。Base URL 用https://taotoken.net/api,这是 OpenAI 兼容协议的入口,绝大多数 SDK 直接改base_url就能用。API Key 在控制台的 API Keys 页面创建,建议按项目建不同的 Key,方便后面看用量。Model ID 按任务选:规划类任务用推理强一点的模型,执行类任务用响应快的模型,评估/反思类可以用便宜模型来打分。
先建一个.env文件,把配置集中管理:
# .env TAOTOKEN_BASE_URL=https://taotoken.net/api TAOTOKEN_API_KEY=sk-你的key MODEL_PLANNER=claude-sonnet-4-20250514 MODEL_EXECUTOR=claude-sonnet-4-20250514 MODEL_EVALUATOR=claude-3-5-haiku-20241022如果你用 Python,装好依赖后这样初始化客户端:
# llm_client.py import os from openai import OpenAI from dotenv import load_dotenv load_dotenv() client = OpenAI( base_url=os.getenv("TAOTOKEN_BASE_URL"), api_key=os.getenv("TAOTOKEN_API_KEY"), ) def chat(model: str, messages: list, temperature: float = 0.2) -> str: resp = client.chat.completions.create( model=model, messages=messages, temperature=temperature, ) return resp.choices[0].message.content如果你用 Node/TypeScript,配置逻辑一样,只是换成openai包:
// llmClient.ts import OpenAI from "openai"; const client = new OpenAI({ baseURL: process.env.TAOTOKEN_BASE_URL, apiKey: process.env.TAOTOKEN_API_KEY, }); export async function chat(model: string, messages: any[]) { const resp = await client.chat.completions.create({ model, messages, temperature: 0.2, }); return resp.choices[0].message.content; }这里有个容易踩的坑:Base URL 末尾不要多加/v1,OpenAI SDK 会自己拼路径,多写一层会 404。另外 Key 别硬编码进代码提交到仓库,用环境变量或密钥管理服务。配好之后先跑一个最小请求验证连通性,确认没问题再往上搭 Agent 循环,不然报错了你分不清是网络问题还是逻辑问题。
3. 三种范式的可复制配置与代码骨架:ReAct、Plan-and-Execute、Reflection 全流程
这一节是核心,我会给出三种范式的可运行骨架。为了让你能直接复现,配置部分用 JSON 描述工具注册,代码用 Python 写,逻辑清晰、方便移植。
先定义工具层。Agent 要能「动手」,就得有工具。这里用两个简单工具演示:计算器和模拟搜索。
# tools.py import json def calculator(expression: str) -> str: """计算数学表达式,例如 '3 + 5 * 12'""" try: result = eval(expression, {"__builtins__": {}}, {}) return str(result) except Exception as e: return f"计算错误: {e}" def search(query: str) -> str: """模拟搜索,返回预设知识""" knowledge = { "北京人口": "北京常住人口约 2184 万(2023 年数据)", "上海人口": "上海常住人口约 2487 万(2023 年数据)", } for k, v in knowledge.items(): if k in query: return v return f"未找到关于「{query}」的信息" TOOLS = { "calculator": calculator, "search": search, } TOOL_SCHEMA = [ { "type": "function", "function": { "name": "calculator", "description": "计算数学表达式", "parameters": { "type": "object", "properties": {"expression": {"type": "string"}}, "required": ["expression"], }, }, }, { "type": "function", "function": { "name": "search", "description": "搜索信息", "parameters": { "type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"], }, }, }, ]ReAct 骨架:核心是循环「让模型输出 Thought + Action,执行工具,把 Observation 塞回上下文」。
# react_agent.py import json from llm_client import chat, client from tools import TOOLS, TOOL_SCHEMA import os REACT_PROMPT = """你是一个 ReAct Agent。每轮按以下格式输出: Thought: 你的推理 Action: 工具名 Action Input: JSON 参数 当可以回答时,输出: Thought: 我已经知道答案 Final Answer: 最终答案 可用工具:calculator, search """ def run_react(question: str, max_steps: int = 6) -> str: messages = [ {"role": "system", "content": REACT_PROMPT}, {"role": "user", "content": question}, ] for step in range(max_steps): resp = client.chat.completions.create( model=os.getenv("MODEL_EXECUTOR"), messages=messages, temperature=0.1, ) text = resp.choices[0].message.content messages.append({"role": "assistant", "content": text}) if "Final Answer:" in text: return text.split("Final Answer:")[-1].strip() # 解析 Action try: action_line = [l for l in text.split("\n") if l.startswith("Action:")][0] input_line = [l for l in text.split("\n") if l.startswith("Action Input:")][0] tool_name = action_line.replace("Action:", "").strip() tool_input = json.loads(input_line.replace("Action Input:", "").strip()) observation = TOOLS[tool_name](**tool_input) except Exception as e: observation = f"解析或执行失败: {e}" messages.append({"role": "user", "content": f"Observation: {observation}"}) return "达到最大步数仍未得出答案"Plan-and-Execute 骨架:先规划,再逐步执行,最后重规划。
# plan_execute_agent.py import json from llm_client import chat import os PLANNER_PROMPT = """你是规划器。把用户目标拆成 2-5 个可执行步骤。 只输出 JSON 数组,每项包含 step 和 tool_hint。 示例:[{"step": "查询北京人口", "tool_hint": "search"}]""" def plan(goal: str) -> list: raw = chat(os.getenv("MODEL_PLANNER"), [ {"role": "system", "content": PLANNER_PROMPT}, {"role": "user", "content": goal}, ]) raw = raw.strip().replace("```json", "").replace("```", "") return json.loads(raw) def execute_step(step: dict, context: str) -> str: prompt = f"当前上下文:{context}\n请执行这一步:{step['step']}\n只输出执行结果。" return chat(os.getenv("MODEL_EXECUTOR"), [{"role": "user", "content": prompt}]) def run_plan_execute(goal: str) -> str: steps = plan(goal) context = "" for i, step in enumerate(steps): result = execute_step(step, context) context += f"\n步骤{i+1}结果:{result}" final = chat(os.getenv("MODEL_EXECUTOR"), [ {"role": "user", "content": f"根据以下执行记录,给出最终答案:\n{context}"}, ]) return finalReflection 骨架:在 ReAct 基础上加评估和反思,失败则带着反思重试。
# reflection_agent.py from llm_client import chat from react_agent import run_react import os EVALUATOR_PROMPT = """评估以下回答是否正确解决了问题。 输出 JSON:{"score": 0-10, "reason": "原因"}""" def evaluate(question: str, answer: str) -> dict: raw = chat(os.getenv("MODEL_EVALUATOR"), [ {"role": "system", "content": EVALUATOR_PROMPT}, {"role": "user", "content": f"问题:{question}\n回答:{answer}"}, ]) raw = raw.strip().replace("```json", "").replace("```", "") return json.loads(raw) def run_reflection(question: str, max_rounds: int = 3) -> str: reflections = [] for r in range(max_rounds): enriched = question if reflections: enriched += "\n\n历史反思:\n" + "\n".join(reflections) answer = run_react(enriched) ev = evaluate(question, answer) if ev["score"] >= 8: return answer reflections.append(f"第{r+1}轮得分{ev['score']}:{ev['reason']}") return answer这三段代码共用同一套llm_client和tools,切换范式只改入口函数。配置上唯一要动的是.env里的模型 ID,规划用强模型、评估用快模型,成本和质量能兼顾。
4. 验证请求与成功结果:三类任务对比跑通规划型智能体
代码写完了,得用真实任务验证。我准备了三个难度递增的任务,分别对应三种范式的强项,你可以照着跑一遍看输出差异。
任务一:单步计算(ReAct 强项)问题:「(3 + 5) * 12 等于多少?」 预期:ReAct 一轮内调用 calculator,返回 96。Plan-and-Execute 会先规划「计算括号内、再乘」,多花一次规划调用,结果一样但更慢。这说明简单任务上 ReAct 更经济。
任务二:多步信息整合(Plan-and-Execute 强项)问题:「北京和上海的人口总和是多少?」 ReAct 可能先搜北京、再搜上海、再算总和,但如果中间忘了已搜过什么,会重复搜索。Plan-and-Execute 会先输出计划:[{"step":"查北京人口"},{"step":"查上海人口"},{"step":"求和"}],执行器逐步跑,上下文里始终有完整记录,不容易漏步。
跑这个任务时,你可以打印 Planner 的输出确认计划合理:
steps = plan("北京和上海的人口总和是多少?") print(json.dumps(steps, ensure_ascii=False, indent=2))正常输出类似:
[ {"step": "查询北京人口", "tool_hint": "search"}, {"step": "查询上海人口", "tool_hint": "search"}, {"step": "计算两地人口总和", "tool_hint": "calculator"} ]任务三:易错任务(Reflection 强项)问题:「北京人口是上海人口的多少倍?保留两位小数。」 这个任务容易出错:模型可能算反、可能忘记保留小数。Reflection 的价值在这里体现——第一轮 ReAct 给出答案后,Evaluator 打分,如果发现「未保留两位小数」或「倍数算反」,Self-Reflection 生成反馈,第二轮带着反馈重跑,通常能修正。
验证时打印每轮评估结果:
answer = run_reflection("北京人口是上海人口的多少倍?保留两位小数") print(answer)成功的话,最终答案应该是0.88左右(2184/2487)。如果第一轮就对了,说明模型够强;如果第一轮错、第二轮对,正好证明 Reflection 有效。
三类任务跑下来,你会直观感受到:ReAct 适合步骤少、反馈快的任务;Plan-and-Execute 适合步骤多、需要全局视角的任务;Reflection 适合对正确率要求高、允许重试的任务。实际项目里三者常常组合使用,比如用 Plan-and-Execute 做外层规划,每个步骤内部用 ReAct 执行,关键步骤加 Reflection 校验。
5. 本篇常见报错排查:401、local proxy failed、reading choices、OAuth 逐条对照
Agent 跑不起来,八成是接入层的问题。我把这几类真实报错和排查路径列出来,你对着查能省不少时间。
401 Unauthorized最常见。原因通常是 Key 没读到、Key 失效、或者 Base URL 写错导致请求打到了别的服务。先确认.env被正确加载:
import os print(os.getenv("TAOTOKEN_API_KEY")[:8]) # 只打印前8位确认非空如果打印出None,说明load_dotenv()没生效或路径不对。如果 Key 有值仍 401,去控制台确认 Key 是否被禁用,以及 Base URL 是否为https://taotoken.net/api。
local proxy failed / connection error这类报错说明请求根本没发出去,通常是本地网络环境或代理配置干扰。检查你的运行环境有没有设置HTTP_PROXY、HTTPS_PROXY环境变量,如果有且指向不可用的地址,SDK 会尝试走它然后失败。临时清掉再试:
unset HTTP_PROXY HTTPS_PROXY另外确认防火墙没拦截出站请求。如果你在公司内网,可能需要找网管确认出口策略。
reading 'choices' of undefined这个报错说明resp.choices是 undefined,即返回体结构和你预期的不一样。常见原因是:请求其实失败了但你没检查状态码,或者模型 ID 写错导致服务返回了错误对象。加一层防御:
resp = client.chat.completions.create(...) if not resp.choices: raise RuntimeError(f"返回异常: {resp}")同时打印完整响应看看服务到底返回了什么。模型 ID 拼写错误(比如把claude-sonnet-4-20250514写成claude-sonnet-4)也会触发类似问题,对照控制台模型列表核对。
OAuth / authentication 相关报错如果你用的是某些 CLI 工具或 IDE 插件,可能会遇到 OAuth 流程失败。这类工具通常要求填 Base URL、API Key、Model ID 三件套,缺一不可。以 Claude Code 类工具为例,配置里要明确写全:
{ "baseUrl": "https://taotoken.net/api", "apiKey": "sk-你的key", "model": "claude-sonnet-4-20250514" }如果工具提示 OAuth 失败,先确认它是否支持 API Key 模式;支持的话优先用 Key,比 OAuth 少一层跳转,排查也简单。Cline、CC Switch 这类工具同理,Base URL 和 Model ID 必须和你的 Key 权限匹配,用错模型会报权限或不存在错误。
排查顺序建议:先确认 Key 非空 → 再确认 Base URL 正确 → 再确认模型 ID 存在 → 最后看网络环境。按这个顺序走,大部分接入问题十分钟内能定位。
6. 把三种范式用起来:从 Demo 到项目的落地建议
跑通 Demo 只是开始,真正落地时还有几个经验值得分享。第一,别一上来就上多 Agent。我见过不少项目,任务明明用 Plan-and-Execute 加几个工具就能解决,非要拆成五个 Agent 互相调用,结果调试成本翻倍、延迟高得离谱。先用单 Agent 加规划,撑不住了再拆。
第二,Reflection 的评估器别用太贵的模型。评估本质是打分和给反馈,不需要最强推理能力,用快模型能省不少成本,而且评估轮次多,积少成多。规划器才值得用强模型,因为计划错了后面全错。
第三,工具描述要写清楚。Agent 选错工具,很多时候不是模型笨,而是你的description太模糊。比如search写成「搜索信息」就不如「根据关键词搜索事实性知识,返回简短答案」来得明确。工具参数也要给示例,模型照着填准确率高很多。
第四,给 Agent 循环设上限。不管是 ReAct 的max_steps还是 Reflection 的max_rounds,都要有硬上限,否则遇到死循环会一直烧 token。上限值按任务复杂度定,一般 ReAct 给 6-10 步,Reflection 给 2-3 轮就够。
最后,把每次运行的轨迹存下来。Thought、Action、Observation、评估分数,这些数据是你后续优化的金矿。哪类任务经常失败、哪个工具老被误用,看轨迹一目了然。等积累够了,你甚至可以用这些数据微调一个专属的规划模型。
Agent 这条路,从 ReAct 到自主规划,核心就一句话:让模型先想清楚再动手,动完手能复盘。把这三件事用代码串起来,你就已经超过大多数只会调 API 的人了。