ReAct智能体原理与LangGraph实践:从零搭建可调工具的大模型Agent

发布时间:2026/10/22 4:27:37

ReAct智能体原理与LangGraph实践:从零搭建可调工具的大模型Agent 做了大半年智能体应用我越来越发现一个事实绝大多数看起来唬人的AI智能体内核其实就是同一套模式——ReAct。这个缩写乍一看学术味十足拆开就是 Reasoning Acting让大模型一边推理、一边行动拿行动的结果继续推理直到得出最终答案。而想把 ReAct 落到工程上目前最顺手的框架之一就是 LangGraph。如果你正准备学大模型应用开发我强烈建议你把这套实践路线从头到尾走一遍因为它足够新、足够底层也足够能打——从零搭一个真正会调用工具、会自我纠错的智能体用它举一反三比背一百个词条都有用。这套教程我按“原理 → 框架 → 代码 → 调参 → 排障”的顺序给你拆开讲。目标是让你看完以后不是只记住一个名字而是能打开编辑器亲手跑通一个最小可用的 ReAct Agent。后文会有大量可以复制的代码和理解性注释也有我实际踩坑以后总结出来的检查清单。想入门的读者完全可以把这篇文章当一份收藏型攻略跟着一步步走。1. ReAct 到底是什么为什么值得专门学1.1 从“只会聊天”到“能干活”大模型本身是一台“没有插电的工具箱”。它能背出大量知识但缺少两样东西一是实时信息二是对外部世界的操作能力。你说“明天杭州到北京有哪些航班”它如果只靠训练数据大概率会给一个模糊或过时的回答甚至明确告诉你“我无法实时查询”。原因很简单模型没有网络、没有数据库也没有和你身边任何系统之间的接口。ReAct 正是为了解决这个问题被提出的一种交互范式。它把大模型当成一个“决策大脑”让大脑在思考时不断决定要不要调用外部工具。比如先想“用户问的是航班信息这不是我记忆里的知识我需要查一下。”再动调用航班查询工具。观察拿到工具返回的航班列表。再想根据结果组织回答或者继续追查更多信息。这个过程和人类处理陌生问题时几乎一模一样一边推理一边采取行动然后用行动结果修正推理。把这种“推理-行动-观察”的循环结构化之后模型就不再是一个只会输出文字的聊天框而是一个能调API、能查库、能写代码、能自己验证答案的智能体雏形。1.2 循环三步曲Thought / Action / ObservationReAct 的核心循环可以抽象成三个角色环节做什么例子Thought思考分析当前状态决定下一步需要什么信息“现在不知道航班列表需要搜一下”Action行动调用某个工具或执行某个动作search(明天杭州到北京航班)Observation观察获得工具返回的结果喂回给模型“共6班最早6:30最晚21:45”模型会在 Thought、Action、Observation 之间反复穿梭直到它觉得自己掌握了足够信息才会输出一个最终答案。这个“最终答案”本质上也是循环的结束条件不再需要调用工具时模型就把当前结果整理成可读的回复。为什么要反复循环而不是让模型一次性把工具都调完因为真实世界的结果是不可预测的。工具可能返回空、可能返回错误、可能返回的结果和你预期不符这些都不能靠“预先猜一遍”来规避。ReAct 的价值就是让模型在每次得到新信息后都能重新做判断相当于给每一步决策都加了一次“反馈校准”。1.3 为什么靠手写循环会很难受看完这个逻辑你可能会想这不就是个 while 循环吗我直接用 Python 写也行的。确实一个最简单的 ReAct 手写循环只要二三十行就能跑通。但一旦进入真实项目事情就没那么浪漫了状态要不断累积。对话历史、工具返回、中间推理结果这些都需要在多个循环之间传递不能丢。判断要足够灵活。模型可能调了一个工具后还想调另一个也可能想直接结束。这个“下一步去哪”的决策放在循环里写分支一多就是一团乱麻。还要考虑终止条件。没有步数上限模型在一个错误思路上反复调用同一个工具能把你的账号余额烧穿。生产环境还要求断点续跑、多轮会话、并发控制、人工干预。这些东西如果用 while 循环硬写大部分时间都会花在“管理代码复杂度”而不是“实现业务逻辑”上。所以我们需要一个能把 ReAct 流程当作“图”来编排的框架。这也是我接下来重点讲 LangGraph 的原因。2. 选 LangGraph 的底气一张图搞定 Agent 流程2.1 LangGraph 的核心概念State / Node / EdgeLangGraph 是一个基于“状态图”思想的编排框架。它在底层把智能体的执行流程抽象成一张有向图由三个核心概念组成State状态一个全局共享的数据对象通常是一个字典保存 messages、计数器、临时变量等。每个节点执行完以后会把更新写回 State下一个节点才能看到最新内容。Node节点一个函数或可调用对象。它接收当前的 State做一些处理然后返回一个 dict表示要更新 State 的哪些字段。Edge边节点与节点之间的连接关系。分普通边和条件边。普通边表示“执行完 A 后一定去 B”条件边表示“根据 A 的输出结果动态决定下一步去哪个节点”。这个模型天然适合描述循环。ReAct 可以画成这样agent 节点 - 如果有工具调用则进入 tools 节点 - tools 节点执行结束后回到 agent 节点 - 如果没有工具调用则走到 END。在 LangGraph 里这个流程不需要你用 while 循环去控制只需要把节点和边定义清楚执行引擎会自动按图跑。状态由框架统一管理断点由框架统一记录条件分支由条件边完成。这样你的业务逻辑就变成了“在节点里写代码”而不是“在循环里堆状态判断”。2.2 为什么“图思维”比“循环思维”更适合作业我最早接触这类框架时也怀疑过有必要为了一个循环引入图概念吗真正写过一个多工具项目后我承认有必要。循环思维是线性的代码从上往下执行状态靠变量传递分支靠 if 控制。一旦分支变多比如“模型既可能调用 A 工具、也可能调用 B 工具、还可能直接结束”你需要在循环体内部组合 if 语句再加上提前退出、重试、回退等逻辑代码就会迅速膨胀。图思维是分层的每条边只负责一个明确关系每个节点只负责一种处理。要加一个“人工审核”节点就加一个节点再连一条边要加一个“代码解释器”工具就把它挂在 tools 节点下。它不改变你想表达的 ReAct 业务逻辑却能让你在新增功能时不破坏原有结构。这也是我后来在多个项目里都坚持用图来编排 Agent 的原因图是流程的自然表达循环是流程的一种特殊情况。2.3 搭建环境需要做哪些准备开始写代码前先把环境准备好。这里我默认你已经有 Python 3.9 以上的环境推荐用虚拟环境隔离依赖避免和系统 Python 打架。python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install langgraph langchain-core如果你还需要调用模型服务还需要安装对应的客户端库。但这一步跟具体模型厂商关系很大不同厂商接入方式不一样。我这里为了说明通用思路后面代码里会用一个占位符llm你只需要把它替换成你实际可用的模型客户端即可。核心逻辑不依赖任何特定厂商这也是 LangGraph 的优势之一。3. 手把手用 LangGraph 做一个最小 ReAct 智能体3.1 准备一个模拟搜索工具为了让示例能本地跑通我们不去接真实搜索服务而是写一个模拟的“本地题库查询工具”。它内部放一个小字典根据 query 返回预设结果。这个工具足够说明问题真实项目里你只需把函数体替换成数据库查询、HTTP 请求、内部 API 调用其他结构完全不用变。from langchain_core.tools import tool tool def search(query: str) - str: 本地信息查询工具。当需要查询航班、天气等实时信息时使用query 为查询关键词。 database { 杭州北京航班: 明天杭州至北京航班共有6班最早6:30最晚21:45。, 北京天气: 北京明天晴2~12℃。, 杭州天气: 杭州明天多云转小雨8~15℃。, } return database.get(query, 未找到对应信息请更换关键词重试。)注意看工具的 docstring。这段描述非常重要因为大模型是“读描述来决定是否调用工具”的。描述越精确工具被正确调用的概率越高。后面排查问题时会专门讲这一点。3.2 定义 Agent 的 State 结构ReAct 循环里最需要保存的就是 messages——所有对话内容、模型思考输出、工具返回结果都要在不断循环中累积。我们用 LangGraph 推荐的方式通过Annotated和operator.add让 messages 字段在每次节点返回后自动累加而不是被新值整体覆盖。from typing import Annotated, TypedDict, operator class AgentState(TypedDict): messages: Annotated[list, operator.add]这里的数据流可以这样理解每个节点都可以给 messages 追加若干条消息LangGraph 会把所有追加的消息拼接到 State 原有消息后面。这样agent 节点和 tools 节点交替运行时整个对话历史就像一个不断增长的日志任何节点都能从头看到尾。3.3 写 Agent 节点让模型决定要不要行动Agent 节点是 ReAct 里的大脑。它把当前全部 messages 交给模型让模型结合工具描述做推理然后返回一个 AI 消息。from langchain_core.messages import SystemMessage, HumanMessage, AIMessage SYSTEM_PROMPT 你是一个会使用工具回答问题的助手。 你有以下工具可用{tool_descs} 使用工具时请根据工具返回的 Observation 继续推理直到能给出最终答案。 def agent_node(state: AgentState) - dict: # 构造送给模型的完整消息列表 system_msg SystemMessage(contentSYSTEM_PROMPT.format( tool_descs\n.join([f- {tool.name}: {tool.description} for tool in tools]) )) messages [system_msg] state[messages] # llm 是占位符需要替换为你自己的模型客户端并绑定工具 response llm_with_tools.invoke(messages) return {messages: [response]}这里有一个很关键的设计llm_with_tools是指“已经告诉模型有哪些工具的模型对象”。现在主流模型都支持原生工具调用function calling你把工具列表传给模型模型返回的 AIMessage 里就会包含tool_calls字段明确表示“我希望调用 search 工具参数是什么”。这就避免了以前那种“让模型输出一段 Action 文字再手动解析”的脆弱方式。3.4 使用 ToolNode 接管工具执行LangGraph 提供现成的ToolNode它负责读取 AIMessage 里的tool_calls自动调用对应工具然后把工具结果包装成 ToolMessage 追加回状态。from langgraph.prebuilt import ToolNode tools [search] tool_node ToolNode(tools)你用不着自己解析 Action 了。模型说“调用 search 工具参数是‘杭州北京航班’”ToolNode 就会真的去执行search(杭州北京航班)再把结果写进 messages。这一步相当于 ReAct 里的 Observation 落地。3.5 把节点连成一张循环图现在把 agent 节点和 tool 节点拼成一张图再加上条件边如果模型返回的 AIMessage 里有 tool_calls就去 tool 节点没有 tool_calls说明模型已经准备好给最终答案可以直接结束。from langgraph.graph import StateGraph, END from langgraph.prebuilt import tools_condition graph StateGraph(AgentState) graph.add_node(agent, agent_node) graph.add_node(tools, tool_node) graph.set_entry_point(agent) graph.add_conditional_edges( agent, tools_condition, {tools: tools, __end__: END} ) graph.add_edge(tools, agent) app graph.compile()这段代码就是 ReAct 循环的骨架。执行顺序是从agent节点进入。模型决定是否调用工具。如果要调用进入tools执行完以后无条件回到agent。再次判断直到模型不再调用工具进入END。整个过程用图表达得清清楚楚。以后你想加一个“在工具执行前做一次安全过滤”的节点只需在agent和tools之间再挂一个节点即可。3.6 跑通第一个 ReAct 循环编译完成后调用它就像调用一个普通函数from langchain_core.messages import HumanMessage result app.invoke({ messages: [HumanMessage(content明天杭州到北京有哪些航班)] }) for msg in result[messages]: msg.pretty_print()如果你已经正确接入了模型服务会看到类似这样的输出轨迹一个 AIMessage带着tool_calls[search(杭州北京航班)]一个 ToolMessage内容是“明天杭州至北京航班共有6班最早6:30最晚21:45”一个 AIMessage整理成最终答案“明天杭州到北京共有6个航班……”注意看第二、第三条模型拿到工具结果之后不是机械地复述而是会结合用户问题组织成一句完整回答。这就是 ReAct 的意义——它在真实信息之上做归纳而不是凭空生成。3.7 如果你还是想看看文本版 ReAct 长什么样原生工具调用虽然稳定但它把 Thought 和 Action 的细节隐藏在了结构化字段里。为了理解 ReAct 本身我们也可以在提示词里要求模型输出固定格式的文本然后手动解析。下面是一个极简示意import re def parse_action(text: str): # 匹配 Action: tool_name(参数) m re.search(rAction:\s*(\w)\((.*?)\), text, re.S) if m: return m.group(1), m.group(2).strip(\) return None, None def manual_tool_node(state: AgentState) - dict: last_content state[messages][-1].content tool_name, tool_arg parse_action(last_content) if tool_name search: result search.invoke(tool_arg) return {messages: [{role: tool, content: fObservation: {result}}]} return {messages: [{role: tool, content: 无法识别动作。}]}文本版最大的问题是格式不稳定。模型偶尔会多换行、少括号、写错工具名你要花很多精力做纠错。所以我的建议是初学阶段用文本版加深理解工程阶段一定要切换到原生 function calling。4. 让智能体更抗造的三个关键调试点4.1 限制最大步数防止死循环大模型在循环里“魔怔”并不是罕见现象。一个简单问题它可能反复调同一个工具甚至连续报错后依然重试。不加限制单次任务可能消耗几十次模型调用又慢又贵。LangGraph 提供了递归上限机制。默认情况下执行超过一定步数会自动抛异常终止try: result app.invoke( {messages: [HumanMessage(content明天杭州到北京有哪些航班)]}, {recursion_limit: 10} ) except Exception as e: print(执行超限, e)更稳妥的做法是把步数直接写进 State让模型和业务代码都能感知到剩余步数。比如class AgentState(TypedDict): messages: Annotated[list, operator.add] remaining_steps: int在 agent 节点开头检查remaining_steps如果小于 1就强制让模型输出最终答案而不是继续调工具。这种方式可以在业务层面兜底避免异常弹出。4.2 工具结果截断保护上下文窗口工具返回的内容可能很长。比如查数据库返回一百行记录也可能是一个超长的错误堆栈。这些内容如果原封不动塞进 messages很快会把上下文窗口塞满导致后续推理质量下降甚至直接调用失败。我常用的做法是在工具执行节点里对结果做长度限制。比如def safe_tool_call(tool_func, arg, max_chars500): result tool_func(arg) result str(result) if len(result) max_chars: result result[:max_chars] ...[已截断] return result这样既给模型提供了足够的 Observation 信息又不会让消息体无限膨胀。注意截断位置不能把最关键的信息切掉最好是根据返回数据结构做“取前 N 行”或“保留关键字段”的智能截断而不是简单一刀切。4.3 低温度 结构化输出减少幻觉ReAct 智能体的最终答案质量一半取决于工具返回的数据一半取决于模型的归纳能力。如果你希望模型尽量“照实说话”而不是在工具结果基础上自由发挥最直接的办法是把推理温度调到接近 0并明确要求模型引用 Observation。对于需要返回 JSON 的场景可以额外做一个结构化输出校验。模型生成的结果先解析成 JSON如果失败就把它作为 Observation 反馈给模型让它自己修正。这个“错误回灌”本身就是 ReAct 的一种应用会让模型越来越靠谱。4.4 用 Checkpointer 保存多轮状态真实产品里用户往往会在多轮对话中向同一个 Agent 提问。比如先问“杭州明天天气”再问“那北京呢”。第二个问题里的“那”指向的是上一轮聊天上下文。这时就需要 LangGraph 的 Checkpointer 机制来保存每个 thread 的消息状态。from langgraph.checkpoint.memory import MemorySaver memory MemorySaver() app graph.compile(checkpointermemory) config {configurable: {thread_id: user-001}} result app.invoke( {messages: [HumanMessage(content那北京呢)]}, config )checkpointer 的作用相当于给 Agent 装了一块“记忆硬盘”图执行到任何节点时状态都会自动落盘下次使用同一个 thread_id 继续调用它就能从上次的状态接着走。这样多轮对话不用你手动拼历史记录LangGraph 全帮你维护了。5. 常见问题与排查技巧实录5.1 模型为什么死活不调用工具这是新手遇到最多的问题。模型明明有工具可用却每次直接给出一个从记忆里编的答案。排查顺序我一般这样来第一确认模型对象真的绑定了工具。如果llm_with_tools没有正确bind_tools(tools)模型根本不知道工具存在。第二确认工具描述足够“像接口文档”。描述太模糊模型不知道什么时候用描述太长又会挤占上下文。理想的描述是这个工具做什么、什么情况下用、参数代表什么意思。第三确认提示词里给了明确指令。比如“当你需要实时信息、数据库内容或工具产生的结果时必须调用工具后再回答”。可以在 System 提示词里注明“宁可多调用一次也不要凭空猜测”。5.2 工具执行时报错或者返回的结果格式不对ToolNode 要求工具函数返回的内容能被序列化最好不要返回一个自定义对象而要返回字符串或字典。一个更稳妥的姿势是在工具内部做好异常捕获tool def search(query: str) - str: ... try: # 真实场景里这里可能是请求外部API return do_search(query) except Exception as e: return f搜索失败{e}请尝试更换关键词。这有一个额外的好处错误信息也被包装成 Observation 送回给模型。模型看到“搜索失败”后往往会自动想一个替代方案比如换关键词或者调整查询条件。这就是 ReAct 智能体“自我纠错”的一个很典型的表现。5.3 执行到一半就退出或者循环永远跑不完先看条件边。tools_condition的返回值有两个tools代表有工具调用__end__代表没有工具调用。如果你自己写条件函数务必保证这两个分支都存在并且映射到正确的节点。一个常见错误是只处理了“有工具调用”的分支忘了“无工具调用也要结束”结果图跑着跑着就没有出口。再看递归上限。如果 tasks 循环超过recursion_limit会抛异常。这不是代码坏了是保护机制生效了。这时你要么调高限制要么在 agent 节点里强化系统提示词让模型更早收敛。5.4 上下文越积越长模型回答开始答非所问ReAct 本身会不断累积消息工具一多一轮对话可能就有几十条中间消息。我的实践是定期“压缩记忆”当 messages 数量超过阈值时把前面的历史消息让模型生成一段摘要然后用摘要替换掉旧消息保留最近几轮原始内容。LangGraph 的节点机制可以很自然地把这个摘要动作写成一个独立节点插在流程里不影响 ReAct 主循环。5.5 最后再强调一个容易忽略的点工具描述一定要写“适合在什么时候使用”而不是只写“这个工具做什么”。比如差的描述“search(query: str) - str搜索函数”好的描述“当用户询问航班、天气、实时价格等需要最新信息时使用。参数 query 应该是一个简洁的中文关键词例如‘杭州北京航班’。”模型没有“常识”它对工具的认知完全来自这段描述。你描述得越像一份完整接口文档工具被正确调用的概率就越高。这一点是我在多次项目中反复验证过的与其花一小时调试代码不如花十分钟把工具描述写清楚。最后分享一个我至今还在用的习惯每次调试 ReAct 智能体我都会先打印出完整的执行链路把agent - tools - agent - END每一步的消息都看一遍。不要只看最终答案而是要观察模型在每个节点做了什么决定、为什么这样做。很多奇怪的问题原因往往不在代码而在提示词或工具描述里。看清楚整个推理轨迹之后修复方向就会非常明确。这个最小示例里的 search 工具完全可以换成内部数据库查询、业务 API、代码解释器甚至是一组需要人工审批的操作。图结构不用改只要更新工具列表和描述你的智能体就能解锁新一轮能力。ReAct 加 LangGraph 的组合最大的价值不是省掉几行代码而是它把一条模糊的“智能体”概念变成了你可以逐步演进、随时插拔、可以调试的工程产物。收藏这篇文章照着跑通一次你会比看一百遍理论都更有底气。
延伸阅读

