LangGraph 完全入门指南:从线性工作流到带中断恢复的有状态 Agent 编排

2 阅读7分钟

前言

做 Agent 开发的同学大概率都有过这种经历:

  • 单 Agent 把所有工具说明、业务 Prompt 全塞进 System Prompt,上下文越来越臃肿,Token 消耗飞涨,还容易被无关信息干扰
  • 业务需要循环重试、条件分流、人工审核介入时,线性的 Chain 根本表达不了复杂流程
  • 多会话场景下,状态管理全靠自己手写,很容易出现会话串数据

LangGraph 就是为解决这些问题而生的 ——LangChain 团队官方推出的有状态 Agent 编排框架,用图的方式组织大模型、工具、业务逻辑,天然支持分支、循环、持久化、中断恢复,是构建复杂多 Agent 系统的标配工具。

本文从 0 到 1 带你跑通 LangGraph 五大核心场景:基础线性流、条件分支、重试循环、状态持久化、人工中断恢复,所有代码均可直接复制运行。


一、核心概念先搞懂

在写代码之前,先把几个核心概念对齐,后面上手会快很多:

表格

概念作用
StateGraph工作流蓝图构建器,用来定义节点、边,最终编译成可执行的图
Annotation定义共享状态(State)的结构,以及新旧状态的合并规则(reducer)
节点(Node)业务逻辑执行单元,接收状态,返回状态增量
边(Edge)定义节点之间的流转顺序,支持普通边和条件边
MemorySaver内存级状态持久化器,按会话保存状态快照
interrupt()中断原语,让流程暂停,等待外部人工输入后恢复
Command控制指令,专门用于向中断喂入恢复值

二、入门:搭建第一个线性工作流

我们先从最简单的线性流程开始,实现一个 hello -> step1 -> step2 的基础工作流。

1. 依赖安装

npm install @langchain/langgraph @langchain/core

2. 完整代码

import {
  Annotation,
  END,
  START,
  StateGraph
} from '@langchain/langgraph';

// 1. 定义状态结构
const StateAnnotation = Annotation.Root({
  text: Annotation({
    default: () => "",      // 字段默认值,必须是工厂函数
    reducer: (_prev, next) => next // 新旧值合并策略:新值直接覆盖旧值
  })
});

// 2. 定义节点:纯函数,接收state,返回状态增量
const step1 = (state) => ({ text: `${state.text} -> step1` });
const step2 = (state) => ({ text: `${state.text} -> step2` });

// 3. 构建图并编译
const graph = new StateGraph(StateAnnotation)
  .addNode("step1", step1)
  .addNode("step2", step2)
  .addEdge(START, "step1")   // 起始边:从开始节点到step1
  .addEdge("step1", "step2") // 普通边:step1执行完到step2
  .addEdge("step2", END)     // 结束边:step2执行完到终点
  .compile();

// 4. 生成可视化流程图(Mermaid)
const drawable = await graph.getGraphAsync();
const mermaid = drawable.drawMermaid({ withStyles: true });
console.log(mermaid);

// 5. 执行工作流
const result = await graph.invoke({ text: "hello" });
console.log(result);
// 输出:{ text: 'hello -> step1 -> step2' }

3. 关键说明

  • 状态更新机制:节点不需要返回完整 state,只需要返回要更新的字段,LangGraph 会根据 reducer 规则合并到全局 state。
  • default 必须是工厂函数:带 reducer 的聚合字段,默认值必须写成 () => 值 的形式,否则会报 initialValueFactory is not a function 错误。
  • 可视化:getGraphAsync() 可以直接生成 Mermaid 流程图,非常适合调试和文档编写。

三、进阶:条件分支与循环重试

线性流只能处理顺序任务,真实业务往往需要判断分流、失败重试,这就用到了 addConditionalEdges 条件边。

1. 条件分支:路由分流

我们实现一个简单路由:如果输入是数学表达式就走计算节点,否则走聊天节点。

import { Annotation, END, START, StateGraph } from '@langchain/langgraph';

const StateAnnotation = Annotation.Root({
  query: Annotation({
    default: () => "",
    reducer: (_prev, next) => next
  }),
  route: Annotation({
    reducer: (_prev, next) => next,
    default: () => "chat"
  }),
  answer: Annotation({
    reducer: (_prev, next) => next,
    default: () => ""
  })
});

// 路由节点:判断走哪个分支
const router = (state) => {
  const isMath = /[+-*]/.test(state.query);
  return { route: isMath ? "math" : "chat" };
};

