一、 为什么我们需要多 Agent?
随着大模型应用的深入,复杂的 Agent 产品基本都是多 Agent 架构。我们在开发中经常会遇到单 Agent 带来的诸多痛点:
- 上下文开销巨大:单 Agent 架构下,所有 Tool 的描述、每个功能的 Prompt 都堆砌到 System Prompt 里。
- 信息干扰严重 :实际执行每个具体功能只需一部分 Prompt,但单 Agent 每次都要带上全部上下文。这不仅导致 Token 消耗更高 ,更致命的是大量的无关信息会导致模型思考效率降低且容易出错。
- 并发困难:单 LLM 只有一个"大脑",只能一步步思考,无法真正并行。
如何拆分成多 Agent?
每个子 Agent 只保留需要的 Prompt。执行具体功能时,消耗的 Token 更少,没有无关信息干扰,准确率自然更高。
多 Agent 的三大核心优势
- 决策准确率高,Token 消耗更低:调用 LLM 次数虽然可能增多,但每次只带必要的最小 Prompt,没有冗余信息干扰,综合成本反而下降。
- 并行思考与任务处理:主 Agent 下发任务,子 Agent 并行处理(如编程 Agent 写代码,测试 Agent 同步写测试代码 TDD),整体效率更高。
- 多角色互相讨论,纠错能力更强:借鉴 AutoGen 的"法庭"模式,通过多角色辩论大幅提升结果可靠性。
Agent 公式 :Agent = LLM + Harness (tool + mcp + rag + skill + ...)。
二、 LangChain vs LangGraph:从线性到网状
在多 Agent 编排领域,我们的工具也在演进:
- LangChain :擅长线性工作流编排(如基础模块:llm api、document loaders、splitter、embedding、vector store、output parser、memory)。
- LangGraph :专为网状 工作流编排设计。它引入了
State(状态图),由工作节点 + 组织方式(API) 构成,完美支持复杂 Agent 协作。
LangGraph 核心概念
在 LangGraph 中,一切皆图(Graph):
- 开始节点 (START) :初始化状态。
- 工作节点 (Node) :职责是接收 State 并返回新的 State。
- 连接工作节点 (Edge) :定义节点间的流转方向。
- 分支与循环 (Conditional Edges) :根据 State 动态决定下一步走向。
- 最终状态 (END) :工作流终止。
此外,持久化(Persistence)至关重要。为了应对 Agent 执行中断、失败、暂停或需要授权的情况,我们需要将 State 保存到内存(MemoryServer / MemorySaver)或数据库(SQLite, Redis)中,下次继续执行,避免从头重跑。
三、 LangGraph 实战演练
下面我们从基础到进阶,逐步实现 LangGraph 的四大核心能力。
1. 基础图构建与可视化 (basic-graph.mjs)
一切从最基础的 Graph 开始。我们需要定义状态(State)和节点(Node)。
javascript
javascript
import {
Annotation, // 注释 工作流的状态值的描述 数据 state
END, // 结束节点
START, // 开始节点
StateGraph, // 状态图 流程编排器 节点的组织
} from '@langchain/langgraph';
// 状态声明,使用 Annotation 声明一个字段
const StateAnnotation = Annotation.Root({
text: Annotation({ // 只有 text 字段
// _prev 来到当前节点之前的状态,next 当前节点得到的状态
// reducer 怎么处理状态的改变
reducer: (_prev, next) => next, // js 数组reduce 消消乐 text状态如何变
default: () => "", // 默认值
})
});
// 定义节点
// 函数就是节点
// 返回值 是下一个节点的状态
// reducer 处理状态的改变
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(); // 编译工作流 执行
// 可视化的图
// mermaid 文本画图工具,简单的markdown,自动生成流程图
// 可视化整个节点流转关系。
const drawable = await graph.getGraphAsync();
const mermaid = drawable.drawMermaid({ withState: true });
console.log(mermaid);
2. 条件分支与路由 (conditional-routing.mjs)
在实际业务中,我们需要根据用户的输入分发给不同的节点处理(例如:数学计算 vs 日常闲聊)。
javascript
javascript
import {
Annotation, END, START, StateGraph
} from '@langchain/langgraph';
// 状态声明
const StateAnnotation = Annotation.Root({
query: Annotation({ reducer: (_prev, next) => next, default: () => "" }),
route: Annotation({ reducer: (_prev, next) => next, default: () => "chat" }),
answer: Annotation({ reducer: (_prev, next) => next, default: () => "" }),
});
// 节点声明 走向可以选择的
const router = (state) => {
const isMath = /[+-*/]/.test(state.query); // 匹配加减乘除任意一个运算符
// 下一步怎么走?
// 如果是数学问题,走math 节点,否则走chat 节点
return {
route: isMath ? "math" : "chat",
}
}
const mathNode = (state) => {
try {
return {
answer: String(eval(state.query)) // 匹配
}
} catch {
return {
answer: "表达式无法计算"
}
}
}
const chatNode = (state) => ({
answer: `你说的是:${state.query}`,
})
const graph = new StateGraph(StateAnnotation)
// 声明路由节点, name 节点的名称
.addNode("router", router)
.addNode("math", mathNode)
.addNode("chat", chatNode)
.addEdge(START, "router") // 走向固定
// 条件跳转 state.route 值判断,math, chat
// 分支条件
.addConditionalEdges(
"router",
(state) => state.route,
{
math: "math",
chat: "chat",
},
)
.addEdge("math", END)
.addEdge("chat", END)
.compile(); // 编译状态图 执行
// 调用测试
console.log("result:", await graph.invoke({query: "你好"}));
console.log("result:", await graph.invoke({query: "1+2"}));
3. 循环与重试机制 (loop-retry.mjs)
Agent 经常会遇到失败重试的场景。利用条件边,我们可以轻松实现循环。
javascript
javascript
import {
Annotation, END, START, StateGraph
} from '@langchain/langgraph';
const StateAnnotation = Annotation.Root({
tries: Annotation({ reducer: (_prev, next) => next, defaultValue: 0 }),
ok: Annotation({ reducer: (_prev, next) => next, default: () => false }),
message: Annotation({ reducer: (_prev, next) => next, default: () => "" }),
})
const attempt = (state) => {
const tries = state.tries + 1;
const ok = tries >= 3;
return {
tries,
ok,
message: ok ? `第${tries}次成功尝试` : `第${tries}次失败`,
}
}
const graph = new StateGraph(StateAnnotation)
.addNode("attempt", attempt)
.addEdge(START, "attempt")
.addConditionalEdges("attempt", (state) => state.ok ? "done" : "retry", {
retry: "attempt", // 循环回到 attempt 节点
done: END,
})
.compile();
console.log("result:", await graph.invoke({tries: 0}));
4. 状态持久化与多用户隔离 (checkpointer-memory.mjs)
如何让 Agent 记住上下文?如何区分不同用户的会话?LangGraph 提供了 MemorySaver。
javascript
javascript
import {
Annotation, END, START, MemorySaver, StateGraph
} from '@langchain/langgraph';
const StateAnnotation = Annotation.Root({
// session 相关会话 某人 访问次数
visitCount: Annotation({ reducer: (_prev, next) => next, default: () => 0 }),
message: Annotation({ reducer: (_prev, next) => next, default: () => "" }),
})
// 计数的节点
function recordVist(state) {
const visitCount = state.visitCount + 1;
const message = visitCount === 1
? "这是你在本会话里第1次进入。"
: `这是你在本会话里第${visitCount}次进入。`;
return {
visitCount,
message,
}
}
const graph = new StateGraph(StateAnnotation)
.addNode("recordVist", recordVist)
.addEdge(START, "recordVist")
.addEdge("recordVist", END)
const checkpointer = new MemorySaver(); // 实例化一个内存保存器
const app = graph.compile({
checkpointer // 挂载持久化
})
// 多用户隔离(通过 thread_id 区分)
const user1 = { configurable: { thread_id: "用户-小张" } };
const user2 = { configurable: { thread_id: "用户-小李" } };
const res1 = await app.invoke({}, user1); // 小张第一次
console.log(res1);
const res2 = await app.invoke({}, user1); // 小张第二次
console.log(res2);
const res3 = await app.invoke({}, user1); // 小张第三次
console.log(res3);
const res4 = await app.invoke({}, user2); // 小李第一次,独立计数
console.log(res4);
5. 中断与恢复:Human-in-the-loop (graph-interrupt.mjs)
对于敏感操作(如转账),我们需要人工确认。LangGraph 允许我们中断图执行,并在获取用户输入后恢复。
javascript
javascript
import {
Annotation, END, START, StateGraph, Command, interrupt, MemorySaver
} from '@langchain/langgraph';
import { createInterface } from 'node:readline/promises';
// graph 设计图
const StateAnnotation = Annotation.Root({
actionSummary: Annotation({ reducer: (_prev, next) => next, default: () => "" }),
userInput: Annotation({ reducer: (_prev, next) => next, default: () => "" }),
})
const showTransfer = () => ({
actionSummary: "向张三转账 $100"
})
const waitConfirm = (state) => {
const text = interrupt({ // 中断
hint: "终端里输入[确认]或者备注后回车,图才会继续",
actionSummary: state.actionSummary
});
return { userInput: 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);
// node 业务恢复执行
const rl = createInterface({
input: process.stdin,
output: process.stdout
});
const line = (await rl.question("> ")).trim();
console.log("你输入了", line);
await rl.close();
// 使用 Command 恢复执行,并传入用户输入
const done = await graph.invoke(new Command({resume: line}), config);
console.log("done:", done);
四、 总结
从 LangChain 的线性编排到 LangGraph 的网状编排,是 AI Agent 从"单兵作战"走向"团队协作"的必经之路。
通过 LangGraph,我们可以轻松实现:
- State 状态共享 :利用
Annotation.Root和reducer精细化控制状态流转。 - 灵活的控制流 :通过条件边(
addConditionalEdges)实现复杂的路由(Router)与循环(Loop/Retry)。 - 生产级的持久化 :结合
MemorySaver/SqliteSaver和thread_id,实现多用户会话隔离与断点续传。 - Human-in-the-loop :通过
interrupt()和Command(resume),在关键业务节点引入人工审核。
多 Agent 协作的大门已经打开,赶紧在你的业务中试试 LangGraph 吧!