更多相关文章

2026/10/22 4:22:37

Redis作为AI短期记忆中枢的三大协同模式与数据建模实践

1. 为什么 Redis 突然成了 AI 系统的“记忆中枢”?最近在几个技术交流群里,频繁看到有人发截图:某大模型服务的响应延迟从 800ms 骤降到 120ms,监控图上 Redis 的 QPS 曲线同步冲高;另一份内部架构文档里,“…

2026/10/22 4:22:37

河北专升本汇编第四章:伪指令与程序设计核心考点突破

1. 为什么第四章是专升本汇编的分水岭1.1 从“能看懂”到“能写出来”的跨越河北专升本计算机专业的微机原理与接口技术这门课,很多人的复习轨迹是这样的:前三章学CPU结构、寄存器、存储器寻址,跟着视频课划划重点,做做选择题&…

2026/10/22 4:22:37

MediaPipe手部关键点实时跟踪:从零上手到手势识别实战

1. 从零上手MediaPipe手部关键点实时跟踪第一次接触MediaPipe是在一个体感交互的Demo项目里,当时需要在不依赖深度相机的前提下,仅用普通RGB摄像头完成手部21个关键点的实时定位。试过OpenPose的手部模型,帧率掉到个位数;也试过自…

2026/10/22 5:27:44

