← 返回文章列表

LangChain 和 LangGraph:不是两个框架,是两层

createAgent 适合标准 agent loop,LangGraph 适合你自己设计的拓扑。分界线不在「简单或复杂」,在「流程的形状由谁决定」。附条件边回退、checkpointer 中断恢复两个可跑实验,以及一份什么时候该往下走的判断表。
LangGraph 2026-10-24 01:50:10 评论 0

先看一个反直觉的事实

打开 langchain 的依赖清单:

$ npm view langchain dependencies
{
  zod: '^3.25.76 || ^4',
  langsmith: '>=0.11.0 <1.0.0',
  '@langchain/langgraph': '^1.4.18',          # ← 这里
  '@langchain/langgraph-checkpoint': '^1.1.5'
}

LangChain 依赖 LangGraph。

再看源码。createAgent 返回的对象叫 ReactAgent,它的构造函数里第一件事就是:

// node_modules/langchain/dist/agents/ReactAgent.js:74
const workflow = new StateGraph(state, { input, output, context })

createAgent 内部 new 了一张 LangGraph 的图。

所以「LangChain 要不要换成 LangGraph」这个问法本身就有问题——你已经在用 LangGraph 了,只是没直接碰它。


三个流传很广的说法,都需要修正

说法一:「LangChain 是单行道,处理不了拐弯」

这个说法在 0.x 时代就不准确,在 1.x 更是错的。

createAgent 的核心是一个循环:

调模型 → 模型说"我要用某个工具" → 执行工具 → 结果喂回去 → 再调模型
                                                          ↓
                                              直到模型不再要工具

这本身就是一个循环。 用户说「帮我查近三个月金额超 1000 的订单」,它完全能:先查,发现漏了条件,再查一次。

它真正做不了的,是改变循环的形状:

标准循环的形状是固定的:model ⇄ tools

但你想要的是:
  先按问题类型分类(这一步不是模型决定的,是你定的)
       ↓
  类型 A → 走 RAG 流程 → 结束
  类型 B → 查订单 → 金额超限?→ 人审 → 执行
  类型 C → 转人工

这个图的形状,createAgent 画不出来。不是因为它不能循环,而是因为它只会画一种形状的循环。

说法二:「LangGraph 会取代 LangChain」

不会,因为 LangChain 就长在 LangGraph 上面。

官方的定位很清楚:

Use LangChain (create_agent) for a highly customizable harness.
Use LangGraph, our low-level orchestration framework, for advanced needs combining deterministic and agentic workflows.

一个是「现成的工作台」,一个是「自己搭工作台的材料」。你把工作台拆了换成木板,不会更先进,只会更慢。

说法三:「人审、断点续跑必须上 LangGraph」

这条在 1.x 之前基本对,现在不完全对。

LangChain 1.x 提供了 humanInTheLoopMiddleware:

import { createAgent, humanInTheLoopMiddleware } from "langchain"

const agent = createAgent({
  model: "claude-sonnet-4-6",
  tools: [readEmail, sendEmail],
  middleware: [
    humanInTheLoopMiddleware({
      interruptOn: { sendEmail: { allowedDecisions: ["approve", "edit", "reject"] } },
    }),
  ],
})

这段代码背后确实用的是 LangGraph 的 interrupt,但你不需要知道这件事。

所以判断标准不是「要不要人审」,而是「人审的时机和粒度是不是标准的那几种」。 标准场景(敏感工具调用前后暂停)用中间件;非标准场景(比如「每隔三步暂停一次」或「两个 agent 互相审核」)才需要自己画。


两层关系

┌─────────────────────────────────────────────┐
│  LangChain 1.x · 高层封装                    │
│  createAgent · middleware · 工具抽象          │
│  ┌───────────────────────────────────────┐  │
│  │  LangGraph 1.x · 底层运行时            │  │
│  │  StateGraph · 状态 · 条件边 · 循环      │  │
│  │  Checkpointer · interrupt · 断点续跑    │  │
│  └───────────────────────────────────────┘  │
└─────────────────────────────────────────────┘
        需要自定义拓扑时,往下走

一句话分工:

LangChain 管「怎么快速搭出一个 agent」,LangGraph 管「这个 agent 的流程长什么样」。


一个需求,七次演进

拿智能客服举例。这个演进顺序基本是必然的,每一层都是被上一层的失败逼出来的。

第 1 次:接大模型

用户问「A 产品和 B 产品比,优劣势是什么」,模型开始编——它没见过你的产品文档。

失败原因:没有知识。

第 2 次:加 RAG

回答前先检索产品文档,照着资料说。能用了。

失败原因:只会说,不会做。

第 3 次:加工具

用户说「帮我把某个订单退了」,它礼貌地念了一大段退款政策,但没退。

配上工具(查订单、提交退款),它能做了。

失败原因:工具多了,编排变复杂。

第 4 次:上 createAgent

五六个工具、一套提示词、还要解析模型的输出格式——手写这些连接代码很容易错。

