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

文章来源声明: 原文作者:黄敬峰; 来源站点:掘金; 原文链接:https://juejin.cn/post/7684615074136588328; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

适合已掌握 LangGraph 线性编排、想补齐分支循环与持久化的开发者。四类 API 对照清晰,MemorySaver 到生产级 Checkpointer 的平滑替换路径可直接落地,中断恢复示例对审批类场景尤其实用。

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

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

为什么需要分支?

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

LangGraph 用 addConditionalEdges 实现分支。

完整代码

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

<span>// ① 状态定义</span>
<span>const</span> <span>StateAnnotation</span> = <span>Annotation</span>.<span>Root</span>({
  <span>query</span>:  <span>Annotation</span>({ <span>reducer</span>: <span>(<span>_prev, next</span>) =></span> next, <span>default</span>: <span>() =></span> <span>""</span> }),
  <span>route</span>:  <span>Annotation</span>({ <span>reducer</span>: <span>(<span>_prev, next</span>) =></span> next, <span>default</span>: <span>() =></span> <span>"chat"</span> }),
  <span>answer</span>: <span>Annotation</span>({ <span>reducer</span>: <span>(<span>_prev, next</span>) =></span> next, <span>default</span>: <span>() =></span> <span>""</span> })
});

<span>// ② 路由节点:判断输入类型</span>
<span>const</span> <span>router</span> = (<span>state</span>) => {
  <span>const</span> isMath = <span>/[+\-*/]/</span>.<span>test</span>(state.<span>query</span>);
  <span>return</span> { <span>route</span>: isMath ? <span>"math"</span> : <span>"chat"</span> };
};

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

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

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

流程图

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

执行过程

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

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

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

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

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


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

为什么需要循环?

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

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

完整代码

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

<span>const</span> <span>attempt</span> = (<span>state</span>) => {
  <span>const</span> tries = state.<span>tries</span> + <span>1</span>;
  <span>const</span> ok = tries >= <span>3</span>;  <span>// 第3次才算成功</span>
  <span>return</span> {
    tries,
    ok,
    <span>message</span>: ok ? <span>`第<span>${tries}</span>次成功`</span> : <span>`第<span>${tries}</span>次失败`</span>
  };
};

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

流程图

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

执行过程

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

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

是的,都是 addConditionalEdges

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

<span>// 分支:映射到不同节点</span>
{ <span>math</span>: <span>"math"</span>, <span>chat</span>: <span>"chat"</span> }

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

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


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

没有持久化的问题

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

第1次调用 → <span>visitCount</span>=<span>0</span> → 结果<span>1</span>
第2次调用 → <span>visitCount</span>=<span>0</span> → 结果<span>1</span>   ← 又从头开始了
第3次调用 → <span>visitCount</span>=<span>0</span> → 结果<span>1</span>

MemorySaver:一行代码加记忆

<span>import</span> { <span>MemorySaver</span> } <span>from</span> <span>'@langchain/langgraph'</span>;

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

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

完整代码

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

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

<span>function</span> <span>recordVisit</span>(<span>state</span>) {
  <span>const</span> visitCount = state.<span>visitCount</span> + <span>1</span>;
  <span>const</span> message = visitCount === <span>1</span>
    ? <span>"这是你在本会话里第1次进入。"</span>
    : <span>`这是你在本会话里<span>${visitCount}</span>次进入`</span>;
  <span>return</span> { visitCount, message };
}

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

<span>const</span> checkpointer = <span>new</span> <span>MemorySaver</span>();
<span>const</span> app = graph.<span>compile</span>({ checkpointer });

多用户会话隔离

<span>const</span> user1 = { <span>configurable</span>: { <span>thread_id</span>: <span>"用户-小张"</span> } };
<span>const</span> user2 = { <span>configurable</span>: { <span>thread_id</span>: <span>"用户-小李"</span> } };

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

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

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

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

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


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

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

场景:转账确认

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

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

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

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

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

流程图

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

执行过程:两阶段调用

<span>const</span> config = { <span>configurable</span>: { <span>thread_id</span>: <span>"interrupt-demo"</span> } };

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

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

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

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

面试高频问: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 解决不了的复杂流程控制问题。


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

<span>// 没有记忆</span>
<span>const</span> app = graph.<span>compile</span>();

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

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

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


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