华为云ModelArts实战:从零完成模型训练与部署全流程

1. 从零到一:为什么我选择用云端平台跑模型训练搞过机器学习项目的人都有一个共识:模型训练这件事,最折磨人的往往不是算法本身,而是环境。CUDA版本对不上、显卡驱动冲突、依赖包互相打架、本地机器显存不够……这些问题随便拎一个…

2026/10/22 5:27:44

零基础用AI开发产品:从需求描述到变现的完整指南

1. 为什么零基础的人现在也能用AI开发产品1.1 从“写代码”到“说需求”的转变放在三年前,一个完全不懂编程的人说“我想做个产品”,基本等于说“我想造火箭”。光是前端、后端、数据库、服务器这一套名词就能把人劝退。但现在情况完全变了,A…

2026/10/22 5:27:44

华为云ModelArts实战:从模型训练到在线部署全流程指南

1. 从零到一:为什么我选择在云上跑通模型训练与部署搞算法的人都有一个共同的痛点:本地机器跑不动。我刚开始做深度学习项目那会儿,用自己那台笔记本跑一个图像分类任务,一个epoch要等四十多分钟,风扇转得像直升机起飞…

2026/10/22 5:27:44

MongoDB索引实战:从慢查询诊断到复合索引与性能调优

1. 索引不是银弹:先搞清楚MongoDB在什么场景下会“慢”很多人第一次接触MongoDB索引,是因为某条查询突然变慢了。翻日志、看监控,发现一个原本几十毫秒的查询涨到了几秒,第一反应就是“加个索引吧”。这个直觉不算错,但…

