第9章:LangGraph 状态图与工作流编排
前言
大家好,我是鲫小鱼。是一名不写前端代码
的前端工程师,热衷于分享非前端的知识,带领切图仔逃离切图圈子,欢迎关注我,微信公众号:《鲫小鱼不正经》
。欢迎点赞、收藏、关注,一键三连!!
🎯 本章学习目标
- 全面掌握 LangGraph 0.3 的核心概念:状态(State)、通道(Channels)、节点(Nodes)、边(Edges)、编译(Compile)
- 掌握分支(Branch)、循环(Loop)、并行(Parallel)、中断(Interrupt)等工作流编排能力
- 将 Runnable、Memory、Callback 与 LangGraph 深度结合,构建可观测与可恢复的长流程
- 掌握人机协同:人工审批 / 审核节点、断点恢复、可回放执行
- 在 Next.js 中构建"流程引擎 API",并提供前端可视化"状态图查看器"
- 实战:构建"知识入库(Ingest)+ RAG 问答 + 反馈闭环"的端到端流程
🧠 LangGraph 基本概念与模型
9.1 为什么需要 LangGraph
- Runnable 更像"函数管道";当流程复杂(多分支/循环/跨请求)时,维护成本高
- LangGraph 提供"显式状态与状态转移"的工作流抽象,更直观可控、易监控与恢复
9.2 核心概念
- State(状态):流程运行时的数据载体,通常是一个对象,包含上下文、步骤结果、错误等
- Channels(通道):对状态字段的声明,控制读写与合并策略(例如 append 合并)
- Node(节点):原子步骤/任务,输入 state,输出 state 的增量
- Edge(边):节点间的有向连接,可条件跳转
- Compile(编译):构建完成后编译为可执行的图(graph app)
9.3 最小示例
typescript
// 文件:src/ch09/minimal.ts
import { StateGraph } from "@langchain/langgraph";
// 声明状态结构
type FlowState = {
input: string;
result?: string;
error?: string;
};
export async function buildMinimalGraph() {
const graph = new StateGraph<FlowState>({
channels: {
input: { value: "" },
result: { value: "" },
error: { value: "" },
},
});
graph.addNode("echo", async (s) => ({ result: `ECHO: ${s.input}` }));
graph.addEdge("start", "echo");
graph.addEdge("echo", "end");
return graph.compile();
}
🧱 State 与 Channels 的设计
9.4 设计要点
- 将"可回放、可观察"的数据都放入 state:输入、步骤结果、检索命中、LLM 输出、错误
- 通道声明决定字段合并策略:例如
messages
通道用 append;result
用覆盖 - 注意隐私字段:敏感数据应脱敏或只存引用
9.5 示例:RAG 流程 State
typescript
// 文件:src/ch09/state.ts
export type RagState = {
question: string;
retriever: "topk" | "mmr" | "hybrid" | "time" | "user";
hits: Array<{ id: string; text: string; meta: any; score?: number }>;
fused: Array<{ id: string; text: string; meta: any }>; // 去冗余/重排后
answer?: { answer: string; citations: any[]; confidence: number };
feedback?: { rating?: number; comment?: string };
logs: string[]; // 回放轨迹
error?: string;
};
🔗 节点(Nodes)编排:检索 → 融合 → 生成 → 校验 → 反馈
9.6 节点实现
typescript
// 文件:src/ch09/nodes.ts
import { RunnableLambda, RunnableSequence } from "@langchain/core/runnables";
import { ChatOpenAI } from "@langchain/openai";
import { JsonOutputParser } from "@langchain/core/output_parsers";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { RagState } from "./state";
import { hybridRetriever, timeAwareRetriever, userAwareRetriever, topKRetriever } from "./retrievers";
export const retrieveNode = async (s: RagState): Promise<Partial<RagState>> => {
let retr = await topKRetriever("news");
if (s.retriever === "hybrid") retr = await hybridRetriever("news");
if (s.retriever === "time") retr = await timeAwareRetriever("news");
if (s.retriever === "user") retr = await userAwareRetriever("news", { id: "u1", dept: "it" });
const hits = await retr(s.question);
return { hits, logs: [`检索到 ${hits.length} 条`] };
};
export const fuseNode = async (s: RagState): Promise<Partial<RagState>> => {
// 简化的去冗余合并
const seen = new Set();
const fused = [] as RagState["fused"];
for (const h of s.hits || []) {
const key = `${h.meta?.source}#${h.meta?.chunkIndex}`;
if (seen.has(key)) continue; seen.add(key);
fused.push({ id: key, text: h.text, meta: h.meta });
}
return { fused, logs: ["已融合去重"] };
};
const prompt = ChatPromptTemplate.fromMessages([
["system", `你是严谨的企业知识助手,仅依据候选片段回答,引用必须给出来源。输出 JSON:
{"answer": string, "citations": [{"source": string, "chunkId": string}], "confidence": number}`],
["human", `问题:{q}\n候选:\n{chunks}\n输出 JSON:`],
]);
export const answerNode = async (s: RagState): Promise<Partial<RagState>> => {
const llm = new ChatOpenAI({ temperature: 0 });
const seq = RunnableSequence.from([
new RunnableLambda((i: any) => ({
q: i.question,
chunks: (i.fused || []).map((c: any, i: number) => `#${i} [${c.meta?.source}] ${String(c.text).slice(0, 300)}`).join("\n"),
})),
prompt,
llm,
new JsonOutputParser(),
]);
const ans = await seq.invoke(s);
return { answer: ans, logs: ["已生成回答"] };
};
export const guardNode = async (s: RagState): Promise<Partial<RagState>> => {
if (!s.answer) return { error: "NO_ANSWER", logs: ["回答为空"] };
const ok = Array.isArray(s.answer.citations) && s.answer.citations.length > 0;
if (!ok) return { error: "INVALID_CITATIONS", logs: ["缺少引用"] };
return { logs: ["引用校验通过"] };
};
export const feedbackNode = async (s: RagState): Promise<Partial<RagState>> => {
// 这里可以把反馈写入数据库/日志系统
return { logs: ["已记录反馈"] };
};
说明:
retrievers.ts
在本章稍后提供,示例对接第7章的检索实现。
🌳 条件分支、循环与并行
9.7 条件分支(Conditional Edges)
typescript
// 文件:src/ch09/graph-basic.ts
import { StateGraph } from "@langchain/langgraph";
import { RagState } from "./state";
import { retrieveNode, fuseNode, answerNode, guardNode, feedbackNode } from "./nodes";
export async function buildRagGraph() {
const graph = new StateGraph<RagState>({
channels: {
question: { value: "" },
retriever: { value: "topk" },
hits: { value: [] },
fused: { value: [] },
answer: { value: { answer: "", citations: [], confidence: 0 } },
feedback: { value: {} },
logs: { value: [], merge: (prev, next) => [...prev, ...next] },
error: { value: "" },
},
});
graph.addNode("retrieve", retrieveNode);
graph.addNode("fuse", fuseNode);
graph.addNode("answer", answerNode);
graph.addNode("guard", guardNode);
graph.addNode("feedback", feedbackNode);
graph.addEdge("start", "retrieve");
graph.addEdge("retrieve", "fuse");
graph.addEdge("fuse", "answer");
// 条件:若 guard 无错误 → end;否则回到 retrieve 重试(最多 1 次,演示)
graph.addConditionalEdges("answer", (s) => (s.error ? "retry" : "guard"), {
guard: "guard",
retry: "retrieve",
});
graph.addConditionalEdges("guard", (s) => (s.error ? "end" : "feedback"), {
feedback: "feedback",
});
graph.addEdge("feedback", "end");
return graph.compile();
}
9.8 循环(Loop)与重试策略
- 可以通过在
addConditionalEdges
中让某节点指向自身或上游节点实现循环 - 应配合计数器 / 超时控制,避免无限循环
typescript
// 文件:src/ch09/retry.ts
import { StateGraph } from "@langchain/langgraph";
import { RagState } from "./state";
export async function buildRetryGraph() {
const graph = new StateGraph<RagState>({ channels: { question: { value: "" }, logs: { value: [], merge: (a,b)=>[...a,...b] }, error: { value: "" } } });
graph.addNode("do", async (s) => {
if (!s.error) {
return { error: "TRANSIENT", logs: ["模拟一次失败"] };
}
return { logs: ["第二次成功"] };
});
graph.addConditionalEdges("do", (s) => (s.error ? "retry" : "end"), { retry: "do" });
graph.addEdge("start", "do");
return graph.compile();
}
9.9 并行(Parallel)与合并
- 0.3 版本推荐将"并行任务"拆为多个节点并在状态上合并结果
- 在 Next.js 层可用
Promise.all
等并发工具;在图中体现为多个支路后汇合
typescript
// 文件:src/ch09/parallel.ts
import { StateGraph } from "@langchain/langgraph";
type PState = { a?: number; b?: number; sum?: number };
export async function buildParallelGraph() {
const g = new StateGraph<PState>({ channels: { a: { value: 0 }, b: { value: 0 }, sum: { value: 0 } } });
g.addNode("A", async () => ({ a: 1 }));
g.addNode("B", async () => ({ b: 2 }));
g.addNode("SUM", async (s) => ({ sum: (s.a || 0) + (s.b || 0) }));
g.addEdge("start", "A");
g.addEdge("start", "B");
g.addEdge("A", "SUM");
g.addEdge("B", "SUM");
g.addEdge("SUM", "end");
return g.compile();
}
⏸️ 中断、人工审批与恢复
9.10 中断点(Interrupt)
- 典型场景:需要产品经理/法务审核、支付确认、人工选择方案
- 思路:在节点中检测条件后,返回
error="NEED_APPROVAL"
并写入approvalToken
- 前端拿到 token,展示审批 UI;审批后通过
/api/graph/resume
继续
typescript
// 文件:src/ch09/approval.ts
import { StateGraph } from "@langchain/langgraph";
type ApprovalState = { input: string; approved?: boolean; token?: string; result?: string; error?: string };
export async function buildApprovalGraph() {
const g = new StateGraph<ApprovalState>({ channels: { input: { value: "" }, approved: { value: false }, token: { value: "" }, result: { value: "" }, error: { value: "" } } });
g.addNode("check", async (s) => {
if (!s.approved) {
const token = Math.random().toString(36).slice(2, 10);
return { error: "NEED_APPROVAL", token };
}
return { result: `已通过:${s.input}` };
});
g.addConditionalEdges("check", (s) => (s.error === "NEED_APPROVAL" ? "pause" : "end"), { pause: "end" });
g.addEdge("start", "check");
return g.compile();
}
说明:在生产中应将 state 存储到数据库,
token
与会话关联,待审批完成后恢复。
🧩 与 Runnable、Memory、Callback 的整合
9.11 将 Runnable 链嵌入节点
- 在
addNode
的处理函数中使用RunnableSequence
/pipe
执行 LLM 与解析 - 将中间日志写入
logs
通道,配合 Callback 上报到观测平台(如 LangSmith)
9.12 Memory:跨节点共享上下文与对话
- 将
messages
放入 state,设置通道合并策略为 append - 节点按需读取最近窗口或摘要(参见第3章)
9.13 Callback:可观测性
- 在每个节点入口/出口上报"开始/结束/耗时/错误"
- 统一 RunId 串联全链路;Next.js API 中注入 requestId
🌐 Next.js 集成:图引擎 API 与前端状态图查看器
9.14 运行图的服务端 API
typescript
// 文件:src/app/api/graph/run/route.ts
import { NextRequest } from "next/server";
import { buildRagGraph } from "@/src/ch09/graph-basic";
export const runtime = "edge";
export async function POST(req: NextRequest) {
const body = await req.json();
const app = await buildRagGraph();
try {
const out = await app.invoke({
question: body.question || "什么是 LangGraph?",
retriever: body.retriever || "hybrid",
hits: [], fused: [], logs: [],
});
return Response.json({ ok: true, data: out });
} catch (e: any) {
return Response.json({ ok: false, message: e.message }, { status: 500 });
}
}
9.15 可视化查看器(前端)
tsx
// 文件:src/app/graph/page.tsx
"use client";
import { useState } from "react";
export default function GraphPage() {
const [q, setQ] = useState("");
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(false);
const run = async () => {
setLoading(true);
const res = await fetch("/api/graph/run", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ question: q || "公司报销政策?", retriever: "hybrid" }),
});
const json = await res.json();
setData(json.data);
setLoading(false);
};
return (
<main className="max-w-3xl mx-auto p-4 space-y-4">
<h1 className="text-2xl font-bold">LangGraph 状态图查看器</h1>
<div className="flex gap-2">
<input value={q} onChange={e=>setQ(e.target.value)} placeholder="输入问题" className="flex-1 border rounded px-3 py-2" />
<button onClick={run} className="px-4 py-2 bg-blue-600 text-white rounded" disabled={loading}>{loading?"运行中...":"运行"}</button>
</div>
{data && (
<section className="space-y-2">
<h2 className="font-semibold">执行结果</h2>
<pre className="whitespace-pre-wrap break-words text-sm bg-gray-50 p-3 rounded">{JSON.stringify(data, null, 2)}</pre>
</section>
)}
</main>
);
}
🚀 实战:知识入库 + RAG 问答 + 反馈闭环
9.16 场景说明
- 用户提问 → 检索与生成 → 用户反馈(满意/不满意)→ 写回反馈与改进数据
- 入库流程可独立为另一张图:文件上传 → 清洗 → 切分 → 向量化 → 索引构建 → 通知完成
9.17 RAG 图 + 反馈节点
typescript
// 文件:src/ch09/app.ts
import { StateGraph } from "@langchain/langgraph";
import { RagState } from "./state";
import { retrieveNode, fuseNode, answerNode, guardNode, feedbackNode } from "./nodes";
export async function buildRagWithFeedback() {
const g = new StateGraph<RagState>({
channels: {
question: { value: "" },
retriever: { value: "hybrid" },
hits: { value: [] }, fused: { value: [] },
answer: { value: { answer: "", citations: [], confidence: 0 } },
feedback: { value: {} },
logs: { value: [], merge: (a,b)=>[...a,...b] },
error: { value: "" },
}
});
g.addNode("retrieve", retrieveNode);
g.addNode("fuse", fuseNode);
g.addNode("answer", answerNode);
g.addNode("guard", guardNode);
g.addNode("feedback", feedbackNode);
g.addEdge("start", "retrieve");
g.addEdge("retrieve", "fuse");
g.addEdge("fuse", "answer");
g.addEdge("answer", "guard");
g.addConditionalEdges("guard", s => (s.error ? "end" : "feedback"), { feedback: "feedback" });
g.addEdge("feedback", "end");
return g.compile();
}
9.18 入库(Ingest)图
typescript
// 文件:src/ch09/ingest.ts
import { StateGraph } from "@langchain/langgraph";
type IngestState = { dir: string; name: string; chunks?: number; ok?: boolean; error?: string; logs: string[] };
export async function buildIngestGraph() {
const g = new StateGraph<IngestState>({ channels: { dir: { value: "./docs" }, name: { value: "news" }, chunks: { value: 0 }, ok: { value: false }, error: { value: "" }, logs: { value: [], merge: (a,b)=>[...a,...b] } } });
g.addNode("load", async (s) => ({ logs: ["加载文档"], chunks: 300 }));
g.addNode("embed", async (s) => ({ logs: ["向量化并入库"], ok: true }));
g.addEdge("start", "load");
g.addEdge("load", "embed");
g.addEdge("embed", "end");
return g.compile();
}
9.19 API:一键运行两张图
typescript
// 文件:src/app/api/pipeline/route.ts
import { NextRequest } from "next/server";
import { buildIngestGraph } from "@/src/ch09/ingest";
import { buildRagWithFeedback } from "@/src/ch09/app";
export const runtime = "edge";
export async function POST(req: NextRequest) {
const { dir, name, question } = await req.json();
try {
const ingest = await buildIngestGraph();
const r1 = await ingest.invoke({ dir: dir || "./docs", name: name || "news", logs: [] });
const app = await buildRagWithFeedback();
const r2 = await app.invoke({ question: question || "今天有哪些新公告?", retriever: "hybrid", hits: [], fused: [], logs: [] });
return Response.json({ ok: true, ingest: r1, rag: r2 });
} catch (e: any) {
return Response.json({ ok: false, message: e.message }, { status: 500 });
}
}
⚙️ 工程化:持久化、恢复、监控与评估
9.20 状态持久化与恢复
- 将每一步的 state 写入数据库(推荐:PostgreSQL/SQLite + JSONB;或 KV 存储)
- RunId 贯穿整个流程;支持 resume(带上最后一个成功节点和 state)
9.21 可观测性
- 关键指标:节点耗时、重试次数、失败率、队列长度、积压时长
- 结合回调与日志上报(参考第4章),并接入 LangSmith 或自建监控
9.22 评估与回放
- 保存输入、命中片段、回答、引用、反馈,构建离线评估集
- 评估指标:Recall@K、MRR、Citation Accuracy、满意度、成本、延迟
9.23 安全与权限
- 节点按功能域划分权限(如敏感数据仅在受限节点可见)
- 人工审批节点要有审计日志;数据脱敏与水印
📚 延伸链接
- LangGraph 文档:
https://langchain-ai.github.io/langgraph/
- LangChain Runnable:
https://js.langchain.com/docs/expression_language/why
- 状态机/工作流理论参考:
https://en.wikipedia.org/wiki/Workflow
、https://en.wikipedia.org/wiki/Finite-state_machine
✅ 本章小结
- 理解了 LangGraph 的状态/节点/边/编译模型
- 掌握了条件分支、循环重试、并行与中断的编排方式
- 将 Runnable、Memory、Callback 与 LangGraph 联动,打造可观测与可恢复的长流程
- 在 Next.js 中提供了图引擎 API 与查看器,完成"入库 + RAG + 反馈"的端到端实战
🎯 下章预告
下一章《RAG 与 Agent 的协同编排实践》中,我们将:
- 用 LangGraph 编排 RAG 与 Agent 的协作
- 构建工具型 Agent 调用检索、调用 API 的完整闭环
- 通过事件总线与可视化,呈现端到端执行轨迹
最后感谢阅读!欢迎关注我,微信公众号:
《鲫小鱼不正经》
。欢迎点赞、收藏、关注,一键三连!!!