做LangGraph也有一段时间了身边不少朋友问我同一个问题Agent图到底怎么画才对。今天这一篇我们把镜头拉远一点专门聊Agent背后的那套核心思想——ReAct也就是Reason Act推理与行动交替进行的决策循环。很多人一上来就盯着StateGraph、Conditional Edge这些API看但如果不理解ReAct循环本身在编排什么写出来的图大概率只是长得像Agent的if-else。这篇我会从循环本质讲起再落到LangGraph的图上接着给出一份能直接跑的查天气Agent示例最后附上我在真实项目中踩过的坑和一套实用排查清单。想搞清楚Agent为什么这么写、挂了怎么救的同学这篇应该能帮到你。1. 为什么Agent必须边想边做ReAct进入正题1.1 从一次失败的聊天说起你问一个纯语言模型“现在上海地表温度多少我出门要不要带伞”它的回答大概率分两种要么告诉你“作为AI我无法获取实时数据”要么干脆编一个“今天26度建议穿短袖”。这就是大模型的基本盘——它只有训练时看到的知识没有实时感知能力也没有操作外部系统的能力。要让它变成一个真正能做事的Agent缺的就是“行动”这一环。很多人把Agent简单理解成“会调API的大模型”这个说法对了一半。会调API只是行为行为背后得有判断什么时候调、调哪个、结果代表什么、下一步怎么办。这一整套判断逻辑就是ReAct这个名字想表达的东西。ReAct不是某个库也不是某个具体的Agent实现它是让推理与行动交替发生、在循环里解决问题的能力框架。你可以把它想象成人在外面办事先想“我需要什么”然后动手去拿拿到东西后看一眼再想“对不对”不对就换一种办法。这个过程不是一次性规划完的而是走一步看一步边看边修正。1.2 ReAct的循环Thought、Action、ObservationReAct的原始论文里模型被要求按一个极简的循环跟外部世界交互。这个循环由三步组成Thought思考模型用自然语言解释自己现在为什么要做某件事把推理过程写下来。Action行动模型选择一个工具并给出参数。例如调用天气查询工具城市参数填“上海”。Observation观察工具执行后返回结果例如“上海当前多云气温26度体感舒适”模型把这段文本作为新的输入继续思考。循环如此反复直到模型认为信息足够可以直接回答用户问题。如果用提示词来实现可以在System Prompt里规定输出格式“当需要外部信息时先输出Thought再输出Action收到Observation后再继续”如果不调用工具就直接输出“Final Answer”。许多早期Agent项目是从这种纯文本协议起步的它简单、通用、甚至不依赖模型是否原生支持函数调用。代价是解析麻烦模型偶尔会偏题输出“Thought:”后面跟了一大段废话或者把Action写错格式。后来大模型API普遍支持了结构化工具调用模型可以直接返回“我要调用某个函数参数是JSON”这套东西在底层依然是Thought-Action-Observation的循环只是格式变成了机器可解析的对象。理解这一点很重要无论你用文本协议还是结构化调用ReAct的思想没变。1.3 为什么不是CoT也不是先规划再执行经常有人问ReAct跟“思维链”有什么区别思维链的核心是让模型在回答前先生成一段推理过程它只发生在模型内部不接触外部世界。ReAct多了“行动”和“观察”两个环节让推理结果能被真实世界验证和修正。如果模型内部知识足够思维链就已经够用但只要任务涉及实时信息、外部系统或数字运算思维链就有硬伤模型只是“想得很合理”但信息本身可能是错的。另一个容易混淆的是“先规划再执行”。那种方式让模型先把完整步骤列出来然后按步骤执行工具。优点是计划清晰缺点是环境稍一变计划就脱节了。ReAct把规划和行动搅在一起每执行一步就重新观察、重新决策更接近人处理不确定任务的方式。下面这个表格应该能说清楚差异方案推理方式外部信息修正能力适合场景思维链一次性内部推理不接触弱知识型问答、数学推理先规划再执行先整体规划再执行按计划接触中流程固定、环境稳定ReAct每一步观察后重新决策每次行动前接触强需要工具、环境不确定强调一句三者不是互斥的。很多真实Agent内部会混合使用思维链生成候选方案再通过ReAct循环去验证和修正。选型时真正要关心的是任务里信息变化有多快、工具结果有多不可预测。2. 用LangGraph把ReAct变成一张会转弯的图2.1 为什么朴素代码循环撑不住真实Agent新手往往试着用最朴素的代码实现ReAct循环大概是下面这个画风while True: response llm.invoke(messages) if not has_tool_call(response): break result call_tool(response.tool_name, response.args) messages.append(result)这段代码在Demo里完全能跑但一旦放进真实项目问题马上冒出来。第一没有状态管理。对话历史、中间步骤、人工审批记录散落在局部变量里服务一重启Agent就失忆了。第二没有可控的流程节点。比如“工具执行前需要人确认一下”“执行完工具先格式化结果再喂给模型”这些需求在while循环里只能靠又臭又长的if-else硬塞。第三没有并发与持久化的基础。多个Agent协作、断点恢复、人工介入这些都不是“循环”这个结构能表达的。ReAct从思想上是一个循环但工程上我们不能只写一个while True需要把循环拆成节点、边和状态让每一步都可观测、可控制、可重放。这也是LangGraph这类编排框架出现的原因它不是要消灭循环而是把循环变成显式的图。2.2 一张图记住LangGraph的四个核心抽象刚接触LangGraph时别急着背API先记住四个词State、Node、Edge、Conditional Edge。State是整张图共享的内存。它通常是一个TypedDict字段按需定义比如messages列表、当前步数、最终答案。每个节点执行完返回的新字段会更新State下一个节点读到的就是最新状态。Node是图里的工作单元。它可以是一次LLM调用、一个工具执行、一段数据清洗甚至可以是一个人工确认动作。Edge是有向箭头表示“执行完A节点后去B节点”。Conditional Edge是带条件的箭头LangGraph会执行一个函数根据函数返回值决定走哪条路径。这四个抽象组合起来表达能力远超普通流程编排。我建议你用画图的方式来想Agent把这个节点画成圆角矩形把边画成箭头条件边画成带问号的箭头然后对照着写代码正确率会高很多。2.3 LangGraph与ReAct的映射关系把ReAct的循环翻译成LangGraph其实是一一对应的事Thought加Action选择由一个Agent节点完成它调用LLMLLM决定是否返回工具调用指令。Action执行由一个Tools节点完成它解析工具调用指令真正去执行外部工具。Observation回填由LangGraph的State更新完成工具返回的ToolMessage被追加到消息列表。是否继续循环由条件边判断最后一条消息里如果还有工具调用就回到Agent否则走到END。用文字画出来大概是这个感觉[agent] --还有工具调用?-- [tools] ^ | |------工具观察结果返回---- | --没有工具调用时-- [end]这样画的好处是你能一眼看出循环的入口和出口在哪里。在实际项目里我们经常在“agent - tools”这条边上加一个“人工审核”节点或者在“tools - agent”的边上加一个“结果清洗”节点。只要图的骨架是ReAct循环加节点就是往线上插一个中转站非常顺手。3. 手把手写一个能查天气的ReAct Agent3.1 环境准备与依赖开发环境建议使用Python 3.10以上。我习惯用虚拟环境隔离每个Demo避免依赖打架。需要安装的依赖其实很少langgraph、langchain-core以及你正在用的大模型API对应的LangChain包再配一个langgraph-cli之类的可选工具。以当前社区的常见做法为例模型初始化一般长这样from langchain_core.messages import HumanMessage, AIMessage, ToolMessage from langchain_core.tools import tool from langgraph.graph import StateGraph, END # 这里用的是你自己已经配置好的模型实例 # 要求必须支持工具调用tool calling model init_llm_with_tools(你的模型名, ...)不少同学卡在这一步不知道模型怎么初始化。这部分通常由前几篇的环境准备覆盖了在这里只需要保证模型完成工具调用配置后能正常返回tool_calls即可。如果还没配好优先去查一下大模型接口里“函数调用Function Calling”怎么开启因为ReAct循环能否跑起来首先取决于模型会不会“说人话”转成“调函数”。3.2 定义工具小而稳定的外部手脚工具是Agent的“手脚”。初次实验我强烈建议从一个假工具开始把外部依赖先抽掉。下面定义一个模拟查天气的工具tool def fake_get_weather(city: str) - str: 查询指定城市当前天气用于判断温度、降水等出行建议。 weather_map { 上海: 多云气温26度午后可能阵雨体感较舒适, 北京: 晴气温18度风力较大早晚偏凉, } return weather_map.get(city, f{city}暂无天气数据请确认城市名称)注意两点。第一tool装饰器会把函数的docstring、参数类型、函数名自动转成模型认识的工具描述。docstring一定要写清楚“这个工具什么时候用”而不只是“它干什么”。第二返回值要尽量是人类可读、语义完整的文本因为它的内容会原封不动进入模型的下一次推理。如果返回垃圾数据模型只能跟着一起猜。3.3 写Agent节点让模型学会“调用”Agent节点负责把当前会话消息交给模型由模型决定是回答问题还是调用工具。实现大概是这个样子class AgentState(TypedDict): messages: list step: int max_steps: int def agent_node(state: AgentState) - dict: current_messages state[messages] response model.bind_tools(tools).invoke(current_messages) return { messages: current_messages [response], step: state[step] 1, }bind_tools把工具列表绑定到模型调用上模型返回的AIMessage里会携带tool_calls字段。如果模型判断不需要工具那么tool_calls就是空列表我们据此判断循环可以结束。注意这里不要自己拼字符串去调用工具一定要把模型返回的message对象原样追加到messages里后续ToolMessage才能对应上。模型返回的消息和工具返回的消息必须保持同一对话链这是很多新手排查到半夜的常见原因。3.4 铺图与条件边让循环转起来接下来是核心铺图。tools [fake_get_weather] def tool_node(state: AgentState) - dict: last_message state[messages][-1] new_messages [] for call in last_message.tool_calls: tool_name call[name] tool_args call[args] if tool_name fake_get_weather: result fake_get_weather.invoke(tool_args) else: result 未知工具请提示用户 new_messages.append(ToolMessage(contentresult, tool_call_idcall[id])) return {messages: state[messages] new_messages} def should_continue(state: AgentState) - str: if state[step] state[max_steps]: return end if state[messages][-1].tool_calls: return tools return end 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, should_continue, {tools: tools, end: END}) graph.add_edge(tools, agent) app graph.compile()这段代码里的关键点有两个。其一ToolMessage必须带上tool_call_id而且要跟call[id]完全一致LangGraph才能把工具结果“挂”到那一次工具调用上。其二条件边返回的字符串要跟映射字典的key一致。第三个需要注意的地方是max_steps我一般设5并不是说Agent只能想5步而是给它一个保险丝防止模型反复调用同一个工具消耗token。你完全可以按任务复杂度调这个数。3.5 跑一步看一步流程复盘假设用户输入“上海今天天气怎么样要不要带伞”整个流程是这样的Agent节点把用户消息发给模型模型生成Thought和调用意图返回AIMessage里面含一个tool_call名字是fake_get_weather参数是{city: 上海}。条件边检查最后一条AIMessage发现tool_calls不为空于是走到Tools节点。Tools节点执行fake_get_weather.invoke({city: 上海})得到“多云气温26度午后可能阵雨”把它包装成ToolMessage。流程自动回到Agent节点。模型看到工具结果后不再新增tool_call而是生成最终回答“上海今天多云最高26度午后可能有阵雨出门建议带伞。”条件边发现tool_calls为空进入END。这5步几乎就是所有LangGraph Agent的基础样板。在这个骨架上你可以增加更多工具、加人工审核节点、加向量检索但底层的这个循环不会变。4. 死循环、幻觉、不调用工具常见问题排查实录4.1 模型不调用工具的排查思路现象用户问“上海天气”Agent直接回答“我无法获取实时天气请自行查看”而不是调用工具。排查思路从三点入手。第一确认模型本身支持工具调用。很多老模型或者小参数模型根本不输出tool_calls这不是Prompt能救回来的。第二检查工具描述。docstring里有没有写清楚“仅当需要获取实时天气时使用”如果工具描述太模糊模型可能不知道它有这个能力。第三降低temperature到0附近。工具调用阶段不需要创造力和随机性。我实测过temperature调高后模型偶尔会突然“忘了”自己会调工具宁可直接编个天气。另一个小技巧在System Prompt里明确写一句“如果你需要最新信息必须使用工具不要猜测”。这句话在开源模型上尤其管用。4.2 死循环与工具错误被当成答案死循环也是高频事故最常见症状是模型反复调用同一个工具或者来回切换两个工具生成出一大串无意义的工具调用。我的保险手段永远是max_steps它不负责优化只负责止损。但这还不够下一步是让模型具备“自我刷新手刹”在Prompt里告诉它如果某个工具已经返回过结果就没有必要再调用一次除非结果明显不满足需求。还有一种更隐蔽的问题工具本身抛异常结果被包装成“工具返回错误”进了上下文模型误以为“它已经尽力了”于是把这个错误作为最终答案输出。有一次我在模拟项目X里测试“帮我查三个城市的天气”模型来回调用同一个城市三次后来发现是工具返回里城市名写错了模型以为自己查了A的天气却没用上。我的习惯是让工具节点对异常做两层处理第一层捕获异常后返回类似于“查询失败原因是XXX请不要把失败当成最终答案”的文本第二层在System Prompt里写清楚工具执行失败时要继续尝试其他工具或向用户说明失败原因绝不能直接以报错信息充当回答。4.3 LangGraph调试三板斧可视化、断点、日志排查问题不能靠猜我这里分享三个我常用的手段。可视化编译后的app对象可以调用get_graph()查看图结构也可以直接打印每个节点的入参和出参。如果整个图在可视化面板里看起来乱了先检查条件边返回值的key和映射字典是否一致。断点LangGraph支持在任意节点前后暂停用interrupt_before[tools]可以让Agent在真正动手执行工具前停下来请求人工批准。这对需要审查工具调用的场景很实用也能帮你看清“模型想干嘛”。日志最省事但最容易忽略的是日志。把每一步的模型原始输出、工具调用参数、工具返回结果全都print出来然后在评测集上跑一遍比对“第几步走歪了”。我的经验是80%的Agent问题都能在日志里定位出来要么是工具描述没写好要么是模型误把Observation当答案。日志里看到“调用同一个工具超过3次”基本就是死循环前兆可以先下手为强。高频问题速查表现象可能原因建议做法完全不调用工具模型不支持或Prompt没说清换支持tool calling的模型把“必须用工具”写进System Prompt反复调用同一工具上下文丢失或模型陷入循环设max_steps加“不要重复调用已返回结果的工具”指令工具报错却当成最终答案异常被包装成Observation工具节点捕获异常明确告诉模型“失败请换方案”工具结果格式乱工具返回非结构化文本让工具返回统一JSON或自然语言摘要避免模型瞎猜回答与工具结果无关模型忽略Observation检查ToolMessage是否按tool_call_id正确关联检查Prompt是否强调“必须基于工具结果回答”5. 从Demo到生产一套实用的落地经验5.1 工具设计少而精还是多而全我在真实项目里吃过“工具太多”的亏。一开始图方便把十几个功能都挂到Agent的工具列表里结果模型经常选错工具或者在两个相似工具之间犹豫。后来我把工具列表砍到只剩核心5个模型的准确率肉眼可见提高了。原因不难理解工具列表是“选择空间”选择空间越大模型就越容易出错。工具设计上我有三个原则。第一单一职责一个工具只解决一类问题不要做“查天气算体感温度建议穿衣”这种大杂烩。第二名称和描述里写清使用条件如果两个工具容易混淆就在描述里互相指路。第三参数尽量少参数越多模型生成JSON时越容易出错能用默认值就用默认值。5.2 上下文膨胀状态里加一个摘要字段ReAct循环跑久了messages列表会越来越长。工具观察结果、历史对话、用户消息全都堆在一起token消耗直线上升而且模型容易被旧观察带偏。我的做法是在State里加一个summary字段在Agent节点前插入一个“摘要节点”如果messages条数超过阈值就调用模型把前面的对话压缩成一个摘要然后只保留最近的几轮消息和摘要。这个操作在LangGraph里就是一个普通的节点从State取messages经过LLM处理后更新messages和summary。它把“无限增长”的窗口变成“有限长度加压缩记忆”效果比简单截断好得多。需要提醒的是摘要节点会额外消耗一次模型调用实际项目中要选择在什么时机触发。我一般设阈值是20条消息超过就压缩你可以根据任务复杂度和成本调整。5.3 评测Agent不是能跑就行Agent项目的困难不是写出来而是不知道它“算不算对”。我经历过“Demo跑得很顺一换测试集就崩”的至暗时刻后来开始老老实实做评测集。起步建议很简单准备5到10个覆盖典型场景的问题每个问题记录三个指标——最终答案是否准确、工具调用次数是否在预算内、有没有出现胡编内容。然后一遍一遍跑看到“通过率”上来了再上真实场景。这个过程里最反直觉的一点是别只盯着正确答案还要看失败路径。比如工具返回“数据不存在”时Agent是优雅地说明问题还是强行编一个数字比如用户连续追问时Agent能不能在摘要压缩后还能记住关键信息。Agent跑通只是起点跑稳才是目标。最后再分享一个我在落地过程中体会最深的事ReAct不是一个需要背下来的固定格式它本质上是一套关于“模型如何与外部工具互信”的治理方案。模型负责推理工具负责提供事实LangGraph负责把这两者的互信过程显式画出来。你完全可以在LangGraph里把ReAct循环改装成多级审核、多Agent协作、带人工复核的形态但骨架清晰、能回溯、能止损永远是第一位的。如果你正准备做自己的Agent应用先花半天把工具边界和评测清单磨好再回来画图省下来的调试时间绝对值得。
阅读完成 · 觉得有帮助?