ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

LangGraph与LangChain对比:从链式调用到图计算的工作流编排实战

2026/8/31 10:51:32 拓冰建站 浏览量
LangGraph与LangChain对比:从链式调用到图计算的工作流编排实战 过去几个月我陆续看了不少 LangGraph 实战项目也动手写了好几个 Agent 工作流。一个很强烈的感受是LangGraph 真正改变的不是“调用大模型的方式”而是我们组织 AI 应用流程的思维模型——从线性的 Chain 链式调用升级成有状态、有分支、有循环的图计算。很多人在 LangChain 里写了大量 Try-Except、If-Else、循环和状态堆栈最后发现代码越来越乱。这不能完全怪业务复杂更多是因为工具本身选错了抽象方式。LangGraph 回答的正是这个问题当流程开始分叉、需要根据中间结果动态决定下一步、甚至要支持循环和子流程时我们应该用什么结构来承载这些逻辑这篇文章我会从 LangGraph 和 LangChain 的差异讲起再拆解 State、Node、Edge、Conditional Edge 这些核心概念最后用一套更接近工程的路径帮你避开新手最容易踩的坑。1. 先搞明白LangGraph 和 LangChain 到底差在哪1.1 大多数人的第一反应是“升级版”其实不一样我第一次看到 LangGraph 时第一反应是这可能是 LangChain 的加强版用更高级的方式写 Chain。实际用下来发现它根本不是 LangChain 的简单升级而是一次抽象层级的切换。LangChain 的 Chain 模式本质是把一步调模型、一步调工具、一步做解析按顺序串成一条固定管道。管道一旦定义好执行路径就是确定的A 节点执行完B 节点执行B 节点做完C 节点执行。如果你想在中间加一个判断比如“如果用户输入包含某个关键词先走工具调用否则直接回复”你通常得在 Chain 内部写一个自定义函数或者用分支 Chain 手动拼接。这种分支逻辑一旦多起来整条链会变得非常难看因为每个 Chain 都只能表达线性关系。LangGraph 换了一种抽象它把整个流程描述成一张图。节点是函数边是节点之间的连接关系。你可以用条件路由也就是由函数来决定下一步去哪个节点你可以加循环让流程在某个节点之间反复迭代直到满足条件你还可以把一个小流程作为子图嵌套进大图里。表面上看LangGraph 也能实现 LangChain 的效果但底层模型不一样。LangChain 更像一条流水线每个工位固定LangGraph 更像一张地图每一步走到哪里由当前状态和路由规则决定。这个差别直接决定了你能不能在复杂 Agent 工作流里持续维护代码。1.2 链式调用的天花板正是图模型的起点我用 LangChain 写过客服 Agent一开始还算顺利用户输入进来调用 LLMLLM 决定是否调用工具然后解析结果最后返回答案。这个流程用 Chain 写起来很简单。但第一个麻烦来了如果 LLM 决定要调用多个工具呢比如“帮我查一下北京和上海的天气然后对比一下”。理想流程是先并行或顺序调两次天气工具再汇总结果。用 Chain 写要么把多个工具塞进一个工具节点要么写一个复杂的 AgentExecutor。如果中间某次工具调用失败还需要重试那 Chain 的线性结构就很难优雅表达。第二个麻烦是状态共享。多轮对话里下一次调用需要知道之前已经拿到了哪些信息。在 Chain 模式里你通常要往外部的 context 字典里塞东西每个节点自己去读取和写入。时间久了你根本不知道哪个节点改过这个字段。LangGraph 对这两个问题的处理方式就很直接状态是显式的图状态任何节点都能往 state 里写数据但必须返回一个字典让状态变更可以被追踪路由是条件函数下一步执行哪个节点完全由当前 state 决定工具调用失败可以走回头路回到某个节点重试或改写策略。这种结构带来的不是某个具体函数更高效而是整个流程的可控性上了一个台阶。所以对于固定流程、单次调用、简单工具链直接用 LangChain Chain 就够了没必要上 LangGraph。但如果你的流程里有分支、循环、状态共享、子流程和多智能体协作LangGraph 的图模型会是更合适的基础设施。2. 核心概念拆解State、Node、Edge、Conditional Edge2.1 状态、节点、边一切用代码理解LangGraph 有四个基础概念名字很直观但细节上要注意。State状态它是一个可共享的数据结构通常用类型定义描述比如 TypedDict。所有节点都接收当前 State 作为输入返回更新后的字段。Node节点一个普通函数以 State 为入参返回字典。返回的字典会更新 State 中对应的字段。Edge边从一个节点到另一个节点的有向连接。可以有普通边表示固定执行顺序也可以有条件边表示根据函数结果动态选择目标。Conditional Edge条件边一条由路由函数决定去向的边。路由函数接收 State返回一个字符串或节点名LangGraph 根据返回值选择下一步节点。先用最小代码把状态更新这个事跑通。下面是一个常见写法具体 API 可能会随版本微调但整体结构是稳定的。from typing import TypedDict from langgraph.graph import StateGraph, START, END class GraphState(TypedDict): question: str answer: str steps: int def node_start(state: GraphState): # 节点函数返回一个字典LangGraph 会把它合并到当前 State return {steps: state[steps] 1, question: state[question].strip()} def node_generate(state: GraphState): # 实际项目中这里会调用 LLM这里用拼接字符串做演示 answer f我对「{state[question]}」的模拟回答 return {answer: answer, steps: state[steps] 1} graph StateGraph(GraphState) graph.add_node(start, node_start) graph.add_node(generate, node_generate) graph.add_edge(START, start) graph.add_edge(start, generate) graph.add_edge(generate, END) app graph.compile() result app.invoke({question: LangGraph 是什么, answer: , steps: 0}) print(result)这个图只有一条直线start 节点先执行再执行 generate最后结束。执行结果里steps会变成 2answer会被写入模拟回答。这里最需要理解的是节点函数的返回方式不是直接修改传入的 State 对象而是返回一个字典LangGraph 负责做合并。这意味着你可以在任意节点里只返回希望更新的字段其他字段不动。这种设计让状态变更变得可追踪但也会带来一个隐藏问题如果你忘记返回某个字段它在后续节点里可能会缺失或保持旧值。2.2 改变 State 时的一个重要细节默认覆盖和自定义合并策略默认情况下State 里的一个字段如果被节点返回就会整体覆盖。比如question字段原来是字符串节点返回新的字符串就会被替换。这在很多场景下没有问题但如果是消息列表你可能想不断追加而不是覆盖。LangGraph 提供了一个更灵活的机制用Annotated和 reducer 函数来定义字段的更新方式。最常见的例子是消息列表from typing import Annotated, TypedDict from langgraph.graph.message import add_messages class AgentState(TypedDict): # 表示 messages 字段的更新方式是调用 add_messages 来合并 messages: Annotated[list, add_messages] step: int这样每个节点返回的messages列表会被追加到上一轮的messages后面而不是直接覆盖。这个机制应对多轮对话非常有用。我在实际踩坑中经常看到的是新手在节点里返回messages: new_message结果下一轮对话列表变短之前的上下文丢了。原因不是 LangGraph 删数据而是默认覆盖策略把它覆盖了。所以只要涉及列表、数组、历史记录这类需要累积更新的字段就要主动定义 reducer。2.3 条件路由让流程自己决定下一步条件路由是 LangGraph 最有价值的能力之一。它的核心逻辑非常简单写一个路由函数函数根据当前 State 返回目标节点名LangGraph 就会把执行权交给那个节点。举一个意图判断的例子用户输入进来先判断意图如果是“查天气”就走天气节点如果是“闲聊”就走回复节点。from typing import Literal from langgraph.graph import StateGraph, START, END class IntentState(TypedDict): query: str intent: str result: str def analyze_intent(state: IntentState): # 这里简化成关键词判断实际项目可以调用 LLM if 天气 in state[query]: intent weather else: intent chat return {intent: intent} def weather_node(state: IntentState): return {result: 今天是晴天适合出门} def chat_node(state: IntentState): return {result: 哈哈这个话题很有意思} def route_after_intent(state: IntentState) - Literal[weather_node, chat_node, END]: if state[intent] weather: return weather_node elif state[intent] chat: return chat_node return END graph StateGraph(IntentState) graph.add_node(analyze_intent, analyze_intent) graph.add_node(weather_node, weather_node) graph.add_node(chat_node, chat_node) graph.add_edge(START, analyze_intent) graph.add_conditional_edges(analyze_intent, route_after_intent) graph.add_edge(weather_node, END) graph.add_edge(chat_node, END) app graph.compile() result app.invoke({query: 北京天气怎么样, intent: , result: }) print(result[result])这里的关键是graph.add_conditional_edges(analyze_intent, route_after_intent)它表示从analyze_intent节点出去时先执行route_after_intent路由函数再根据返回值决定下一步。条件路由本质上就是一张表路由函数返回什么图就走哪条边。这个设计看起来简单但它解决了一个很核心的问题LLM 应用里你经常需要根据模型输出、工具结果、用户反馈来动态决定下一步。在 Chain 模式里这是很难写优雅的在 LangGraph 里就是一个普通函数。3. 进阶实战分支控制、循环检测、子图和并行分支3.1 条件路由再往前一步不要让节点之间互相裸跳条件路由用多了以后很多人会忍不住把所有判断逻辑塞进一个路由函数里。比如先判断意图再判断用户情绪再判断当前轮次然后根据四个维度组合出下一步。这会让路由函数变成一个巨型 if-else维护性反而下降。我的建议是不要让一个路由函数承担太多职责可以把判断拆成多个阶段节点。比如先判断意图写入 state再判断是否需要工具调用写入 state最后用一个路由函数读取这两个字段决定下一步。这种方式看起来多走了一步但每个节点职责单一日志也容易定位。LangGraph 的优势在这里体现得很明显你可以像画流程图一样把复杂的多条件判断拆成多个小节点再用条件边串联。这样每个节点都能独立测试。3.2 循环检测不是无限循环而是设计出口LangGraph 支持循环这是很多实际场景的刚需。比如一个 Agent 要反复调用工具直到拿到信息够为止或者一个“反思”流程先生成答案再检查不满意就重新生成。循环如果控制不好就是灾难。LangGraph 本身提供了几个保护机制。最直接的一个是recursion_limit它限制了图在一次执行中最多执行多少步。如果在invoke时不设置默认值通常很低但具体情况要看版本。更稳妥的做法是主动设置一个相对保守的上限。result app.invoke( input_state, config{recursion_limit: 50} )当执行步数超过限制时LangGraph 会抛异常提示你可能的循环问题。这样至少不会让程序一直跑下去。但真正避免死循环不是靠限制步数而是要在路由函数里设计好退出条件。比如你写一个“反思优化”循环就必须在某个节点里检查是否已经迭代了 N 次、结果是否满足分数阈值。这通常需要你维护一个iterations字段每次循环加一然后在路由函数里判断def route_after_reflect(state): if state[iterations] 2 or state[score] 90: return final_node return revise_node使用循环时我建议始终遵循一个小原则每个循环都至少在两个地方设置出口。一个在路由函数里根据状态判断是否继续一个在外部执行参数里用recursion_limit做兜底。这样即使路由函数写漏了程序也不会失控。3.3 子图Subgraph把复杂流程打包成可复用节点当图的复杂度增加以后你会发现整张图密密麻麻看不清一个业务边界。LangGraph 提供子图能力你可以把一个子流程定义成一个独立的图然后把它作为一个节点挂到父图中。举个例子在一个客服 Agent 里有一个“多轮对话”子流程这个子流程内部有意图识别、槽位填充、条件路由非常长。如果直接平铺在总图里可读性很差。这时可以先把“多轮对话”做成一个子图dialogue_subgraph StateGraph(DialogueState) # 添加子图内部节点和边 dialogue_subgraph.add_node(...) dialogue_subgraph.add_node(...) dialogue_subgraph.add_edge(...) subgraph_app dialogue_subgraph.compile()然后在父图里parent_graph.add_node(dialogue, subgraph_app)子图编译后变成一个可调用对象它接收父图的 State 输入返回更新后的 State。子图内部的状态字段如果不冲突可以直接沿用父图字段如果需要独立也可以定义单独的状态结构然后在接口处做字段映射。子图的核心价值不是缩短代码而是让整个流程的结构变得可管理。你可以把“意图判断”“工具调用”“反思优化”“用户交互”分别封装成子图在总图里只保留主干连接。这对团队协作很有帮助不同人负责不同子图互不干扰。3.4 并行分支不是所有流程都要并行但用对地方很高效LangGraph 里从一个节点出发如果同时加多条普通边那么这些边指向的节点会在同一轮中并行执行。这种模式适合做“Map-Reduce”多个节点针对同一份输入做独立计算然后汇聚到一个汇总节点。比较简单的做法是先有一个拆分节点然后从拆分节点分别连到几个处理节点最后这些处理节点再汇入同一个汇总节点。比如“先让三个不同角色分别起草方案再汇总成一个方案”。如果分支数量是动态的也就是你不确定到底要并几个任务那么可以用 LangGraph 的SendAPI。它允许你根据当前 State 动态生成多个并行任务。不过这个概念相对进阶新手可以先只掌握静态并行分支也就是固定数量、固定节点的并行。并行分支虽然能提高吞吐但也会带来状态合并问题。多个并行节点同时修改同一个 state 字段最终合并结果可能不符合预期。我在实践中会尽量让并行节点只更新各自独立的字段最后在汇总节点统一定义合并逻辑。贸然让多个节点写同一个字段会产生竞态和不稳定结果。4. 从跑通到落地调试、排查和工程化建议4.1 新手最容易踩的五个坑在我看了大量 LangGraph 相关提问后发现真正容易卡住新人的不是复杂的并行或子图而是下面这几个基础问题。节点返回空字典节点函数如果返回{}LangGraph 会认为这个节点不更新任何状态。如果后续节点依赖某些字段就会拿不到新值。更危险的是如果你以为某个字段被更新了但其实没有排查起来很费劲。条件路由返回的字符串不在映射表里add_conditional_edges如果不传第二个参数映射字典LangGraph 会默认按字符串节点名去查找。如果你路由函数返回的字符串对应的节点不存在执行会直接报错。图没有设置 END 出口如果某些分支永远指向另一个节点但某个路由分支写错了导致它返回一个不存在的节点名或者直接没有返回 END就可能陷入死循环。虽然recursion_limit会兜底但用户体感很差。列表字段被意外覆盖前面讲过如果没有用 reducer 函数返回列表会整体覆盖历史记录容易丢。版本差异导致 API 不同LangGraph 的 API 一直处于迭代中。不同版本的StateGraph、add_conditional_edges、START/END写法可能有差异。网上很多教程用的还是旧版照抄很难跑通。4.2 一套可复现的排查链路遇到 LangGraph 运行问题我通常按下面的顺序排查比一头扎进代码里要高效得多。先看报错现象是“KeyError”还是“Node not found”还是“Recursion limit reached”不同现象指向不同层。再看输入 State确保调用invoke时传入的初始字典里包含了所有节点会读取的字段。最好提前打印初始 State。检查每个节点的返回值在每个节点函数入口和出口加一行print或日志确认它拿到了什么返回了什么。检查路由函数确认路由函数能访问正确的 state 字段返回的节点名一定是图里存在的节点或END。检查图编译状态graph.compile()是否成功添加边时是否引用了不存在的节点。最后检查版本如果代码本来跑得好好的但是换了一个环境就出问题优先查langgraph和langchain的版本依赖。这个排查顺序并不是官方的但它是工程上更稳妥的路径先定位是哪一层出了问题再决定修哪里而不是直接怀疑语言模型或工具调用。4.3 真正要长期使用还需要补哪些工程化能力如果你只是做一次 Demo 或学习验证把图编译完能跑通就够了。但如果你要把 LangGraph 放进一个实际产品我建议至少补上这几块拼图。可观测性每个节点执行前和执行后都记录日志至少包括输入摘要、输出摘要、耗时、是否进入异常分支。这样线上排查时你才能知道 Agent 卡在哪一步。重试与超时节点里如果有外部 API 调用要考虑超时、重试、降级。LangGraph 本身主要负责流程编排不会替你处理网络中断或模型限流。状态持久化多轮对话场景里你可以把 State 保存到 Redis 或数据库这样用户下次进来还能继续上下文。LangGraph 有 checkpointer 机制但实践时还是要结合自己的存储方案。结构化输出不要把 LLM 的输出直接塞进 State最好先让模型输出结构化 JSON或者至少经过一个解析校验节点再更新状态。这样能减少很多奇怪的运行时错误。测试与回归Graph 是由多个函数组成的每个节点可以单独单元测试整张图可以准备几条典型输入做集成测试。如果以后改了某个节点至少能保证原有流程不炸。4.4 什么时候不该用 LangGraph这个话题很少被认真聊。LangGraph 很强大但它不是万能的。如果你的业务流程很简单比如“用户输入 - 调一次模型 - 返回结果”那用 LangGraph 引入 State、Node、Edge、路由函数反而会增加心智负担和代码量。这种场景直接用 LangChain Chain 甚至直接调用模型 SDK 都更轻量。如果你的流程虽然复杂但基本是固定的多步骤流水线且不会有太多分支和循环那 LangChain Chain 或者函数式脚本也能胜任。LangGraph 真正的优势是流程本身带有动态决策、循环、资源共享和子流程嵌套。换句话说你的流程越像一张“图”越适合 LangGraph越像一条“直线”越不值得为它引入一套图执行引擎。给一个实用的判断标准先画流程图。如果你画的流程图里到处都是菱形判断框、循环回环、分支合并那就大胆用 LangGraph。如果你画出来的图基本上是一连串矩形框没有回头路那你更需要的是一个普通的管道。5. 一套可以复用的学习路径5.1 先跑通最小图再迭代替换学习 LangGraph 时我见过最典型的错误是一上来就想做一个包含 20 个节点的多智能体系统最后被各种 API 细节和状态问题淹没。建议的顺序是先写一个只有两个节点、一条边的图跑通compile和invoke。加一个条件路由让流程能根据输入选择不同分支。加一个循环设计一个最多跑三圈的迭代流程并设置recursion_limit。加一个子图把其中两个节点打包成一个子图挂到父图里。再加并行分支处理一个“同时算几个结果再汇总”的场景。每一步都独立验证再叠加下一个功能。不要一开始就探索全部能力。这个路径的意义不只是学 API而是让大脑逐步适应“图状态机”的思考方式。5.2 状态设计是所有设计的核心LangGraph 里节点的函数逻辑一般都比较简单难点在于 State 设计。State 就是工作流里的共享内存。你要提前想清楚哪些字段会被多个节点读取哪些字段只需要在一个子图内部使用哪些字段需要累积追加哪些字段要覆盖我的经验是把 State 里的字段分成三类输入字段用户问题、外部参数通常在流程启动时注入。中间字段意图、工具结果、候选答案只在一个阶段内使用尽量不跨子图。输出字段最终回答、统计信息、状态码流程结束后需要对外暴露。在定义 State 类型时就按这三类分好。中间字段尽量少因为字段越多状态合并出错的可能性越大。子图之间只暴露必须传递的字段其他内部字段留在子图里。5.3 用“最小可用图”思维替代“完整功能图”思维很多刚接触 LangGraph 的人会倾向于把功能全部画进一张图觉得这样才是“完整”。但工程上更好的是先做一个最小可用图只包含能跑通主流程的节点和边然后根据实际业务逐步加分支、循环、异常处理。这种思维和写代码是一样的你不是先写一个庞大系统再调试而是先写一个最小核心流程再迭代扩展。LangGraph 的图结构让这种增量迭代特别方便你可以先只做“意图判断 - 天气查询 - 返回结果”跑通以后再加“咨询量太大时转人工”“多次查询失败时换重写”每次只加一个小分支单独验证。长此以往整个流程会变得稳定又可扩展。站在更长的时间轴上看 LangGraph如果把 LangGraph 放进 AI 应用开发的演进坐标系里它真正改变的不是某个 API 的用法而是让我们可以用工程化的方式管理 Agent 的“不确定性”。LLM 的输出天然不可控但状态、路由、循环、子图这些结构是可控的。LangGraph 的价值就是在这两种东西之间建立一层稳定的桥。所以与其说 LangGraph 是一个工具库不如说它是一套“AI 工作流工程化”的思维工具。你越早理解 State 和 Conditional Edge 背后的设计意图就越不会在复杂 Agent 项目里陷入“长了又拆、拆了又长”的泥潭。我建议你从今天开始不要只收藏教程而是把最小的两节点流程跑一遍。只有当你亲手看到 State 被一个节点更新、又被另一个节点读取再被条件路由决定走向时LangGraph 和普通 Chain 的区别才会真正变成你自己的判断。这一步跑通之后后面的子图、并行、持久化都只是同一套逻辑的自然延伸。