一文搞懂 LangGraph 分支、循环与状态持久化

上一篇我们搞定了 LangGraph 的线性流程编排和多 Agent 协作基础。这一篇深入 LangGraph 最核心的三个能力:条件分支 、循环重试 、状态持久化 ,最后再来一个硬核的 中断/恢复 实战。


一、条件分支:让工作流"会思考"

为什么需要分支?

线性流程只能 A → B → C 一路走到黑。真实场景中,Agent 需要根据输入决定走哪条路------用户问数学题走计算器,闲聊走对话,代码走 Code Interpreter。

LangGraph 用 addConditionalEdges 实现分支。

完整代码

javascript 复制代码
import {
  Annotation, END, START, StateGraph
} from '@langchain/langgraph';

// ① 状态定义
const StateAnnotation = Annotation.Root({
  query:  Annotation({ reducer: (_prev, next) => next, default: () => "" }),
  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" };
};

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

// ④ 对话节点
const chatNode = (state) => ({ answer: `你说的是:${state.query}` });

// ⑤ 编排
const graph = new StateGraph(StateAnnotation)
  .addNode("router", router)
  .addNode("math", mathNode)
  .addNode("chat", chatNode)
  .addEdge(START, "router")
  .addConditionalEdges("router", (state) => state.route, {
    math: "math",
    chat: "chat"
  })
  .addEdge("math", END)
  .addEdge("chat", END)
  .compile();

流程图

graph TD; __start__([__start__]) --> router{router} router -->|route=math| math[math] router -->|route=chat| chat[chat] math --> __end__([__end__]) chat --> __end__

执行过程

javascript 复制代码
// 输入含数学符号 → 走 math 分支
await graph.invoke({ query: "1+2" });
// → router 判断: isMath=true → route="math"
// → mathNode: eval("1+2") = 3
// → { query:"1+2", route:"math", answer:"3" }

// 普通文字 → 走 chat 分支
await graph.invoke({ query: "你好" });
// → router 判断: isMath=false → route="chat"
// → chatNode: answer="你说的是:你好"
// → { query:"你好", route:"chat", answer:"你说的是:你好" }

面试高频问:addConditionalEdges 和 addEdge 有什么区别?

API 作用 决定时机
addEdge(from, to) 固定连线,永远走 to 设计时确定
addConditionalEdges(from, fn, map) 运行时根据状态动态选路 运行时决定

一句话:addEdge 是"写死的路",addConditionalEdges 是"到路口再决定"。


二、循环重试:让工作流"会坚持"

为什么需要循环?

Agent 执行不是一次就成功的------API 调用超时、LLM 输出格式不对、用户确认不通过......都需要重试机制。

LangGraph 用 addConditionalEdges 让节点回到自己,形成循环。

完整代码

javascript 复制代码
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")
  // 条件边:失败 → 回到自己,成功 → 结束
  .addConditionalEdges("attempt", (state) => state.ok ? "done" : "retry", {
    retry: "attempt",
    done: END
  })
  .compile();

流程图

graph TD; __start__([__start__]) --> attempt[attempt] attempt -->|ok=false| attempt attempt -->|ok=true| __end__([__end__])

执行过程

javascript 复制代码
await graph.invoke({ tries: 0 });
// 第1轮: tries=1, ok=false → "第1次失败" → 回到 attempt
// 第2轮: tries=2, ok=false → "第2次失败" → 回到 attempt
// 第3轮: tries=3, ok=true  → "第3次成功" → END
// → { tries:3, ok:true, message:"第3次成功" }

面试高频问:分支和循环用的是同一个 API?

是的,都是 addConditionalEdges。

区别在于 map 里映射的目标:

javascript 复制代码
// 分支:映射到不同节点
{ math: "math", chat: "chat" }

// 循环:映射回自己
{ retry: "attempt", done: END }

一句话:分支是"走哪条路",循环是"要不要重来",底层同一个 API,映射目标不同。


三、状态持久化:让工作流"有记忆"

没有持久化的问题

每次 graph.invoke() 状态都从默认值开始,相当于每次都失忆。

ini 复制代码
第1次调用 → visitCount=0 → 结果1
第2次调用 → visitCount=0 → 结果1   ← 又从头开始了
第3次调用 → visitCount=0 → 结果1

MemorySaver:一行代码加记忆

javascript 复制代码
import { MemorySaver } from '@langchain/langgraph';

const checkpointer = new MemorySaver();   // 内存保存器
const app = graph.compile({ checkpointer });

加上 checkpointer 后,每次 invoke 结束自动保存状态快照,下次同一会话调用时自动恢复。

完整代码

javascript 复制代码
import {
  Annotation, END, START, MemorySaver, 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 === 1
    ? "这是你在本会话里第1次进入。"
    : `这是你在本会话里${visitCount}次进入`;
  return { visitCount, message };
}

const graph = new StateGraph(StateAnnotation)
  .addNode("recordVisit", recordVisit)
  .addEdge(START, "recordVisit")
  .addEdge("recordVisit", END);

const checkpointer = new MemorySaver();
const app = graph.compile({ checkpointer });

多用户会话隔离

javascript 复制代码
const user1 = { configurable: { thread_id: "用户-小张" } };
const user2 = { configurable: { thread_id: "用户-小李" } };

await app.invoke({}, user1);  // { visitCount:1, message:"这是你在本会话里第1次进入。" }
await app.invoke({}, user1);  // { visitCount:2, message:"这是你在本会话里2次进入" }
await app.invoke({}, user1);  // { visitCount:3, message:"这是你在本会话里3次进入" }
await app.invoke({}, user2);  // { visitCount:1, message:"这是你在本会话里第1次进入。" }
//                        ↑ 小李是独立会话,从头开始

thread_id 就是会话标识------不同用户、不同对话,状态完全隔离。

面试高频问:MemorySaver 和数据库存储有什么区别?

MemorySaver 数据库存储
存储位置 进程内存 SQLite / Redis / PostgreSQL
重启后 丢失 保留
适用场景 开发调试、无状态要求 生产环境、需要持久化
实现方式 LangGraph 内置 需要换 Checkpointer 实现

生产环境把 MemorySaver 换成 SqliteSaver 或 RedisSaver,API 完全一样,只需改一行 import。


四、中断与恢复:让工作流"会等待"

这是 LangGraph 最强的能力之一------让图暂停,等人类输入后再继续。

场景:转账确认

javascript 复制代码
import {
  Annotation, END, START, StateGraph,
  Command,        // 用于恢复时传入数据
  interrupt,      // 中断函数
  MemorySaver
} from '@langchain/langgraph';
import { createInterface } from "node:readline/promises";

const StateAnnotation = Annotation.Root({
  actionSummary: Annotation({ reducer: (_prev, next) => next, default: () => "" }),
  userInput:     Annotation({ reducer: (_prev, next) => next, default: () => "" })
});

// 展示转账信息
const showTransfer = () => ({
  actionSummary: "向张三转账 $100"
});

// 中断:等待用户确认
const waitConfirm = (state) => {
  const text = interrupt({
    hint: "终端里输入[确认]或者备注后回车,图才会继续",
    actionSummary: state.actionSummary
  });
  return { userInput: String(text) };
};

const graph = new StateGraph(StateAnnotation)
  .addNode("showTransfer", showTransfer)
  .addNode("waitConfirm", waitConfirm)
  .addEdge(START, "showTransfer")
  .addEdge("showTransfer", "waitConfirm")
  .addEdge("waitConfirm", END)
  .compile({ checkpointer: new MemorySaver() });

流程图

graph TD; __start__([__start__]) --> showTransfer[showTransfer] showTransfer --> waitConfirm[waitConfirm] waitConfirm -.->|interrupt: 等待输入| waitConfirm waitConfirm --> __end__([__end__])

执行过程:两阶段调用

javascript 复制代码
const config = { configurable: { thread_id: "interrupt-demo" } };

// 第一阶段:执行到 interrupt 自动暂停
const paused = await graph.invoke({}, config);
console.log("待你确认:", paused.__interrupt__?.[0]?.value);
// → "终端里输入[确认]或者备注后回车,图才会继续"

// 此时图的状态已保存到 MemorySaver,进程可以退出

// 第二阶段:用户输入后,用 Command 恢复执行
const rl = createInterface({ input: process.stdin, output: process.stdout });
const line = (await rl.question("> ")).trim();
await rl.close();

const done = await graph.invoke(new Command({ resume: line }), config);
console.log("done:", done);
// → { actionSummary:"向张三转账 $100", userInput:"确认" }

面试高频问:interrupt 和普通暂停有什么区别?

interrupt 普通暂停
状态保存 自动保存到 checkpointer 丢失
恢复方式 new Command({ resume }) 无法恢复
多次中断 支持,状态链式保存 不支持
生产可用 是,支持数据库持久化 仅开发调试

一句话:interrupt 是 LangGraph 的"人工审批节点"------暂停时状态自动存档,恢复时用 Command 传入人类决策,整个过程可审计、可持久化。


五、核心概念总结

API 速查表

API 作用 类比
Annotation.Root({}) 定义状态结构 数据库建表
StateGraph 创建流程编排器 画流程图
.addNode(name, fn) 添加处理节点 流程图里的方框
.addEdge(from, to) 固定连线 "做完 A 做 B"
.addConditionalEdges(from, fn, map) 条件分支/循环 "如果...就..." / "重来"
.compile({ checkpointer }) 编译 + 注入持久化 编译 + 配置运行时
interrupt({}) 中断等待人类输入 审批暂停点
new Command({ resume }) 恢复中断的图 传入审批结果
thread_id 会话隔离标识 用户 session ID

三种模式对比

模式 核心 API 典型场景
分支 addConditionalEdges → 不同节点 输入分类、意图路由
循环 addConditionalEdges → 回到自己 重试、轮询、等待条件满足
持久化 MemorySaver + thread_id 多轮对话、断点续传、多用户
中断 interrupt + Command 人工审批、确认操作

面试万能回答

LangGraph 的核心是有状态的图编排 。用 Annotation 定义共享状态,用 StateGraph 组织节点和边,用 addConditionalEdges 实现分支和循环,用 checkpointer 实现状态持久化和多会话隔离,用 interrupt 实现人类审批节点。它和 LangChain 共享底层基础设施,但解决了 LangChain 解决不了的复杂流程控制问题。


六、给你的代码加记忆,就一行的事

javascript 复制代码
// 没有记忆
const app = graph.compile();

// 有记忆(开发环境)
const app = graph.compile({ checkpointer: new MemorySaver() });

// 有记忆(生产环境)
import { SqliteSaver } from '@langgraph/checkpoint-sqlite';
const app = graph.compile({ checkpointer: new SqliteSaver() });

记住这个模式,所有 LangGraph 工作流都能一键升级为有记忆的版本。


觉得有帮助的话,点个赞👍支持一下~

相关推荐
怕浪猫7 小时前
GEO 优化到底是什么?AI时代内容创作者必须懂的新技能
算法·面试·github
IT大白鼠13 小时前
搜索系列 · 第 08 篇——面试收官:高频题与全景总结
elasticsearch·面试·nosql
JAVA面经实录91715 小时前
线上故障标准处理流程【完整版|面试背诵+生产落地】
java·面试·职场和发展
浪浪山_大橙子16 小时前
公司里的 AI,终于不只会聊天:我用 GPT‑6 把企业工作伙伴开源了
前端·后端·面试
无限码力17 小时前
华为OD面试手撕真题 【合并相邻且相等的元素】多语言题解
华为od·面试·华为od面试真题·华为od面试手撕真题·华为od面试算法真题
乌暮17 小时前
JVM 原理与实践:从运行时数据区到线上故障排查
java·开发语言·jvm·后端·学习·面试
时间的拾荒人19 小时前
Qt 界面布局与容器控件详解:从分组框到布局管理器
开发语言·qt·面试
ShineWinsu20 小时前
对于Redis:ZSet类型的解析
数据库·c++·redis·缓存·面试·有序集合·zset
keke.shengfengpolang21 小时前
2026年财务经营分析校招技术能力拆解:SQL取数、Power BI看板、AI辅助分析,面试到底考什么
面试
王中阳Go21 小时前
简历写「QPS 提升 3 倍」,面试官问「怎么压测的」,我卡在并发数怎么定
人工智能·后端·面试