# LangChain vs LangGraph:从链到图的LLM应用架构选型
## 一、问题的起点:线性链什么时候会撞墙
2023 年 LangChain 0.0.x 刚发布时,它解决的问题很具体:把「Prompt → LLM → 解析」这条链路标准化。开发者不用再手写字符串拼接、不用为每个模型单独写调用代码。LCEL(LangChain Expression Language)在 0.1 版本引入后,`|` 组合子配合 `invoke / batch / stream / astream` 四套统一接口,确实让 RAG 管道和聊天机器人的开发成本大幅下降。
但生产环境很快暴露出边界。一个典型的客服 Agent 需要:先判断意图,命中知识库就走检索,检索置信度低就改写 query 重试(循环),涉及退款要走人工审批(中断),审批通过后调用工单 API(外部副作用),失败要回滚重来。这些需求里有三个词——**循环、中断、状态回滚**——线性 DAG 一个都给不了。
LangGraph 就是为这个边界设计的。2025 年 10 月 LangChain 1.0 与 LangGraph 1.0 同期发布后,两者的关系已经不是「二选一」:LangChain 1.0 的 `create_agent` 直接构建在 LangGraph 运行时之上。理解它们的分层关系,比记住功能对比表更有价值。
## 二、执行模型差异:DAG 与状态图
**LangChain 的执行模型是单向数据流。** 一个 `Runnable` 序列编译成有向无环图,数据从入口流到出口,每个节点只拿到上游的输出,不感知全局状态。分支只能靠 `RunnableBranch` 或 `route` 预先声明,运行时无法根据中间结果动态改变拓扑。
**LangGraph 的执行模型是 Pregel 风格的超步(super-step)。** 它把流程建模为 `StateGraph`:节点是普通 Python 函数,边是状态转移,支持条件边、并行扇出(`Send` API)和**反向边**。反向边就是循环——这恰恰是 Agent 的核心。一次工具调用失败,图可以退回 agent 节点重新推理,这在 LangChain 里只能靠外部 `while` 循环硬编码。
```python
# langgraph 1.0.x
from langgraph.graph import StateGraph, START, END
builder = StateGraph(State)
builder.add_node("agent", call_model)
builder.add_node("tools", tool_node)
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", should_continue, {"tools": "tools", "end": END})
builder.add_edge("tools", "agent") # 反向边,构成 ReAct 循环
```
这段代码的价值不在于「写法更花哨」,而在于**循环被声明为图的拓扑结构**,而不是散落在控制流里。调试时你看到的是一张可序列化的图,而不是一堆嵌套的 `if/for`。
## 三、状态管理:隐式上下文 vs 显式 reducer
这是两者最深层的分野,也是最容易被低估的一点。
LangChain 通过 memory 模块(后迁移到 LangGraph 的 checkpoint)自动传递上下文,本质是「把历史消息塞进 prompt」。开发者对「哪些字段被保留、如何合并、何时截断」几乎没有控制权。多轮对话里想同时维护 `messages`、`user_profile`、`retrieved_docs`、`retry_count` 四类状态,就得自己写容器类。
LangGraph 把这个过程显式化了。状态用一个 `TypedDict` 声明,每个字段通过 `Annotated[..., reducer]` 指定合并策略:
```python
# langgraph 1.0.x
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages
class State(TypedDict):
messages: Annotated[list, add_messages] # 追加而非覆盖
retry_count: int # 默认覆盖
plan: list[str]
```
`add_messages` 会按 message id 去重并追加,这正是多轮对话需要的语义。而 `retry_count` 走默认的覆盖策略,节点只返回增量 `{"retry_count": 1}`,框架负责合并。**状态更新变成了一等公民,可观测、可测试、可持久化。**
配合 checkpointer,每个超步结束后状态快照落盘。`MemorySaver` 用于开发,`SqliteSaver` 用于单机,`langgraph-checkpoint-postgres 2.x` 用于生产。有了快照就有时间旅行:传入历史 `checkpoint_id` 就能回放任意一步,这对排查「Agent 为什么第三次调用工具时走错了分支」这类问题几乎是刚需。
## 四、代码对比:同一个 RAG 任务,两种写法
**LangChain 1.0(LCEL):**
```python
# langchain-core 1.0.x, langchain-openai 1.0.x
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
prompt = ChatPromptTemplate.from_template(
"仅依据上下文回答,无法回答时回复'不知道'。\n上下文:{context}\n问题:{question}"
)
chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| ChatOpenAI(model="gpt-4o-mini", temperature=0)
| StrOutputParser()
)
chain.invoke("LangGraph 的 checkpointer 解决什么问题?")
```
大约 15 行,覆盖 80% 的 RAG 场景。这是 LangChain 依然是原型首选的原因——学习曲线低,`stream` 和 `batch` 免费获得。
**LangGraph 1.0(带人工审批的 Agent):**
```python
# langgraph 1.0.x, langgraph-checkpoint-sqlite 2.x
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.types import interrupt, Command
def human_approval(state: State):
decision = interrupt({"tool_calls": state["messages"][-1].tool_calls})
if decision != "approve":
return {"messages": [reject_message(state)]}
return tool_node(state)
builder = StateGraph(State)
builder.add_node("agent", call_model)
builder.add_node("approval", human_approval)
builder.add_node("tools", tool_node)
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", should_continue,
{"approval": "approval", "tools": "tools", "end": END})
builder.add_edge("tools", "agent")
graph = builder.compile(checkpointer=SqliteSaver.from_conn_string("agent.sqlite"))
config = {"configurable": {"thread_id": "user-42"}}
graph.invoke({"messages": [("user", "把这周日志汇总成周报并发送")]}, config)
# 图在 interrupt 处挂起,进程可以退出
graph.invoke(Command(resume="approve"), config) # 数小时后从断点继续
```
`interrupt()` 是 LangGraph 相对 LangChain 最实用的能力。LangChain 时代做人机协同,标准做法是把整个状态 dump 到 Redis,等审批回调再重新组装上下文——脆弱且难测。LangGraph 把「挂起—恢复」做进了运行时,`Command(resume=...)` 从最后一个 checkpoint 继续执行,中间不需要保持进程存活。
## 五、LangChain 与 LangGraph 能力对照
| 维度 | LangChain 1.0 | LangGraph 1.0 |
| --- | --- | --- |
| 架构 | 线性工作流,组件串联 | 图结构,节点+边,支持环与分支 |
| 流程形态 | 逐步顺序执行 | 迭代、循环、可重访 |
| 状态管理 | 隐式,靠 memory/context 模块传递 | 显式 TypedDict + reducer |
| 学习曲线 | 低,半天可上手 | 陡,需要图编程思维 |
| 人工介入 | 需手写脚本定制 | `interrupt()` 原生支持 |
| 复杂度承载 | 简单管道,分支靠 workaround | 原生分支、并行、循环 |
| 适用场景 | 原型、RAG、文档处理、聊天机器人 | 生产级多 Agent、长时状态化应用 |
注意表中「隐式 vs 显式」这一行。它不是程度差异,而是**能力边界差异**:状态不可见,就无法在任意点暂停、恢复和回放;不能暂停恢复,就做不了需要人类审批或跨天运行的业务。
## 六、选型决策
判断标准可以收敛成三个问题:
**流程里有没有循环?** 有 query 改写重试、工具调用失败重试、自我反思迭代,选 LangGraph。纯一次性的检索问答,LangChain 足够。
**单次会话是否需要跨进程存活?** 需要等待人工审批、需要断点续跑、需要用户次日回来接着聊,选 LangGraph + Postgres checkpointer。
**是否需要多个 Agent 分工?** 规划者、执行者、审查者各司其职,并可能并行探索再汇总,只有 LangGraph 的 `Send` API 和子图能自然表达。
反过来,如果需求是「上传 PDF → 切片 → 向量化 → 问答」,硬上 LangGraph 只会增加维护成本。图的表达能力有代价:状态 schema、reducer 冲突、checkpoint 序列化、超步调优,都是额外工作量。
两者也能混用。LangGraph 的节点内部完全可以是一段 LCEL 链——把 `chain.invoke` 包进节点函数即可。实践中常见的分层是:**LangGraph 管流程与状态,LangChain 管单步内的模型调用与检索**。LangChain 1.0 的 `create_agent` 就是这个思路的官方实现。
## 七、收敛趋势
LangChain 1.0 把 agent 抽象收敛到 LangGraph 运行时之上,同时把 `langchain-core` 稳定为 1.0.x 接口层,`langchain-community` 中的集成逐步迁往独立包(`langchain-openai`、`langchain-anthropic` 等均按 1.0.x 节奏发版)。这个动作说明维护者自己也承认:**线性链是入口,状态图才是终点。**
对团队而言,现实建议是:原型阶段用 LangChain 快速验证 prompt 与检索质量,一旦需求出现「重试」「审批」「多轮规划」中的任意一个词,就该迁移到 LangGraph。迁移成本主要在状态 schema 的重新设计,而不是代码重写——把现有的 LCEL 链原样塞进节点,先跑通,再逐步把散落在函数闭包里的状态提取到 `State` 中。
选框架的本质是选状态模型。状态越复杂、生命周期越长,就越需要一张显式的图。