从 LangChain 到 LangGraph:多 Agent 不是玄学,而是 token 账本与信息干扰问题
英文标题:从 LangChain 到 LangGraph:多 Agent 不是玄学,是 token 账本和干扰问题
文章解释了 Agent 应用为何会从 LangChain 的线性链式编排走向 LangGraph 的图式工作流:核心原因并非架构潮流,而是多 Agent 拆分能够降低全量 prompt 带来的 token 成本和无关信息干扰。LangGraph 的核心组成是 State、节点和边,并通过条件边实现分支与循环,通过 checkpointer 和 thread_id 实现状态持久化与会话隔离。文中还展示了 `interrupt()` 与 `Command({resume})` 构成的人工确认闭环,适用于转账等高风险操作。5 个示例均未调用 LLM API,旨在聚焦图编排机制,运行结果尚未验证。
5.7 KB
中文
从 LangChain 到 LangGraph:多 Agent 不是玄学,是 token 账本和干扰问题
摘要:这篇文章回答一个具体问题——为什么会从 LangChain 走到 LangGraph,图编排、状态持久化、人工中断这些关键词落到代码上长什么样。核心判断:LangGraph 的本质是把工作流组织方式从线性链升级为网状图,三件套是 State + 节点 + 边。文中 5 个最小示例均不调用 LLM API,为静态阅读整理,运行未验证。
一个决策冲突:所有功能塞给一个 Agent,还是拆开?
先把冲突摆出来。做 Agent 应用时有一个看似省事的选择:单 Agent,把所有 tool 描述、所有功能的 prompt 全部写进 system prompt。代价在仓库笔记里写得很直白(readme.md#L7-12):每次调用全量携带,token 消耗高;无关信息干扰模型,准确率下降。
拆成多个 Agent 后,每个 Agent 只带最少 prompt——省 token、无干扰、准确率高;主 Agent 下发任务,子 Agent 还能并行处理、互相讨论纠错(readme.md#L17-31)。所以本文的第一个判断是:多 Agent 拆分不是架构玄学,是 token 开销与信息干扰的工程权衡。
而组织多个 Agent(以及任何带分支、循环、暂停的工作流),LangChain 的线性链不够用了。LangChain 与 LangGraph 的分工(readme.md#L33-38):LangChain = 线性工作流编排 + 基础模块(LLM API/loaders/splitter/embedding/vector store/output parser/memory);LangGraph = 网状工作流编排,负责“工作节点 + 组织方式”。两者是配合关系,不是替代关系。
下面按“遇到什么问题 → LangGraph 给什么答案”的顺序,过一遍 5 个最小示例。
问题 0:一张图最少长什么样?
答案:State + 节点 + 边三件套。这是全文最重要的一段代码(basic-graph.mjs):
import { Annotation, StateGraph, START, END } from '@langchain/langgraph'
const StateAnnotation = Annotation.Root({
text: Annotation({
reducer: (_prev, next) => next,
default: () => "",
}),
})
const step1 = (state) => ({ text: `${state.text} -> step1` })
const step2 = (state) => ({ text: `${state.text} -> step2` })
const graph = new StateGraph(StateAnnotation)
.addNode("step1", step1)
.addNode("step2", step2)
.addEdge(START, "step1")
.addEdge("step1", "step2")
.addEdge("step2", END)
.compile()
const drawable = await graph.getGraphAsync()
console.log(drawable.drawMermaid({ withStyles: true }))
console.log(await graph.invoke({ text: "hello" }))
读这段代码抓三个点:
Annotation.Root({...})声明状态 schema(basic-graph.mjs#L9-16):字段级reducer: (_prev, next) => next决定状态怎么更新(这里新值覆盖旧值),default给初始值。- 节点即函数(basic-graph.mjs#L18-21):返回值不是完整状态,而是“本节点的更新”,交给 reducer 合并。
invoke({text:"hello"})沿START → step1 → step2 → END流转,最终 text 为hello -> step1 -> step2(根据 reducer 逻辑推导,运行未验证)。
对应图 API 四要素心智模型(readme.md#L40-48):开始节点(初始状态)、工作节点(职责 + state)、边(连接)、结束节点(最终状态)。drawMermaid({withStyles:true}) 可以导出 mermaid 文本,随时检查图的形状。
问题 1:分支怎么写?
答案:决策写在节点里,路由交给条件边。示例是“算式走计算、否则走聊天”(conditional-routing.mjs):
const router = (state) => ({
route: /[+\-*]/.test(state.query) ? "math" : "chat",
})
const math = (state) => {
try {
return { answer: String(eval(state.query)) }
} catch {
return { answer: "数学公式有误" }
}
}
const chat = (state) => ({ answer: `聊天模式:${state.query}` })
graph.addConditionalEdges("router", (state) => state.route, {
math: "math",
chat: "chat",
})
注意关注点分离:router 节点只做正则判断 /[+\-*]/.test(query),返回 {route:"math"|"chat"}(#L23-28);addConditionalEdges 的条件函数返回 key,映射表把 key 翻译成下一个节点(#L51)。eval 包在 try/catch 里,失败兜底“数学公式有误”(#L31-39);eval("1+2") 返回 3 可单独验证(test.mjs)。
问题 2:循环怎么写?
答案:没有专门的循环 API,条件边指向自己就是循环(loop-retry.mjs):
const attempt = (state) => {
const tries = state.tries + 1
const ok = tries >= 3
return { tries, ok, message: `第${tries}次${ok ? "成功" : "失败"}` }
}
graph.addConditionalEdges(
"attempt",
(state) => (state.ok ? "done" : "retry"),
{ retry: "attempt", done: END }
)
tries 计数 + ok = tries >= 3 判定在节点里(#L23-31),条件边在未达标时把路由指回 attempt 自身,达标走 END(#L36-39)。自环 + 计数 + 终止条件三要素,缺少终止条件就是死循环。
问题 3:第二次调用怎么记得上一次?
答案:compile({checkpointer}) + thread_id(checkpointer-memory.mjs):
import { MemorySaver } from '@langchain/langgraph'
const StateAnnotation = Annotation.Root({
visitCount: Annotation({
reducer: (_prev, next) => next,
default: () => 0,
}),
})
const visit = (state) => ({ visitCount: state.visitCount + 1 })
const checkpointer = new MemorySaver()
const graph = new StateGraph(StateAnnotation)
.addNode("visit", visit)
.addEdge(START, "visit")
.addEdge("visit", END)
.compile({ checkpointer })
const config = { configurable: { thread_id: "用户_小张" } }
await graph.invoke({}, config)
await graph.invoke({}, config)
await graph.invoke({}, { configurable: { thread_id: "用户_小李" } })
关键行为(#L36-45):同一 thread_id 连续两次 invoke,第 1 次 visitCount = 1,第 2 次基于上次状态累加为 2;更换 thread_id 后从初始值重新计数,会话相互隔离。MemorySaver 只存内存,中断、暂停、失败后可继续;真正持久化按材料指引可换 sqlite、redis(readme.md#L56-63)。
问题 4:高危操作执行前怎么让人确认?
答案:interrupt() 暂停 + Command({resume}) 恢复。场景是转账确认——图执行“向张三转账 $100”前停下来等待人工审批(graph-interrupt.mjs):
const showTransfer = (state) => ({
actionSummary: "向张三转账$100",
})
const waitConfirm = (state) => {
const text = interrupt({
hint: "中断里输入[确认]或者备注后回车,图才会继续执行",
actionSummary: state.actionSummary,
})
return { useInput: String(text) }
}
const graph = new StateGraph(StateAnnotation)
.addNode("showTransfer", showTransfer)
.addNode("waitConfirm", waitConfirm)
.addEdge(START, "showTransfer")
.addEdge("showTransfer", "waitConfirm")
.addEdge("waitConfirm", END)
.compile({ checkpointer: new MemorySaver() })
const config = { configurable: { thread_id: "interrupt-demo" } }
const paused = await graph.invoke({}, config)
console.log("待你确认", paused.__interrupt__?.[0]?.value)
const done = await graph.invoke(new Command({ resume: line }), config)
console.log("done", done)
闭环四步:节点内 interrupt({hint, actionSummary}) 暂停并抛出 payload(#L27-33)→ 调用方从 paused.__interrupt__?.[0]?.value 读取中断信息(#L55-56)→ 示例用 node:readline/promises 读取命令行输入,模拟人工确认(#L10、L59-66)→ new Command({resume: line}) 配合同一个 thread_id再次 invoke,从暂停点继续(#L68)。这张图由 drawMermaid 导出的形状为:__start__ → showTransfer → waitConfirm → __end__(g.md)。一个推导判断:interrupt 依赖 checkpointer,暂停后状态无处保存就无法恢复。
收藏资产:机制—写法对照表 + 迁移自检清单
机制对照表(对应上面 5 个示例):
| 机制 | 解决什么 | 关键写法 | 出处 |
|---|---|---|---|
| State | 状态怎么声明与合并 | Annotation.Root + 字段级 reducer/default |
basic-graph.mjs#L9-16 |
| 条件边 | 分支路由 | addConditionalEdges(节点, 条件函数, 映射表) |
conditional-routing.mjs#L51 |
| 条件边自环 | 循环重试 | 条件函数未达标时指回自身 + 终止条件 | loop-retry.mjs#L36-39 |
| checkpointer + thread_id | 多会话持久化与隔离 | compile({checkpointer}) + configurable.thread_id |
checkpointer-memory.mjs#L36-45 |
| interrupt + Command | 高危操作人工确认 | interrupt(payload) → __interrupt__[0].value → new Command({resume}) |
graph-interrupt.mjs |
迁移自检清单(把 LangChain 链改造成图时逐项过):
结尾:一个可迁移的判断
编排方式从线性升级到网状,不改变“节点是函数、状态是数据”的本质——先写最小图建立心智模型,再叠加分支、循环、持久化、中断四个机制。本文 5 个示例基于静态阅读整理、运行未验证,且刻意不调用 LLM API,以聚焦图机制。可以立即执行的一步:跑通 basic-graph,把 drawMermaid 的输出贴进任意 mermaid 渲染器,对照本文核对每张图的形状。
核验说明:平台规则基线 last_verified 2026-07-13;文中技术事实来自本地材料,运行结果未验证。
English / 原文
从 LangChain 到 LangGraph:多 Agent 不是玄学,是 token 账本和干扰问题
摘要 :这篇文章回答一个具体问题——为什么会从 LangChain 走到 LangGraph,图编排、状态持久化、人工中断这些关键词落到代码上长什么样。核心判断:LangGraph 的本质是把工作流组织方式从线性链升级为网状图,三件套是 State + 节点 + 边。文中 5 个最小示例均不调用 LLM API,为静态阅读整理,运行未验证 。
一个决策冲突:所有功能塞给一个 Agent,还是拆开?
先把冲突摆出来。做 Agent 应用时有一个看似省事的选择:单 Agent,把所有 tool 描述、所有功能的 prompt 全部写进 system prompt。代价在仓库笔记里写得很直白(readme.md#L7-12):每次调用全量携带,token 消耗高;无关信息干扰模型,准确率下降 。
拆成多个 Agent 后,每个 Agent 只带最少 prompt——省 token、无干扰、准确率高;主 Agent 下发任务,子 Agent 还能并行处理、互相讨论纠错(readme.md#L17-31)。所以本文的第一个判断是:多 Agent 拆分不是架构玄学,是 token 开销与信息干扰的工程权衡。
而组织多个 Agent(以及任何带分支、循环、暂停的工作流),LangChain 的线性链不够用了。LangChain 与 LangGraph 的分工(readme.md#L33-38):LangChain = 线性工作流编排 + 基础模块(LLM API/loaders/splitter/embedding/vector store/output parser/memory);LangGraph = 网状工作流编排,负责"工作节点 + 组织方式" 。两者是配合关系,不是替代关系。
下面按"遇到什么问题 → LangGraph 给什么答案"的顺序,过一遍 5 个最小示例。
问题 0:一张图最少长什么样?
答案:State + 节点 + 边三件套。这是全文最重要的一段代码(basic-graph.mjs):
import { Annotation, StateGraph, START, END } from '@langchain/langgraph'
const StateAnnotation = Annotation.Root({
text: Annotation({
reducer: (_prev, next) => next,
default: () => "",
}),
})
const step1 = (state) => ({ text: `${state.text} -> step1` })
const step2 = (state) => ({ text: `${state.text} -> step2` })
const graph = new StateGraph(StateAnnotation)
.addNode("step1", step1)
.addNode("step2", step2)
.addEdge(START, "step1")
.addEdge("step1", "step2")
.addEdge("step2", END)
.compile()
const drawable = await graph.getGraphAsync()
console.log(drawable.drawMermaid({ withStyles: true }))
console.log(await graph.invoke({ text: "hello" }))
读这段代码抓三个点:
Annotation.Root({...})声明状态 schema(basic-graph.mjs#L9-16):字段级reducer: (_prev, next) => next决定状态怎么更新(这里新值覆盖旧值),default给初始值。- 节点即函数 (basic-graph.mjs#L18-21):返回值不是完整状态,是"本节点的更新",交给 reducer 合并。
invoke({text:"hello"})沿START → step1 → step2 → END流转,最终 text 为hello -> step1 -> step2(reducer 逻辑推导,运行未验证)。
对应图 API 四要素心智模型(readme.md#L40-48):开始节点(初始状态)、工作节点(职责 + state)、边(连接)、结束节点(最终状态) 。drawMermaid({withStyles:true}) 可以导出 mermaid 文本,随时检查图的形状。
问题 1:分支怎么写?
答案:决策写在节点里,路由交给条件边。示例是"算式走计算、否则走聊天"(conditional-routing.mjs):
const router = (state) => ({
route: /[+\-*]/.test(state.query) ? "math" : "chat",
})
const math = (state) => {
try {
return { answer: String(eval(state.query)) }
} catch {
return { answer: "数学公式有误" }
}
}
const chat = (state) => ({ answer: `聊天模式:${state.query}` })
graph.addConditionalEdges("router", (state) => state.route, {
math: "math",
chat: "chat",
})
注意关注点分离:router 节点只做正则判断 /[+\-*]/.test(query),返回 {route:"math"|"chat"}(#L23-28);addConditionalEdges 的条件函数返回 key,映射表把 key 翻译成下一个节点(#L51)。eval 包在 try/catch 里,失败兜底"数学公式有误"(#L31-39);eval("1+2") 返回 3 可单独验证(test.mjs)。
问题 2:循环怎么写?
答案:没有专门的循环 API,条件边指向自己就是循环 (loop-retry.mjs):
const attempt = (state) => {
const tries = state.tries + 1
const ok = tries >= 3
return { tries, ok, message: `第${tries}次${ok ? "成功" : "失败"}` }
}
graph.addConditionalEdges(
"attempt",
(state) => (state.ok ? "done" : "retry"),
{ retry: "attempt", done: END }
)
tries 计数 + ok = tries >= 3 判定在节点里(#L23-31),条件边在未达标时把路由指回 attempt 自身,达标走 END(#L36-39)。自环 + 计数 + 终止条件 三要素,缺终止条件就是死循环。
问题 3:第二次调用怎么记得上一次?
答案:compile({checkpointer}) + thread_id(checkpointer-memory.mjs):
import { MemorySaver } from '@langchain/langgraph'
const StateAnnotation = Annotation.Root({
visitCount: Annotation({
reducer: (_prev, next) => next,
default: () => 0,
}),
})
const visit = (state) => ({ visitCount: state.visitCount + 1 })
const checkpointer = new MemorySaver()
const graph = new StateGraph(StateAnnotation)
.addNode("visit", visit)
.addEdge(START, "visit")
.addEdge("visit", END)
.compile({ checkpointer })
const config = { configurable: { thread_id: "用户_小张" } }
await graph.invoke({}, config)
await graph.invoke({}, config)
await graph.invoke({}, { configurable: { thread_id: "用户_小李" } })
关键行为(#L36-45):同一 thread_id 连续两次 invoke,第 1 次 visitCount = 1,第 2 次基于上次状态累加为 2;换 thread_id 从初始值重新计数,会话相互隔离 。MemorySaver 只存内存,中断/暂停/失败后可继续;真正持久化按材料指引可换 sqlite、redis(readme.md#L56-63)。
问题 4:高危操作执行前怎么让人确认?
答案:interrupt() 暂停 + Command({resume}) 恢复。场景是转账确认——图执行"向张三转账 $100"前停下来等人工审批(graph-interrupt.mjs):
const showTransfer = (state) => ({
actionSummary: "向张三转账$100",
})
const waitConfirm = (state) => {
const text = interrupt({
hint: "中断里输入[确认]或者备注后回车,图才会继续执行",
actionSummary: state.actionSummary,
})
return { useInput: String(text) }
}
const graph = new StateGraph(StateAnnotation)
.addNode("showTransfer", showTransfer)
.addNode("waitConfirm", waitConfirm)
.addEdge(START, "showTransfer")
.addEdge("showTransfer", "waitConfirm")
.addEdge("waitConfirm", END)
.compile({ checkpointer: new MemorySaver() })
const config = { configurable: { thread_id: "interrupt-demo" } }
const paused = await graph.invoke({}, config)
console.log("待你确认", paused.__interrupt__?.[0]?.value)
const done = await graph.invoke(new Command({ resume: line }), config)
console.log("done", done)
闭环四步:节点内 interrupt({hint, actionSummary}) 暂停并抛出 payload(#L27-33)→ 调用方从 paused.__interrupt__?.[0]?.value 读中断信息(#L55-56)→ 示例用 node:readline/promises 读命令行输入模拟人工确认(#L10、L59-66)→ new Command({resume: line}) 配合同一个 thread_id 再次 invoke,从暂停点继续(#L68)。这张图 drawMermaid 导出的形状:__start__ → showTransfer → waitConfirm → __end__(g.md)。一个推导判断:interrupt 依赖 checkpointer ,暂停后状态无处保存就无法恢复。
收藏资产:机制—写法对照表 + 迁移自检清单
机制对照表 (对应上面 5 个示例):
| 机制 | 解决什么 | 关键写法 | 出处 |
|---|---|---|---|
| State | 状态怎么声明与合并 | Annotation.Root + 字段级 reducer/default |
basic-graph.mjs#L9-16 |
| 条件边 | 分支路由 | addConditionalEdges(节点, 条件函数, 映射表) |
conditional-routing.mjs#L51 |
| 条件边自环 | 循环重试 | 条件函数未达标时指回自身 + 终止条件 | loop-retry.mjs#L36-39 |
| checkpointer + thread_id | 多会话持久化与隔离 | compile({checkpointer}) + configurable.thread_id |
checkpointer-memory.mjs#L36-45 |
| interrupt + Command | 高危操作人工确认 | interrupt(payload) → __interrupt__[0].value → new Command({resume}) |
graph-interrupt.mjs |
迁移自检清单 (把 LangChain 链改造成图时逐项过):
结尾:一个可迁移的判断
编排方式从线性升级到网状,不改变"节点是函数、状态是数据"的本质 ——先写最小图建立心智模型,再叠加分支、循环、持久化、中断四个机制。本文 5 个示例静态阅读整理、运行未验证,且刻意不调用 LLM API 以聚焦图机制。可以立即执行的一步:跑通 basic-graph,把 drawMermaid 的输出贴进任意 mermaid 渲染器,对照本文核对每张图的形状。
核验说明:平台规则基线 last_verified 2026-07-13;文中技术事实来自本地材料,运行结果未验证。