恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
LangChain 与 LangGraph:从链式调用到有状态编排的演进
首页
资讯中心
/
LangChain 与 LangGraph:从链式调用到有状态编排的演进
LangChain 与 LangGraph:从链式调用到有状态编排的演进
发布时间:2026/10/18 21:27:45
我做了两周的模型 API 调用写出了各种“一问一答”的脚本但真正要交付一个能用的应用时问题就来了怎么让模型在回答之前先查一下数据库怎么让它在搜索完网页之后再总结怎么让用户上一轮说的话不至于到下一轮就忘光这些问题本质上都不是“调用模型”而是“编排任务”。LangChain 和 LangGraph 就是用来解决这一层问题的开源框架——前者把模型、提示词、工具、记忆这些零件做成了标准化模块后者则把这些模块组合成一张可控制的、有状态的图。如果你刚接触这两个名字或者已经看过不少文档但还没理清它们之间的关系这篇文章应该能帮你把概念串起来。先说一个最直观的感受LangChain 像一条流水线原料从一头进去经过一道道固定工序从另一头出来LangGraph 则更像一个车间中间有多个工作台师傅们根据手里的单子和现场情况决定下一步找谁、要不要返工。真实应用里你需要让 AI“先判断再执行”“执行完再判断”这种循环和分支的编排刚好是 LangGraph 的主场。下面我从实际开发的角度把两个框架的核心设计、典型用法和踩坑经验一起拆开讲讲。1. 从“调 API”到“编排任务”LLM 应用开发缺的到底是什么1.1 单纯调用模型接口的瓶颈如果你只是想要“给一段文字让模型返回一段文字”那根本用不上任何框架直接请求模型服务商的接口就行。但真实业务几乎都不是这种形态。我给你举一个最常见的例子做一个能查询订单状态的客服助手。用户说“帮我看看上周买的耳机发货了没”。这个请求背后至少涉及几步判断这是一个“查询物流”的意图而不是闲聊。从用户说的话里抽取出“耳机”“上周”这两个关键信息。调用订单系统的接口传参查询。把接口返回的物流状态整理成自然语言回复用户。如果每一步都自己用 if-else 写死也不是不行但代码会迅速膨胀成一大坨难以维护的逻辑。更麻烦的是模型并不总是听话它返回的内容有时候是一段纯文本有时候是想调用工具的 JSON你要区分这些情况还要处理格式解析失败时的重试。说白了你缺的不是“模型能力”而是一套把模型能力和外部系统串起来的编排框架。1.2 应用层真正要解决的四类问题我做过的几个项目里模型本身很少出问题出问题的几乎都在“应用层”。把这些应用层问题归纳一下基本就是这四类模型路由与切换今天用 A 家的模型明天可能换 B 家的接口参数、返回格式都不一样业务代码不应该跟着改。工具调用模型需要查天气、查数据库、发邮件就必须有一种标准的方式让模型“声明”它想调用什么工具并在实际执行后把结果塞回上下文里。上下文管理多轮对话中历史记录保留多少轮哪些内容需要压缩总结哪些信息要持久化到存储里这个不能每次都在业务代码里手搓。流程控制先做什么、后做什么、出错怎么办、哪些分支可以并行。这是最容易被忽视但最关键的部分。LangChain 解决的是前三个问题组件化、标准化、即插即用。LangGraph 解决的主要是第四个问题它引入了“有状态”的图编排让流程控制变得显式和可控。1.3 为什么“链式调用”还不够当你第一次接触 LangChain 时“链Chain”这个抽象会让你觉得很顺眼Prompt 模板、模型、输出解析器串在一起一个链完成一个固定任务。但一旦任务变得复杂你会发现“链”是线性的、单向的它表达不了“循环”和“条件分支”。还是拿客服助手举例链式调用没法自然地表达“如果用户问的是订单就去查订单系统如果问的是退货就去查售后系统”。你可以强行用一个大 Prompt 让模型自己决定但那是把逻辑塞进提示词里既不可控也难调试。LangGraph 把流程画成一张图节点是执行单元边是跳转关系图上可以有环可以有条件路径每一步都基于共享的“状态”。这才是复杂 Agent 该有的样子。2. LangChain 核心模块拆解Models、Chains、Memory、Tools 各管哪一段2.1 Models把不同模型接口统一成一套调用方式LangChain 里最底层的抽象就是“模型封装”。它把市面上各种模型服务商的接口统一成类似的一套调用签名输入文本或消息列表返回文本或消息对象。这让上层业务代码不用绑定某一家服务商切换时只需改配置或改一行初始化代码。这个设计对我们做项目的人来说非常实用。我记得有一次一个项目原本用的是某家模型后来那家接口的限流策略变了并发上不去。我们当时只改了模型实例化处的一个参数把请求指向另一家服务商剩下的 Chain 和 Agent 逻辑完全没动。这就是标准化封装带来的好处业务逻辑和模型实现解耦。需要提醒的是LangChain 里模型分为“文本生成模型”和“对话模型”两类。早期版本命名比较混乱现在的版本基本将两者分开处理并提供了统一的消息对象比如系统消息、用户消息、AI 消息。写代码之前建议先查一下你锁定版本的文档因为不同版本的 API 变动不小。2.2 Chains把 Prompt、模型、输出解析串成流水线Chain 是 LangChain 的核心执行单元。最常见的组成是这样Prompt 模板定义一个字符串模板里面的变量用花括号占位。模型对象真正执行推理。输出解析器把模型返回的文本解析成结构化数据比如 JSON、字典、布尔值。以我以前做过的“合同关键条款提取”Demo 为例典型代码逻辑是这样的from langchain.prompts import PromptTemplate from langchain.llms import SomeModelProvider # 示意实际按你的模型服务商导入 from langchain.output_parsers import JSONOutputParser prompt PromptTemplate.from_template( 从下面的合同中提取关键条款输出为 JSON 格式\n{contract_text} ) llm SomeModelProvider(api_keyyour_api_key_here, modelyour_model_name) parser JSONOutputParser() # 旧版本的 LLMChain 写法新版本直接使用 pipe 运算符 chain prompt | llm | parser result chain.invoke({contract_text: 甲方……乙方……}) print(result)这种“管道式”写法在现在的 LangChain 版本里很常见|符号表示数据在组件之间流动。它的好处是阅读起来一目了然文本先进模板模板变量填充后送模型模型输出被解析成 JSON。如果你只是做一个简单的单步任务这种组件化方式非常够用。2.3 Tools让模型“有手有脚”模型本身没有能力查天气、读数据库但你可以通过“工具Tool”把外部能力暴露给它。LangChain 对工具的定义很简单一个函数带名称和描述。模型在推理时根据工具的描述决定要不要调用某个工具并以 JSON 形式输出“工具名称 参数”。这里有一个关键认知工具调用的决策者是模型工具的执行者是代码。模型只负责说“我想调用 search_weather参数是 Beijing”真正去请求天气 API 的是你写的那个 Python 函数。这就是 LangChain 中 Agent 自动调用工具的底层逻辑。实际开发中我建议你给工具起名时尽量描述清晰比如“search_orders_by_user”而不是“db_query”因为模型是通过描述来理解工具的。描述写得好模型选工具的准确率会高很多。2.4 Memory让对话记住该记的事“上下文管理”在 LangChain 里的实现叫 Memory。它做的事情很简单把历史对话塞回下一次请求的 Prompt 里。但“塞什么、塞多少、怎么塞”是有讲究的。粗略可以分成两类短期记忆只保留最近几轮对话用一个滑动窗口。简单有效但超过窗口的旧信息会丢失。长期记忆把重要信息持久化到外部存储比如向量数据库、普通数据库在需要时再检索出来放回上下文。这种方式更接近真实产品需求也更难做。我自己的经验是早期做 Demo 用短期记忆就够了生产环境一定得考虑上下文压缩策略。一个真实用户聊了三十轮你把三十轮全塞给模型token 成本高不说模型还会被无关历史干扰。后来我在项目里引入了一个简单的方案把用户的“需求摘要”单独记下来用摘要替换掉冗长的历史对话。效果好了很多成本也降下来了。2.5 LangChain 里的 Agent自动决定下一步做什么用 Chain 做固定流程没问题但如果你想让模型“自己决定”接下来该调用哪个工具或者该不该结束对话LangChain 里的 Agent 概念就要登场了。Agent 本质上是一个循环模型基于当前状态选择一个动作执行动作后观察结果再决定下一个动作直到它认为任务完成。LangChain 的早期 Agent 通过一个 AgentExecutor 来跑这个循环。如果你用过早期的 LangChain会记得那个“AgentExecutor”里充满了不可控的 Prompt 拼接方式和复杂的停止条件。这也是我后来转向 LangGraph 的原因之一。LangChain 把 Agent 的循环逻辑包在一个黑盒 Executor 里你想自定义“什么时候停止”“中途要不要让用户确认”都很难下手。而 LangGraph 把循环拆成了显式的图和状态让我能清楚看到每一步发生了什么。3. LangGraph 的设计逻辑State、Node、Edge 与循环为什么重要3.1 图模型与链式模型的本质区别如果用一个比喻Chain 是“流水线”材料从 A 工位传到 B 工位再传到 C 工位顺序永远固定。而 LangGraph 是“车间里的工作流”你有一个工作台状态几个师傅节点师傅干完活就在工作台上更新记录并告诉你“下一步去找另一个师傅”或者“这事干完了可以交付了”。流程图和线性链条最大的区别在于两点可以有环。节点可以多次被访问也就是循环。可以有条件跳转。下一步去哪个节点由当前节点处理完后的结果决定。这两点正是“复杂 Agent”所需要的。你可以把 LangGraph 想象成把原来隐藏在 AgentExecutor 内部的循环逻辑公开化、可视化。模型不再是黑盒里转圈的“大脑”而是图上某个节点里的“决策者”。3.2 State所有节点共享的操作台LangGraph 里最核心的概念是State。它是一个在整张图运行期间持续存在的共享数据结构所有节点都能读它、改它。通常你会用TypedDict来定义状态结构。比如我做客服助手时状态字段大概长这样from typing import TypedDict, Annotated, list from langgraph.graph import add_messages class AgentState(TypedDict): messages: Annotated[list, add_messages] # 对话消息新消息追加 intent: str # 用户意图 order_result: dict # 订单查询结果 final_answer: str # 最终回复这里有个重要的类型注解概念Annotated[list, add_messages]。它告诉 LangGraph“当多个节点都更新这个字段时应该采用追加方式而不是覆盖方式”。这个机制叫 Reducer稍后再展开。State 的另一个好处是整张图的执行过程可以回放——你保存每一步的 State 快照就能清楚地复现“模型当时看到了什么、为什么走出这一步”。3.3 Node 与 Edge做什么、下一步去哪在 LangGraph 里节点Node就是一个 Python 函数接收当前的 State返回一份“更新内容”。它不直接修改 State而是把更新结果返回给运行时由运行时调用 Reducer 合并更新。这样做的好处是每个节点都是纯函数式的方便测试和回放。边Edge有两种普通边从一个节点确定性地连到下一个节点。条件边从一个节点出发根据返回值动态选择下一个节点。看一个最小示例from langgraph.graph import StateGraph, START, END def node_a(state: AgentState) - dict: return {intent: query_order} def node_b(state: AgentState) - dict: return {final_answer: 您的订单正在运输中} graph StateGraph(AgentState) graph.add_node(A, node_a) graph.add_node(B, node_b) graph.add_edge(START, A) graph.add_edge(A, B) graph.add_edge(B, END)条件边的典型写法是在节点返回值之后用一个路由函数决定走向def route_after_a(state: AgentState): if state[intent] query_order: return B return C # 另一个节点 graph.add_conditional_edges( A, route_after_a, {B: B, C: C} )3.4 Reducer状态更新时的“合并规则”为什么需要 Reducer因为 LangGraph 允许你“多个节点并行或先后更新同一个字段”。如果不定义合并规则后写入的字段就会覆盖之前的这在很多场景下不是你想要的结果。比如定义messages字段用add_messages时聊天记录会不断追加定义steps字段用operator.add时每一步执行记录会累加成列表。你也可以写自定义 Reducer比如“保留非空值”“取最新时间戳”。这有点像前端状态管理库里的 Reducer 概念本质上都是“决定多个更新如何合并”。在实际项目中Reducer 最常见的坑是给某个字段定义了覆盖式更新结果条件边分支跑了两遍最后只保留了一个分支的结果另一个分支的结果白白丢失。所以遇到共享字段时多想一想这个字段的语义到底是“追加日志”还是“最终结果变量”。3.5 循环Agent 工作流的灵魂LangGraph 最大的亮点是支持“环”。你可以从节点 B 指回节点 A形成循环。例如一个“AI 写代码再自我检查”的流程模型生成代码。静态检查节点运行发现错误。带着错误信息回到“生成代码”节点让模型重新生成。循环直到检查通过或者到达最大迭代次数。这个流程在 Chain 里几乎没法写因为你没有“回到上一步”的概念但在 LangGraph 里一条边的事再加一个“最大迭代次数”的检查条件就行。我做过一个模拟的“AI 内容审校”工作流模型生成文章草稿另一个模型负责审校并输出修改意见然后带着意见回到生成节点再修改。这个“生成-审校-再生成”的循环如果不写死循环终止条件很容易变成死循环或耗尽 token。LangGraph 的条件边很方便地控制了退出路径——当审校方认为“无需修改”时走结束边。4. 一个能“先查再答、边答边记”的示例流程这一节我带你完整看一个用 LangGraph 搭建的小型流程。它不是玩具 demo而是我很多项目里会用到的模板结构。场景定义为一个“有搜索能力的小助手”用户提问助手先判断这个问题是否需要搜索外部资料如果需要执行一次搜索再基于结果输出答案最终把这次问答记录追加到笔记里。4.1 定义状态结构我们需要的共享状态有这些字段用户问题、意图判断结果、搜索结果、最终回答、操作日志。from typing import TypedDict, Annotated, List import operator class QAState(TypedDict): question: str need_search: str # yes 或 no search_payload: dict # 模拟搜索参数 search_result: str # 搜索返回的文本 answer: str trace: Annotated[List[str], operator.add] # 日志追加注意trace字段用的 Reducer 是追加模式这样每一步执行了什么都能累计下来方便调试。4.2 定义各个节点每个节点都是一个函数输入是 State输出是“要更新的字段字典”。def judge_node(state: QAState): # 实际项目中这里会让模型判断示例里直接用关键词模拟 need_search yes if 查询 in state[question] else no return {need_search: need_search, trace: [judge_node: 需要搜索 need_search]} def search_node(state: QAState): # 模拟外部搜索根据问题关键词拼一段资料 kw state[question][:5] result_text f关于「{kw}」的模拟搜索结果为该领域近期有若干注意事项…… return {search_result: result_text, trace: [search_node: 搜索完成]} def answer_direct_node(state: QAState): direct_answer 这个问题不需要搜索直接回答您好请补充更多信息。 return {answer: direct_answer, trace: [answer_direct_node: 直接回答]} def answer_with_search_node(state: QAState): combined_answer f根据检索到的资料我可以这样回答您{state[search_result][:50]} return {answer: combined_answer, trace: [answer_with_search_node: 结合搜索生成回答]} def note_node(state: QAState): note f已记录问答Q{state[question]}A{state[answer]} return {trace: [note]}4.3 定义条件边路由判断节点的输出决定了流程下一个去向def route_by_need_search(state: QAState): if state.get(need_search) yes: return search return answer_direct def route_to_next(state: QAState): if answer in state and state[answer]: return note return answer_with_search4.4 装配并运行整张图from langgraph.graph import StateGraph, START, END builder StateGraph(QAState) builder.add_node(judge, judge_node) builder.add_node(search, search_node) builder.add_node(answer_direct, answer_direct_node) builder.add_node(answer_with_search, answer_with_search_node) builder.add_node(note, note_node) builder.add_edge(START, judge) builder.add_conditional_edges( judge, route_by_need_search, {search: search, answer_direct: answer_direct} ) builder.add_edge(search, answer_with_search) builder.add_edge(answer_direct, note) builder.add_edge(answer_with_search, note) builder.add_edge(note, END) app builder.compile() result app.invoke({ question: 请查询上周发布的行业报告要点, trace: [] }) print(result[answer]) print(result[trace])运行这段后result[trace]会展示一条清晰执行路径judge → search → answer_with_search → note。如果问题改成“你好”路径会变成 judge → answer_direct → note。整张图的行为一目了然。4.5 这个流程设计里值得学习的两点第一路由判断独立成节点而不是塞在 Prompt 里让模型自由发挥。这样可以单独测试“判断逻辑”的准确性。第二每个节点都写 trace 日志你可以在生产环境把 trace 写到日志系统出问题时直接回放执行路径而不是靠猜。这就是“有状态图”带来的可见性优势。5. 实际项目中踩过的坑与选型建议5.1 什么时候选 LangChain什么时候选 LangGraph这两个不是竞争关系更像不同层次的工具。我的经验是这样的场景推荐理由单步的文本生成、格式转换、摘要提炼LangChain 的 Chain简单直接组件化足够固定流程比如“读文件→清洗→总结→写库”LangChain 的 Chain没有分支循环不需要图需要模型自己决定调用哪个工具LangGraph循环条件分支更可控多步骤且可能失败重试LangGraph状态可回放、易加终止条件需要人工确认介入的流程LangGraph可以在任意节点暂停等待输入纯 RAG 应用LangChain 足够大多数 RAG 是线性流程如果你的核心逻辑里出现了“如果……则重新试一次”或者“根据上一步结果决定下一步”就该从 Chain 升级到 LangGraph 了。5.2 LangGraph 状态字段别无脑塞新手最容易踩的坑就是恨不能把所有中间结果都塞进 State 里。State 字段一多每次节点传递的上下文变大一方面浪费 token另一方面排查问题也更困难。我的建议是 State 只保存“跨节点必须共享的东西”比如提问、最终结果、关键中间结论。像大文本原文、详细日志这类东西应该放外部存储或通过 trace 输出而不要全堆在 State 里。5.3 模型输出解析失败时不要让整个流程崩掉这是我在 LangChain 阶段踩得最深的一个坑。模型返回的内容一旦不是合法 JSON输出解析器直接抛异常整条链断裂。后来我在项目里统一加了一个“容错解析”层先尝试严格解析失败后用正则抽取再失败就返回原始文本并标记“解析降级”。流程宁可让后续节点处理“脏数据”也不要因为抛异常直接中断。LangGraph 里同样适用。你可以在节点之间加一个独立的“清洗节点”专门修正格式异常或者用条件边让流程在解析失败时走“重新生成”分支。5.4 条件边一定要给默认路径LangGraph 的条件边需要你提供一个路由映射表。如果路由函数返回了一个映射表里没有的键编译期可能不报错运行时就会出问题。更隐蔽的问题是路由函数里因为字段缺失抛异常直接让整张图挂掉。我现在的习惯是路由函数里所有分支都显式覆盖并在末尾加一个 fallback 返回“默认节点”。比如return answer_direct哪怕前面的条件都漏判也至少有个去处。5.5 版本演进是最大的“隐形开销”LangChain 和 LangGraph 都还在快速迭代中。网上搜到的教程尤其是两年前的很多 API 已经不能直接跑了。我自己经历过LLMChain被管道操作符替代、工具装饰器写法变化、Memory 相关模块大规模重构等多次调整。我给你的建议是一旦项目稳定立刻锁定版本并写死在依赖配置里。学习新概念时优先读官方文档的“当前版本”页面而不是四处找博客。框架的抽象概念是相对稳定的但 API 细节变动太快跟着版本走才不会被坑。5.6 调试手段可视化与逐步回放LangGraph 提供了一些可视化工具可以把编译后的图输出成图表文件。我建议你在开发阶段把图结构打印出来看一眼确认每个节点的连线符合预期。更重要的调试手段是保存每一步的 State 快照。我做过一个模拟的“多轮客服 Agent”当时遇到一个诡异问题第二轮会话时用户的历史摘要没有进入上下文。后来我就是靠打印每一步 State发现节点在更新历史字段时使用了覆盖式 Reducer把之前保存的摘要冲掉了。改成追加式 Reducer 之后问题立刻消失。这种问题在无状态代码里很难一次定位在 LangGraph 里因为状态是显式的反而更好查。最后说一点个人体会。我前后用 LangChain 和 LangGraph 做了好几个项目最大的感受是框架引入的复杂度是真实存在的但它们确实把“模型编排”这件事从手写魔改中解放了出来。如果你刚开始学习别急着上 LangGraph 画大图先把 LangChain 的一个 Chain 跑通理解 Prompt、模型、解析器之间的关系然后再把一条链扩展成一张图慢慢体会“状态”和“路由”带来的自由度。框架只是一个工具核心还是你想清楚自己的业务流程是什么、哪里要分支、哪里要循环、哪里要容错。把这些想明白用哪个框架其实都顺手。