写在前面:之前学 LangChain 时,我们做过不少线性工作流------
chain = prompt | model | parser,一步接一步,像流水线。但 readme 今天抛出了一个残酷的现实------复杂的 Agent 产品基本都是多 Agent 架构 。为什么?因为单 Agent 把什么都扛在身上太累了。这就像一家餐厅:LangChain 是一条"传菜流水线",所有菜都从同一个窗口出;而真正的连锁餐饮是"中央厨房"------切菜、炒菜、装盘、质检各有专人,并行开工。今天的主角 LangGraph,就是那张把各种"专人"串成网状协作的流程图。readme 说得直白:LangChain 线性编排,LangGraph 网状编排。以下所有代码均来自课堂真实文件。
一、单 Agent 到底累在哪?
readme 先分析了单 Agent 架构的痛点:
"单 Agent 架构下,所有 tool 的描述,每个功能的 prompt 都放到 system prompt 里。实际上执行每个功能只需要一部分 prompt,但每次都带上。token 消耗更高,更重要的是很多无关信息干扰,思考效率低且容易出错。"
翻译一下------假设一个 Agent 会 10 个技能(查天气、写代码、订机票、翻译......)。它的 system prompt 里躺着全部 10 个技能的描述。可你这次只是问天气------它却背着另外 9 个技能的说明书在思考。
两个代价:
| 代价 | 说明 |
|---|---|
| token 浪费 | 无关的 prompt 每次都算钱 |
| 干扰思考 | 无关信息多了,LLM 容易"想歪",准确率下降 |
readme 的公式先回顾了一下:
"Agent = LLM(大脑)+ Harness(tool + mcp + rag + skill ...)"
单 Agent 只有一个 LLM 大脑------所有决策都靠它一步步想、一个个 tool 调。大脑再强,同时扛 10 个工种也累。
二、多 Agent:从全能小作坊到中央厨房
readme 描述了多 Agent 的理想形态:
"多 Agent 多个大脑,并行思考。主 Agent 下发任务,子 Agent 并行处理完成后返回。每个大脑需要选择合适的模型。agent 组合式,按需加载,动态加载。"
"多 agent 分工合作,编程 Agent 负责写代码,让测试 agent 测试 TDD,让验证 Agent 验证代码是否符合预期。告诉主 Agent 通过了。"
一个经典的"写代码"分工场景:
markdown
主 Agent(产品经理/项目经理)
│ 下发任务
├── 编程 Agent:写代码
├── 测试 Agent:TDD 测试
└── 验证 Agent:验证代码是否符合预期
│
└── 结果上报主 Agent
编程的写代码、测试的写测试、验证的做验收------各管一摊,并行开工。
readme 总结了多 Agent 的三大好处:
"基于三个原因:1. 决策准确率高,token 消耗更低。每个 Agent 只带必要的最少 Prompt,没有冗余信息干扰。调用 llm 次数多,但更省 token。2. 并行思考和任务处理。主管分派子任务,子 Agent 并行处理,整体效率更高。3. 多角色互相讨论,纠错能力更强。AutoGen 法庭。"
| 优势 | 关键点 |
|---|---|
| 决策准确率 ↑ / token ↓ | 每个 Agent 只带自己需要的 prompt |
| 并行处理 | 主管分派,子 Agent 同时干活 |
| 纠错能力 | 多角色互评------"AutoGen 法庭" |
"AutoGen 法庭"这个例子很形象------微软 AutoGen 做过一个 demo:多个 Agent 扮演检察官、辩护律师、法官,互相辩论质证,最终得出更严谨的结论。多角色互喷,反而比一个人自说自话更靠谱。
"单 Agent 只有一个 llm 大脑,需要一步步思考,调用 tool。规则。多 Agent 多个大脑,并行思考。"
规则 = 单线程;并行 = 多线程。这就是本质区别。
三、LangChain → LangGraph:从流水线到地铁网
readme 给出了演进路线:
"llm api, document loaders, splitter, embedding, vector store, output parser, memory... 基础模块。langchain 线性工作流编排。langgraph 网状工作流编排。"
基础模块两边共用------LLM API、文档加载、向量库、解析器、记忆......LangGraph 不是推倒重来,而是把 LangChain 的积木换了一种拼法。
| 对比 | LangChain | LangGraph |
|---|---|---|
| 编排形态 | 线性(一条道走到黑) | 网状(分支、循环、暂停) |
| 比喻 | 流水线 / 队列 | 地铁线路网 |
| 复杂度 | 简单流程够用 | 复杂 Agent 协作 |
| 典型能力 | prompt → model → parser | 状态 + 节点 + 边 + 条件跳转 |
为什么要网状?因为真实流程没有一条直线走完的------要判断走哪条分支(你是数学题还是闲聊?)、要循环重试(失败了再来一次)、要暂停等人确认(转账确认)。 这些都是线性编排做不到的。
readme 对网状编排 API 的概括只有一句话,但字字关键:
"工作节点 + 组织方式(api)。开始节点------初始状态。工作节点------职责 状态 state。链接工作节点。"
三个概念:节点(做什么)、状态(记什么)、边(怎么走)。
四、状态图基础:第一个 LangGraph 程序
basic-graph.mjs 是最小可运行的 LangGraph 程序------两个节点串成一条线。
完整代码
javascript
import {
Annotation, // 状态值的描述
END, // 结束节点
START, // 开始节点
StateGraph, // 状态图:流程编排器,节点的组织
} from "@langchain/langgraph";
const StateAnnotation = Annotation.Root({
text: Annotation({
// reducer 怎么处理状态的改变
reducer: (_prev, next) => next, // js 数组reduce 消消乐
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 result = await graph.invoke({ text: "hello" });
console.log(result); // { text: "hello -> step1 -> step2" }
四个核心概念
1. Annotation:状态的定义
javascript
const StateAnnotation = Annotation.Root({
text: Annotation({
reducer: (_prev, next) => next,
default: () => "",
})
})
Annotation.Root 定义整个图共享的状态结构。每个字段两个配置:
reducer:节点返回新值时怎么合并旧状态。(_prev, next) => next就是"用新的覆盖旧的"default:初始默认值
注释打了个比方:
"js 数组 reduce 消消乐"
reduce 是 JS 数组的归并方法------把一数组"消"成一个值。这里 reducer 决定"上一站的状态"和"节点返回的新值"怎么变成"下一站的状态"。(_prev, next) => next 最简单------旧的不看,直接上新值。
2. 节点:函数就是节点
javascript
const step1 = (state) => ({ text: `${state.text} -> step1` });
在 LangGraph 里,一个普通函数就是一个节点 ------接收当前 state,返回要更新的部分。step1 收到 {text: "hello"},返回 {text: "hello -> step1"}。
3. 边:节点怎么连
javascript
.addNode("step1", step1)
.addNode("step2", step2)
.addEdge(START, "step1") // 开始 → step1
.addEdge("step1", "step2") // step1 → step2
.addEdge("step2", END) // step2 → 结束
addNode 注册节点(名字 + 函数),addEdge 连接节点。START 和 END 是内置的特殊节点------图的起点和终点。
整个流程:
sql
START → step1 → step2 → END
↓ ↓ ↓
"hello" 加 step1 加 step2
4. compile + invoke
javascript
.compile()
const result = await graph.invoke({ text: "hello" });
compile() 编译工作流(准备执行),invoke({text: "hello"}) 用初始状态启动图。结果:
arduino
{ text: "hello -> step1 -> step2" }
状态像接力棒一样,从 START 一路传到 END,每经过一个节点就多一段内容。
附赠技能:mermaid 可视化
basic-graph.mjs 里还有一段画图代码:
javascript
const drawable = await graph.getGraphAsync();
const mermaid = drawable.drawMermaid({ withStyles: true });
console.log(mermaid);
代码注释说:
"mermaid 文本画图工具,简单的 markdown,自动生成流程图。可视化整个节点流转关系。"
LangGraph 自带把图转成 mermaid 流程图的能力------一种用文本描述流程图的标记语言。运行这段代码,控制台会输出 mermaid 语法,粘贴到支持 mermaid 的工具(Typora、GitHub、mermaid.live)就能看到节点流转的可视化图。
用代码描述流程,还能自动画出流程图------这就是 StateGraph 的"可视化红利"。
五、条件路由:地铁换乘的分叉口
basic-graph 是直线------一条道走到黑。但真实业务要分叉:用户问"1+2"要走计算节点,用户说"你好"要走闲聊节点。
conditional-routing.mjs 演示了条件路由。
状态定义
javascript
const StateAnnotation = Annotation.Root({
query: Annotation({
reducer: (_prev, next) => next,
default: () => "",
}),
route: Annotation({
reducer: (_prev, next) => next,
default: () => "chat", // 默认走 chat
}),
})
两个字段:query(用户问题)、route(路由决定)。默认 route: "chat"。
路由器:看门人
javascript
const router = (state) => {
const isMath = /[+\-*]/.test(state.query); // 检测是否含 + - *
return { route: isMath ? "math" : "chat" }
}
router 节点用正则 检测 query 里有没有 +、-、*------有就是数学问题,否则是闲聊。
javascript
console.log("result", await graph.invoke({query: "你好"})) // 走 chat
console.log("result", await graph.invoke({query: "1+2"})) // 走 math
两个处理节点
javascript
const mathNode = (state) => {
try {
return { answer: String(eval(state.query)) } // 计算表达式
} catch {
return { answer: "表达式无法计算" }
}
}
const chatNode = (state) => ({ answer: `你说的是:${state.query}` })
- math 节点:
eval(state.query)计算表达式------"1+2" → "3" - chat 节点:原样复述------"你说的是:你好"
readme 专门强调了一句:
"eval() 会把传入的字符串当作 JS 代码来执行,返回代码执行结果。"
test.mjs 也验证了这一点:
javascript
let str = "1+2";
console.log(eval(str)); // 3
eval 是把字符串当代码执行------"1+2" 被当作 JS 表达式算出 3。注意课堂代码用 try/catch 兜底------因为 eval 遇到不合法表达式(比如 "1+")会抛异常,捕获后返回"表达式无法计算"。
条件边:路由分叉的关键
javascript
const graph = new StateGraph(StateAnnotation)
.addNode("router", router)
.addNode("math", mathNode)
.addNode("chat", chatNode)
.addEdge(START, "router") // 走向固定:先进 router
.addConditionalEdges("router", (state) => state.route, {
math: "math", // route === "math" → 走 math 节点
chat: "chat", // route === "chat" → 走 chat 节点
})
.addEdge("math", END)
.addEdge("chat", END)
.compile()
核心是 addConditionalEdges------三个参数:
| 参数 | 含义 | 本例 |
|---|---|---|
| 起始节点 | 从哪开始判断 | "router" |
| 判断函数 | 根据 state 决定走哪条路 | (state) => state.route |
| 路由表 | 返回值 → 目标节点映射 | { math: "math", chat: "chat" } |
流程变成:
sql
┌──────────────┐
▼ │
START → router │
│ │
state.route? │
┌────┴────┐ │
▼ ▼ │
math chat │
│ │ │
▼ ▼ │
END END ────────┘
地铁到这里出现了换乘站------同一个入口(router),根据目的地(route)分向不同线路。
六、为什么要从"流水线"升级到"地铁网"
回到开头的问题------LangGraph 到底比 LangChain 强在哪?
一条流水线只能做一件事:A → B → C → D。它假设流程是确定的、线性的。
但真实的 Agent 流程充满了不确定性:
| 真实场景 | 需要的编排能力 | LangChain 能吗 |
|---|---|---|
| 判断问题是数学还是闲聊 | 条件分支 | 勉强(写死在代码里) |
| 调用工具失败要重试 | 循环 | 不能 |
| 转账前要等用户确认 | 暂停 / 中断 | 不能 |
| 记住上次聊到哪 | 状态持久化 | 不能 |
LangGraph 的 StateGraph 把这些都变成了一等公民------状态(Annotation)、节点(函数)、边(addEdge)、条件边(addConditionalEdges)、还有下期要讲的循环、检查点、中断。
网状不是炫技,是复杂流程的刚需。
readme 一句话总结了两代框架的关系:
"工作节点 + 组织方式(api)。简单 Agent -> 复杂多 Agent 协作。"
LangChain 负责简单 Agent(一条流水线),LangGraph 负责复杂多 Agent 协作(一张地铁网)。两者共享底层 LLM 基础设施,只是"组织方式"升级了。
PS:下期我们继续坐地铁------遇到坐过站怎么办(循环重试)、出站忘带卡怎么办(状态持久化)、过闸机要人工开箱检查怎么办(中断确认)。LangGraph 的网状世界,才刚刚铺开。