LangGraph 实战入门:用状态图写出分支、重试与人工确认

LangGraph 实战入门:用状态图写出分支、重试与人工确认

我第一次把一段 Agent 逻辑写长以后,代码很快变成了这种形态:先判断问题类型,再调用不同工具;失败就重试;碰到转账一类高风险操作,还要暂停等用户确认。继续用一串 if/else 和函数调用当然能写,但流程藏在代码里,状态散落在局部变量中,出错后也很难从中间恢复。

这正是我学习 LangGraph 的原因。LangGraph 的核心不是"再封装一次大模型调用",而是把流程显式建模为状态图(State Graph):节点负责工作,边负责决定下一步,状态负责在节点间传递数据。

我用五个小例子按"直线流程 → 条件分支 → 循环重试 → 状态持久化 → 人工确认"的顺序逐步实现。前四个非交互示例已经在本地运行;人工确认示例需要终端输入,这里重点解释控制流程。

1. 先把最小状态图跑通

一个最小 LangGraph 包含三类东西:

  • State:整个流程共享的数据;
  • Node:接收旧状态、返回局部新状态的函数;
  • Edge:节点之间的流转关系。

先从一个只有 text 字段的基础示例开始,两个节点依次向它追加文本:

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

const State = Annotation.Root({
  text: Annotation({
    reducer: (_previous, next) => next,
    default: () => "",
  }),
});

const step1 = (state) => ({ text: `${state.text} -> step1` });
const step2 = (state) => ({ text: `${state.text} -> step2` });

const graph = new StateGraph(State)
  .addNode("step1", step1)
  .addNode("step2", step2)
  .addEdge(START, "step1")
  .addEdge("step1", "step2")
  .addEdge("step2", END)
  .compile();

console.log(await graph.invoke({ text: "hello" }));

本地输出为:

text 复制代码
{ text: 'hello -> step1 -> step2' }

Annotation.Root 定义状态结构。每个字段的 reducer 决定"节点返回的新值怎样合并进旧状态"。这里直接采用 next,相当于覆盖;如果状态是消息数组,也可以让 reducer 做追加。

我一开始容易把节点理解成"下一步命令"。更准确的理解是:节点只描述一次状态转换,图才负责调度。节点知道输入和输出,不需要知道自己前后连接了谁。

2. 条件边:把路由决策从节点里拿出来

真实流程不会永远直线前进。比如数学表达式走计算节点,普通文本走聊天节点。路由节点只写入决策结果,条件边再把结果映射到目标节点:

js 复制代码
const State = Annotation.Root({
  query: Annotation({ reducer: (_p, n) => n, default: () => "" }),
  route: Annotation({ reducer: (_p, n) => n, default: () => "chat" }),
  answer: Annotation({ reducer: (_p, n) => n, default: () => "" }),
});

const router = (state) => ({
  route: /[+\-*/]/.test(state.query) ? "math" : "chat",
});

const mathNode = (state) => ({
  answer: String(eval(state.query)),
});

const chatNode = (state) => ({
  answer: `你说的是:${state.query}`,
});

const graph = new StateGraph(State)
  .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();

这个示例本地分别得到 3 和"你说的是:你好"。但其中的 eval() 只能用来演示分支,不能接收真实用户输入:它会执行任意 JavaScript。生产代码应该换成受限表达式解析器,或者把允许的运算明确实现出来。

这里也体现了方案 A 与方案 B 的区别:

  • 方案 A:在 router 内部直接调用 math/chat。代码短,但路由、执行和错误处理耦合在一起;
  • 方案 B:router 只返回结构化决策,条件边负责跳转。节点更容易测试,流程也能画成图。

当流程只有两个函数时,两者差别不大;一旦增加评估、重试和人工审批,方案 B 更容易维护。

3. 循环不是 while,而是"回到哪个节点"

Agent 常见逻辑是"尝试一次---检查结果---失败再试"。LangGraph 可以用条件边连回当前节点:

js 复制代码
const RetryState = Annotation.Root({
  tries: Annotation({ reducer: (_p, n) => n, default: () => 0 }),
  ok: Annotation({ reducer: (_p, n) => n, default: () => false }),
  message: Annotation({ reducer: (_p, n) => n, default: () => "" }),
});

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

const retryGraph = new StateGraph(RetryState)
  .addNode("attempt", attempt)
  .addEdge(START, "attempt")
  .addConditionalEdges(
    "attempt",
    (state) => (state.ok ? "done" : "retry"),
    { done: END, retry: "attempt" },
  )
  .compile();

实测最终状态是:

text 复制代码
{ tries: 3, ok: true, message: '第3次成功' }

这个写法比把 while 藏在节点里多了一点配置,却换来了两个好处:每次尝试都是可观察的状态转换;以后要在重试之间插入日志、退避等待、换模型或人工介入,不需要拆开一个大循环。

生产环境还必须增加最大步数或失败出口。示例用"第三次必定成功"保证终止,真实的外部调用没有这个保证。

4. Checkpointer:同一个图,不同会话各自续跑

大模型本身是无状态的,图执行也不会自动记住上一次结果。Checkpointer(检查点保存器)负责把某个线程的状态保存下来。

