从零构建AI Agent:深入解析ReAct框架与Python实战

📅 2026/8/14 3:22:18
从零构建AI Agent:深入解析ReAct框架与Python实战
1. 从“黑盒”到“白盒”拆解Agent的运行骨架最近和不少刚入行AI应用开发的朋友聊天发现一个挺普遍的现象大家用LangChain或者Dify这类框架搭Agent流程跑通了结果也出来了但被问到“这Agent到底是怎么一步步跑起来的”时往往就卡壳了。感觉像个黑盒输入问题输出答案中间的过程云里雾里。这种感觉我特别理解几年前我刚接触时也一样。今天我就想抛开那些复杂的框架术语用一个最朴素的“白盒”视角带你亲手拆解一个Agent从启动、思考到执行、再思考的完整生命循环。我们不用任何重型框架就用最基础的Python代码结合OpenAI的API来还原其核心机制——ReActReasoning Acting。你会发现所谓智能体其内核逻辑远比想象中清晰和优雅。简单来说一个能够自主完成任务的Agent其核心是一个循环它接收一个目标比如“查一下北京今天的天气然后告诉我是否需要带伞”然后开始“思考”下一步该做什么接着去“执行”这个动作比如调用一个搜索工具拿到结果后再基于结果和原始目标进行下一轮“思考”直到任务完成或无法继续。这个“思考-执行”的循环就是Agent的引擎。而驱动这个引擎的燃料是大语言模型LLM的推理能力引擎连接的齿轮则是各种工具Tools。我们的目标就是亲手打造这个引擎。2. 核心循环ReAct模式深度解构为什么是ReAct在AI Agent的发展中ReAct范式是一个里程碑。它正式将“推理”和“行动”明确地、结构化工地结合在一个循环里。在此之前LLM要么是纯推理Chain-of-Thought要么是简单调用工具缺乏在复杂、多步任务中动态规划行动的能力。2.1 ReAct的核心思想让LLM学会“三思而后行”ReAct的精髓在于它要求LLM在每一步都以一种特定的结构化格式进行输出。这个格式通常包含三个部分Thought思考分析当前状况。我有什么信息我的目标是什么我下一步应该做什么为什么Action行动根据思考决定一个具体的行动。这个行动必须是对一个可用工具的调用格式如Action: 工具名称。Action Input行动输入调用该工具所需要的输入参数格式如Action Input: 参数。LLM输出这个结构后系统会解析它执行指定的工具调用获取工具的返回结果我们称之为Observation观察。然后系统将之前的“思考-行动”历史和这个新的“观察”一并喂给LLM让它进行下一轮的“思考”。如此循环直到LLM在“思考”后认为任务已经完成输出最终的Final Answer。这个过程完美模拟了人类解决问题的方式先想再做看结果再想下一步。它极大地提升了LLM在需要与环境工具、API、数据库交互的任务中的可靠性和可解释性。2.2 与简单链式调用的本质区别你可能会问这和LangChain里的LCEL链LangChain Expression Language有什么区别一个简单的检索问答链RAG不也是“调用检索器 - 组合上下文 - 调用LLM生成答案”吗关键区别在于“状态”和“决策”的归属。简单链式调用流程是预设的、静态的。就像一条流水线数据从A工序到B工序再到C工序路径固定。如果B工序的结果不理想C工序也只能基于这个不理想的结果工作无法回头。ReAct Agent流程是动态的、由LLM实时决策的。Agent内部维护着一个“状态”包含了目标、已执行的历史、工具的返回结果。LLM在每一轮都基于完整的“状态”来决定下一步。如果某一步工具返回了错误或无关信息LLM在下一轮“思考”时可以意识到这一点并尝试换一种方法或工具。决策权在LLM手中而非在预设的流程图中。这就好比自动驾驶固定路线的轨道电车是“链式调用”而具备感知、决策、控制能力的汽车就是“Agent”。后者能处理突发状况比如前方修路它会自己决策是绕行还是等待。3. 从零构建一个极简天气查询Agent理论说再多不如动手。我们来实现一个具体的例子一个能理解复杂意图的天气查询Agent。用户可能问“北京和上海明天天气对比如何”我们的Agent需要自己拆解出需要查询两个城市然后对比结果。3.1 环境准备与工具定义首先我们需要“燃料”LLM和“齿轮”工具。这里我们使用OpenAI的GPT-3.5-turbo作为推理引擎并模拟一个天气查询工具。import openai import json import re # 设置你的OpenAI API Key (实践中请使用环境变量等安全方式) openai.api_key your-api-key-here # 模拟一个天气查询工具函数 def get_weather(city: str, date: str today) - str: 模拟天气查询工具。 参数: city: 城市名 date: 日期如 today, tomorrow 返回: 模拟的天气信息字符串 # 这里本应调用真实天气API如和风、OpenWeatherMap等 # 为了演示我们返回一个模拟数据 weather_data { 北京: {today: 晴15~25°C微风, tomorrow: 多云转阴18~28°C东南风3-4级}, 上海: {today: 小雨18~22°C东风2级, tomorrow: 阴19~24°C微风}, 广州: {today: 雷阵雨25~32°C南风3级, tomorrow: 多云26~33°C微风}, } city_data weather_data.get(city) if not city_data: return f错误未找到城市 {city} 的天气信息。 weather city_data.get(date, city_data.get(today, 信息暂不可用)) return f{city}{date}的天气是{weather} # 定义工具列表供LLM知晓和选择 TOOLS [ { name: get_weather, description: 查询指定城市在指定日期的天气。日期可以是today或tomorrow。, parameters: { type: object, properties: { city: {type: string, description: 城市名称例如北京、上海}, date: {type: string, description: 日期today或tomorrow默认为today} }, required: [city] } } ]这里的关键是工具描述。我们必须清晰、准确地向LLM描述每个工具能干什么、需要什么参数。LLM就是根据这些描述来决定何时调用以及如何调用工具的。描述写得模糊Agent就容易出错。3.2 构建系统提示词为LLM设定角色与规则接下来我们需要给LLM一个明确的“工作说明书”也就是系统提示词System Prompt。这个提示词定义了Agent的角色、工作流程、输出格式和可用工具。def build_system_prompt(): tool_descriptions \n.join([f- {tool[name]}: {tool[description]} for tool in TOOLS]) prompt f 你是一个智能助手能够通过使用工具来帮助用户解决问题。 你必须严格按照以下格式进行回应 Thought: 你需要在这里思考当前的情况。分析用户的最终问题是什么你已经掌握了哪些信息Previous Thought, Action, Observation以及下一步应该做什么。 Action: 根据Thought选择你要使用的工具名称。必须是以下工具之一{, .join([t[name] for t in TOOLS])} Action Input: 你选择的工具所需要的输入参数必须是一个合法的JSON字符串。 或者当你认为已经获得了足够的信息来回答用户的问题时你必须使用 Final Answer: 你的最终回答。 你可以使用的工具 {tool_descriptions} 开始 return prompt这个提示词是Agent行为的“宪法”。它强制LLM以固定的格式Thought/Action/Action Input 或 Final Answer进行输出便于我们程序化地解析。没有这个强约束LLM的输出会天马行空无法形成有效的循环。3.3 解析与执行引擎让循环转起来现在我们创建最核心的部分——驱动循环的引擎。这个函数负责与LLM对话、解析其输出、调用工具、并管理整个对话历史即Agent的状态。def run_agent(user_query: str, max_steps: int 5): 运行Agent主循环。 # 初始化对话历史包含系统提示和用户问题 messages [ {role: system, content: build_system_prompt()}, {role: user, content: user_query} ] print(f用户问题: {user_query}) print(*50) for step in range(max_steps): print(f\n--- 步骤 {step 1} ---) # 1. 调用LLM进行“思考” try: response openai.ChatCompletion.create( modelgpt-3.5-turbo, messagesmessages, temperature0, # 温度设为0保证输出的格式稳定性 max_tokens500 ) llm_output response.choices[0].message.content.strip() except Exception as e: print(f调用LLM失败: {e}) break print(fLLM原始输出:\n{llm_output}) # 2. 解析LLM的输出 thought_match re.search(rThought:\s*(.*?)(?\nAction:|\nFinal Answer:|$), llm_output, re.DOTALL) action_match re.search(rAction:\s*(\w), llm_output) action_input_match re.search(rAction Input:\s*(.*?)(?\n|$), llm_output, re.DOTALL) final_answer_match re.search(rFinal Answer:\s*(.*), llm_output, re.DOTALL) # 3. 判断输出类型并处理 if final_answer_match: # 任务完成 final_answer final_answer_match.group(1).strip() print(f\n✅ 任务完成最终答案: {final_answer}) return final_answer elif action_match and action_input_match: # 需要执行动作 thought thought_match.group(1).strip() if thought_match else action action_match.group(1).strip() action_input_str action_input_match.group(1).strip() print(fThought: {thought}) print(fAction: {action}) print(fAction Input: {action_input_str}) # 4. 执行工具调用 observation if action get_weather: try: # 解析JSON格式的输入参数 params json.loads(action_input_str) city params.get(city) date params.get(date, today) observation get_weather(city, date) except json.JSONDecodeError: observation f错误Action Input 不是有效的JSON格式: {action_input_str} except Exception as e: observation f工具执行出错: {e} else: observation f错误未知的工具名称 {action}。 print(fObservation: {observation}) # 5. 将本轮Thought/Action/Action Input/Observation添加到历史供下一轮思考 # 注意这里我们把LLM的完整输出和Observation都加进去这是标准的ReAct格式。 messages.append({role: assistant, content: llm_output}) messages.append({role: user, content: fObservation: {observation}\n\n现在请基于以上观察继续思考。}) else: # LLM的输出不符合预期格式这是一个常见错误 print(f❌ 无法解析LLM的输出格式。输出内容为:\n{llm_output}) # 一种容错策略将错误信息作为Observation反馈给LLM让它纠正 observation f错误你的回复格式不正确。请严格按照Thought/Action/Action Input或Final Answer的格式回复。 messages.append({role: assistant, content: llm_output}) messages.append({role: user, content: fObservation: {observation}\n\n请纠正你的格式并继续。}) # 如果循环结束还没得到Final Answer print(f\n⚠️ 已达到最大步数{max_steps}任务未完成。) return 任务超时未完成。这个run_agent函数就是Agent的“心脏”。它清晰地展示了单次循环的五个阶段思考 - 解析 - 判断 - 执行 - 更新状态。其中messages列表承载了全部的历史对话它就是Agent的“工作记忆”或“状态”。每一轮新的LLM调用都是基于这个完整的上下文进行的这使得Agent具备了连贯的推理能力。3.4 实战运行看Agent如何思考让我们用一个稍微复杂的问题来测试一下。if __name__ __main__: # 测试一个需要多步推理的问题 result run_agent(我想知道北京明天和上海今天的天气有什么区别, max_steps6)运行这段代码你会在控制台看到类似下面的输出具体文字可能因模型版本略有差异用户问题: 我想知道北京明天和上海今天的天气有什么区别 --- 步骤 1 --- LLM原始输出: Thought: 用户想比较北京明天和上海今天的天气。我需要先分别获取这两个信息。我可以使用get_weather工具。先查北京明天的天气。 Action: get_weather Action Input: {city: 北京, date: tomorrow} Observation: 北京明天的天气是多云转阴18~28°C东南风3-4级 --- 步骤 2 --- LLM原始输出: Thought: 我已经得到了北京明天的天气。现在我需要获取上海今天的天气。然后我才能比较两者的区别。 Action: get_weather Action Input: {city: 上海, date: today} Observation: 上海今天的天气是小雨18~22°C东风2级 --- 步骤 3 --- LLM原始输出: Thought: 现在我有了北京明天多云转阴18~28°C东南风3-4级和上海今天小雨18~22°C东风2级的天气信息。我需要分析它们的区别。区别可能包括天气现象、温度范围和风力风向。 Final Answer: 北京明天和上海今天的天气区别如下 1. **天气现象**北京明天是“多云转阴”而上海今天是“小雨”。上海有降水北京没有。 2. **温度范围**北京明天温度较高为18~28°C上海今天温度较低为18~22°C。北京的最高温明显高于上海。 3. **风力风向**北京明天是东南风3-4级风力稍大上海今天是东风2级风力较小。 总结北京明天天气以阴云为主温度较高风力较大上海今天则有小雨温度凉爽风力柔和。看Agent活起来了它自动将用户的一个复合问题拆解成了两个顺序执行的工具调用get_weather。在获得所有必要数据后它没有再次调用工具而是直接输出了分析和比较的最终答案。整个过程完全自主逻辑清晰。注意这里我们设置了temperature0以保证格式稳定。在实际复杂应用中可能需要稍微调高temperature如0.1-0.2以激发LLM更多样的推理但同时要辅以更强大的输出解析Parser和错误处理机制。4. 工程化挑战与进阶架构我们上面实现的是一个最简化的、单线程的ReAct Agent。它能够清晰地展示原理但距离一个健壮的、可投入生产的Agent系统还有很大距离。LangChain、LangGraph这些框架的出现正是为了解决这些工程化难题。4.1 简化版Agent的局限性脆弱的输出解析我们依赖正则表达式来解析LLM的输出。一旦LLM的回复格式稍有偏差比如多了一个换行或者用中文写了“动作”而不是“Action”解析就会失败。生产系统需要更鲁棒的解析器比如基于Pydantic的模式强制解析LangChain的OutputParser就干这个。有限的工具管理工具是硬编码的增加新工具需要修改代码。理想情况是能动态注册和管理工具。缺乏状态管理我们的“状态”就是完整的对话历史messages列表。对于长对话或复杂任务这会导致上下文长度快速增长增加成本并可能触及模型上下文窗口限制。需要更精细的状态管理比如只保留关键的摘要信息。无错误恢复与超时控制除了格式错误工具调用可能失败网络超时、API限流。我们的Agent缺乏重试、降级或向用户求助的机制。单一执行流我们的Agent是顺序思考的。对于一些任务可能需要并行执行多个工具调用比如同时查询三个城市的天气或者根据条件走不同的分支if-else逻辑。4.2 LangChain Agent提供了标准化组件LangChain的Agent框架为我们解决了上述大部分问题。它提供了标准化的Agent类型如ZERO_SHOT_REACT_DESCRIPTION就是我们实现的这种、OPENAI_FUNCTIONS利用OpenAI的函数调用特性格式更稳定、STRUCTURED_CHAT_REACT_DESCRIPTION等。强大的工具抽象将Python函数、API封装成标准工具并自动生成LLM可理解的描述。内建的输出解析器能可靠地将LLM的输出解析为AgentAction或AgentFinish对象。执行器AgentExecutor它封装了循环逻辑、错误处理、最大迭代次数控制等让我们只需关注Agent和工具的定义。用LangChain重写上面的天气Agent代码会更简洁、健壮。4.3 LangGraph为Agent引入“工作流”与“记忆”当任务超越简单的线性循环变得复杂、需要分支、循环、甚至多个Agent协作时LangChain Agent就显得力不从心了。这时就需要LangGraph。LangGraph的核心思想是用“图”来定义Agent的工作流。节点Node可以是执行一个工具调用、调用一个LLM、或者执行一段自定义逻辑。边Edge定义了节点之间的流转条件。这带来了几个质变显式的状态管理LangGraph有一个明确的State对象你可以自定义其中包含哪些字段如messages,intermediate_steps,selected_city等。每个节点读取和更新这个状态的一部分而不是传递整个对话历史。复杂控制流你可以轻松实现条件分支根据工具执行结果决定下一步是调用工具A还是工具B。循环可以定义一个“规划器”节点只要任务未完成就循环回到该节点。并行与汇聚可以同时运行多个查询然后在一个节点汇总结果。持久化与检查点由于状态是结构化的可以方便地将其保存到数据库实现Agent的“长期记忆”和任务恢复。比如一个客服Agent可以在对话中断后下次接着上次的状态继续。多Agent协作你可以定义多个具备不同能力的Agent作为图中的不同节点让它们通过共享状态来协同完成一个宏大任务。例如一个电商客服Agent的工作流用LangGraph定义可能是这样的开始 - 意图识别节点 - [是退货吗] - 是 - 退货流程子图 - 否 - [是查订单吗] - 是 - 调用订单查询工具 - 生成回答 - 结束 - 否 - 转人工节点 - 结束这个工作流清晰、可维护、可可视化远比一堆if-else嵌套在代码里要强大。5. 避坑指南与效能优化实战基于我过去几年搭建各类Agent的经验下面这些坑你大概率会遇到这里给出一些实用的解决方案。5.1 提示词工程让Agent更“听话”Agent的表现九成由提示词决定。除了基本的格式指令还有几个关键点明确工具选择逻辑在系统提示中加入类似“一次只使用一个工具”、“如果你不确定用哪个工具请先思考用户问题最核心的需求是什么”的指令可以减少LLM的困惑。提供丰富的示例Few-Shot在系统提示里加入1-2个完整的、格式正确的ReAct循环示例能极大地提升LLM输出格式的稳定性。这就是Few-Shot Prompting的威力。限制行动空间当工具很多时LLM可能选择困难。可以在每轮提示中根据当前对话上下文动态筛选出最相关的3-5个工具描述给LLM而不是每次都列出所有工具。5.2 工具设计的艺术工具是Agent的手和脚设计不好会处处掣肘。单一职责一个工具只做一件事。不要设计一个search_and_summarize的工具而应该拆成search_web和summarize_text两个工具由LLM来组合调用。这更符合ReAct的哲学也更具灵活性。输入输出标准化工具的输入参数尽量简单、明确输出也应该是结构化的字符串或JSON。模糊的工具描述如“查询信息”会导致LLM误用。健壮性高于一切工具函数内部必须有完善的异常处理try-catch。永远返回一个字符串结果即使是错误信息如“网络请求失败请稍后重试”。不要让工具抛出未处理的异常这会直接导致Agent崩溃。为工具结果添加元数据有时除了结果文本工具还会返回一些置信度、来源URL等元数据。可以考虑将这些信息以特定格式如[结果正文]\n来源xxx附加在观察中供LLM在后续思考时参考。5.3 处理“循环失控”与“幻觉”Agent最常见的两个故障模式一是陷入死循环二是基于错误观察进行“幻觉”推理。设置硬性限制max_steps最大步数是必须的通常设为5-10步。对于开放式任务可以设得大一些但一定要有。检测重复动作在状态中记录最近几次的(Action, Action Input)对。如果检测到完全相同的动作在短期内被重复执行可以中断循环并将“检测到重复操作可能陷入循环”作为Observation反馈给LLM让它调整策略。验证观察结果对于关键的工具返回结果尤其是来自外部API的可以设计一个“验证器”节点或工具。例如在调用计算器工具后再用一个简单的规则验证计算结果是否在合理范围内。或者对于搜索工具返回的文本让LLM自己判断其是否与问题相关。引入“人类审核”节点在关键决策点比如要执行一个具有副作用的操作如发送邮件、下单可以让工作流暂停将决策和相关信息发送给人类审核确认后再继续。LangGraph很容易实现这种“暂停”机制。5.4 成本与延迟优化Agent需要多次调用LLM和工具成本和延迟是现实问题。选择性价比模型对于“思考”步骤可以使用能力强但贵的模型如GPT-4对于简单的文本格式化或摘要可以换用便宜快速的模型如GPT-3.5-Turbo。这就是混合模型策略。压缩历史上下文随着步数增加messages会越来越长。可以在每轮结束后用一个LLM对之前的对话历史进行摘要只保留关键信息然后用摘要替换掉冗长的原始历史。这能显著减少token消耗。并行化工具调用如果Agent需要执行多个独立的工具调用如查询多个不相关城市的天气可以在工作流中设计并行分支同时发起请求最后再汇聚结果。这能大大减少总体延迟。设置超时与降级为每个工具调用设置超时。如果超时则提供一个默认的降级结果如“查询超时请参考其他信息”让Agent能够继续运行而不是卡死。6. 面向未来Agent架构的演进思考我们目前构建的还属于“单一LLM驱动”的经典Agent架构。这个领域正在飞速演进一些更先进的模式开始出现。1. 分层规划与执行Hierarchical Planning对于极其复杂的任务如“策划一场公司年会”让一个LLM直接进行每一步的ReAct思考可能效率低下且容易迷失。更先进的架构是引入一个“规划器”Agent。它先进行高层任务分解分解成“预订场地”、“安排餐饮”、“组织节目”等子任务然后每个子任务再由一个“执行器”Agent可能就是我们上面实现的这种去完成。规划器还可以根据执行器的反馈动态调整计划。这模仿了人类项目经理的工作方式。2. 多专家Agent协作Multi-Agent Collaboration一个Agent包打天下是不现实的。未来的方向是让多个具备专业技能的Agent协作。比如一个数据分析Agent、一个文案撰写Agent、一个代码生成Agent共同协作来完成一份市场分析报告。它们之间需要通过一个共享的工作区或消息总线进行通信和协调。LangGraph的多Agent支持正是为此而生。3. 工具学习Tool Learning与自我进化目前的工具都是开发者预先定义好的。更智能的Agent应该能够“学习”使用新工具。一种方向是给Agent提供工具的API文档如OpenAPI Spec让它自己学习如何调用。更进一步Agent甚至可以根据频繁出现的任务模式向开发者建议创建新的工具或者自动组合现有工具来形成新的“复合工具”。4. 具身AgentEmbodied Agent与长期记忆当Agent不再局限于数字世界而是需要控制机器人、软件界面RPA时就进入了具身AI的范畴。这对动作的精确性、环境的实时感知提出了更高要求。同时这样的Agent需要“长期记忆”记住过去几天、几周甚至几个月与环境和用户的交互历史才能表现出连贯的个性与能力。向量数据库与LangGraph的持久化状态结合是构建长期记忆系统的常见方案。构建一个稳定、可靠、高效的Agent系统依然充满挑战。但理解其最核心的“思考-执行”循环原理是我们应对一切复杂性的基石。从我们这几十行代码的极简Demo到支撑庞大业务的智能体平台其内核精神一脉相承。希望这次“白盒”拆解能帮你拨开Agent神秘的面纱在下次使用LangChain或设计自己的Agent时能更清楚地知道每一行代码背后那个忙碌的“小机器人”究竟在如何运转。