你有没有遇到过这种情况:用 LangChain 写了个 Agent,一切顺利,直到你的 PM 说"能不能让它先思考,再问用户确认,失败了还能重试?"——然后你盯着那条线性 chain,陷入了沉默。别担心,LangGraph 就是来拯救你的。
为什么需要它?(Why)
LangChain LCEL 的"天花板"
LangChain 的 LCEL(LangChain Expression Language)用来搭线性 pipeline 非常爽:
prompt → model → parser → tool → 完事
但现实中的 Agent 往往不是"一条直线"——它需要:
- 循环:调用工具 → 分析结果 → 再调用工具 → ……直到找到答案
- 等待:暂停执行,等人类审批,再继续
- 回溯:某一步失败了,回滚状态重试
- 并行:同时处理多个子任务,汇总结果
LCEL 遇到这些场景,基本就翻车了。
AgentExecutor 已在倒计时
如果你在用 AgentExecutor,官方已经宣布它将在 2026 年底废弃。现在是时候把手里的项目迁移到 LangGraph 了——越早越省心。
LangGraph 解决了什么
LangGraph 的核心思路是把 Agent 的执行过程建模成一张有向图(Directed Graph):
- 节点(Node) = 一个处理步骤(调用 LLM、执行工具、做决策……)
- 边(Edge) = 步骤之间的跳转关系,可以是固定的,也可以是条件分支
- 状态(State) = 贯穿所有节点的共享数据,自动持久化
这样,循环、分支、等待、并行都变成了图结构的自然表达,而不是代码层面的硬编码逻辑。
它是什么?(What)
LangGraph 的 TypeScript 版本由 @langchain/langgraph npm 包提供(当前版本约 1.2.6)。你只需要理解四个核心概念:
1. State — 贯穿全局的共享内存
State 是图的"血液",每个节点都可以读取和更新它。用 StateSchema + MessagesValue 定义:
import { StateSchema, MessagesValue, ReducedValue } from "@langchain/langgraph";
import * as z from "zod";
// 定义 State:messages 字段自动追加,不会覆盖
const AgentState = new StateSchema({
messages: MessagesValue, // 内置消息列表 reducer,自动 append
stepCount: new ReducedValue( // 自定义 reducer:累加步骤数
z.number().default(0),
{ reducer: (x, y) => x + y }
),
});
MessagesValue 是 TypeScript 版中 Python add_messages 的等价物,它内置了消息列表的追加逻辑——节点只需返回新消息,框架自动合并,不会覆盖历史。
2. Node — 图中的处理单元
Node 就是一个 TypeScript 函数,接收当前 State,返回对 State 的更新(不是全量替换):
import { GraphNode } from "@langchain/langgraph";
import { AIMessage } from "@langchain/core/messages";
// 类型注解:GraphNode<typeof YourState>
const agentNode: GraphNode<typeof AgentState> = async (state) => {
// 读取 state
const lastMessage = state.messages.at(-1);
// 返回部分更新,MessagesValue reducer 会自动 append
return {
messages: [new AIMessage("我思考完毕,准备调用工具")],
stepCount: 1, // reducer 会把这个 +1 累加进去
};
};
3. Edge — 控制执行流程的箭头
边分两种:
import { StateGraph, START, END } from "@langchain/langgraph";
import { ConditionalEdgeRouter } from "@langchain/langgraph";
import { AIMessage } from "@langchain/core/messages";
// 固定边:总是从 A 到 B
graph.addEdge("agentNode", "toolNode");
// 条件边:根据 State 决定去哪里(实现循环的关键!)
const shouldContinue: ConditionalEdgeRouter<typeof AgentState, "toolNode"> = (state) => {
const lastMessage = state.messages.at(-1);
// 如果 AI 发起了工具调用,继续循环
if (AIMessage.isInstance(lastMessage) && lastMessage.tool_calls?.length) {
return "toolNode";
}
// 否则结束
return END;
};
graph.addConditionalEdges("agentNode", shouldContinue, ["toolNode", END]);
注意:TypeScript 版全程用驼峰命名(
addEdge、addConditionalEdges),Python 版是下划线(add_edge)。别混了。
4. Checkpointer — 状态的持久化引擎
Checkpointer 负责把每一步的 State 快照存储下来,这是实现"记忆"和"Human-in-the-Loop"的基础:
import { MemorySaver } from "@langchain/langgraph";
// 开发环境用内存版,零配置
const checkpointer = new MemorySaver();
const graph = new StateGraph(AgentState)
// ...添加节点和边...
.compile({ checkpointer }); // 编译时传入 checkpointer
怎么用?(How)
最小可运行示例:天气查询 Agent(无需 API Key)
先把环境搭好,下面这个示例用 mock 函数模拟 LLM,不需要任何 API Key,npm install && npm start 即可运行。
package.json:
{
"name": "langgraph-ts-demo",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "tsx src/index.ts"
},
"dependencies": {
"@langchain/langgraph": "^1.2.6",
"@langchain/core": "^0.3.0"
},
"devDependencies": {
"tsx": "^4.0.0",
"typescript": "^5.0.0"
}
}
tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
src/index.ts:
import {
StateGraph,
StateSchema,
MessagesValue,
GraphNode,
ConditionalEdgeRouter,
START,
END,
} from "@langchain/langgraph";
import {
HumanMessage,
AIMessage,
ToolMessage,
} from "@langchain/core/messages";
import * as z from "zod";
// ── 1. 定义 State ──────────────────────────────────────
const AgentState = new StateSchema({
messages: MessagesValue, // 自动 append,不覆盖历史
});
// ── 2. Mock 工具:天气查询(替代真实 API)──────────────
const weatherDatabase: Record<string, string> = {
北京: "晴天,25°C,微风",
上海: "多云,22°C,东南风",
广州: "小雨,28°C,南风",
};
function mockWeatherTool(city: string): string {
return weatherDatabase[city] ?? `抱歉,暂无 ${city} 的天气数据`;
}
// ── 3. Mock LLM:模拟"决策 → 调用工具 → 总结"的两轮对话 ──
let callCount = 0;
function mockLLM(messages: (HumanMessage | AIMessage | ToolMessage)[]): AIMessage {
callCount++;
if (callCount === 1) {
// 第一轮:LLM 决定调用天气工具
const userMsg = messages[0].content as string;
const cityMatch = userMsg.match(/北京|上海|广州/);
const city = cityMatch ? cityMatch[0] : "北京";
return new AIMessage({
content: "",
tool_calls: [
{
id: "call_001",
name: "getWeather", // 工具名
args: { city },
type: "tool_call",
},
],
});
} else {
// 第二轮:LLM 收到工具结果,生成最终回复
const toolMsg = messages.at(-1) as ToolMessage;
return new AIMessage(`根据最新数据:${toolMsg.content}。祝您出行愉快!`);
}
}
// ── 4. 定义 Agent 节点(调用 LLM)──────────────────────
const agentNode: GraphNode<typeof AgentState> = async (state) => {
// 用 mock LLM 替代真实 ChatOpenAI
const response = mockLLM(state.messages as any);
return { messages: [response] };
};
// ── 5. 定义 Tool 节点(执行工具调用)──────────────────────
const toolNode: GraphNode<typeof AgentState> = async (state) => {
const lastMessage = state.messages.at(-1);
// 只处理 AIMessage 且有 tool_calls 的情况
if (!AIMessage.isInstance(lastMessage) || !lastMessage.tool_calls?.length) {
return { messages: [] };
}
const results: ToolMessage[] = [];
for (const toolCall of lastMessage.tool_calls) {
if (toolCall.name === "getWeather") {
const result = mockWeatherTool(toolCall.args.city as string);
results.push(
new ToolMessage({
content: result,
tool_call_id: toolCall.id ?? "call_001", // 与 AIMessage 中的 id 对应
})
);
}
}
return { messages: results };
};
// ── 6. 定义条件边:是否继续调用工具(实现循环)──────────
const shouldContinue: ConditionalEdgeRouter<typeof AgentState, "toolNode"> = (
state
) => {
const lastMessage = state.messages.at(-1);
// 如果最后一条是 AIMessage 且有工具调用 → 去 toolNode
if (AIMessage.isInstance(lastMessage) && lastMessage.tool_calls?.length) {
return "toolNode";
}
// 否则结束
return END;
};
// ── 7. 构建并编译图 ────────────────────────────────────
const graph = new StateGraph(AgentState)
.addNode("agentNode", agentNode) // 注册节点
.addNode("toolNode", toolNode)
.addEdge(START, "agentNode") // 入口 → agentNode
.addConditionalEdges("agentNode", shouldContinue, ["toolNode", END])
.addEdge("toolNode", "agentNode") // 工具执行完 → 回到 agentNode
.compile();
// ── 8. 运行并流式输出每步结果 ──────────────────────────
async function main() {
console.log("🚀 天气查询 Agent 启动\n");
const inputMessages = [new HumanMessage("帮我查一下北京的天气")];
// .stream() 可以看到每个节点的输出,调试利器
for await (const step of await graph.stream({ messages: inputMessages })) {
const [nodeName, nodeOutput] = Object.entries(step)[0];
const msgs = (nodeOutput as any).messages as any[];
console.log(`📍 节点 [${nodeName}] 输出:`);
for (const msg of msgs) {
if (AIMessage.isInstance(msg)) {
if (msg.tool_calls?.length) {
console.log(` → AI 决定调用工具: ${JSON.stringify(msg.tool_calls[0].args)}`);
} else {
console.log(` → AI 最终回复: ${msg.content}`);
}
} else if (msg instanceof ToolMessage) {
console.log(` → 工具返回: ${msg.content}`);
}
}
console.log();
}
}
main().catch(console.error);
运行效果:
🚀 天气查询 Agent 启动
📍 节点 [agentNode] 输出:
→ AI 决定调用工具: {"city":"北京"}
📍 节点 [toolNode] 输出:
→ 工具返回: 晴天,25°C,微风
📍 节点 [agentNode] 输出:
→ AI 最终回复: 根据最新数据:晴天,25°C,微风。祝您出行愉快!
运行方式:
npm install && npm start
快速上手:接入真实 LLM(ChatOpenAI 版)
替换 mock LLM 为真实的 ChatOpenAI,只需改两处:
import { ChatOpenAI } from "@langchain/openai";
import { tool } from "@langchain/core/tools";
import * as z from "zod";
// 定义真实工具
const getWeather = tool(
({ city }: { city: string }) => weatherDatabase[city] ?? "暂无数据",
{
name: "getWeather",
description: "查询指定城市的天气",
schema: z.object({ city: z.string().describe("城市名称") }),
}
);
const toolsByName = { getWeather };
// 替换为真实 LLM,绑定工具
const model = new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0 });
const modelWithTools = model.bindTools([getWeather]);
// agentNode 改为调用真实 LLM
const agentNode: GraphNode<typeof AgentState> = async (state) => {
const response = await modelWithTools.invoke(state.messages);
return { messages: [response] };
};
// toolNode 改为真实执行工具
const toolNode: GraphNode<typeof AgentState> = async (state) => {
const lastMessage = state.messages.at(-1);
if (!AIMessage.isInstance(lastMessage) || !lastMessage.tool_calls?.length) {
return { messages: [] };
}
const results: ToolMessage[] = [];
for (const toolCall of lastMessage.tool_calls) {
const tool = toolsByName[toolCall.name as keyof typeof toolsByName];
const observation = await tool.invoke(toolCall);
results.push(observation);
}
return { messages: results };
};
核心用法
1. 带记忆的多轮对话(MemorySaver + thread_id)
有了 Checkpointer,同一个 thread_id 下的多轮对话会共享历史消息:
import { MemorySaver } from "@langchain/langgraph";
const checkpointer = new MemorySaver(); // 开发环境用内存版
const graphWithMemory = new StateGraph(AgentState)
.addNode("agentNode", agentNode)
.addNode("toolNode", toolNode)
.addEdge(START, "agentNode")
.addConditionalEdges("agentNode", shouldContinue, ["toolNode", END])
.addEdge("toolNode", "agentNode")
.compile({ checkpointer }); // 关键:传入 checkpointer
// 每次调用都带上同一个 thread_id,Agent 就能"记住"上下文
const config = { configurable: { thread_id: "user-alice-session-1" } };
// 第一轮
await graphWithMemory.invoke(
{ messages: [new HumanMessage("你好,我叫 Alice")] },
config
);
// 第二轮:Agent 知道你叫 Alice
const result = await graphWithMemory.invoke(
{ messages: [new HumanMessage("我叫什么名字?")] },
config
);
console.log(result.messages.at(-1)?.content);
// → "你叫 Alice。"
换一个 thread_id,就是全新的对话,互不干扰。
2. Human-in-the-Loop(interrupt + Command)
这是 LangGraph 最强大的特性之一:在 Agent 执行到关键步骤时暂停,等待人类审批:
import { interrupt, Command, MemorySaver } from "@langchain/langgraph";
// 在节点中调用 interrupt() 暂停执行
const reviewNode: GraphNode<typeof AgentState> = async (state) => {
const pendingAction = "发送邮件给全体员工";
// 暂停!把 pendingAction 返回给调用方,等待人类决策
const approved = interrupt({
question: "是否批准以下操作?",
action: pendingAction,
});
if (approved) {
return { messages: [new AIMessage(`✅ 已执行:${pendingAction}`)] };
} else {
return { messages: [new AIMessage("❌ 操作已取消")] };
}
};
// 必须有 checkpointer 才能使用 interrupt!
const graphHITL = new StateGraph(AgentState)
.addNode("reviewNode", reviewNode)
.addEdge(START, "reviewNode")
.compile({ checkpointer: new MemorySaver() });
const config = { configurable: { thread_id: "hitl-demo" } };
// 第一次调用:执行到 interrupt() 时暂停
const paused = await graphHITL.invoke(
{ messages: [new HumanMessage("帮我发一封全员邮件")] },
config
);
console.log(paused.__interrupt__);
// → [{ value: { question: "是否批准...", action: "发送邮件..." }, ... }]
// 人类审批后,用 Command({ resume: true }) 继续执行
const resumed = await graphHITL.invoke(
new Command({ resume: true }), // 传入审批结果
config // 同一个 thread_id!
);
console.log(resumed.messages.at(-1)?.content);
// → "✅ 已执行:发送邮件给全体员工"
3. 并行子图(Send API)
需要同时处理多个独立任务时,用 Send API 实现 Map-Reduce:
import { Send, StateSchema, ReducedValue, GraphNode, StateGraph, START, END } from "@langchain/langgraph";
import * as z from "zod";
// 整体 State:收集所有子任务的结果
const PipelineState = new StateSchema({
cities: z.array(z.string()), // 待查询的城市列表
results: new ReducedValue( // 使用 reducer 收集并行结果
z.array(z.string()).default(() => []),
{ reducer: (x, y) => x.concat(y) }
),
summary: z.string().default(""),
});
// 单个城市查询节点
const queryCityWeather: GraphNode<typeof PipelineState> = async (state) => {
// state.city 是通过 Send 注入的临时字段
const city = (state as any).city as string;
const weather = weatherDatabase[city] ?? "暂无数据";
return { results: [`${city}: ${weather}`] };
};
// 汇总节点
const summarize: GraphNode<typeof PipelineState> = async (state) => {
const summary = state.results.join("\n");
return { summary: `今日天气汇报:\n${summary}` };
};
// 用 Send 动态分发并行任务
const fanOut = (state: typeof PipelineState.State) => {
// 为每个城市创建一个独立的并行任务
return state.cities.map((city) => new Send("queryCityWeather", { city }));
};
const parallelGraph = new StateGraph(PipelineState)
.addNode("queryCityWeather", queryCityWeather)
.addNode("summarize", summarize)
.addEdge(START, "fanOut" as any) // 注意:fanOut 是条件边的路由函数
.addConditionalEdges(START, fanOut) // 从 START 直接 fan-out
.addEdge("queryCityWeather", "summarize")
.addEdge("summarize", END)
.compile();
// 并行查询三个城市
for await (const step of await parallelGraph.stream({
cities: ["北京", "上海", "广州"],
results: [],
summary: "",
})) {
console.log(step);
}
// → queryCityWeather: { results: ['北京: 晴天,25°C,微风'] }
// → queryCityWeather: { results: ['上海: 多云,22°C,东南风'] }
// → queryCityWeather: { results: ['广州: 小雨,28°C,南风'] }
// → summarize: { summary: '今日天气汇报:\n北京: ...\n上海: ...\n广州: ...' }
最佳实践
1. 用 MessagesValue 管理消息,节点只返回新消息
// ✅ 正确:返回新消息,reducer 自动 append
const node: GraphNode<typeof State> = (state) => {
return { messages: [new AIMessage("新消息")] };
};
// ❌ 错误:手动拼接破坏 reducer 语义
const badNode: GraphNode<typeof State> = (state) => {
return { messages: [...state.messages, new AIMessage("新消息")] };
};
2. 开发用 MemorySaver,生产上持久化后端
MemorySaver 数据存在进程内存中,重启就丢失。生产环境需要接入数据库:
// 开发环境
import { MemorySaver } from "@langchain/langgraph";
const checkpointer = new MemorySaver();
// 生产环境(以 PostgreSQL 为例,需安装 @langchain/langgraph-checkpoint-postgres)
// import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";
// const checkpointer = PostgresSaver.fromConnString(process.env.DATABASE_URL!);
3. 节点语义化命名,条件边明确声明目标节点
// ✅ 清晰:节点名 + 条件边目标列表一目了然
graph.addConditionalEdges("callLlm", shouldContinue, ["executeTool", END]);
// ❌ 模糊:单字母节点名,维护噩梦
graph.addConditionalEdges("a", r, ["b", END]);
4. 用 .stream() 调试,而不是盲猜
.invoke() 只返回最终结果,.stream() 能看到每个节点的输出,排查问题效率高 10 倍:
// 调试时优先用 stream,上线后按需切换 invoke
for await (const step of await graph.stream(input)) {
const [node, output] = Object.entries(step)[0];
console.log(`[${node}]`, JSON.stringify(output, null, 2));
}
常见误区
误区一:LangGraph ≠ LangChain 替代品
LangGraph 是 LangChain 生态的补充,不是替代。简单的单次 LLM 调用、RAG 问答,直接用 LCEL 就行,引入 LangGraph 反而增加复杂度。
判断标准:你的流程需要循环、条件分支、持久化、人工干预中的任意一项吗?需要 → LangGraph;不需要 → LCEL 够用。
误区二:不是所有场景都需要 Agent + LangGraph
很多人一上来就用 LangGraph 包装一切,结果维护成本爆炸。记住这条原则:
"能用 if-else 解决的问题,不要用 Agent。"
客服机器人需要查订单 → 普通 LCEL 链就够了。需要"思考 → 查多个系统 → 协调多部门 → 等审批" → 才值得上 LangGraph。
误区三:interrupt() 必须搭配 Checkpointer
这是新手最常踩的坑。没有 Checkpointer,interrupt() 无法保存状态,调用会直接报错:
// ❌ 忘记传 checkpointer,调用 interrupt() 时炸掉
const badGraph = new StateGraph(State)
.addNode("review", reviewNode) // reviewNode 里调用了 interrupt()
.addEdge(START, "review")
.compile(); // 没有 checkpointer!
// ✅ 正确:必须传入 checkpointer
const goodGraph = new StateGraph(State)
.addNode("review", reviewNode)
.addEdge(START, "review")
.compile({ checkpointer: new MemorySaver() }); // ← 必须!
同时,恢复执行时必须使用相同的 thread_id,否则找不到保存的状态。
误区四:不要直接修改 State 对象
TypeScript/JavaScript 没有不可变数据的语言级保证,但 LangGraph 依赖 State 的不可变性来做差量更新和时间旅行(time travel)。直接修改 State 会导致不可预期的行为:
// ❌ 危险:直接 push 修改了原始 state 对象
const badNode: GraphNode<typeof State> = (state) => {
state.messages.push(new AIMessage("直接改原始对象!")); // 🚫 不要这样
return {};
};
// ✅ 安全:返回新对象,让框架合并
const goodNode: GraphNode<typeof State> = (state) => {
return {
messages: [new AIMessage("返回新消息,框架来合并")], // ✅
};
};
总结
一句话定位:LangGraph 是构建有状态、可循环、支持人工干预的 AI Agent 的"基础设施"——当你的 Agent 不再是一条直线,就是它登场的时候。
四步上手路径:
- 装包:
npm install @langchain/langgraph @langchain/core - 跑通 Mock 示例:用本文的天气查询 Demo,理解 State → Node → Edge 的数据流
- 接入真实 LLM:换上
ChatOpenAI+bindTools,观察.stream()输出 - 按需叠加能力:需要记忆 → 加
MemorySaver;需要审批 → 加interrupt;需要并行 → 用Send
参考文档: