LangChain.js 完全开发手册(九)LangGraph 状态图与工作流编排

第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/Workflowhttps://en.wikipedia.org/wiki/Finite-state_machine

✅ 本章小结

  • 理解了 LangGraph 的状态/节点/边/编译模型
  • 掌握了条件分支、循环重试、并行与中断的编排方式
  • 将 Runnable、Memory、Callback 与 LangGraph 联动,打造可观测与可恢复的长流程
  • 在 Next.js 中提供了图引擎 API 与查看器,完成"入库 + RAG + 反馈"的端到端实战

🎯 下章预告

下一章《RAG 与 Agent 的协同编排实践》中,我们将:

  • 用 LangGraph 编排 RAG 与 Agent 的协作
  • 构建工具型 Agent 调用检索、调用 API 的完整闭环
  • 通过事件总线与可视化,呈现端到端执行轨迹

最后感谢阅读!欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!!

相关推荐
鹏多多2 小时前
深入解析vue的keep-alive缓存机制
前端·javascript·vue.js
JarvanMo2 小时前
用 `alice` 来检查 Flutter 中的 HTTP 调用
前端
小图图2 小时前
Claude Code 黑箱揭秘
前端·后端
吃饺子不吃馅2 小时前
为什么SnapDOM 比 html2canvas截图要快?
前端·javascript·面试
这里有鱼汤2 小时前
miniQMT下载历史行情数据太慢怎么办?一招提速10倍!
前端·python
用户21411832636023 小时前
dify案例分享-免费玩转 AI 绘图!Dify 整合 Qwen-Image,文生图 图生图一步到位
前端
IT_陈寒3 小时前
Redis 性能翻倍的 7 个冷门技巧,第 5 个大多数人都不知道!
前端·人工智能·后端
mCell10 小时前
GSAP ScrollTrigger 详解
前端·javascript·动效
gnip10 小时前
Node.js 子进程:child_process
前端·javascript