
先说个自己的真实经历。前一阵用LangChain写Agent写到一个阶段特别难受——prompt稍微复杂一点Agent的行为就开始变得“玄学”。明明上一步模型还要调用搜索工具下一步就开始自己编答案有时候工具返回了很关键的信息模型偏偏无视它继续胡扯。最气人的是我根本不知道它中间经历了什么只能一遍遍改prompt碰运气。后来把工作流切到LangGraph回头才看清问题出在哪LangChain把Agent执行过程中最核心的“思考→行动→观察”循环封装得太黑了全程没有给你介入的机会。这篇文章就围绕LangGraph实战展开重点讲循环机制怎么帮LangChain Agent工作流摆脱“黑盒状态”。我会从Agent的本质说起拆解为什么循环是Agent的核心动作再对比LangGraph和LangChain在编排层面的差异最后用一份完整的代码示例演示如何把老式AgentExecutor改造成LangGraph循环工作流。适合已经被LangChain“玄学过”、想理解AI Agent背后执行逻辑的开发者也适合准备给团队引入LangGraph做技术选型的朋友。1. Agent的本质它本来就是一个循环1.1 从AgentExecutor说起藏在黑盒里的ReAct循环先说一个可能被很多人忽略的事实Agent的真正执行逻辑不是一套多复杂的算法而是一个非常朴素的循环。大家可能听过ReAct这个词ReAct全称是Reason Act推理和行动交替进行。它的执行流程可以压缩成一句话模型先思考下一步该怎么办决定调用某个工具拿到工具返回的结果然后再思考、再决定、再调用……直到模型认为不需要调用任何工具循环结束。这个过程写出来就是while 模型认为还需要调用工具: 思考Thought - 选择工具Action - 获取结果ObservationLangChain里的AgentExecutor本质上就是把这个循环包在了内部。它对外暴露的参数无非就是max_iterations、max_execution_time这类兜底选项你扔进去一个prompt和一组工具它自己内部跑圈跑完给你一个最终字符串。用生活里的例子好理解。你自己装修房子的时候就是一套ReAct循环先看房子情况观察决定要不要找工人行动工人干完你看效果观察哪里不行再让工人改行动直到满意了才验收结束。但问题是如果这一切都让一个不透明的管家替你完成你看不到每一步的决策依据也不知道工人为什么干了这步没干那步出了问题就只能干瞪眼。LangChain的AgentExecutor就是那个黑盒管家。1.2 黑盒循环的三个痛点我在项目里用AgentExecutor跑了几个月踩到最典型的三个坑相信很多人也有同感。第一中途无法介入。AgentExecutor虽然能设置max_iterations但你说不了“模型第一次调用搜索工具之后如果没搜到结果就直接让它换个工具别继续搜同一个词”。因为循环在它内部你只能等整轮跑完才能看到发生了什么。生产环境里一个Agent跑三十秒甚至几分钟中间任何一步决策不符合预期你都没有办法即时修正。第二状态不可见、不可保存。每一步的中间结果只存在于进程内存里进程一断就没了。我想复现刚才那次“Agent先调用搜索、再胡编乱造”的异常行为根本无从下手因为没有中间状态可以回放。日志要是不小心没打好一切归零。第三错误定位成本极高。一个Agent跑了二十步才死掉帮你定位是第几步出的问题、那一步用什么prompt、模型输出了什么、工具返回了什么基本只能靠猜。我自己排查过一个线上问题最后靠的是把每一步的输入输出全部打满日志重新跑了三遍才定位到是一个工具偶尔返回空字符串导致的。这种事情在AgentExecutor的封装下付出的时间成本非常不值。所以问题的本质不是“Agent这个概念不行”而是循环机制本身没有被暴露出来。你控制不了的东西你自然无法优化它。这时候LangGraph就来了它把循环从黑盒里拿出来变成了你可以看见、可以修改、可以随时随地插一手的图。2. LangGraph凭什么解决循环控制问题2.1 定位差异LangChain是工具箱LangGraph是流水线很多人搞不清LangChain和LangGraph的区别网上也一直有langchain和langgraph的区别这类热词。我的理解是这样的LangChain解决的是“组件从哪里来”模型接口、工具封装、解析器、记忆模块这些都是现成的乐高积木而LangGraph解决的是“积木怎么拼”它给你一张工作台每个积木摆在什么位置、连线怎么走、什么时候走哪条路完全由你来定义。这个区别决定了你看问题的方式。用LangChain写Agent你拿到手的是一堆封装好的方法看起来快但内部流程是别人定的。用LangGraph写Agent你得先把整条流水线画出来第一步做什么第二步做什么什么条件下跳到第三步什么条件下结束。它给了你编排能力代价是你得多想一层。打个比方LangChain是给你各种厨具和食材LangGraph则是厨房的动线设计。你可以在动线上设置“炒完菜必须尝一口”这样的循环判断也可以在“菜太咸”这条分支上让厨师加水回去再炖一轮。你拥有了对过程的控制权。2.2 核心概念StateGraph、节点、边、条件边LangGraph的基础概念不复杂总共就那么几个。我尽量不拽术语直接说人话。状态State是贯穿整个工作流的共享数据容器。在Agent场景里状态最常见的形态就是“消息列表计数器”。消息列表存了一轮轮对话和工具调用结果计数器记录了已经循环了多少次。节点Node是处理状态的一个函数。它接收当前状态读取里面感兴趣的内容处理后返回一个新的状态字段。边Edge决定节点之间的流转路径。最简单的边就是“A跑完接着跑B”。条件边Conditional Edge是LangGraph最核心的东西。它相当于给节点的出口装了一个智能分拣器你写一个函数函数根据当前状态返回一个字符串返回“continue”就走循环分支返回“end”就结束流程。如果还不直观可以想象一个快递分拣中心。几十个包裹带着标签状态进入分拣流水线图先经过扫码机节点扫码机根据目的地条件边函数决定包裹去哪个出口去华东的口去华南的口面单不清楚的就走人工通道。整个工作流就是一个图稍有经验的人一看图就能明白整套流程在做什么。2.3 循环的真正价值让每一步都可控、可改、可回放LangGraph的循环和普通代码里的for循环有个本质差别循环的每一轮都会经过一个带状态的图节点而每一轮经过时状态都有机会被保存、查看和修改。这意味着你可以做三件AgentExecutor时代很难做到的事情。一是观察中间结果。每一轮模型输出是什么工具返回了哪些内容全部能看到。排查问题的时候对着日志看思路会清晰得多。二是动态修改下一步行为。比如判断工具连续出错两次就直接让条件边返回终止信号不再浪费模型调用次数。这种“中途干预”在AgentExecutor里做不到在LangGraph里只是一个条件判断的事。三是断点回放。LangGraph可以配合checkpointer把每一步的状态持久化跑完一个长任务之后可以回头查看“当时在第几步、状态是什么”。线上出了问题也能恢复到指定状态重新跑这在线下调试里是救命级的特性。下一节我用一个实际的Agent例子把LangChain写法和LangGraph写法放在一起做对比亲眼看一下改造前后的差异。3. 实战改造把LangChain Agent重写为LangGraph循环工作流3.1 场景设定一个能搜索、能计算的多工具Agent为了贴近实际我设计一个复合型任务做一个“新闻信息整理数据计算”的Agent。用户输入类似这样的一句话搜索最近一周AI行业的重要新闻统计新闻中提到的知名AI公司数量最后输出一份简洁总结说明本周AI行业最重要的三个趋势。这个任务有个特点它不是一个单次查询就能完成的模型需要先调用搜索工具拿到结果之后可能还要再搜索补充信息做一次计数计算最后基于完整的上下文做总结。整个流程天然需要多轮循环非常适合展示LangGraph循环机制的价值。工具方面我准备两个一个是模拟的新闻搜索工具一个是模拟的计数工具。演示时不必接真实API用定义好的函数代表搜索和计数的动作就够了这样你可以直接把代码跑起来体验一遍流程。3.2 LangChain版AgentExecutor的写法与局限先看LangChain的传统写法。用create_openai_functions_agent加AgentExecutor代码量确实少from langchain.agents import create_openai_functions_agent, AgentExecutor from langchain_openai import ChatOpenAI from my_tools import search_news_tool, count_companies_tool llm ChatOpenAI(modelgpt-4o) tools [search_news_tool, count_companies_tool] agent create_openai_functions_agent(llm, tools, prompt) executor AgentExecutor( agentagent, toolstools, max_iterations5, verboseTrue, ) result executor.invoke( {input: 搜索最近一周AI行业的重要新闻统计新闻中提到的知名AI公司数量最后输出一份简洁总结} ) print(result[output])这段代码看起来简单但你仔细观察会发现整个过程都是“约定好的”AgentExecutor在内部替你做了循环、工具调用解析、结果回填。max_iterations是唯一的兜底参数。一旦出现模型反复调用同一个搜索工具、或者搜索工具返回了结果但模型坚持不依赖这些结果的情况你没有任何办法在循环中间插入一个纠正步骤。我当时最头疼的就是这个Agent每一步都在消耗token而它对工具结果的依赖程度却完全不可预测。prompt里写了“请根据搜索结果作答”也没用模型可能在某一步突然“自信”起来无视工具结果直接开编。在没有过程控制的情况下这个问题显得无解。3.3 LangGraph版把循环从黑盒变成显式图下面用LangGraph重写同一个Agent。核心思路是拆解为两个节点一个负责调用模型做决策一个负责执行工具中间用条件边控制循环的走向。第一步先定义状态。状态里我放了两个字段messages用来记录完整对话历史step_count用来记录已执行的循环次数。from typing import TypedDict, Annotated from langgraph.graph import StateGraph, END from langgraph.graph.message import add_messages class AgentState(TypedDict): messages: Annotated[list, add_messages] step_count: int注意Annotated[list, add_messages]这个写法。它告诉LangGraph每次节点返回新的消息时自动追加到已有的消息列表里而不是覆盖掉旧的消息。这是Agent能保留完整上下文的关键稍后我会详细讲为什么这么设计。第二步定义两个节点函数。第一个是call_model它把绑定工具后的模型调用封装起来并将模型新输出和步数加一写入状态第二个是call_tool它拿到模型最新输出的工具调用请求逐个执行工具把结果以ToolMessage形式写回状态。from langchain_core.messages import ToolMessage def call_model(state: AgentState): messages state[messages] response llm.bind_tools(tools).invoke(messages) return { messages: [response], step_count: state.get(step_count, 0) 1, } def call_tool(state: AgentState): last_message state[messages][-1] tool_results [] for tc in last_message.tool_calls: tool_name tc[name] tool_args tc[args] tool {t.name: t for t in tools}[tool_name] result tool.invoke(tool_args) tool_results.append( ToolMessage(contentstr(result), tool_call_idtc[id]) ) return {messages: tool_results}这两个节点的分工很明确call_model是“大脑”call_tool是“手脚”。大脑说“我要干什么”输出工具调用请求手脚去执行执行结果再交给大脑判断循环往复。第三步定义条件边这是整个循环机制的灵魂。should_continue函数根据当前状态决定是继续循环还是结束MAX_ITERATIONS 5 def should_continue(state: AgentState): last_message state[messages][-1] # 业务层硬上限达到最大步数强制结束避免死循环 if state.get(step_count, 0) MAX_ITERATIONS: return end # 模型不再要求调用工具说明任务已完成 if not last_message.tool_calls: return end return continue这个函数的返回值会决定图走哪条路。返回continue就回到模型节点开始下一轮循环返回end就走向结束节点。第四步组装图把节点和边连起来graph StateGraph(AgentState) graph.add_node(model, call_model) graph.add_node(tools, call_tool) graph.add_conditional_edges( model, should_continue, { continue: tools, end: END, }, ) graph.add_edge(tools, model) graph.set_entry_point(model) agent_app graph.compile()这里最关键的是add_conditional_edges它为model节点增加了一个条件出口出口的方向由should_continue返回的字符串决定。continue走向toolsend直接走向图结束。最后调用from langchain_core.messages import HumanMessage result agent_app.invoke( {messages: [HumanMessage(content搜索最近一周AI行业的重要新闻统计新闻中提到的知名AI公司数量最后输出一份简洁总结)], step_count: 0} ) print(result[messages][-1].content)运行之后你会直观地看到流程的每一次走向模型输出工具调用请求、工具执行、结果回填、再交给模型判断……整个过程就像流水线一样清晰。3.4 进阶玩法条件早停、断点续跑、人工介入上面是LangGraph循环机制的基础用法。把基础跑通之后循环真正值钱的地方在于你可以在图的任意节点旁边加上各种控制逻辑。我实际项目里用得最多的有四个。第一个是动态早停。有些情况不需要跑满MAX_ITERATIONS比如某个工具连续报了两次错误再跑下去大概率还是错。此时可以让条件边额外判断一个错误计数一旦超过阈值就返回end。这种逻辑用AgentExecutor几乎写不出来但在LangGraph里就是在条件函数里加两个if的事情。第二个是断点续跑。给compile传入checkpointer后LangGraph会把每一步的状态快照保存下来from langgraph.checkpoint.memory import MemorySaver checkpointer MemorySaver() agent_app graph.compile(checkpointercheckpointer) # 带上线程ID运行之后可以从任意节点恢复 config {configurable: {thread_id: project-demo-001}} result agent_app.invoke(input_data, configconfig)这个能力在线下调试非常管用。我可以让Agent跑到第三步停下来把状态打出来看然后再从指定节点接着跑不用重新开一轮完整执行。第三个是人工介入。在call_tool节点之前插入一个interrupt_beforeAgent执行到该节点时会暂停等外部确认之后再继续。这在需要人工审核工具调用的场景比如调用支付接口、发邮件、执行写操作里是刚需。功能设计上你只需要agent_app graph.compile( checkpointercheckpointer, interrupt_before[tools], )第四个是子图。当Agent的工具比较多、流程比较复杂时可以把“新闻搜索”做成一个独立的子图主图的call_tool里调用这个子图。这样整个图谱更有结构感每个子图还能单独测试、单独维护。4. 循环机制设计与调参的5条实战经验4.1 状态对象别贪大该存什么不该存什么状态设计是LangGraph项目里最重要的决策之一它直接决定了图的复杂度。我的原则是八个字能推理出来的不存下一步要用的才存。用户的原始输入、模型的历史消息、工具返回结果这些肯定要存但是像“模型上一次的决策原因”这种东西不用额外存因为它不在下一步执行里被消费存了只会让状态越来越臃肿。消息列表本身务必全量保留。工具返回的Observation可以裁剪、可以总结但绝对不能丢。丢失这一步相当于让Agent失忆下一轮循环模型就要重新猜“刚才工具返回了什么”决策质量会直线下降。在LangGraph里实现消息追加靠的是Annotated[list, add_messages]。如果没有这个注解节点返回{messages: [...]}时默认会直接覆盖已有列表Agent第二轮就看不到第一轮的工具结果了。我第一次没加注解调试了半天才发现是这个原因。4.2 退出逻辑集中在条件边别写在节点里新手经常犯的一个错误是在call_model节点里写“如果模型没有tool_calls就不再跑工具”这样的逻辑。功能上能跑但破坏了图的可读性。我的建议是节点只负责“干活”退出判断只放在条件边里。好处有三层一是图结构一眼就能看出循环的走向二是条件函数可以单测传不同状态进来验证返回值三是将来想改退出条件只需要改一个函数不用动执行逻辑。实际项目里我把所有条件边的函数放在一个router.py文件里统一命名成should_continue、should_retry、should_halt整个工作流的决策逻辑集中在一个地方维护起来非常舒服。4.3 工具调用结果必须“清洗”后再塞回循环这是我在生产环境踩过的最深一个坑。工具的原始返回千奇百怪搜索接口可能返回大段HTML、数据库查询可能返回几百行记录、异常的时候还可能吐出一整段堆栈。直接把原始字符串塞回messages模型下一轮就要在大量噪声里找有效信息既浪费token又容易把模型带偏。经验做法是给每个工具写一个format_result函数统一在返回前做清洗def format_result(raw_result, max_len800): # 提取关键字段、去HTML标签、截断超长文本 text extract_plain_text(raw_result) if len(text) max_len: text text[:max_len] ...(截断) return text如果工具调用抛异常也不要直接抛出去终止整个图而是把异常信息转成一条错误消息返回给模型让模型自行判断下一步是重试还是放弃。很多场景下模型会根据报错信息自我修正这也是循环机制发挥价值的地方。4.4 recursion_limit只是底线业务层硬上限必须自己设LangGraph内部有一个递归深度限制用来兜底防止图无限循环。但这个值配的是“能跑”而不是“跑得好”真正的成本控制要靠业务层自己设上限。我的习惯是在条件边函数里自己判断step_count达到业务允许的最大值就强制结束。比如给客户做对话机器人单轮任务我设的是4步超过4步意味着这个任务超出了Agent当前能力直接结束并返回“暂时处理不了”提醒用户换一种问法。这个界限比框架默认值严格得多但它能精确控制单次调用的token成本让线上成本可预期。另外建议在最后一步返回给用户时把step_count也带出来。这样用户能感知到任务的复杂度内部排查时也多一个线索。4.5 日志、追踪、监控从第一天就做最后一个经验也是最容易忽视的。LangGraph让循环变得可见但前提是你把自己关心的信息记录下来了。我在每个节点里都会打一条结构化的日志包含节点名、当前步数、输入消息摘要、输出结果摘要、token使用量。格式统一为JSON方便后续接入日志系统搜索。logger.info( json.dumps({ node: call_model, step: state.get(step_count, 0), model: llm.model_name, tool_calls: [tc[name] for tc in response.tool_calls], token_usage: response.response_metadata.get(token_usage), }, ensure_asciiFalse) )这些日志在Agent跑出异常结果时是唯一的救命稻草。线上环境一般不能随便打断调式但完整的日志能让你快速还原“第几步做了什么决策、工具返回了什么”的完整过程。我上一轮排查Agent卡死问题就是靠日志发现模型连续三轮调用同一个搜索工具、而工具每次都返回相同结果——这种问题如果没打日志单靠推理是不可能定位的。5. 从LangChain迁移LangGraph的常见问题与排查技巧5.1 循环不退出卡死问题排查这是迁移初期最常见的问题。表现是任务开始之后一直跑迟迟不出结果直到触发框架的递归限制或超时。拿到这种问题先别急着改代码按顺序查三件事第一确认条件边的返回值字符串是否一一对应。LangGraph的条件边是靠字符串匹配决定走哪条路径的should_continue里返回end但map里只配置了continue: tools和end: END就很容易因为大小写不匹配直接报错。检查所有字符串是否和配置表完全一致是最基础的排错步骤。第二检查模型是否真的会输出空的tool_calls。我们在设计时假设“没有tool_calls就应该结束”但实际有些模型在输出文本内容的同时也会在tool_calls里出现奇怪的残留。稳妥的做法是判断not last_message.tool_calls而不要解析字符串去匹配。第三确认step_count有在状态里正确递增。如果step_count一直是0条件边里的上限判断就永远不会触发结果就是无限循环。我见过不少次这种低级错误排查起来又快又简单日志里打一下每轮的step_count就知道了。5.2 状态被覆盖消息丢失与上下文断裂第二个高频问题是上下文莫名丢失。主要表现是Agent第二轮开始“失忆”忘了第一轮搜到过什么反复问同一个问题。这时候优先检查状态定义里有没有加Annotated[list, add_messages]。如果消息字段没加这个注解每次节点返回都会覆盖旧消息Agent自然记不住之前的步骤。第二个检查点是节点返回的字段名是否和State定义完全一致拼写不一致时LangGraph可能不会把它合并进去。解决起来不复杂但排查时容易忽略建议动手前先打印一次中间状态看看messages列表里是否有完整的历史内容。5.3 流式输出体验差首字延迟与卡顿LangGraph默认是整段执行完后才一次性返回结果交互体验上比直接调用模型差一些用户会感觉“半天没反应”。如果要做流式输出可以用LangGraph的stream方法按节点逐步返回结果for event in agent_app.stream(input_data, configconfig): for key, value in event.items(): if key model: content value.get(messages, [])[-1].content if content: yield content这里有个经验call_model节点里模型输出的是整个消息对象包含tool_calls和content两部分。在流式输出时如果只想给用户看最终的文本回答就需要在model节点里把tool_calls阶段的内容和最终回答阶段的content区分开。我一开始没区分导致用户在工具调用阶段也看到了一堆半截文字体验很怪。5.4 版本差异与API变更LangChain和LangGraph的版本更新很快网上搜到的一些教程可能已经过时。比如早期版本条件边用add_conditional_edges的写法后来变成了add_conditional_edges加map参数再往后又有新写法。我的经验是别死背教程里的API以官方文档为准。看教程时注意看发布时间和import路径如果教程里的import路径和当前安装版本不匹配优先改成当前版本对应的写法。好多看着非常高级的功能换一个版本就能用更简单的方式实现这也是LangGraph迭代本身带来的变化。5.5 快速排查表症状可能原因快速解法循环不退出条件边字符串不匹配step_count没递增模型一直输出tool_calls打印条件边返回值检查map配置确保step_count正确递增消息丢失/上下文断裂消息字段没加add_messages返回字段名拼写不一致检查状态定义和节点返回字段确认Annotated注解首次调用很慢图编译和状态初始化开销提前做一次预热调用或改用stream模式流式输出异常没区分tool_calls阶段和content阶段在model节点区分输出内容工具调用阶段不输出流式文字迁移后行为不一致prompt写法差异、工具返回格式差异统一工具返回格式重新评估prompt描述线上异常难复现没有保存中间状态配置checkpointer记录完整日志链路写在最后的个人体会LangGraph对我最大的启发不是它比LangChain“高级”而是它把“循环”这个Agent的底层逻辑放到了你能看见、能修改、能暂停的地方。工具调用失败、模型决策异常、上下文爆掉这些问题在LangChain里只能靠调prompt碰运气而到了LangGraph里全部变成了可以逐段排查、逐段验证的工程问题。有一次我排查一个线上Agent卡死的bug就是因为从日志里看到模型连续三轮都在重复调用同一个搜索工具而工具每次都返回相同结果——这种问题在AgentExecutor里几乎是不可见的但在LangGraph里三步就定位了。如果你正在用LangChain做Agent我的建议很简单超过两个工具、步骤超过三步或者你已经开始感觉到“控制不住”了那就花一个周末把LangGraph学起来。学的时候别急着背API先把图的思维建立起来——状态怎么流转、条件怎么判断、循环怎么退出这三个问题想清楚了LangGraph就只是表达你想法的工具而已。