1. 从「链」到「图」到底在说什么
第一次接触 LangChain 的人,脑子里冒出来的问题往往不是“这玩意儿怎么用”,而是“它到底是个啥”。官方文档翻了三页,看到 Chain、Agent、Tool、Memory、Retriever 一堆名词砸过来,很容易直接劝退。我当初也是这么过来的,后来用久了才慢慢摸清楚:LangChain 本质上就是一套帮你把大模型调用、外部数据接入、工具调用、上下文管理这些零散环节串起来的开发框架。它的核心抽象叫“链”,也就是 Chain,把若干个处理步骤按顺序连起来,前一步的输出喂给后一步,像流水线一样跑完整个流程。
那 LangGraph 又是什么?简单说,它是 LangChain 团队后来推出的一个专门用来构建“有状态、多步骤、可循环”的智能体工作流的库。它把整个流程建模成一张图,节点是具体的处理逻辑,边是节点之间的跳转关系。你可以把它理解成从“一条直线走到底”升级成了“一张可以来回走、可以分叉、可以汇合的路网”。
这两个东西经常被放在一起讨论,很多人搞不清楚它们是什么关系,是不是要二选一,是不是 LangGraph 要取代 LangChain。我写这篇东西的目的,就是把我自己从踩坑到跑通的过程中理解到的这些东西,尽可能用大白话讲清楚。不管你是刚听说这两个名字的新手,还是已经用 LangChain 写过几个 Demo 但遇到瓶颈想换思路的开发者,应该都能从里面找到对自己有用的部分。
核心关键词我先摆出来:LangChain、LangGraph、链式编排、图结构工作流、有状态智能体、循环与分支、多智能体协作。后面所有内容都围绕这些展开,不会跑偏。
2. 先搞清楚 LangChain 的「链」是怎么一回事
2.1 链式编排的直觉理解
你可以把 LangChain 的“链”想象成工厂里的流水线。原材料从一头进去,经过第一道工序加工,半成品传给第二道工序,再加工,再传,最后从另一头出来成品。每一道工序就是一个处理单元,在 LangChain 里对应的是各种组件:Prompt 模板、大模型调用、输出解析器、外部检索器等等。
举个最典型的例子。你要做一个“根据用户问题查资料然后回答”的功能,用 LangChain 的链式写法大概是这样:先有一个 Prompt 模板把用户问题和检索到的文档拼成一段完整的提示词,然后把这个提示词送给大模型,大模型返回结果后再经过一个输出解析器把文本整理成结构化格式。这三步串起来就是一条链。
这种写法最大的好处是逻辑清晰、调试方便。每一步的输入输出都是确定的,你可以在中间任何位置打日志、做检查。对于流程固定的任务,比如文档问答、文本摘要、简单分类,链式编排完全够用,而且写起来很快。
2.2 链式结构的天然局限
但用着用着就会碰到天花板。最典型的问题是:链只能往前走,不能回头。一旦你把流程定义成 A 到 B 到 C,它就只会按这个顺序执行。可现实中很多任务不是这样的。
比如你让大模型帮你写一段代码,它写完之后你需要判断这段代码能不能跑通。如果能跑通就结束,如果跑不通就需要把错误信息反馈给模型让它重新写。这个“判断-反馈-重写”的过程是一个循环,链式结构表达起来就非常别扭。你当然可以用代码写一个 while 循环包在外面,但这样一来 LangChain 本身的编排能力就用不上了,整个逻辑又散落在 Python 代码里。
再比如多智能体协作的场景。一个智能体负责分析问题,一个负责搜索资料,一个负责撰写答案,还有一个负责审核。审核不通过要打回去让撰写的智能体改。这种互相调用、条件跳转的关系,用链来建模会非常痛苦。
还有一个问题是状态管理。链式结构里,每一步之间传递的数据是临时的,前一步的输出直接变成后一步的输入。但如果你需要在多个步骤之间共享一些全局信息,比如对话历史、用户偏好、中间结论,链式结构没有一个统一的地方来存放这些东西。你只能手动在每一步的输入输出里带上,很容易漏掉或者搞乱。
我自己的经验是:当你发现自己在 LangChain 的链外面套了好几层 if-else 和 while 循环的时候,就该考虑换 LangGraph 了。
3. LangGraph 的「图」带来了什么新东西
3.1 图结构的基本概念
LangGraph 把工作流建模成一张有向图。图里面有节点和边。节点代表一个具体的处理步骤,比如调用一次大模型、执行一次检索、运行一段代码。边代表节点之间的跳转关系,分为普通边和条件边。普通边就是 A 执行完直接去 B,条件边是根据某个判断结果决定下一步去 B 还是去 C。
这个模型听起来很简单,但它带来的表达能力提升是巨大的。因为图里面可以有环,也就是可以从一个节点出发经过若干步之后又回到这个节点。这就天然支持了循环。同时条件边支持分支,你可以根据运行时的状态动态决定走哪条路。
还有一个关键概念叫状态。LangGraph 里整个图共享一个状态对象,每个节点都可以读取这个状态、修改这个状态。状态可以包含消息列表、中间结果、计数器、标志位等等。这就解决了链式结构里状态管理混乱的问题。
3.2 循环、分支与状态:三个核心能力
我把 LangGraph 相比 LangChain 链式编排的核心增强总结为三个词:循环、分支、状态。
循环让“反复尝试直到成功”这类逻辑变得自然。你只需要把“执行任务”和“检查结果”分别做成两个节点,然后用条件边把它们连起来:检查通过就去结束节点,检查不通过就回到执行节点。整个循环在图的框架内表达,不需要在外面套 Python 的 while。
分支让“根据不同情况走不同路径”变得清晰。比如一个客服机器人,用户问的是技术问题就走技术支持的流程,问的是账单问题就走账单处理的流程。你可以在入口节点后面接一个分类节点,然后根据分类结果用条件边跳到不同的处理链路。
状态让“跨步骤共享信息”变得简单。所有节点读写同一个状态对象,你不需要在每一步之间手动传递数据。对话历史、用户信息、中间计算结果,统统放在状态里,谁需要谁去读,谁产生谁去写。
这三个能力加在一起,就让 LangGraph 能够表达远比链式结构复杂的工作流。多智能体协作、带人工审核的流程、需要反复迭代优化的任务,这些用 LangGraph 来做会顺手很多。
3.3 两者的关系不是替代而是互补
很多人会问:有了 LangGraph 是不是就不需要 LangChain 了?我的理解是:不是替代,是互补。
LangChain 提供了大量的基础组件:各种大模型的接口封装、Prompt 模板、输出解析器、文档加载器、向量存储集成、检索器等等。这些东西在 LangGraph 里照样可以用。你在 LangGraph 的节点函数里面,完全可以调用 LangChain 的组件来完成具体的工作。
LangGraph 负责的是编排层,也就是决定“先做什么、后做什么、什么条件下走哪条路、什么时候循环、什么时候结束”。LangChain 的链和组件负责的是执行层,也就是“具体每一步怎么调用模型、怎么处理文本、怎么检索文档”。
打个比方:LangChain 像是一套工具箱,里面有锤子、螺丝刀、扳手。LangGraph 像是一张装配图纸,告诉你先用哪个工具、再用哪个工具、哪个地方需要反复拧直到拧紧。你可以只用工具箱不用图纸,做简单的东西没问题。但要做复杂的东西,图纸就很重要了。
4. 从链到图的迁移:什么时候该换、怎么换
4.1 判断是否需要 LangGraph 的几个信号
不是所有项目都需要上 LangGraph。如果你的任务就是一条直线走到底,中间没有循环、没有复杂分支、不需要跨步骤共享大量状态,那用 LangChain 的链式写法完全没问题,而且更简单。
但如果你发现自己遇到了下面这几种情况,就该认真考虑迁移到 LangGraph 了。
第一种情况是需要循环迭代。比如让模型生成内容然后自我检查、不满意就重新生成,或者让模型调用工具、根据工具返回结果决定是否继续调用。这种“执行-判断-再执行”的模式用链来表达非常别扭。
第二种情况是需要多步条件分支。不是简单的 if-else,而是多个步骤之间互相跳转,不同条件下走完全不同的路径,路径之间还可能汇合。这种复杂度用链来管理会变得难以维护。
第三种情况是需要多智能体协作。多个智能体各自有各自的职责,它们之间需要传递消息、协调行动、互相审核。这种场景天然适合用图来建模,每个智能体是一个节点或者一组节点。
第四种情况是需要人工介入。比如某些关键步骤需要人工审核确认后才能继续,或者需要人工在多个选项中选择一个。LangGraph 支持在图的执行过程中暂停等待外部输入,这个能力在链式结构里很难实现。
4.2 迁移的基本思路和步骤
迁移不是把原来的代码全部推翻重写,而是把原来的链拆解成节点,然后用图的方式重新组织。
第一步是识别状态。想清楚整个流程中哪些数据是需要跨步骤共享的。对话历史、用户输入、中间结果、最终输出,把这些整理成一个状态对象的结构。在 LangGraph 里通常用一个 TypedDict 或者 Pydantic 模型来定义状态的结构。
第二步是拆分节点。把原来链里面的每一步拆成一个独立的节点函数。每个节点函数接收当前状态,执行自己的逻辑,然后返回对状态的更新。拆分的粒度可以根据需要调整,一个节点可以只做一件事,也可以做几件事,关键是逻辑上要内聚。
第三步是定义边和条件。确定节点之间的跳转关系。哪些是固定顺序的用普通边,哪些需要根据条件判断的用条件边。如果有循环,确定循环的入口和出口条件。
第四步是编译和测试。LangGraph 的图定义好之后需要编译成一个可执行的对象,然后就可以像调用普通函数一样调用它。测试的时候可以逐步检查每个节点的输入输出,确认状态流转符合预期。
我自己的做法是先用 LangGraph 画一个最简单的版本跑通主流程,确认状态传递和跳转逻辑没问题,然后再逐步把原来链里面的复杂逻辑迁移过来。不要一上来就追求一步到位,容易调试到崩溃。
4.3 一个具体的迁移示例
假设原来有一个 LangChain 的链,功能是:接收用户问题,检索相关文档,让模型基于文档生成回答,然后检查回答是否包含引用来源,如果没有就重新生成。
用链式写法,你大概会写一个函数,里面先调用检索链,再调用生成链,然后检查结果,如果不符合要求就再调用一次生成链。这个“再调用一次”的逻辑是用 Python 的 if 写在链外面的。
迁移到 LangGraph 之后,你会定义这样几个节点:一个检索节点、一个生成节点、一个检查节点。状态里面包含用户问题、检索到的文档、生成的回答、检查结果。边的关系是:检索节点到生成节点,生成节点到检查节点,检查节点根据结果决定是去结束节点还是回到生成节点。
这样整个循环就在图里面表达出来了,不需要在外面写 if。而且状态是显式管理的,每一步读了什么写了什么一目了然。调试的时候也可以很方便地在检查节点打日志,看每次生成的结果和检查结果。
5. 实操中的关键细节与避坑经验
5.1 状态设计是最容易出问题的地方
状态设计得好不好,直接决定了后面写起来顺不顺。我踩过的坑包括:状态字段太多导致每个节点都要处理一大堆不相关的数据;状态字段太少导致节点之间需要额外传递参数;状态更新逻辑不清晰导致某个节点意外覆盖了其他节点的修改。
我的经验是:状态字段按功能分组,每个节点只读写自己关心的字段。比如把状态分成“输入相关”“中间结果”“输出相关”几组。节点函数在返回状态更新的时候,只返回自己修改的字段,不要整个状态都返回。LangGraph 支持部分更新,你返回一个字典包含要更新的键值对就行,没返回的字段保持不变。
还有一个细节是状态的合并策略。如果多个节点可能同时修改同一个字段,你需要定义合并逻辑。比如消息列表通常是追加而不是覆盖,那就需要指定一个 reducer 函数来处理。这个在定义状态结构的时候就要想清楚,不然后面会出现数据丢失或者覆盖的问题。
5.2 条件边的判断逻辑要尽量简单
条件边是 LangGraph 里非常强大的一个能力,但也很容易用过头。我见过有人在条件边的判断函数里写了非常复杂的逻辑,调用了外部服务、做了大量计算,结果整个图的执行变得很慢而且很难调试。
我的建议是:条件边的判断函数尽量只做简单的状态检查。比如检查某个标志位是否为真、检查某个计数器是否超过阈值、检查某个字段是否为空。复杂的判断逻辑应该放在前面的节点里面完成,把判断结果写进状态,条件边只负责读这个结果然后决定跳转方向。
这样做的好处是判断逻辑和跳转逻辑分离,调试的时候可以清楚地看到是判断出了问题还是跳转出了问题。而且判断逻辑放在节点里可以方便地打日志、做测试。
5.3 循环要有明确的退出条件
LangGraph 支持循环,但循环最怕的就是死循环。如果你的图里面有一个环,一定要确保存在至少一个退出条件,而且这个条件在正常情况下能够被满足。
常见的做法是设置一个最大迭代次数。在状态里放一个计数器,每次进入循环节点就加一,条件边检查计数器是否超过阈值,超过就强制退出。这样可以防止因为判断逻辑的 bug 导致无限循环。
另一个做法是设置超时。虽然 LangGraph 本身没有内置的超时机制,但你可以在节点函数里面检查执行时间,超过一定时间就主动抛出异常或者设置退出标志。
我自己的习惯是:任何带循环的图,第一版一定加上最大迭代次数限制,哪怕逻辑上觉得不可能死循环也要加。因为调试过程中改来改去,很容易不小心引入死循环的条件。
5.4 调试和可观测性怎么做好
LangGraph 的图执行过程相比链式结构更复杂,因为路径不是固定的,可能走不同的分支、可能循环多次。这就给调试带来了更大的挑战。
我的做法是在每个节点函数的入口和出口都打日志,记录当前状态的关键字段。这样当流程走偏的时候,可以通过日志回溯是哪个节点出了问题、当时的状态是什么。
另外 LangGraph 支持流式输出中间状态,你可以利用这个能力实时观察图的执行过程。对于复杂的图,我建议先用一个小规模的测试用例跑一遍,确认路径正确之后再换真实数据。
还有一个技巧是把图可视化出来。LangGraph 提供了生成图结构图的能力,你可以把节点和边画出来,直观地检查结构是否符合预期。特别是条件边比较多的时候,画出来看比在代码里读要清楚得多。
6. 常见问题速查与排查思路
6.1 节点执行顺序不符合预期
这是最常见的问题。你定义了 A 到 B 到 C 的顺序,但实际执行的时候发现 B 在 A 之前就跑了,或者 C 跑了两次。
排查思路:首先检查边的定义是否正确。LangGraph 里面边的方向很重要,A 到 B 的边和 B 到 A 的边是完全不同的。其次检查是否有多个边指向同一个节点,导致该节点被多次触发。最后检查条件边的判断函数是否返回了预期的结果,有时候判断函数里的逻辑写错了会导致走错分支。
还有一个容易忽略的点是节点的入口条件。LangGraph 里面如果一个节点有多条入边,它会在所有入边都完成之后才执行。如果你希望某个节点在任意一条入边完成之后就执行,需要特别配置。这个默认行为经常导致执行顺序和预期不一致。
6.2 状态更新丢失或覆盖
你明明在节点 A 里修改了某个状态字段,到了节点 B 里发现这个字段还是旧值。或者节点 A 和节点 B 都修改了同一个字段,结果只有其中一个的修改生效了。
排查思路:首先确认节点函数返回的字典里面包含了你要更新的字段。LangGraph 只更新你返回的字段,没返回的字段保持不变。如果你在节点 A 里修改了字段 X 但返回的时候忘了带上 X,那这个修改就不会生效。
其次检查是否有多个节点同时修改同一个字段。如果有,需要确认合并策略是否正确。对于列表类型的字段,通常需要指定追加而不是覆盖。对于字典类型的字段,可能需要指定合并而不是替换。
还有一个坑是状态的可变性。如果你在节点函数里直接修改了状态对象的某个可变字段(比如往列表里 append 了一个元素),但没有返回这个字段的更新,LangGraph 可能检测不到变化。建议总是返回新的值而不是原地修改。
6.3 循环无法退出
图进入了一个环之后一直转,永远不结束。
排查思路:首先检查退出条件的判断逻辑是否正确。在条件边的判断函数里打日志,看每次判断的结果是什么。如果判断结果一直是“继续循环”,那说明退出条件没有被满足。
然后检查退出条件依赖的状态字段是否被正确更新了。比如你依赖一个计数器来判断是否退出,但计数器在某个节点里没有被正确加一,那它就永远不会达到阈值。
最后检查是否有多个循环嵌套或者互相触发。有时候一个循环的退出条件依赖于另一个循环的结果,如果另一个循环出了问题,这个循环也退不出来。
6.4 性能问题:图跑得太慢
图的结构没问题,逻辑也正确,但执行起来很慢。
排查思路:首先确认是否有不必要的节点执行。比如某个节点在每次循环里都重新做一次耗时的检索,但实际上检索结果可以缓存起来复用。把不必要重复执行的操作移到循环外面,或者用状态缓存结果。
其次检查是否有可以并行执行的节点。LangGraph 支持并行执行没有依赖关系的节点,如果你的图里面有两个节点互不依赖,可以让它们并行跑,节省时间。
最后检查是否有节点在做不必要的计算。有时候节点函数里包含了一些调试用的日志或者检查逻辑,在生产环境可以去掉。还有一些节点可能调用了外部服务但结果没有被使用,这些调用可以省掉。
| 常见问题 | 可能原因 | 排查方法 |
|---|---|---|
| 节点执行顺序不对 | 边定义错误、多入边触发时机不对 | 检查边的方向、检查节点触发配置 |
| 状态更新丢失 | 返回字段遗漏、合并策略错误、原地修改 | 检查节点返回值、检查 reducer 配置 |
| 循环无法退出 | 退出条件逻辑错误、依赖字段未更新 | 打日志检查判断结果、检查字段更新 |
| 执行速度慢 | 重复计算、未并行、无用调用 | 缓存结果、并行化、去掉无用逻辑 |
7. 我个人的一些实践体会
用 LangGraph 做项目有一段时间了,最大的感受是:它把复杂工作流的复杂度从代码层面转移到了结构层面。以前用链式写法,复杂的控制逻辑都散落在 Python 代码的 if-else 和 while 里面,读代码的时候要在脑子里模拟执行流程。现在用图的方式,结构是显式的,节点和边画出来一目了然,读图比读代码快得多。
另一个体会是状态设计值得花时间。我一开始图省事,状态字段随便定义,结果写到后面发现各种不够用、各种冲突。后来重新设计了一版状态结构,把字段按功能分组、明确每个字段的读写者、定义好合并策略,后面写起来就顺畅多了。这个前期投入是值得的。
还有一点是不要为了用图而用图。我见过一些项目,明明就是一个简单的线性流程,非要用 LangGraph 来搭,结果增加了很多不必要的复杂度。工具是拿来解决问题的,不是拿来炫技的。简单的任务用简单的方案,复杂到一定程度再上 LangGraph,这个判断很重要。
最后分享一个小技巧:LangGraph 的图定义和节点函数可以分开放在不同的文件里。图定义文件只负责结构,节点函数文件只负责逻辑。这样修改逻辑的时候不会影响到结构,修改结构的时候也不会碰到逻辑代码。对于稍微大一点的项目,这个组织方式会让代码清晰很多。