// 计算节点
const mathNode = (state) => {
  try {
    return { answer: String(eval(state.query)) };
  } catch (error) {
    return { answer: "表达式无法计算" };
  }
};

// 聊天节点
const chatNode = (state) => {
  return { answer: `你说的是:${state.query}` };
};

// 构建图
const graph = new StateGraph(StateAnnotation)
  .addNode("router", router)
  .addNode("math", mathNode)
  .addNode("chat", chatNode)
  .addEdge(START, "router")
  // 条件边:根据route字段的值,映射到不同的目标节点
  .addConditionalEdges("router", (state) => state.route, {
    math: "math",
    chat: "chat"
  })
  .addEdge("math", END)
  .addEdge("chat", END)
  .compile();

console.log(await graph.invoke({ query: "1+2" }));
// 输出:{ query: '1+2', route: 'math', answer: '3' }

2. 循环重试:失败自动重试

很多场景下任务可能失败,需要自动重试,我们实现一个 “最多尝试 3 次,成功就结束” 的循环流程。

import { Annotation, END, START, StateGraph } from '@langchain/langgraph';

const StateAnnotation = Annotation.Root({
  tries: Annotation({
    reducer: (_prev, next) => next,
    default: () => 0
  }),
  ok: Annotation({
    reducer: (_prev, next) => next,
    default: () => false
  }),
  message: Annotation({
    reducer: (_prev, next) => next,
    default: () => ''
  })
});

// 尝试执行节点
const attempt = (state) => {
  const tries = state.tries + 1;
  const ok = tries >= 3; // 模拟第3次才成功
  return {
    tries,
    ok,
    message: ok ? `第${tries}次执行成功` : `第${tries}次执行失败`
  };
};

const graph = new StateGraph(StateAnnotation)
  .addNode("attempt", attempt)
  .addEdge(START, "attempt")
  // 条件边:成功就结束,失败就回到attempt重试
  .addConditionalEdges("attempt", (state) => state.ok ? "done" : "retry", {
    retry: "attempt",
    done: END
  })
  .compile();

console.log(await graph.invoke({ tries: 0 }));
// 输出:{ tries: 3, ok: true, message: '第3次执行成功' }

💡 addConditionalEdges 第二个参数是条件函数,返回分支名称;第三个参数是分支映射表,把分支名对应到目标节点。


四、状态持久化:MemorySaver 与会话隔离

上面的例子都是无状态的,每次调用都是全新的。真实 Agent 应用需要多会话隔离,记住每个用户的历史状态,这就需要 Checkpointer。

1. MemorySaver 入门

MemorySaver 是 LangGraph 内置的内存持久化器,按 thread_id 区分会话,保存每一步的状态快照。

import { Annotation, END, MemorySaver, START, StateGraph } from '@langchain/langgraph';

const StateAnnotation = Annotation.Root({
  visitCount: Annotation({
    reducer: (_prev, next) => next,
    default: () => 0
  }),
  message: Annotation({
    reducer: (_prev, next) => next,
    default: () => ''
  })
});

function recordVisit(state) {
  const visitCount = state.visitCount + 1;
  const message = `这是你第${visitCount}次进入会话`;
  return { visitCount, message };
}

const graph = new StateGraph(StateAnnotation)
  .addNode("recordVisit", recordVisit)
  .addEdge(START, "recordVisit")
  .addEdge("recordVisit", END)
  .compile({ checkpointer: new MemorySaver() }); // 开启持久化

// 定义两个用户会话
const user1 = { configurable: { thread_id: "用户_小张" } };
const user2 = { configurable: { thread_id: "用户_小王" } };

console.log(await graph.invoke({}, user1)); // 小张第1次
console.log(await graph.invoke({}, user2)); // 小王第1次
console.log(await graph.invoke({}, user1)); // 小张第2次
console.log(await graph.invoke({}, user2)); // 小王第2次
console.log(await graph.invoke({}, user1)); // 小张第3次

2. 关键注意点

⚠️ 踩坑提醒

  1. configurable.thread_id 是固定键名:不能写成 threadId、user_id,也不能少了 configurable 这一层,否则会报 Missing thread_id 错误。
  2. 变量拼写要注意:checkpointer 不要拼成 checkponiter,对象简写语法下变量名就是属性名,拼错了框架读不到,持久化直接失效。
  3. MemorySaver 是内存存储:进程重启数据清空,只适合开发调试;生产环境需要替换为 Redis、Postgres 等持久化 Checkpointer。

五、高阶:中断(Interrupt)与人工恢复

Agent 执行到敏感操作(比如转账、下单)时,往往需要人工确认。LangGraph 提供了 interrupt() 原语,可以让流程暂停在断点,等待人工输入后再恢复执行。

1. 完整实战:转账人工确认

我们实现一个完整流程:展示转账信息 → 中断等待人工确认 → 确认后继续执行。

import {
  Annotation,
  Command,
  END,
  interrupt,
  MemorySaver,
  START,
  StateGraph
} from '@langchain/langgraph';
import { createInterface } from "readline/promises";

// 定义状态
const StateAnnotation = Annotation.Root({
  actionSummary: Annotation({
    reducer: (_prev, next) => next,
    default: () => ''
  }),
  userInput: Annotation({
    reducer: (_prev, next) => next,
    default: () => ''
  })
});

// 节点1:展示转账信息
const showTransfer = () => {
  return { actionSummary: "向张三转钱 100块" };
};

// 节点2:等待人工确认
const waitConfirm = (state) => {
  // 执行到这里,流程直接暂停,退出本次invoke
  const text = interrupt({
    hint: "终端里输入[确认]或者备注后回车",
    actionSummary: state.actionSummary
  });
  // 恢复执行后,interrupt返回值就是人工输入
  return { userInput: String(text) };
};

// 构建图,开启持久化(中断必须依赖checkpointer)
const graph = new StateGraph(StateAnnotation)
  .addNode("showTransfer", showTransfer)
  .addNode("waitConfirm", waitConfirm)
  .addEdge(START, "showTransfer")
  .addEdge("showTransfer", "waitConfirm")
  .addEdge("waitConfirm", END)
  .compile({ checkpointer: new MemorySaver() });

const config = { configurable: { thread_id: "user1" } };

// 1. 启动流程,触发中断
const paused = await graph.invoke({}, config);
console.log("待确认操作:", paused.__interrupt__?.[0]?.value);

// 2. 终端读取用户输入
const rl = createInterface({
  input: process.stdin,
  output: process.stdout
});
const line = (await rl.question("> ")).trim();
console.log("你输入了:", line);
rl.close();

// 3. 用Command恢复中断,继续执行
const done = await graph.invoke(new Command({ resume: line }), config);
console.log("最终结果:", done);

2. 执行流程拆解

  1. 第一次 invoke:流程走到 waitConfirm 节点,执行 interrupt() 后立刻暂停,返回结果带 __interrupt__ 标记,代表流程卡在断点。
  2. 人工输入:通过 readline 在终端读取用户输入。
  3. 恢复执行:用 new Command({ resume: 输入值 }) 调用 invoke,框架找到同 thread_id 的断点,把输入值作为 interrupt() 的返回值,继续执行剩下的逻辑。

💡 为什么用 Command 而不是直接传对象?

  • Command.resume 是专门的恢复指令,语义清晰,专门给中断喂返回值
  • 不会强制把输入写入 state,状态更新完全由节点代码控制
  • 避免和普通状态更新混淆,减少 bug

六、延伸:为什么说 LangGraph 是多 Agent 的基础

单 Agent 架构下,所有能力都挤在一个大脑里,Prompt 越来越长,干扰越来越多。LangGraph 的图结构天然适合多 Agent 协作:

  • 分工明确:编程 Agent 只写代码,测试 Agent 只写单测,验证 Agent 只做校验,每个 Agent 只带自己领域的 Prompt,Token 更少、准确率更高
  • 并行处理:主 Agent 拆分任务,多个子 Agent 并行执行,整体效率更高
  • 互相纠错:不同角色 Agent 互相评审、博弈,降低幻觉和错误率

你可以把上面的单个节点替换成独立的 Agent,每个节点内部调用各自的 LLM 和工具,就能快速搭建出多 Agent 系统。


总结

本文从基础到高阶覆盖了 LangGraph 的核心能力:

  1. 线性工作流:用 StateGraph + Annotation 搭建基础流程
  2. 条件分支与循环:用 addConditionalEdges 实现分流和重试
  3. 状态持久化:用 MemorySaver + thread_id 实现多会话隔离
  4. 中断恢复:用 interrupt() + Command 实现人工介入场景

LangGraph 的核心思想是把业务流程结构化、状态化,让 Agent 的行为可预测、可调试、可扩展。如果你正在做复杂 Agent 开发,非常推荐上手试试。