手写一个 ReAct 智能体:从提示词模板到工具调用与记忆的完整拆解
手写一个 ReAct 智能体从提示词模板到工具调用与记忆的完整拆解市面上智能体框架层出不穷但无论封装得多漂亮绝大多数智能体的思想内核都源自同一个范式ReActReasoning Acting。它由普林斯顿大学与谷歌的研究者在 2023 年提出核心洞察是——让大模型在思考Thought→ 行动Action→ 观察Observation的循环中交替推进而不是一次性地输出答案。这种设计模仿了人类解决复杂问题的方式先想清楚要做什么动手做看结果再决定下一步。本文不依赖任何高级框架用最朴素的代码从零实现一个具备工具调用与多轮记忆能力的 ReAct 智能体把它的每一块拼图拆开讲透。理解这些底层细节是后续驾驭任何框架的前提。一、ReAct 为什么有效传统 Prompt 方式让模型直接回答问题模型只能依赖训练时见过的知识一旦任务需要外部信息查天气、算数学、查数据库它就无能为力只能靠猜——这就是幻觉的重要来源。ReAct 的关键在于模型被允许在回答前先动手。一个 ReAct 循环的标准输出格式长这样Question: 北京今天多少度如果超过20度帮我算 (2510)*2 Thought: 我需要先查询北京的天气。 Action: get_weather Action Input: 北京 Observation: 晴25°C Thought: 温度25度超过20度需要计算表达式。 Action: calculator Action Input: (2510)*2 Observation: 计算结果: 70 Thought: 我现在知道答案了。 Final Answer: 北京今天晴25度超过20度所以(2510)*2 70。每一步的 Thought 让模型的推理过程显式化Action 让它可以调用外部工具Observation 把工具的返回喂回给模型。这四步循环往复直到模型认为信息足够、输出最终答案。这种带轮子的推理大幅减少了幻觉因为关键事实来自工具返回而不是模型记忆。二、环境准备与两个基础工具先准备依赖Python 3.9 即可pipinstalllangchain langchain-openai python-dotenv定义工具是整个系统的地基。工具的本质是一段描述 一个函数描述告诉模型这个工具什么时候该用、参数是什么意思函数负责真正执行。我们用装饰器来定义fromlangchain.toolsimporttooltooldefget_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:resulteval(expression,{__builtins__:{}})returnf计算结果:{result}exceptExceptionase:returnf计算错误:{str(e)} 这里有两个容易被忽略的细节。第一docstring 就是模型的使用说明书写得越具体模型选错工具的概率越低。第二示例代码里为了演示用了 eval生产环境必须替换为受限计算引擎或 ast 解析否则模型完全可能生成恶意表达式——工具安全性是智能体开发的红线后面会专门展开。## 三、提示词模板ReAct 循环的引擎ReAct 智能体的行为完全由提示词模板塑造。模板教模型按固定格式输出我们则负责解析它的输出、执行工具、把结果塞回上下文 pythonfromlangchain.promptsimportPromptTemplatefromlangchain_openaiimportChatOpenAIfromlangchain.agentsimportAgentExecutor,create_react_agent promptPromptTemplate.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})llmChatOpenAI(modelgpt-4o-mini,temperature0)agentcreate_react_agent(llm,tools,prompt)executorAgentExecutor(agentagent,toolstools,max_iterations5,# 防止死循环handle_parsing_errorsTrue# 模型输出格式乱了也能兜底) 两个参数值得单独说明temperature 设为0因为工具调用需要确定性模型在要不要调用工具、调用哪个的决策上不应该有创造力max_iterations 设上限是保命符否则遇到模型反复输出错误格式的场景会无限循环烧钱。agent_scratchpad 是模板里的特殊变量运行时会被替换成前面已经产生的 Thought/Action/Observation 历史让模型记得自己刚才做过什么。## 四、带记忆的对话版本上面的版本每次调用都是独立的用户问刚才算的那个数加 5 等于多少时模型一无所知。引入对话记忆后智能体才能真正连续工作 pythonfromlangchain.memoryimportConversationBufferMemoryfromlangchain.agentsimportinitialize_agent,AgentType memoryConversationBufferMemory(memory_keychat_history,return_messagesTrue)agentinitialize_agent(tools,llm,agentAgentType.CONVERSATIONAL_REACT_DESCRIPTION,memorymemory,max_iterations3)print(agent.invoke({input:北京天气如何超过20度就帮我算 (2510)*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 则在多智能体协作上各有所长。框架的意义是省去重复造轮子但选型的前提是你知道底层发生了什么——这也是本文坚持手写一遍的价值所在。当你把工具调用、记忆、安全、可观测性这些拼图全部装齐一个能思考、会动手、记得住、不怕坏的智能体就真正成型了。下一步可以在它的基础上扩展数据库查询、文件读写、邮件发送等更多工具甚至让多个这样的智能体组成团队协作完成更复杂的任务。