手写一个 ReAct 智能体:从提示词模板到工具调用与记忆的完整拆解
市面上智能体框架层出不穷,但无论封装得多漂亮,绝大多数智能体的思想内核都源自同一个范式:ReAct(Reasoning + Acting)。它由普林斯顿大学与谷歌的研究者在 2023 年提出,核心洞察是——让大模型在"思考(Thought)→ 行动(Action)→ 观察(Observation)"的循环中交替推进,而不是一次性地输出答案。这种设计模仿了人类解决复杂问题的方式:先想清楚要做什么,动手做,看结果,再决定下一步。本文不依赖任何高级框架,用最朴素的代码从零实现一个具备工具调用与多轮记忆能力的 ReAct 智能体,把它的每一块拼图拆开讲透。理解这些底层细节,是后续驾驭任何框架的前提。
一、ReAct 为什么有效
传统 Prompt 方式让模型直接回答问题,模型只能依赖训练时见过的知识,一旦任务需要外部信息(查天气、算数学、查数据库),它就无能为力,只能靠"猜"——这就是幻觉的重要来源。ReAct 的关键在于:模型被允许在回答前先"动手"。
一个 ReAct 循环的标准输出格式长这样:
Question: 北京今天多少度?如果超过20度,帮我算 (25+10)*2 Thought: 我需要先查询北京的天气。 Action: get_weather Action Input: 北京 Observation: 晴,25°C Thought: 温度25度超过20度,需要计算表达式。 Action: calculator Action Input: (25+10)*2 Observation: 计算结果: 70 Thought: 我现在知道答案了。 Final Answer: 北京今天晴,25度,超过20度,所以(25+10)*2 = 70。每一步的 Thought 让模型的推理过程显式化,Action 让它可以调用外部工具,Observation 把工具的返回喂回给模型。这四步循环往复,直到模型认为信息足够、输出最终答案。这种"带轮子的推理"大幅减少了幻觉,因为关键事实来自工具返回而不是模型记忆。
二、环境准备与两个基础工具
先准备依赖,Python 3.9+ 即可:
pipinstalllangchain langchain-openai python-dotenv定义工具是整个系统的地基。工具的本质是"一段描述 + 一个函数":描述告诉模型这个工具什么时候该用、参数是什么意思,函数负责真正执行。我们用装饰器来定义:
fromlangchain.toolsimporttool@tooldefget_weather(city:str)->str:"""查询指定城市的当前天气(返回城市、天气、温度)"""weather_db={"北京":"晴 25°C","上海":"多云 28°C","深圳":"雷阵雨 30°C"}returnweather_db.get(city,f"未找到{city}的天气数据")@tooldefcalculator(expression:str)->str:"""计算数学表达式,例如 '3 * (4 + 2)'"""try:result=eval(expression,{"__builtins__":{}})returnf"计算结果:{result}"exceptExceptionase:returnf"计算错误:{str(e)}"``` 这里有两个容易被忽略的细节。第一,docstring 就是模型的"使用说明书",写得越具体,模型选错工具的概率越低。第二,示例代码里为了演示用了 `eval`,生产环境必须替换为受限计算引擎或 `ast` 解析,否则模型完全可能生成恶意表达式——工具安全性是智能体开发的红线,后面会专门展开。## 三、提示词模板:ReAct 循环的引擎ReAct 智能体的行为完全由提示词模板塑造。模板教模型按固定格式输出,我们则负责解析它的输出、执行工具、把结果塞回上下文: ```pythonfromlangchain.promptsimportPromptTemplatefromlangchain_openaiimportChatOpenAIfromlangchain.agentsimportAgentExecutor,create_react_agent prompt=PromptTemplate.from_template("""你是一个能调用工具的智能助手。可用工具如下: {tools} 工具名称: {tool_names} 严格按以下格式回答: Question: 用户的问题 Thought: 你下一步要做什么及理由 Action: 工具名称,必须是 [{tool_names}] 之一 Action Input: 工具的输入 Observation: 工具返回的结果 (Thought/Action/Action Input/Observation 可重复多次) Thought: 我现在知道答案了 Final Answer: 给用户的最终回答 开始! Question: {input} Thought: {agent_scratchpad}""")llm=ChatOpenAI(model="gpt-4o-mini",temperature=0)agent=create_react_agent(llm,tools,prompt)executor=AgentExecutor(agent=agent,tools=tools,max_iterations=5,# 防止死循环handle_parsing_errors=True# 模型输出格式乱了也能兜底)``` 两个参数值得单独说明:`temperature` 设为0,因为工具调用需要确定性,模型在"要不要调用工具、调用哪个"的决策上不应该有创造力;`max_iterations` 设上限是保命符,否则遇到模型反复输出错误格式的场景会无限循环烧钱。`agent_scratchpad` 是模板里的特殊变量,运行时会被替换成前面已经产生的 Thought/Action/Observation 历史,让模型"记得"自己刚才做过什么。## 四、带记忆的对话版本上面的版本每次调用都是独立的,用户问"刚才算的那个数加 5 等于多少"时,模型一无所知。引入对话记忆后,智能体才能真正连续工作: ```pythonfromlangchain.memoryimportConversationBufferMemoryfromlangchain.agentsimportinitialize_agent,AgentType memory=ConversationBufferMemory(memory_key="chat_history",return_messages=True)agent=initialize_agent(tools,llm,agent=AgentType.CONVERSATIONAL_REACT_DESCRIPTION,memory=memory,max_iterations=3)print(agent.invoke({"input":"北京天气如何?超过20度就帮我算 (25+10)*2"}))print(agent.invoke({"input":"刚才算出的数字加5等于多少?"}))第二轮提问时,智能体会从记忆里读出上一轮计算出的 70,直接算出 75,而无需重新调用工具——这正是记忆的价值:不仅维持话题连续,还能避免重复劳动、节省 token。
需要提醒的是记忆的容量问题:ConversationBufferMemory会把全部历史塞进上下文,长会话很快会超出窗口。生产方案通常是两级结构——短期用滑动窗口只保留最近 N 轮,超出的部分由模型压缩成摘要存入长期记忆,需要时再检索回来。
五、生产落地的七个工程要点
Demo 能跑和能上线是两回事,以下七点是从工程角度必须补的功课:
1. 工具安全是红线。凡是执行代码、SQL、Shell 命令的工具,必须做输入白名单校验、限制执行时长、隔离沙箱环境。模型是"不可信输入源",它生成的内容要当作攻击载荷来对待。
2. 全局超时与熔断。除了迭代次数上限,还要给整个执行包一层超时(比如 60 秒),超时后记录状态快照便于事后分析,同时给用户一个友好的兜底回复。
3. 错误分类。工具报错要区分两类:业务性错误(查无此数据)和模型性错误(参数格式不对)。前者正常反馈给模型让它换方案,后者可能是模型在幻觉,需要回炉修正提示词。
4. 可观测性。记录每一步的 Thought/Action/Observation 三元组,线上出问题时能完整回放决策路径。LangSmith 或自建日志都行,但绝不能没有。
5. 工具数量控制。模型在同一上下文里能"看得过来"的工具数量有限,工具越多选择准确率越低。按需动态注入工具(根据用户意图只暴露相关的那几个)是进阶技巧。
6. 参数校验前置。在工具内部用类型约束(如 Pydantic schema)校验模型传入的参数,非法参数直接返回明确错误信息,避免模型把错误当成"工具坏了"反复重试。
7. 成本监控。智能体的 token 消耗通常是普通问答的几倍到几十倍,必须对每次执行做用量记录和告警,防止失控循环烧钱。
六、从手写到框架:理解之后再做选择
亲手实现一遍 ReAct,最大的收获是理解了智能体并非黑盒——它由提示词模板、工具接口、记忆模块、循环控制这些可替换的零件构成。在此基础上,LangChain 的 AgentExecutor 帮你封装了循环,LangGraph 帮你把循环升级成可持久化、可中断、可并行分支的状态图,AutoGen 和 CrewAI 则在多智能体协作上各有所长。框架的意义是省去重复造轮子,但选型的前提是你知道底层发生了什么——这也是本文坚持手写一遍的价值所在。
当你把工具调用、记忆、安全、可观测性这些拼图全部装齐,一个"能思考、会动手、记得住、不怕坏"的智能体就真正成型了。下一步,可以在它的基础上扩展数据库查询、文件读写、邮件发送等更多工具,甚至让多个这样的智能体组成团队协作完成更复杂的任务。