Runnable 链就像一个「单线程脚本」,只能按顺序执行。但真实世界的问题往往需要「规划 → 执行 → 评估 → 调整 → 再执行」的循环。LangGraph 让你的 LLM 拥有了真正的「思考能力」。
为什么我们需要 LangGraph?
先看一个真实的对比
| 场景 | RunnableSequence(链) | LangGraph(图) |
|---|---|---|
| 执行路径 | 固定的直线:A → B → C | 动态的图:A → B → ? → ? → END |
| 能否循环 | ❌ 不能 | ✅ 可以(执行 → 评估 → 再执行) |
| 能否分支 | ❌ 不能 | ✅ 可以(条件路由) |
| 状态传递 | 隐式(下一个节点只能拿到上一个的输出) | 显式(所有节点共享全局状态) |
| 适合场景 | 单轮 RAG、固定流程问答 | 复杂推理、多工具 Agent、规划执行 |
举个例子:用户问「北京有什么美食推荐?」
| Runnable 链(固定 3 步) | LangGraph(动态规划) |
|---|---|
| 1. 拆解问题 | 1. 制定计划:① 搜索北京美食 ② 搜索烤鸭历史 ③ 整理推荐 |
| 2. 研究回答 | 2. 执行步骤 ①:搜索北京美食 |
| 3. 综合总结 | 3. 执行步骤 ②:搜索烤鸭历史 |
| 4. 评估进度:是否需要补充? | |
| 5. 生成总结 |
核心差异:Runnable 链是「我让你做什么你就做什么」,而 LangGraph 是「你自己想想要做什么,然后去做」。
一、从 Runnable 链到 LangGraph
阶段 1:RunnableSequence(固定三步)
这是我最初的 deep.ts 实现——固定的三步推理:
const analyzePrompt = PromptTemplate.fromTemplate(`请分析以下问题,拆解成 2-3 个关键要点:{question}`);
const researchPrompt = PromptTemplate.fromTemplate(`请针对以下要点,给出详细的回答思路:{points}`);
const synthesizePrompt = PromptTemplate.fromTemplate(`请综合以下信息,给出最终回答:{analysis}{research}`);
// 固定的三步链const chain = RunnableSequence.from([ { points: analyzePrompt.pipe(llm).pipe(parser) }, { research: researchPrompt.pipe(llm).pipe(parser) }, synthesizePrompt.pipe(llm).pipe(parser),]);执行路径:
问题 → 拆解 → 研究 → 综合 → 答案 (固定) (固定) (固定)问题:
- ❌ 步骤是硬编码的,不管问题简单还是复杂,都是三步
- ❌ 没有评估环节,第一步错了后面全错
- ❌ 不能根据中间结果调整计划
阶段 2:LangGraph Plan & Execute(动态规划)
我用 LangGraph 重构后的 deep-langgraph.ts——真正的「思考型」Agent:
图结构:
[START] ↓ planner ← 制定计划(分解成 N 个步骤) ↓executor ← 执行当前步骤 ↓replanner ← 评估:完成了吗?需要补充吗? ↓ ↘ ↓ (完成) → [END] ↓(继续执行) → executor ← 回到执行节点,循环核心优势:
- ✅ 动态规划:简单问题拆成 2 步,复杂问题拆成 5 步
- ✅ 循环执行:执行 → 评估 → 再执行,直到满意
- ✅ 全局状态:所有节点共享 plan、completed、currentStepIndex
- ✅ 可观测:每一步都可以把状态推送给前端(用户能看到 AI 在「思考」)
二、LangGraph 核心概念拆解
概念 1:State(状态)—— 图的「记忆」
LangGraph 的精髓在于所有节点共享同一个全局状态。这是与 Runnable 链最大的区别。
// src/server/handlers/deep-langgraph.ts:57const PlanExecuteState = Annotation.Root({ // 原始任务(只读,不会变) task: Annotation<string>(),
// 当前计划(数组 reducer:新值覆盖旧值) plan: Annotation<string[]>({ reducer: (_prev, next) => next, default: () => [], }),
// 已完成的步骤结果(追加 reducer:新值追加到旧值) completed: Annotation<string[]>({ reducer: (prev, next) => [...prev, ...next], default: () => [], }),
// 当前执行到第几步(覆盖) currentStepIndex: Annotation<number>({ reducer: (_prev, next) => next, default: () => 0, }),
// 最终结果(有值就代表任务完成) result: Annotation<string>({ reducer: (_prev, next) => next, default: () => "", }),});Reducer 是关键:
| Reducer 行为 | 适用场景 | 例子 |
|---|---|---|
| 覆盖 | (_prev, next) => next | plan、currentStepIndex、result |
| 追加 | (prev, next) => [...prev, ...next] | completed(每步结果都要保留) |
状态流转示例:
| 节点执行后 | task | plan | completed | currentStepIndex | result |
|---|---|---|---|---|---|
| 初始 | 北京美食 | [] | [] | 0 | "" |
| planner 后 | 北京美食 | [“搜索美食”,“搜索历史”] | [] | 0 | "" |
| executor 第 1 次 | 北京美食 | [“搜索美食”,“搜索历史”] | [“烤鸭、炸酱面…“] | 1 | "" |
| executor 第 2 次 | 北京美食 | [“搜索美食”,“搜索历史”] | [“烤鸭…”, “烤鸭起源…“] | 2 | "" |
| replanner 后 | 北京美食 | [“搜索美食”,“搜索历史”] | [“烤鸭…”, “烤鸭起源…“] | 2 | ”北京推荐烤鸭…” |
概念 2:Node(节点)—— 图的「执行者」
每个节点是一个纯函数:输入 State,输出 State 的更新。
节点 1:Planner — 制定计划
// src/server/handlers/deep-langgraph.ts:91const plannerPrompt = PromptTemplate.fromTemplate(`你是一个任务规划专家。请将用户的请求分解为具体的执行步骤。
可用工具:{tools}
请输出 JSON 格式的计划,格式如下:{{"steps": ["步骤1的描述", "步骤2的描述", ...]}}
用户请求:{task}`);
async function planNode( state: typeof PlanExecuteState.State, ws: WebSocket,): Promise<typeof PlanExecuteState.Update> { sendStatus(ws, "🧠 正在分析任务并制定计划...");
const chain = plannerPrompt.pipe(llm).pipe(parser); const result = await chain.invoke({ tools: tools.map((t) => `${t.name}: ${t.description}`).join("\n"), task: state.task, });
// 解析 JSON(LLM 可能输出解释文字,用正则提取 JSON 部分) let steps: string[]; try { const jsonMatch = result.match(/\{[\s\S]*\}/); steps = jsonMatch ? JSON.parse(jsonMatch[0]).steps : [state.task]; } catch { steps = [state.task]; }
const planText = steps.map((s, i) => ` ${i + 1}. ${s}`).join("\n"); sendStatus(ws, `📝 已制定计划:\n${planText}`);
// 返回状态更新 return { plan: steps, currentStepIndex: 0 };}Planner 做了什么:
- 接收当前 State(只有 task 有值)
- 调用 LLM 把任务分解成步骤列表
- 更新 State:
plan= 步骤列表,currentStepIndex= 0 - 把计划推送给前端(用户能看到 AI 在「规划」)
节点 2:Executor — 执行单步
// src/server/handlers/deep-langgraph.ts:137const executorPrompt = PromptTemplate.fromTemplate(`你是一个任务执行专家。请执行以下步骤,并返回执行结果。
步骤:{step}
请直接返回执行结果,简洁明了。`);
async function executeNode( state: typeof PlanExecuteState.State, ws: WebSocket,): Promise<typeof PlanExecuteState.Update> { const currentStep = state.plan[state.currentStepIndex];
sendStatus( ws, `🔄 正在执行步骤 [${state.currentStepIndex + 1}/${state.plan.length}]:${currentStep}`, );
const chain = executorPrompt.pipe(llm).pipe(parser); const result = await chain.invoke({ step: currentStep });
sendStatus(ws, ` ✅ 步骤完成`);
// 返回状态更新:把结果追加到 completed,索引 +1 return { completed: [result], currentStepIndex: state.currentStepIndex + 1, };}Executor 做了什么:
- 从 State 取出
plan[currentStepIndex](当前要执行的步骤) - 调用 LLM 执行这一步
- 更新 State:
completed追加结果,currentStepIndex+ 1 - 把执行状态推送给前端(用户能看到 AI 在「执行」)
节点 3:Replanner — 评估决策
这是整个图最聪明的节点——它负责「思考」:我做完了吗?还需要继续吗?
// src/server/handlers/deep-langgraph.ts:171const replannerPrompt = PromptTemplate.fromTemplate(`你是一个任务重规划专家。请评估执行结果,并决定下一步。
原始任务:{task}已完成的步骤和结果:{completed}剩余计划:{remaining}
请判断:1. 如果任务已经完成,返回:{{"status": "complete", "result": "最终结果"}}2. 如果需要继续执行,返回:{{"status": "continue"}}
请直接输出 JSON。`);
async function replanNode( state: typeof PlanExecuteState.State, ws: WebSocket,): Promise<typeof PlanExecuteState.Update> { const remaining = state.plan.slice(state.currentStepIndex);
// 所有步骤执行完毕 → 自动生成总结 if (remaining.length === 0) { sendStatus(ws, "📋 所有步骤执行完毕,正在生成总结...");
const summaryPrompt = PromptTemplate.fromTemplate(`请根据以下执行结果,生成一个简洁的最终总结。
任务:{task}执行结果:{results}
请用 2-3 句话总结。`); const chain = summaryPrompt.pipe(llm).pipe(parser); const summary = await chain.invoke({ task: state.task, results: state.completed .map((r, i) => `步骤${i + 1}:${r}`) .join("\n"), }); return { result: summary }; }
sendStatus(ws, "📋 正在评估执行进度...");
const chain = replannerPrompt.pipe(llm).pipe(parser); const response = await chain.invoke({ task: state.task, completed: state.completed .map((r, i) => `步骤${i + 1}:${r}`) .join("\n"), remaining: remaining.map((s, i) => `${i + 1}. ${s}`).join("\n"), });
// 解析决策 JSON let decision: { status: string; result?: string }; try { const jsonMatch = response.match(/\{[\s\S]*\}/); decision = jsonMatch ? JSON.parse(jsonMatch[0]) : { status: "continue" }; } catch { decision = { status: "continue" }; }
if (decision.status === "complete") { sendStatus(ws, "🎉 任务完成!"); return { result: decision.result || "任务已完成" }; }
sendStatus(ws, "📋 继续执行剩余计划..."); return {};}Replanner 做了什么:
- 如果所有步骤都执行完了 → 生成最终总结,设置
result(任务完成) - 如果还有步骤没执行 → 调用 LLM 评估:
- 满意了?→
status: "complete",返回结果 - 还需要继续?→
status: "continue",回到 executor 循环
- 满意了?→
概念 3:Conditional Edge(条件边)—— 图的「大脑」
固定的边(A → B)谁都会,条件边才是 LangGraph 的灵魂。
// src/server/handlers/deep-langgraph.ts:256function shouldContinue( state: typeof PlanExecuteState.State,): "executor" | "replanner" | typeof END { if (state.result) return END; // 有结果了 → 结束 if (state.currentStepIndex < state.plan.length) return "executor"; // 还有步骤没执行 → 去执行 return "replanner"; // 执行完了 → 去评估}
function afterReplan( state: typeof PlanExecuteState.State,): "executor" | typeof END { if (state.result) return END; // 评估完说可以结束 → 结束 return "executor"; // 评估完说还要继续 → 回去执行}完整的图构建:
// src/server/handlers/deep-langgraph.ts:278function createGraph(ws: WebSocket) { return new StateGraph(PlanExecuteState) .addNode("planner", (s) => planNode(s, ws)) .addNode("executor", (s) => executeNode(s, ws)) .addNode("replanner", (s) => replanNode(s, ws)) .addEdge(START, "planner") // 起点 → 规划 .addConditionalEdges("planner", shouldContinue) // 规划后 → 决定去哪 .addConditionalEdges("executor", () => "replanner") // 执行完 → 去评估 .addConditionalEdges("replanner", afterReplan) // 评估后 → 决定继续还是结束 .compile();}图执行的可视化:
用户问:北京有什么美食推荐? │ ▼[START] │ ▼planner: 🧠 正在分析任务并制定计划... 📝 已制定计划: 1. 搜索北京著名美食 2. 了解烤鸭的历史背景 3. 整理推荐清单 │ ▼executor: 🔄 正在执行步骤 [1/3]:搜索北京著名美食 ✅ 步骤完成 │ ▼replanner: 📋 正在评估执行进度... 📋 继续执行剩余计划... │ ▼executor: 🔄 正在执行步骤 [2/3]:了解烤鸭的历史背景 ✅ 步骤完成 │ ▼replanner: 📋 正在评估执行进度... 📋 继续执行剩余计划... │ ▼executor: 🔄 正在执行步骤 [3/3]:整理推荐清单 ✅ 步骤完成 │ ▼replanner: 📋 所有步骤执行完毕,正在生成总结... 🎉 任务完成! │ ▼[END]概念 4:Handler 入口 — 把图变成服务
最后把图封装成 WebSocket handler,接入整个服务:
// src/server/handlers/deep-langgraph.ts:299export async function handleDeepLangGraph( ws: WebSocket, session: Session, content: string,): Promise<void> { if (!content.trim()) { ws.send(JSON.stringify({ type: "error", content: "消息不能为空" })); return; }
addUserMessage(session, content);
try { const start = Date.now(); sendStatus(ws, `🚀 启动 LangGraph 深度思考模式...`);
// 创建图并执行 const graph = createGraph(ws); const result = await graph.invoke({ task: content });
const elapsed = Date.now() - start; addAIMessage(session, result.result);
// 返回给前端:最终答案 + 耗时 + 每步的详细信息 ws.send( JSON.stringify({ type: "done", mode: "deep-langgraph", content: result.result, elapsed, summary: { totalSteps: result.completed.length, completed: result.completed, }, }), );
logger.info( `[${session.id}] 🧠 deep-langgraph 完成 (${elapsed}ms, ${result.completed.length} 步)`, ); } catch (error: any) { logger.error(`[${session.id}] deep-langgraph 调用失败`, error.message); ws.send(JSON.stringify({ type: "error", content: `请求失败: ${error.message}` })); }}三、我的工程化经验
经验 1:每个节点都应该是「可观测的」
不要让用户在那干等——把每一步的状态推送给前端:
function sendStatus(ws: WebSocket, content: string) { try { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: "status", content })); } } catch (err: any) { logger.error(`❌ 发送状态消息失败: ${err.message}`); }}前端看到的效果:
🚀 启动 LangGraph 深度思考模式...🧠 正在分析任务并制定计划...📝 已制定计划: 1. 搜索北京著名美食 2. 了解烤鸭的历史背景 3. 整理推荐清单🔄 正在执行步骤 [1/3]:搜索北京著名美食 ✅ 步骤完成📋 正在评估执行进度......🎉 任务完成!用户能看到 AI 在「思考」,体验提升 10 倍。
经验 2:LLM 输出是不可靠的,必须有容错
LLM 可能返回无效的 JSON、可能格式不对、可能超时。你的代码必须扛住:
// Planner 的容错let steps: string[];try { const jsonMatch = result.match(/\{[\s\S]*\}/); steps = jsonMatch ? JSON.parse(jsonMatch[0]).steps : [state.task];} catch { steps = [state.task]; // 解析失败?兜底:直接把整个任务当一步}
// Replanner 的容错let decision: { status: string; result?: string };try { const jsonMatch = response.match(/\{[\s\S]*\}/); decision = jsonMatch ? JSON.parse(jsonMatch[0]) : { status: "continue" };} catch { decision = { status: "continue" }; // 解析失败?默认继续}容错原则:宁可继续执行,也不要崩溃。
经验 3:图的节点要小而专注,不要一个节点做太多事
| 好的设计(单一职责) | 坏的设计(职责混杂) |
|---|---|
| Planner 只负责制定计划 | Planner 既制定计划又执行第一步 |
| Executor 只负责执行单步 | Executor 执行多步还顺便总结 |
| Replanner 只负责评估决策 | Replanner 既评估又改计划又总结 |
为什么?
- ✅ 容易调试(出问题知道是哪个节点的锅)
- ✅ 容易测试(每个节点可以单独写单元测试)
- ✅ 容易修改(改 Planner 不影响 Executor)
经验 4:WebSocket 注入,不是闭包捕获
注意我是怎么把 ws 传给节点的:
// ✅ 好的做法:每次 invoke 时把 ws 注入节点function createGraph(ws: WebSocket) { return new StateGraph(PlanExecuteState) .addNode("planner", (s) => planNode(s, ws)) .addNode("executor", (s) => executeNode(s, ws)) .addNode("replanner", (s) => replanNode(s, ws)) // ... .compile();}
// handler 里调用const graph = createGraph(ws);const result = await graph.invoke({ task: content });为什么不在外面定义 ws 然后在节点里闭包捕获?
- ❌ 闭包捕获会导致「上次的连接」和「这次的连接」混在一起
- ❌ 多个客户端同时连接时会串消息
- ✅ 每次调用 createGraph 时传入当前的 ws,保证每个连接有自己的图实例
四、下一步进化:从「规划执行」到「工具调用 Agent」
当前的 Plan & Execute 只是让 LLM「自己执行」,下一步是让它能「调用工具」:
进化路径
| 阶段 | 能力 | 实现方式 |
|---|---|---|
| 第 1 阶段 | 固定三步推理 | RunnableSequence |
| 第 2 阶段 | 动态规划执行 | LangGraph Plan & Execute(当前) |
| 第 3 阶段 | 自主选择工具 | LangGraph + Tool Calling |
| 第 4 阶段 | 多 Agent 协作 | Multi-Agent Graph |
第 3 阶段预览:Tool Calling Agent
// 给 Executor 加上工具选择能力async function executeNodeWithTools(state, ws) { // 1. LLM 决定:用哪个工具?还是直接回答? // 2. 如果选工具 → 调用工具 // 3. 如果不选工具 → 直接回答 // 4. 把结果存入 State}图结构进化:
planner → tool_selector → tool_executor → replanner五、全文总结
Runnable 链 vs LangGraph 对比
| 维度 | RunnableSequence | LangGraph |
|---|---|---|
| 执行路径 | 固定直线 | 动态图(循环、分支) |
| 状态管理 | 隐式(只能拿到上一个输出) | 显式(全局共享) |
| 可观测性 | 差(只能看到最终结果) | 好(每一步状态都能推送) |
| 灵活性 | 低(步骤固定) | 高(动态规划、循环执行) |
| 适合场景 | RAG、简单问答 | 复杂推理、Agent、规划执行 |
| 学习曲线 | 平缓 | 陡峭 |
我的 4 条 LangGraph 工程化原则
- State 是图的灵魂:精心设计你的 State 结构,用正确的 Reducer
- 节点要单一职责:每个节点只做一件事,不要贪多
- LLM 输出不可靠:每个节点都要有容错和降级逻辑
- 用户体验很重要:把每一步的思考过程推送给前端,不要让用户干等
一句话总结
Runnable 链让 LLM「按脚本做事」,LangGraph 让 LLM「有思考能力」——这是从「工具」到「Agent」的关键一跃。
项目源码:learn-langchain.js
上一篇:[LangChain 工程模块化:从 100 行脚本到可维护的后端服务](LangChain 工程模块化(三).md) 下一篇预告:[Agent 多工具调用:让 LLM 从「回答者」变成「行动者」](Agent 多工具调用(五).md)
Some information may be outdated