const agent = createAgent({
  model: "claude-sonnet-4-6",
  tools: [searchKb, getOrder, submitRefund],
  systemPrompt: "你是客服助手。涉及退款必须先核对订单状态。",
})

到这里,80% 的场景已经够了。

第 5 次:第一次撞墙

用户问:「帮我查近三个月金额超过 1000 的订单,导成表格。」

模型第一次只查了 amount > 1000,发现结果里有些是半年前的;于是它补上时间条件,再查一次。

这个 createAgent 能做——它的循环就是干这个的。

但接下来这个需求它做不了:

「退款金额超过 500 的,必须先让主管审批;低于 500 的直接退。审批完再执行,中途如果用户撤回了就终止。」

这不是「循环几次」的问题,是流程里有一个人工闸门,而且这个闸门的位置是你定的。

第 6 次:自己画图

这时候才需要 LangGraph。因为你要的拓扑是这样的:

parse → query → judge ─┬─ 够了 ──→ approve(人审) → execute → END
      ↑                │
      └──── 不够 ──────┘

三件事必须显式表达:

  1. 条件边:judge 的结果决定往哪走
  2. 回退边:条件不满足,回到 parse 补条件(不是回到 model 重新想)
  3. 中断点:approve 节点必须停下来等人

第 7 次:加持久化

流程一长,新问题出现:

任务跑到第 6 步,进程重启了 → 前面 5 步白跑
退款这种操作,模型判断完直接执行 → 没人兜底

这时候才需要 checkpointer。

注意顺序:先是流程需要图,然后图需要状态持久化。 不是反过来。


实测一:一张会拐弯的图

先不带任何模型,纯逻辑节点,把「条件不满足就回退重来」跑通。

import { StateGraph, Annotation, START, END } from "@langchain/langgraph"

const State = Annotation.Root({
  keywords: Annotation({ reducer: (_, b) => b, default: () => [] }),
  attempts: Annotation({ reducer: (a, b) => a + b, default: () => 0 }),
  rows: Annotation({ reducer: (_, b) => b, default: () => [] }),
  enough: Annotation({ reducer: (_, b) => b, default: () => false }),
})

const parse = (s) => {
  const n = s.attempts + 1
  // 第一次只抓了「金额>1000」,漏了时间条件
  return { keywords: n === 1 ? ['amount>1000'] : ['amount>1000', 'days<90'], attempts: 1 }
}

const query = (s) => ({
  rows: DB.filter((r) => s.keywords.every((k) => match(r, k))),
})

const judge = (s) => ({ enough: s.rows.length >= 1 && s.keywords.length >= 2 })

const graph = new StateGraph(State)
  .addNode('parse', parse)
  .addNode('query', query)
  .addNode('judge', judge)
  .addEdge(START, 'parse')
  .addEdge('parse', 'query')
  .addEdge('query', 'judge')
  .addConditionalEdges('judge', (s) => (s.enough ? END : 'parse'))
  .compile()

跑出来:

[parse] 第 1 次解析条件 → ["amount>1000"]
[query] 命中 3 条:A1,A3,A4
[judge] 条件 1 个、命中 3 条 → 不够,回去补条件
[parse] 第 2 次解析条件 → ["amount>1000","days<90"]
[query] 命中 3 条:A1,A3,A4
[judge] 条件 2 个、命中 3 条 → 够了
最终结果: A1, A3, A4

这就是「拐弯」。 关键在于 addConditionalEdges 的返回目标是节点名,不是布尔值——你指定回哪里,它就回哪里。

这里踩了一个坑,值得单独说

我第一版忘了在 State 里声明 enough 字段,只写了 keywords / attempts / rows。

结果:judge 返回了 { enough: true },但这个字段被静默丢弃了(Annotation.Root 只保留声明过的键),条件边读到的 s.enough 永远是 undefined,于是一路走 'parse',直到撞上默认的递归上限:

GraphRecursionError: Recursion limit of 25 reached without hitting a stop condition.

两个可以直接带走的经验:

  1. State 里没声明的字段会被静默丢弃,不报错。条件边如果要读某个判断结果,必须在 State 里声明它。
  2. 循环必须有出口,LangGraph 用 recursionLimit(默认 25)兜底。 这个数字在调试死循环时很有用,但也意味着:如果你的业务真的需要 30 步,得显式调大。

实测二:checkpointer,断点续跑和人审

这是 LangGraph 最值钱的部分。加一个 approve 节点,用 interrupt() 暂停。

import { MemorySaver, interrupt, Command } from "@langchain/langgraph"

const approve = (s) => {
  // 危险操作前暂停,等人点头
  const decision = interrupt({ question: '这 3 笔要发起退款吗?', rows: s.rows })
  return { approved: decision.approved }
}

