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 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅4 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端
爱敲代码的小鱼4 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax