打开 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 了,只是没直接碰它。
这个说法在 0.x 时代就不准确,在 1.x 更是错的。
createAgent 的核心是一个循环:
调模型 → 模型说"我要用某个工具" → 执行工具 → 结果喂回去 → 再调模型
↓
直到模型不再要工具
这本身就是一个循环。 用户说「帮我查近三个月金额超 1000 的订单」,它完全能:先查,发现漏了条件,再查一次。
它真正做不了的,是改变循环的形状:
标准循环的形状是固定的:model ⇄ tools
但你想要的是:
先按问题类型分类(这一步不是模型决定的,是你定的)
↓
类型 A → 走 RAG 流程 → 结束
类型 B → 查订单 → 金额超限?→ 人审 → 执行
类型 C → 转人工
这个图的形状,createAgent 画不出来。不是因为它不能循环,而是因为它只会画一种形状的循环。
不会,因为 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.
一个是「现成的工作台」,一个是「自己搭工作台的材料」。你把工作台拆了换成木板,不会更先进,只会更慢。
这条在 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 的流程长什么样」。
拿智能客服举例。这个演进顺序基本是必然的,每一层都是被上一层的失败逼出来的。
用户问「A 产品和 B 产品比,优劣势是什么」,模型开始编——它没见过你的产品文档。
失败原因:没有知识。
回答前先检索产品文档,照着资料说。能用了。
失败原因:只会说,不会做。
用户说「帮我把某个订单退了」,它礼貌地念了一大段退款政策,但没退。
配上工具(查订单、提交退款),它能做了。
失败原因:工具多了,编排变复杂。
五六个工具、一套提示词、还要解析模型的输出格式——手写这些连接代码很容易错。
const agent = createAgent({
model: "claude-sonnet-4-6",
tools: [searchKb, getOrder, submitRefund],
systemPrompt: "你是客服助手。涉及退款必须先核对订单状态。",
})
到这里,80% 的场景已经够了。
用户问:「帮我查近三个月金额超过 1000 的订单,导成表格。」
模型第一次只查了 amount > 1000,发现结果里有些是半年前的;于是它补上时间条件,再查一次。
这个 createAgent 能做——它的循环就是干这个的。
但接下来这个需求它做不了:
「退款金额超过 500 的,必须先让主管审批;低于 500 的直接退。审批完再执行,中途如果用户撤回了就终止。」
这不是「循环几次」的问题,是流程里有一个人工闸门,而且这个闸门的位置是你定的。
这时候才需要 LangGraph。因为你要的拓扑是这样的:
parse → query → judge ─┬─ 够了 ──→ approve(人审) → execute → END
↑ │
└──── 不够 ──────┘
三件事必须显式表达:
流程一长,新问题出现:
任务跑到第 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.
两个可以直接带走的经验:
recursionLimit(默认 25)兜底。 这个数字在调试死循环时很有用,但也意味着:如果你的业务真的需要 30 步,得显式调大。这是 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:
每执行完一个节点 → 状态写一次存储 → 进程挂了能读回来
这就是为什么它能同时支撑三件事:
| 能力 | 原理 |
|---|---|
| 断点续跑 | 读回最后一个 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 一样能人审,只是一个用配置、一个用代码。