js 复制代码
import { MemorySaver } from "@langchain/langgraph";

const graph = new StateGraph(VisitState)
  .addNode("recordVisit", (state) => {
    const visitCount = state.visitCount + 1;
    return {
      visitCount,
      message: `这是你在本会话里第${visitCount}次进入。`,
    };
  })
  .addEdge(START, "recordVisit")
  .addEdge("recordVisit", END)
  .compile({ checkpointer: new MemorySaver() });

const userA = { configurable: { thread_id: "用户-A" } };
const userB = { configurable: { thread_id: "用户-B" } };

await graph.invoke({}, userA);
await graph.invoke({}, userB);
console.log(await graph.invoke({}, userA));
console.log(await graph.invoke({}, userB));

本地运行时,A 和 B 的第二次结果都为 2,彼此没有串线。关键不是用户名,而是 thread_id:它是状态分区键。

MemorySaver 适合学习和单进程演示,进程退出后数据不会可靠保留。真正的服务要换成数据库或 Redis 一类持久化后端,并考虑线程 ID 的鉴权,不能让用户随便猜到另一个会话的 ID。

5. interrupt:高风险动作前暂停,而不是"问完继续猜"

自动化流程最危险的情况之一,是模型把"应该询问用户"误当成"用户已经同意"。LangGraph 的 interrupt 会真正暂停图,保存状态,直到外部传入恢复命令。

js 复制代码
import { interrupt, Command, MemorySaver } from "@langchain/langgraph";

const waitConfirm = (state) => {
  const decision = interrupt({
    hint: "请输入确认或取消",
    actionSummary: state.actionSummary,
  });
  return { userInput: String(decision) };
};

const graph = new StateGraph(TransferState)
  .addNode("showTransfer", () => ({
    actionSummary: "向指定收款人转账 100 元",
  }))
  .addNode("waitConfirm", waitConfirm)
  .addEdge(START, "showTransfer")
  .addEdge("showTransfer", "waitConfirm")
  .addEdge("waitConfirm", END)
  .compile({ checkpointer: new MemorySaver() });

const config = { configurable: { thread_id: "transfer-demo" } };
const paused = await graph.invoke({}, config);

// 用户确认后,再显式恢复
const done = await graph.invoke(
  new Command({ resume: "确认" }),
  config,
);

我最初尝试直接传入普通状态对象恢复流程。按当前 LangGraph API,更清晰的做法是使用 Command({ resume })。这里的转账仅用于说明控制流,不会触发真实操作。

对比两种方案:

  • 普通聊天确认:模型说"请确认"后仍可能在同一轮继续执行,确认状态也不一定持久化;
  • 图中断:执行引擎停止,检查点保存,只有明确的恢复输入才能继续。

涉及删除、付款、发布、发消息等外部副作用时,我会优先选择后者。

6. 我会怎样组织一个真实 Agent 图

把这些积木放在一起,一个可维护的 Agent 流程通常是:

  1. 路由节点产生结构化决策;
  2. 条件边选择工具或直接回答;
  3. 执行节点写入结果;
  4. 评估节点判断是否重试;
  5. 达到次数上限后转人工处理;
  6. 高风险动作前用 interrupt 暂停;
  7. 全程由 Checkpointer 按 thread 保存状态。

节点应该"小而纯":输入状态,返回状态。数据库写入、HTTP 请求等副作用要集中在少数明确节点中,这样回放或重试时才知道哪些动作不能重复执行。

结尾

我对 LangGraph 最有用的认识,不是记住 addNode 或 addEdge,而是把 Agent 当作一个有状态、可暂停、可恢复的业务流程:

  • 直线步骤用普通边;
  • 动态选择用条件边;
  • 失败重试用回边,并设置终止护栏;
  • 多轮续跑用 Checkpointer;
  • 高风险操作用 interrupt 做真正的人工确认。

下一步可以在这个骨架上加入结构化输出,让路由节点不再靠正则;再把向量检索、评估和网络兜底接进图里,形成 Agentic RAG。后续三篇文章会沿着这条路线继续。

相关推荐
桃西西呀1 小时前
LangChain 之五:智能体与工具
人工智能·langchain·llm
夫子3961 小时前
【第三部分:第一个 Agent 应用】14. 开发一个完整的企业知识助手:从 RAG 到 Agent
llm·agent
GustorWang1 小时前
我开源了一个 agent harness:同一颗 2B 模型,四个框架得分从 0.017 到 0.821
agent
栈知见1 小时前
03-AgentScope核心概念全景:Agent / Message / Model 与 ReAct
agent
liulilittle1 小时前
短期内不存在通用 AI 工作流魔法
大数据·开发语言·c++·人工智能·llm·agent·tools
不好听6132 小时前
当 Agent 框架开始「交钥匙」——DeepAgents 系列之一
langchain
BenedictHook2 小时前
技能商店 SkillHub:Windows端 AI Skill 管理工具,支持搜索、查看与安装
c++·agent·桌面应用·skill·windows开发·skillhub
方方洛2 小时前
ai-agent教程-03-大模型接口与工具调用
人工智能·llm·agent
光影少年2 小时前
langchain与langgraph区别以及学习路线
elasticsearch·langchain·llm