LangGraph 基础学习 记忆 中断

14 阅读10分钟

导语

如果你写过 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 是"网状编排"

维度LangChainLangGraph
结构直线链式网状图
分支/循环弱原生支持
跨会话记忆需要额外拼装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 用于引用类型(数组/对象),避免多实例共享引用。

image.png

重难点 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执行后 triesok路由
101falseretry → attempt
212falseretry → attempt
323truedone → 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 })

image.png

重难点 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)

image.png

如何攻克?

记住这个对应关系:

能力依赖关键 API
跨调用记忆checkpointer + thread_idMemorySaver
暂停/恢复checkpointer + interruptinterrupt() + 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.Rootreducer / default
分支条件边addConditionalEdges
循环边指回自己addConditionalEdges + 映射
记忆checkpointerMemorySaver + thread_id
暂停/恢复checkpointer + interruptinterrupt() + Command
可视化getGraphAsyncdrawMermaid()

一句话记住:

LangGraph = State(运单) + Node(工位) + Edge(传送带) + Checkpointer(仓储) ,让你能像画流程图一样编排 AI 工作流。