const graph = new StateGraph(State)
  // ...省略节点定义
  .addConditionalEdges('judge', (s) => (s.enough ? 'approve' : 'parse'))
  .addEdge('approve', 'execute')
  .addEdge('execute', END)
  .compile({ checkpointer: new MemorySaver() })     // ← 关键

第一次 invoke(跑到中断点会停):

执行轨迹: parse → query → judge → parse → query → judge → approve(中断点)

返回的 __interrupt__: [{
  "value": { "question": "这 3 笔要发起退款吗?", "rows": [A1, A3, A4] }
}]

此刻进程如果崩了,状态还在不在? 查一下:

已落盘的状态: {
  "attempts": 2,
  "keywords": ["amount>1000", "days<90"],
  "rows": ["A1", "A3", "A4"],
  "approved": null
}
下一个待执行节点: ["approve"]

带同一个 thread_id 恢复,并给出审批结果:

await graph.invoke(new Command({ resume: { approved: true } }), {
  configurable: { thread_id: 'session-001' },
})
执行轨迹(注意有没有重跑 parse/query): approve(中断点) → execute(真正执行)
approved = true

轨迹里没有 parse 和 query。 它从存档点接上了,不是从头再来。

再换一个 thread_id:

session-002 的状态: {}  next = []

干净的空会话——thread_id 就是会话号,同一个号能续,不同号互不干扰。

checkpointer 到底做了什么

一句话:把「状态」从进程内存搬到了外部存储。

没有 checkpointer:
    状态在变量里 → 进程一挂,全没了 → 用户得从头说一遍

有 checkpointer:
    每执行完一个节点 → 状态写一次存储 → 进程挂了能读回来

这就是为什么它能同时支撑三件事:

能力 原理
断点续跑 读回最后一个 checkpoint,从 next 继续
人审中断 在节点里 interrupt(),状态已经存好了,等 Command({resume})
时间旅行 历史 checkpoint 都在,可以回到任意一步重跑

一个必须说的细节:MemorySaver 不能上生产

MemorySaver 存在进程内存里——进程一重启就没了,跟没存一样。

生产要用持久化实现:

npm install @langchain/langgraph-checkpoint-postgres    # 1.1.2
# 或
npm install @langchain/langgraph-checkpoint-sqlite      # 1.1.0

选型很简单:单机用 SQLite,多实例用 Postgres。 多实例部署时如果用内存版,用户第二次请求打到另一台机器,会话就丢了——这个 bug 在压测时发现不了,上线后随机出现。


判断表:什么时候往下走

你的情况 用什么 原因
标准「模型选工具 → 执行 → 再判断」 createAgent 这张图它已经画好了
需要摘要压缩、PII 脱敏、工具调用前后拦截 createAgent + middleware 内置的,不用画图
敏感操作要人审(标准时机) humanInTheLoopMiddleware 底层就是 interrupt,但不用你写
流程里有你定的分类/路由分支 LangGraph 这是拓扑问题,不是循环问题
多个角色互相审核、handoff LangGraph(子图) 标准循环里没有「角色」概念
长任务要能断点续跑 LangGraph + checkpointer 也可以只为了这个下潜
需要回到某一步重跑(时间旅行) LangGraph 只在有图的前提下成立

分界线不在「简单/复杂」,在「流程的形状由谁决定」。

形状由框架定 → createAgent
形状由你定   → LangGraph

下潜的代价,也要说清

用 LangGraph 不是免费的,三个成本:

① 图的拓扑本身成了资产,要维护。

写了 20 个节点、15 条边之后,改一个分支得先看懂整张图。而 createAgent 的拓扑是别人维护的,你只改配置。

② 调试从「看日志」变成「看状态快照」。

标准 agent 出问题,看消息列表就知道它调了什么。图出问题,你要看的是「走到了哪个节点、当时 State 是什么」。建议从第一天就把每个节点的输入输出记下来,否则排查时只能靠猜。

③ 简单的活儿用图,是负优化。

一个三步线性流程(解析 → 查库 → 拼答案),用 LCEL 或者直接写函数更短。上图之后多了一层抽象,收益是零。

所以顺序应该是:

先用 createAgent 跑到撞墙
   ↓ 撞的墙是「流程形状不对」
再用 LangGraph 重画那一段
   ↓ 撞的墙是「跑一半挂了」
最后才加 checkpointer

最后

回到那个问法:「LangGraph 会不会取代 LangChain?」

不会,因为它们是同一套栈的两层。 你在 LangChain 里写的每一个 agent,跑的都是 LangGraph 的图——只是那张图是它替你画的。

真正值得问的是另一个问题:

我的流程,是标准的 agent loop,还是我自己设计的一张图?

  • 如果是标准的,createAgent 帮你省掉两个月的脚手架代码
  • 如果不是,你需要那支笔

而「要不要 checkpointer」是另一个独立问题,它取决于你的任务会不会跑很久,以及有没有不能自动执行的动作——跟用哪层框架没关系。createAgent 加 middleware 一样能人审,只是一个用配置、一个用代码。

评论: