)
1. 从零构建智能体大脑为什么选这三种范式智能体Agent这个词最近被聊得很多但真正动手写一个能跑起来的“大脑”很多人会卡在第一步到底该用哪种推理结构我试过直接拿一个 while 循环套大模型结果它要么原地打转要么把简单问题想复杂。后来才明白ReAct、Plan-and-Solve、Reflection 这三种范式其实对应了三种不同的“思考节奏”把它们拆开实现一遍比看十篇综述都管用。这篇内容面向的是想用 Python 从零搭智能体的开发者不需要你精通 LangChain也不需要你理解复杂的图编排。我们会用最朴素的函数调用和字符串解析把三种范式的骨架一行行写出来再统一用 TaoToken 的 Key 做模型接入这样你本地跑通之后换模型、换工具都只是改配置的事。核心检索词就三个ReAct 负责边想边做Plan-and-Solve 负责先谋后动Reflection 负责自我纠错。适合谁适合已经会写 Python、调过 OpenAI 兼容接口、但还没亲手实现过 Agent 循环的人。我踩过的坑是一开始把三种范式混在一个类里结果 Prompt 互相污染调试时根本分不清是规划错了还是执行错了。所以下面的结构是分开实现、分开验证最后再讲怎么组合。你跟着敲完至少能得到三个能独立运行的最小智能体以及一套统一的模型接入配置。2. TaoToken 前置统一 Key 与模型接入层在写 Agent 逻辑之前先把模型接入层固定下来。三种范式都会频繁调用大模型如果每个文件都写一遍OpenAI(api_key...)后面换模型会非常痛苦。TaoToken 提供的是 OpenAI 兼容接口你只需要一个 Key 和一个 base_url就能在 ReAct、Plan-and-Solve、Reflection 之间共用同一个客户端。先准备环境变量。在项目根目录建一个.env文件内容如下TAOTOKEN_API_KEY你的Key TAOTOKEN_BASE_URLhttps://taotoken.net/api注意 base_url 不要带多余路径OpenAI SDK 会自动拼接/v1/chat/completions。如果你用的是其他兼容库也保持这个根地址即可。Key 的获取入口在控制台建议单独建一个项目专用 Key方便后面排查调用量。然后写一个极简的llm_client.py三种范式都从这里拿模型能力import os import time from openai import OpenAI from dotenv import load_dotenv load_dotenv() class LLMClient: def __init__(self, model: str gpt-4o-mini): self.model model self.client OpenAI( api_keyos.getenv(TAOTOKEN_API_KEY), base_urlos.getenv(TAOTOKEN_BASE_URL), ) def chat(self, messages, temperature0.2, stopNone, retries3): for i in range(retries): try: resp self.client.chat.completions.create( modelself.model, messagesmessages, temperaturetemperature, stopstop, ) return resp.choices[0].message.content except Exception as e: print(f[LLM Error] attempt {i1}: {e}) time.sleep(1.5) return 这里把temperature默认设成 0.2是因为 Agent 的推理步骤需要稳定太高的随机性会让 ReAct 的 Action 格式飘忽不定。stop参数后面在 ReAct 里会用到用来在模型输出Observation:之前截断防止它自己编造工具返回结果。如果你还没有 Key可以先到模型对话页面体验一下接口连通性确认账号能正常调用后再继续。接入文档里有完整的请求示例遇到 401 或 404 时对照检查 base_url 和 Key 是否匹配。3. 可复制配置ReAct 范式的 Python 骨架ReAct 的核心是让模型在“思考”和“行动”之间交替。它的 Prompt 必须严格约束输出格式否则解析器会崩。下面这个骨架可以直接复制运行工具先用一个假的搜索函数占位方便你验证循环逻辑。3.1 ReAct 的 Prompt 模板REACT_PROMPT 你是一个可以使用工具的智能体。 可用工具 {tool_desc} 请严格按照以下格式输出每次只输出一步 Thought: 你的思考过程 Action: 工具名[输入参数] 当你可以给出最终答案时使用 Thought: 我已经知道答案了 Action: Finish[最终答案] 问题{question} 关键点是Action: 工具名[输入参数]这个格式。模型如果输出Action: Search(关键词)你的解析器就要兼容圆括号如果输出Action: Search[关键词]就按方括号解析。为了减少兼容负担可以在 Prompt 里明确写“必须使用方括号”。3.2 工具注册与解析器import re def fake_search(query: str) - str: # 这里替换成真实搜索 API return f关于「{query}」的模拟搜索结果这是占位内容。 TOOLS { Search: fake_search, } def parse_action(text: str): match re.search(rAction:\s*(\w)\[(.*?)\], text, re.DOTALL) if not match: return None, None return match.group(1), match.group(2).strip()解析器一定要做兜底。如果模型输出Action: Search[北京天气]parse_action返回(Search, 北京天气)如果格式错了返回(None, None)主循环里就把错误信息塞回上下文让模型自己纠正。3.3 ReAct 主循环def run_react(llm: LLMClient, question: str, max_steps: int 6): tool_desc \n.join([f- {name}: 用于查询信息 for name in TOOLS]) prompt REACT_PROMPT.format(tool_desctool_desc, questionquestion) history for step in range(max_steps): full_input prompt history output llm.chat( [{role: user, content: full_input}], stop[Observation:], ) print(f--- Step {step1} ---) print(output) tool_name, tool_input parse_action(output) if tool_name Finish: return tool_input if tool_name in TOOLS: observation TOOLS[tool_name](tool_input) else: observation f错误未知工具 {tool_name}请重新选择。 history f\n{output}\nObservation: {observation}\n return 达到最大步数未得到最终答案。运行run_react(llm, 帮我查一下北京今天的天气)你会看到模型先输出 Thought再输出 Action然后你的代码执行fake_search把结果作为 Observation 拼回上下文。下一轮模型看到 Observation 后要么继续调用工具要么输出 Finish。这里有个细节stop[Observation:]很重要。如果不加模型可能会自己续写一行Observation: 晴天导致你的真实工具结果被覆盖。加上 stop 之后模型输出到 Action 行就会停控制权回到你的代码手里。4. Plan-and-Solve 与 Reflection 的配置与验证ReAct 适合短链路任务但遇到“先查资料、再对比、最后写总结”这种多步任务它容易在中间步骤迷失。Plan-and-Solve 的思路是先让模型产出一个步骤列表再逐步执行。Reflection 则是在执行之后加一轮自我批评把低质量输出打回重做。4.1 Plan-and-Solve 的规划器PLAN_PROMPT 请把下面的问题拆解成 3 到 5 个可执行的步骤。 只输出一个 Python 列表不要输出其他内容。 问题{question} def make_plan(llm: LLMClient, question: str): raw llm.chat([{role: user, content: PLAN_PROMPT.format(questionquestion)}]) start, end raw.find([), raw.rfind(]) 1 try: import ast return ast.literal_eval(raw[start:end]) except Exception: return []拿到计划后用一个执行器逐步跑。每一步都可以复用 ReAct 的循环也可以直接调模型。关键是每一步的结果要拼进下一步的上下文这样后面的步骤才知道前面发生了什么。def run_plan_and_solve(llm: LLMClient, question: str): plan make_plan(llm, question) print(计划, plan) context for i, step in enumerate(plan): step_prompt f已知信息\n{context}\n\n当前任务{step}\n请给出这一步的结果。 result llm.chat([{role: user, content: step_prompt}]) print(f步骤 {i1} 结果{result}) context f\n步骤{i1}{step}\n结果{result}\n final llm.chat([{role: user, content: f根据以下信息回答问题{question}\n{context}}]) return final验证时用一个多步问题比如“比较 ReAct 和 Plan-and-Solve 的适用场景并给出选择建议”。如果规划器输出[解释 ReAct 特点, 解释 Plan-and-Solve 特点, 对比两者, 给出建议]说明结构化拆解生效了。4.2 Reflection 的批评循环Reflection 需要一个“执行者”和一个“批评者”。执行者先出初稿批评者指出问题执行者再改。下面是最小实现CRITIC_PROMPT 请检查下面的回答是否有事实错误、逻辑漏洞或遗漏。 如果有问题列出具体修改意见如果没有问题只输出 PASS。 回答{answer} def run_reflection(llm: LLMClient, question: str, rounds: int 2): answer llm.chat([{role: user, content: question}]) for r in range(rounds): critique llm.chat([{role: user, content: CRITIC_PROMPT.format(answeranswer)}]) print(f第 {r1} 轮批评{critique}) if PASS in critique.upper(): break refine_prompt f原问题{question}\n原回答{answer}\n修改意见{critique}\n请重写回答。 answer llm.chat([{role: user, content: refine_prompt}]) return answer验证 Reflection 是否生效可以问一个容易出错的常识题观察第二轮回答是否修正了第一轮的偏差。如果批评者一直输出 PASS说明你的批评 Prompt 太宽松可以加上“必须找出至少一个可改进点”的约束。三种范式跑通后你可以把它们组合起来用 Plan-and-Solve 做顶层规划每个步骤内部用 ReAct 调工具最后用 Reflection 做质量检查。这就是一个简化版的“智能体大脑”。5. 本篇常见错排查报错一openai.AuthenticationError: 401先检查.env里的TAOTOKEN_API_KEY是否有多余空格再确认TAOTOKEN_BASE_URL是否写成https://taotoken.net/api。如果 Key 是从控制台复制的注意不要漏掉前缀。接入文档里有 curl 示例可以先用命令行验证 Key 是否有效。报错二ReAct 循环停不下来一直调用同一个工具通常是 Observation 没有拼回上下文或者拼回去的格式不对。检查history f\n{output}\nObservation: {observation}\n这一行确保 Observation 紧跟在 Action 后面。另外max_steps一定要设防止无限循环烧 Token。报错三Plan-and-Solve 返回空列表模型可能输出了 Markdown 代码块比如python\n[...]\n。你的ast.literal_eval会因为前面的反引号报错。可以在解析前先做一次清洗raw raw.replace(python, ).replace(, )。报错四Reflection 批评者总是输出 PASS把批评 Prompt 里的“如果没有问题只输出 PASS”改成“请至少指出一个可以改进的细节即使回答整体正确”。同时把temperature稍微调高到 0.5让批评者更愿意挑刺。报错五模型输出格式飘忽解析器频繁失败在 Prompt 里加一句“不要输出任何解释性文字只输出指定格式”。如果还是不稳定可以在解析失败时把错误信息作为 Observation 返回让模型自己修正格式。实测下来两轮之内基本能收敛。6. 下一步把 Key 用起来三种范式的骨架都已经能跑了接下来就是接真实工具和真实模型。如果你主要做排障和接入建议先把 API Keys 页面里的 Key 管理好再对照接入文档把 base_url 和模型名确认一遍。如果你更想先验证模型输出质量可以直接在模型对话里测试同一套 Prompt观察不同模型对 ReAct 格式的遵循程度。长期做编码类 Agent 的话Coding Plan 里有一套更完整的工程化配置适合把今天这三个骨架扩展成可持续运行的项目。我自己的习惯是每加一个新工具先用 ReAct 跑一遍单步调用确认解析器和工具函数没问题再放进 Plan-and-Solve 的步骤里。这样出问题时你能快速定位是规划错了、执行错了还是工具本身返回了异常。智能体不难难的是把每一步的输入输出都变得可观测。