导语
如果你写过 LangChain,你一定体验过它的线性链(Chain) 有多"死板"——A → B → C 一条路走到黑,想加个分支、想重试、想暂停等用户确认?链式结构基本玩不转。
而 LangGraph 的出现,把"工作流编排"从直线升级成了网状图。它用 State(共享状态)+ Node(节点)+ Edge(边)+ Checkpointer(记忆/中断)四件套,让你能写循环、能条件分支、能跨会话记忆、还能中途暂停等人确认。
这不是一篇 API 手册,而是一位"踩过坑的老兵"给你的复习总结。我会带你把官方示例里最核心的 5 段代码一次讲透,看完你就能自己设计 Agent 流程图。
目标读者:有 Node.js 基础、想上手 LangGraph / 多 Agent 架构的初中级后端 & 前端工程师。
一、核心概念:用"快递分拣中心"理解 LangGraph
先说人话。LangGraph 用一句话概括:
用一个共享的 State + 若干节点函数 + 边(含条件边)来描述"状态如何一步步变化"的流程引擎。
拿"快递分拣中心"打比方:
| LangGraph 概念 | 快递中心类比 | 作用 |
|---|---|---|
State | 一个贴在包裹上的运单 | 全流程共享的数据 |
Node(节点) | 分拣工位 | 读运单 → 处理 → 更新运单 |
Edge(边) | 传送带 | 决定下一站去哪 |
Conditional Edge | 智能分岔口 | 根据运单内容决定走哪条线 |
Checkpointer | 仓储系统 | 记录每一步,支持取回/续跑 |
interrupt() | 卡在"人工审核"栏 | 暂停等你输入 |
为什么需要它? 因为真实业务里,流程从来不是直线:
- 转账前要人工确认 → 需要暂停/恢复
- 用户访问要跨会话计数 → 需要持久化记忆
- 数学题走 math 分支、闲聊走 chat 分支 → 需要条件路由
- 调用失败要重试 3 次 → 需要循环
这些,LangChain 的线性链都做得很别扭,LangGraph 才是正解。
二、痛点与场景:为什么我们要从 LangChain 升级到 LangGraph?
2.1 LangChain 是"线性编排",LangGraph 是"网状编排"
| 维度 | LangChain | LangGraph |
|---|---|---|
| 结构 | 直线链式 | 网状图 |
| 分支/循环 | 弱 | 原生支持 |
| 跨会话记忆 | 需要额外拼装 | Checkpointer 内建 |
| 中断/恢复 | 不支持 | interrupt + Command |
| 适用 | 简单链式调用 | 复杂多 Agent 协作 |
2.2 多 Agent 架构为什么是趋势?
官方文档点出三个核心原因,我用自己的话翻译一遍:
① 单 Agent 的 prompt 太"臃肿"
单 Agent 架构下,所有工具描述、所有功能的 prompt 全塞进 system prompt。执行某个功能时,其实只需要一小部分 prompt,但每次都要带全量——token 消耗高不说,更致命的是无关信息干扰,导致准确率下降。
多 Agent 拆分的思路:每个 Agent 只带执行自己功能所需的最少 prompt,无冗余、无干扰,调用 LLM 次数虽多,但总 token 更省、准确率更高。
② 并行思考
主 Agent 下发任务,多个子 Agent 并行处理,完成后汇总。多个"大脑"同时干活,整体效率远高于单大脑一步步串行思考。
③ 多角色互纠
不同角色互相讨论、互相纠错(如 AutoGen 风格)。编程 Agent 写代码、测试 Agent 写测试(TDD)、验证 Agent 验证结果,三者形成闭环,纠错能力显著增强。
Agent = LLM + Harness(Tool + MCP + RAG + Skill...)
拆多 Agent 的本质,就是给每个大脑配上它专属的最小 Harness。
三、重难点剖析(核心)
我把官方 5 段示例浓缩为 3 个核心重难点,逐个攻破。
重难点 1:State 与 Reducer —— 状态到底怎么"变"
设计者为什么这么写?
javascript
const StateAnnotation = Annotation.Root({
text: Annotation({
reducer: (_prev, next) => next, // 核心:如何合并旧值和新值
default: () => "", // 初始值
}),
})
Annotation.Root 是状态的总声明,它规定了整张图 State 有哪些字段、初始值、以及每个字段如何合并。
关键点:节点并不直接修改 State,而是返回一个"补丁" 。补丁怎么合并进旧状态,由 reducer 决定。
javascript
const step1 = (state) => ({ text: `${state.text} ->> step1` })
// ↑ 这是补丁,不是直接改 state
执行流:
text
旧state + 节点返回的补丁 --reducer--> 新state
reducer: (_prev, next) => next 表示"覆盖"(丢弃旧值)。如果你想累加,就得写 (prev, next) => prev + next。
如何攻克?
记住三个名词的职责:
| 名词 | 职责 | 举例 |
|---|---|---|
default | 第一次进入图时的初始值 | () => 0 |
reducer | 每次合并补丁的规则 | (_p, n) => n(覆盖) |
| 节点返回值 | 只返回要更新的字段 | { tries, ok } |
避坑:defaultValue 用于基本类型(数字),default 用于引用类型(数组/对象),避免多实例共享引用。
重难点 2:条件边与循环 —— 流程如何"拐弯"和"转圈"
设计者为什么这么写?
LangGraph 的循环,本质就是边的目标指回了自己。看这段"重试 3 次"的代码:
javascript
const graph = new StateGraph(StateAnnotation)
.addNode("attempt", attempt)
.addEdge(START, "attempt")
.addConditionalEdges(
"attempt",
(state) => state.ok ? "done" : "retry", // 路由函数:只读不写,决定走哪条
{
retry: "attempt", // ← 关键:这条边指回自己 = 循环
done: END
}
)
.compile()
真正制造循环的是 retry: "attempt" 这个映射——它让 attempt 的出边又指回 attempt。路由函数只是每次判断该不该继续的"开关"。
执行轨迹:
| 轮次 | 进入时 tries | 执行后 tries | ok | 路由 |
|---|---|---|---|---|
| 1 | 0 | 1 | false | retry → attempt |
| 2 | 1 | 2 | false | retry → attempt |
| 3 | 2 | 3 | true | done → END |
分支场景也一样,用条件边做路由:
javascript
const router = (state) => {
const isMath = /[1-9][3456789][+-*/]/.test(state.query)
return { route: isMath ? "math" : "chat" }
}
// ...
.addConditionalEdges("router", (state) => state.route, {
math: "math",
chat: "chat",
})
如何攻克?
理解执行引擎的本质是一个 while 循环:
text
current = START
while (current !== END) {
state = 执行 current 节点(state) // 1. 跑节点,更新状态
current = 根据边/路由函数决定的下一个节点 // 2. 决定下一步
}
避坑:正因为"边指回自己",如果状态不收敛(比如忘记 +1),就是死循环。用 recursionLimit 兜底:
javascript
await graph.invoke({ tries: 0 }, { recursionLimit: 25 })
重难点 3:Checkpointer + interrupt —— 记忆与"暂停/恢复"的魔法
这是 LangGraph 最强大的能力,也是最容易踩坑的地方。
3.1 Checkpointer:跨调用记忆
javascript
const checkpointer = new MemorySaver()
const app = graph.compile({ checkpointer })
const user1 = { configurable: { thread_id: '用户-小张' } }
await app.invoke({}, user1) // 第1次
await app.invoke({}, user1) // 第2次(接着上次的状态继续!)
thread_id 是记忆的"钥匙" ,决定恢复/保存哪一份历史。同一个 thread_id 共享记忆,不同 thread_id 完全隔离——这就是多用户会话隔离的原理。
执行引擎流程:
text
1. 从 config.configurable.thread_id 取出钥匙
2. 拿钥匙去 checkpointer 查历史
有 → 恢复,作为起点 State
无 → 用 State 的 default 作为起点
3. 合并本次 invoke 的 input
4. 跑图
5. 存回 checkpointer
3.2 interrupt:人在回路(Human-in-the-loop)
这是审批流、Agent 工具调用确认的核心能力。看转账确认案例:
javascript
const waitConfirm = (state) => {
const text = interrupt({
hint: '终端里输入[确认]或者备注后回车,图才会继续',
actionSummary: state.actionSummary,
})
return { userInput: String(text) } // 恢复后才执行到这里
}
"两段式执行"是精髓:
- 首次执行:跑到
interrupt(...)→ 图暂停,参数被记录,return不执行,State 存入 checkpointer。 - 恢复执行:用
new Command({resume: line})+ 同一个 thread_id → 节点从头重跑 → 再次遇到interrupt()时直接返回 resume 值,不再暂停。
javascript
// 第一次:触发中断
const paused = await graph.invoke({}, config)
console.log(paused.__interrupt__?.[0].value) // 拿到 interrupt 里传的对象
// 用户输入
const line = (await rl.question("> ")).trim()
// 第二次:恢复执行
const done = await graph.invoke(new Command({ resume: line }), config)
如何攻克?
记住这个对应关系:
| 能力 | 依赖 | 关键 API |
|---|---|---|
| 跨调用记忆 | checkpointer + thread_id | MemorySaver |
| 暂停/恢复 | checkpointer + interrupt | interrupt() + Command |
生产环境:MemorySaver 存内存,进程结束就丢。要持久化换 SqliteSaver / PostgresSaver / RedisSaver。
四、避坑指南 / 最佳实践
以下是新手最容易踩的坑,全部来自代码注释和实战教训。
坑 1:interrupt() 会让节点从头重跑 ⚠️
javascript
// ❌ 错误示范:副作用在 interrupt 之前
const waitConfirm = (state) => {
await db.writeLog('开始确认') // 会执行两次!
await sendNotifyEmail() // 会发两封邮件!
const text = interrupt({...})
return { userInput: String(text) }
}
// ✅ 正确示范:副作用放在 interrupt 之后
const waitConfirm = (state) => {
const text = interrupt({...}) // 先中断
await db.writeLog('用户已确认', text) // 恢复后才执行,只跑一次
return { userInput: String(text) }
}
坑 2:没有 checkpointer,interrupt 无法工作
javascript
// ❌ 无 checkpointer,interrupt 会失效
.compile()
// ✅ 必须挂 checkpointer
.compile({ checkpointer: new MemorySaver() })
坑 3:恢复必须用同一个 thread_id
javascript
// ❌ 换了 thread_id,找不到暂停点
await graph.invoke(new Command({ resume: line }), { configurable: { thread_id: 'other' } })
// ✅ 用同一个
await graph.invoke(new Command({ resume: line }), cofig)
坑 4:invoke 的两个参数别搞混
javascript
app.invoke(input, config)
| 参数 | 作用 | 是否进 State |
|---|---|---|
input | 业务数据 | ✅ 会被 reducer 合并进 State |
config | 运行时配置(含 thread_id) | ❌ 不进 State,只影响引擎 |
一句话:input 管"数据",config 管"怎么跑"。
坑 5:configurable 这层壳不能省
javascript
// ❌ 不生效:LangGraph 只认 configurable 里的字段
{ thread_id: '用户-小张' }
// ✅ 正确
{ configurable: { thread_id: '用户-小张' } }
坑 6:循环图一定要留"退出条件"
javascript
// ❌ 状态不收敛 = 死循环
const attempt = (state) => ({ tries: state.tries }) // 忘记 +1
// ✅ 让状态收敛 + 设置 recursionLimit 兜底
const attempt = (state) => ({ tries: state.tries + 1 })
await graph.invoke({}, { recursionLimit: 25 })
坑 7:eval() 是双刃剑
示例里用 eval(state.query) 计算数学表达式,方便但极度危险:
javascript
// ❌ 危险:用户输入可执行任意代码
eval("process.exit()") // 直接搞崩进程
// ✅ 用安全库替代
import { evaluate } from 'mathjs'
return { answer: String(evaluate(state.query)) }
生产环境永远不要用 eval 处理用户输入。
五、面试高频考点
考点 1:LangGraph 的 State 更新机制是怎样的?
回答要点:
- 节点不直接修改 State,而是返回一个"补丁"对象。
- LangGraph 拿到补丁后,通过每个字段的
reducer决定如何合并进旧状态。 reducer(prev, next)定义合并语义,默认是"覆盖",也可自定义累加等。- 只返回要改的字段,未返回的字段保持不变。
- 补充:
Annotation.Root定义字段结构、初始值(default/defaultValue)、合并规则(reducer)。
考点 2:LangGraph 的循环是怎么实现的?会不会死循环?
回答要点:
- 循环的本质是条件边的目标指回了上游节点(如
retry: "attempt")。 - 路由函数是"开关",每次判断该不该继续;真正"转圈"的是执行引擎(本质是 while 循环)。
- 死循环风险靠状态收敛(节点让条件最终满足)+
recursionLimit兜底。 - 补充:循环看的是"节点之间是否有环",不必自己连自己。
考点 3:interrupt + Command 的暂停/恢复原理是什么?
回答要点:
interrupt()在节点内暂停图,参数被记录,State 存入 checkpointer。- 节点会从头重跑,所以
interrupt()前的副作用会重复执行——副作用要放后面。 - 恢复时用
new Command({ resume: 值 })+ 同一个thread_id。 - 恢复后节点重跑,再次遇到
interrupt()时返回 resume 值,不再暂停。 - 强依赖 checkpointer;生产环境用数据库持久化。
考点 4(进阶):为什么多 Agent 架构比单 Agent 更省 token?
回答要点:
- 单 Agent 把全部工具描述和 prompt 塞进 system prompt,每次调用都带全量。
- 多 Agent 拆分后,每个 Agent 只带自己功能所需的最小 prompt。
- 虽然调用 LLM 次数增加,但单次 token 大幅下降,且无冗余信息干扰,准确率更高。
- 附加收益:并行处理、多角色互纠。
六、总结:LangGraph 四件套速查表
| 能力 | 依赖 | 关键 API |
|---|---|---|
| 共享状态 | Annotation.Root | reducer / default |
| 分支 | 条件边 | addConditionalEdges |
| 循环 | 边指回自己 | addConditionalEdges + 映射 |
| 记忆 | checkpointer | MemorySaver + thread_id |
| 暂停/恢复 | checkpointer + interrupt | interrupt() + Command |
| 可视化 | getGraphAsync | drawMermaid() |
一句话记住:
LangGraph = State(运单) + Node(工位) + Edge(传送带) + Checkpointer(仓储) ,让你能像画流程图一样编排 AI 工作流。