2026/10/22 5:27:44

基于ROS的多无人机编队仿真:架构、算法与避坑指南

简介:一套基于ROS的多无人机编队仿真项目源码,面向无人机控制、算法和智能机器方向的研究者、竞赛队伍及ROS进阶学习者。项目中覆盖PX4/ArduPilot等底层控制接口、多机通信、路径规划、避障、视觉定位与编队协同等关键模块,并配套Gazebo等仿真…

2026/10/22 5:22:43

智慧药房赛项资源包解析:ROS导航与YOLOX检测工程实践

简介:这份资源是第二十五届中国机器人及人工智能大赛智慧药房组的完整参赛作品包,面向备战同类机器人竞赛的高校学生与指导教师,尤其适合需要参考赛题实现思路、搭建仿真与实机调试环境的中高级参赛者。压缩包共150个文件,约38.19…

2026/10/21 4:53:08

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/22 0:01:49

电商智能运营体系从0到1:架构设计、落地步骤与避坑指南

前两年我们团队接了一个电商平台的运营中台项目,目标很直白:把过去靠运营拍脑袋做的选品、定价、发券、推活动这些事,逐步用数据算法和自动化流程代替。项目名字内部就叫“电商智能运营体系”。最近有不少朋友问我当时是怎么设计的、踩过哪些…

2026/10/22 0:01:49

若依框架AI代码生成不稳定?用Agent Skill三层结构+五步法搞定

最近一段时间,我一直想把后台管理系统里那些机械重复的增删改查代码交给AI去写。框架用的是若依,按理说这类业务代码模式非常固定,应该是AI最擅长的事情。可真试下来却不是那么回事——你让AI生成一个用户管理模块,它给出的Contro…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