LOADING
3665 words
18 minutes
LangGraph 多步推理(四)

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 实现——固定的三步推理:

src/server/handlers/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:57
const 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) => nextplan、currentStepIndex、result
追加(prev, next) => [...prev, ...next]completed(每步结果都要保留)

状态流转示例

节点执行后taskplancompletedcurrentStepIndexresult
初始北京美食[][]0""
planner 后北京美食[“搜索美食”,“搜索历史”][]0""
executor 第 1 次北京美食[“搜索美食”,“搜索历史”][“烤鸭、炸酱面…“]1""
executor 第 2 次北京美食[“搜索美食”,“搜索历史”][“烤鸭…”, “烤鸭起源…“]2""
replanner 后北京美食[“搜索美食”,“搜索历史”][“烤鸭…”, “烤鸭起源…“]2”北京推荐烤鸭…”

概念 2:Node(节点)—— 图的「执行者」

每个节点是一个纯函数:输入 State,输出 State 的更新。

节点 1:Planner — 制定计划

// src/server/handlers/deep-langgraph.ts:91
const 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 做了什么

  1. 接收当前 State(只有 task 有值)
  2. 调用 LLM 把任务分解成步骤列表
  3. 更新 State:plan = 步骤列表,currentStepIndex = 0
  4. 把计划推送给前端(用户能看到 AI 在「规划」)

节点 2:Executor — 执行单步

// src/server/handlers/deep-langgraph.ts:137
const 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 做了什么

  1. 从 State 取出 plan[currentStepIndex](当前要执行的步骤)
  2. 调用 LLM 执行这一步
  3. 更新 State:completed 追加结果,currentStepIndex + 1
  4. 把执行状态推送给前端(用户能看到 AI 在「执行」)

节点 3:Replanner — 评估决策

这是整个图最聪明的节点——它负责「思考」:我做完了吗?还需要继续吗?

// src/server/handlers/deep-langgraph.ts:171
const 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 做了什么

  1. 如果所有步骤都执行完了 → 生成最终总结,设置 result(任务完成)
  2. 如果还有步骤没执行 → 调用 LLM 评估:
    • 满意了?→ status: "complete",返回结果
    • 还需要继续?→ status: "continue",回到 executor 循环

概念 3:Conditional Edge(条件边)—— 图的「大脑」

固定的边(A → B)谁都会,条件边才是 LangGraph 的灵魂。

// src/server/handlers/deep-langgraph.ts:256
function 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:278
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))
.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:299
export 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 对比

维度RunnableSequenceLangGraph
执行路径固定直线动态图(循环、分支)
状态管理隐式(只能拿到上一个输出)显式(全局共享)
可观测性差(只能看到最终结果)好(每一步状态都能推送)
灵活性低(步骤固定)高(动态规划、循环执行)
适合场景RAG、简单问答复杂推理、Agent、规划执行
学习曲线平缓陡峭

我的 4 条 LangGraph 工程化原则

  1. State 是图的灵魂:精心设计你的 State 结构,用正确的 Reducer
  2. 节点要单一职责:每个节点只做一件事,不要贪多
  3. LLM 输出不可靠:每个节点都要有容错和降级逻辑
  4. 用户体验很重要:把每一步的思考过程推送给前端,不要让用户干等

一句话总结

Runnable 链让 LLM「按脚本做事」,LangGraph 让 LLM「有思考能力」——这是从「工具」到「Agent」的关键一跃。

项目源码:learn-langchain.js

上一篇:[LangChain 工程模块化:从 100 行脚本到可维护的后端服务](LangChain 工程模块化(三).md) 下一篇预告:[Agent 多工具调用:让 LLM 从「回答者」变成「行动者」](Agent 多工具调用(五).md)

LangGraph 多步推理(四)
/posts/2026-7-1/4-langgraph-多步推理/
Author
Atopos
Published at
2026-07-01
License
CC BY-NC-SA 4.0

Some information may be outdated