LangGraph 实战:从 StateGraph 基础到多 Agent 编排,分支循环中断恢复全掌握

LangGraph 实战:从 StateGraph 基础到多 Agent 编排,分支循环中断恢复全掌握

。LangChain 的工作流是"线性的"------一条直线走到底;LangGraph 的工作流是"网状的"------可以分支、循环、中断、恢复。本文从"为什么需要多 Agent"讲起(上下文开销 vs 分工协作),然后手写五个实战案例:基础状态图(节点+边+状态)、条件路由(数学题 vs 聊天)、循环重试(失败自动重来)、MemorySaver 持久化(跨会话记忆)、interrupt 中断恢复(转账确认场景)。全部代码可运行,建议收藏后动手实操。


一、为什么需要多 Agent?

1.1 单 Agent 的上下文开销

css 复制代码
单 Agent 架构的问题:

  所有工具描述、所有功能 Prompt 都塞进 system prompt

  system prompt = {
    ...工具1的描述
    ...工具2的描述
    ...工具3的描述
    ...功能A的prompt
    ...功能B的prompt
    ...功能C的prompt
  }

  问题一:Token 消耗更高
  → 每次调用都要带上全部描述
  → 实际只执行一个功能
  → 90% 的 prompt 是冗余的

  问题二:无关信息干扰
  → 大量无关的 prompt 文本
  → 干扰 LLM 的注意力
  → 思考效率低
  → 更容易出错

  类比:
  → 每个部门开会都全员参加
  → 研发会议带销售、市场、客服一起
  → 效率低下,还互相干扰

1.2 多 Agent 的拆分思路

ini 复制代码
如何拆分多个 Agent?

  每个 Agent 只保留自己需要的 Prompt:

  编程 Agent → 只需要编程相关的 prompt
  测试 Agent → 只需要测试相关的 prompt
  验证 Agent → 只需要验证相关的 prompt

  优势:
  → 执行功能时消耗的 token 更少
  → 没有无关信息干扰
  → 准确率更高

  核心公式:
  Agent = LLM(大脑) + Harness(tool + mcp + rag + skill ...)

  ┌──────────────────────────────────────────────────────────┐
│  单 Agent vs 多 Agent                                    │
│                                                          │
│  单 Agent:                                              │
│  → 只有一个 LLM 大脑                                     │
│  → 需要一步步思考,调用 tool                             │
│  → 串行处理,所有能力塞进一个大脑                       │
│  → 上下文开销大,容易混乱                                │
│                                                          │
│  多 Agent:                                              │
│  → 多个大脑,并行思考                                    │
│  → 主 Agent 下发任务                                     │
│  → 子 Agent 并行处理完成后返回                           │
│  → 每个大脑选择适合的模型                                │
│  → Agent 组合式,按需加载,动态加载                      │
└──────────────────────────────────────────────────────────┘

1.3 多 Agent 分工协作

arduino 复制代码
多 Agent 分工合作的经典场景:

  主 Agent(项目经理)
    │
    ├── 编程 Agent:负责写代码
    ├── 测试 Agent:编写测试代码(TDD)
    └── 验证 Agent:验证代码是否符合预期
         │
         └── 告诉主 Agent:"通过了!"

  流程:
  → 主 Agent 拆解任务
  → 编程 Agent 写代码
  → 测试 Agent 写测试(TDD 先写测试)
  → 验证 Agent 验证代码
  → 全部通过 → 回报主 Agent

1.4 多 Agent 的三个核心理由

arduino 复制代码
为什么复杂 Agent 产品基本都是多 Agent 架构?

  ① 决策准确率高,Token 消耗更低
  → 每个 Agent 只带必要的最少 Prompt
  → 没有冗余信息干扰
  → 调用 LLM 次数变多,但总 token 更省
  → 准确率反而更高

  ② 并行思考和任务处理
  → 主管分派子任务
  → 子 Agent 并行处理
  → 整体效率更高

  ③ 多角色互相讨论,纠错能力更强
  → 不同角色的 Agent 互相审查
  → AutoGen 的"法庭"模式
  → 多个 Agent 辩论,互相纠错
  → 最终结论更可靠

  这就是 LangGraph 存在的意义:
  → 把多个 Agent 组织成"网状工作流"
  → 分支、循环、并行、中断
  → 多 Agent 协作的编排框架

二、LangChain → LangGraph:从线性到网状

2.1 LangChain 的基础模块

lua 复制代码
LangChain 提供的基础能力:

  ┌──────────────────────────────────────────────────────────┐
│  llm api           → 大模型调用接口                       │
│  document loaders  → 文档加载器                           │
│  splitter          → 文本分割器                           │
│  embedding         → 向量化                               │
│  vector store      → 向量数据库                           │
│  output parser     → 输出解析器                           │
│  memory            → 记忆管理                             │
│  ...               → 更多基础模块                         │
│                                                          │
│  → 这些是"积木"                                          │
│  → 负责单个能力的封装                                   │
│  → LangChain 是工具库                                    │
└──────────────────────────────────────────────────────────┘

2.2 线性 vs 网状

css 复制代码
LangChain 工作流:线性(一条直线)

  A → B → C → D
  │   │   │   │
  └───┴───┴───┘
  只能串行,不能回头,不能分支

  LangGraph 工作流:网状(灵活多路)

        ┌─→ B ─┐
  A ──→  │     ├──→ D
        └─→ C ─┘
  可以分支、循环、并行、中断、恢复

  ┌──────────────────────────────────────────────────────────┐
│  线性 vs 网状                                             │
│                                                          │
│                LangChain          LangGraph              │
│  ──────────────────────────────────────────────────      │
│  编排方式      线性(一条线)      网状(多路)           │
│  分支          不支持              条件路由               │
│  循环          不支持              回环重试               │
│  并行          不支持              并行节点               │
│  中断          不支持              interrupt              │
│  状态持久化    不支持              MemorySaver            │
│  场景          简单流程            复杂多 Agent           │
│  演进          简单 Agent          → 复杂多 Agent 协作    │
│                                                          │
│  LangGraph = 工作节点 + 组织方式(API)                   │
│  → 简单 Agent 可以用,复杂多 Agent 更要用                 │
└──────────────────────────────────────────────────────────┘

2.3 网状工作流编排的四大要素

sql 复制代码
LangGraph 的核心 API:

  ┌──────────────────────────────────────────────────────────┐
│  ① 开始节点(START)                                     │
│  → 工作流的入口                                          │
│  → 初始状态从这里进入                                    │
│                                                          │
│  ② 工作节点(Node)                                      │
│  → 每个节点有明确的职责                                  │
│  → 函数就是节点                                          │
│  → 接收 state,返回新 state                             │
│                                                          │
│  ③ 边(Edge)                                            │
│  → 连接工作节点                                          │
│  → 决定流程走向                                          │
│  → 可以是固定边、条件边                                  │
│                                                          │
│  ④ 结束节点(END)                                       │
│  → 工作流的出口                                          │
│  → 最终状态在这里输出                                   │
│                                                          │
│  流程:                                                   │
│  START → 节点 → 节点 → ... → END                        │
│  (初始状态)(职责)(边)(最终状态)                   │
└──────────────────────────────────────────────────────────┘

三、基础图实战:StateGraph 的 Hello World

3.1 完整代码

javascript 复制代码
// basic-graph.mjs --- 最基础的状态图
import {
  Annotation,  // 工作流的状态值的描述(数据 state)
  END,         // 结束节点
  START,       // 开始节点
  StateGraph   // 状态图,流程编排器,节点的组织
} from '@langchain/langgraph';

// Annotation 声明一个字段(数据部分)
const StateAnnotation = Annotation.Root({
  text: Annotation({
    // _prev: 来到当前节点之前的状态,next: 当前节点得到的状态
    // reducer: 怎么处理状态的改变
    reducer: (_prev, next) => next, // 类似数组 reduce,text 状态如何变
    default: () => '', // 默认值
  })
});

// 定义节点:函数就是节点
// 返回值是下一个节点的状态
const step1 = (state) => ({ text: `${state.text} -> step1` });
const step2 = (state) => ({ text: `${state.text} -> step2` });

// 实例化图(工作流编排器)
const graph = new StateGraph(StateAnnotation)
  .addNode('step1', step1)          // 声明所有节点
  .addNode('step2', step2)
  .addEdge(START, 'step1')          // 连接节点(边)
  .addEdge('step1', 'step2')
  .addEdge('step2', END)
  .compile();                       // 编译工作流,执行

// 可视化:mermaid 文本画图工具
// 简单的 markdown,自动生成流程图
const drawable = await graph.getGraphAsync();
const mermaid = drawable.drawMermaid({ withStyles: true });
console.log(mermaid);

const result = await graph.invoke({ text: 'hello' });
console.log(result);
arduino 复制代码
运行流程:

  invoke({ text: 'hello' })
    │
    ▼
  START(初始状态: text = "hello")
    │
    ▼
  step1 → text: "hello -> step1"
    │
    ▼
  step2 → text: "hello -> step1 -> step2"
    │
    ▼
  END → 返回 { text: "hello -> step1 -> step2" }

  最终输出:
  { text: 'hello -> step1 -> step2' }
ini 复制代码
生成的 mermaid 图:

  graph TD;
    __start__ --> step1;
    step1 --> step2;
    step2 --> __end__;

  → 可视化整个节点的流转关系
  → mermaid 是文本画图工具
  → 简单的 markdown 自动生成流程图
  → 方便调试和展示工作流

3.2 Annotation:状态声明

vbnet 复制代码
Annotation 的作用:

  → 描述工作流的状态结构
  → 定义有哪些字段
  → 定义字段如何变化(reducer)
  → 定义字段的默认值(default)

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

  字段名:text
  → 状态里只有一个 text 字段

  reducer: (_prev, next) => next
  → 状态如何更新
  → _prev: 旧值,next: 新值
  → (_, next) => next:直接覆盖
  → 类似数组 reduce,所以叫 reducer

  default: () => ''
  → 初始默认值
  → 状态未初始化时的兜底

3.3 节点:函数就是节点

javascript 复制代码
节点的本质:

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

  → 节点是一个普通函数
  → 入参:当前状态 state
  → 返回值:要更新的状态片段
  → 返回的对象会经过 reducer 处理合并进状态

  节点职责:
  → 接收状态
  → 处理业务逻辑
  → 返回新状态

  类比:
  → 状态 = 流水线上的"工件"
  → 节点 = 流水线上的"工位"
  → 每个工位加工后交给下一个工位

3.4 图的构建链式调用

scss 复制代码
StateGraph 的构建:

  new StateGraph(StateAnnotation)  // 传入状态定义
    .addNode('step1', step1)       // 注册节点
    .addNode('step2', step2)
    .addEdge(START, 'step1')       // 连接边
    .addEdge('step1', 'step2')
    .addEdge('step2', END)
    .compile();                    // 编译

  addNode(name, fn)
  → 注册节点
  → name: 节点名(字符串标识)
  → fn: 节点函数

  addEdge(from, to)
  → 连接两个节点
  → START / END 是内置的特殊节点
  → 固定走向,没有分支

  compile()
  → 编译工作流
  → 校验图的完整性
  → 生成可执行的图

四、条件路由:让工作流自己"选路"

4.1 场景设计

lua 复制代码
场景:一个入口,两个出口

  用户输入:
  → "1+2"(数学表达式)→ 走 math 节点,计算
  → "你好"(普通聊天)→ 走 chat 节点,回复

  流程图:
  ┌──────────────────────────────────────────────────────────┐
│                                                          │
│  START ──→ router ──→ math ──→ END                       │
│                │                                          │
│                └──→ chat ──→ END                          │
│                                                          │
│  router 是"路口":                                       │
│  → 根据 query 判断走哪条路                               │
│  → 含 + - * / 字符 → math                               │
│  → 否则 → chat                                            │
└──────────────────────────────────────────────────────────┘

4.2 完整代码

javascript 复制代码
// conditional-routing.mjs --- 条件路由
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);
  // 如果是数学问题走 math,否则走 chat
  return { route: isMath ? 'math' : 'chat' }
};

// 数学计算节点
const mathNode = (state) => {
  try {
    return { answer: String(eval(state.query)) } // "1+2" → 3 → "3"
  } 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')  // 固定走向:先到 router
  // 条件跳转:根据 state.route 的值判断,去 math 还是 chat
  .addConditionalEdges('router', (state) => state.route, {
    math: 'math',
    chat: 'chat'
  })
  .addEdge('math', END)
  .addEdge('chat', END)
  .compile();

console.log('result:', await graph.invoke({ query: '你好' }))
// { query: '你好', route: 'chat', answer: '你说的是:你好' }

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

4.3 addConditionalEdges:条件边

javascript 复制代码
条件边的三个参数:

  .addConditionalEdges(
    'router',                        // ① 从哪个节点出发
    (state) => state.route,          // ② 决定函数:返回路由值
    {                                // ③ 路由表:值 → 目标节点
      math: 'math',
      chat: 'chat'
    }
  )

  执行流程:
  router 节点执行完
    │
    ▼
  决定函数执行:(state) => state.route
    │
    ├── 返回 "math" → 查路由表 → 去 math 节点
    │
    └── 返回 "chat" → 查路由表 → 去 chat 节点

  类比:
  → 路由器(router)根据数据包的"目的地"选择转发路径
  → 这就是"路由"(routing)名字的由来

  与 addEdge 的区别:
  → addEdge:固定走向(一条路)
  → addConditionalEdges:根据状态动态选路(多条路)

4.4 eval():教学用,生产禁用!

javascript 复制代码
数学节点用到了 eval():

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

  eval('1+2') → 3
  eval('5*3') → 15

  eval() 是什么?
  → 把传入的字符串当作 JS 代码执行
  → 返回代码执行结果
  → 非常强大,也非常危险!

  ⚠️ 安全警告:
  ┌──────────────────────────────────────────────────────────┐
│  eval() 的危险:                                         │
│                                                          │
│  输入 "process.exit()"                                   │
│  → eval 直接执行 → 程序退出!                            │
│                                                          │
│  输入 "require('fs').readFileSync('/etc/passwd')"        │
│  → eval 直接执行 → 读取系统文件!                        │
│                                                          │
│  输入 "1; 恶意代码"                                      │
│  → eval 直接执行 → 任意代码注入!                        │
│                                                          │
│  这就是代码注入(Code Injection)攻击                    │
│                                                          │
│  ✅ 安全替代方案:                                       │
│  → 数学表达式解析库(mathjs 等)                         │
│  → 只解析数学表达式,不执行代码                          │
│  → 生产环境禁止使用 eval                                 │
└──────────────────────────────────────────────────────────┘

  教学目的:
  → 演示"分支路由"的核心思想
  → 用 eval 简化代码,聚焦路由逻辑
  → 理解危险,比回避危险更重要

五、循环重试:失败自动重来

5.1 场景设计

sql 复制代码
场景:重试直到成功

  → 某些操作可能失败(网络、临时错误)
  → 需要自动重试
  → 达到最大次数才算结束

  流程图:
  ┌──────────────────────────────────────────────────────────┐
│                                                          │
│  START ──→ attempt ──→ 成功?                            │
│                ↑            │                             │
│                │            ├─ 成功 → END                │
│                │            │                             │
│                └── 失败(retry)←───┘                     │
│                                                          │
│  attempt 节点:                                          │
│  → tries + 1                                             │
│  → 达到 3 次 → 成功 → 结束                              │
│  → 未达到 → 失败 → 回到 attempt 再来                    │
└──────────────────────────────────────────────────────────┘

5.2 完整代码

javascript 复制代码
// loop-retry.mjs --- 循环重试
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: () => ''
  })
});

// 尝试节点:每来一次尝试次数 +1
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('result:', await graph.invoke({ tries: 0 }))
yaml 复制代码
执行过程(invoke({tries: 0})):

  第 1 次进入 attempt:
  → tries: 0 + 1 = 1
  → ok: 1 >= 3 → false
  → message: "第1次失败"
  → 条件边 → 返回 "retry" → 回到 attempt

  第 2 次进入 attempt:
  → tries: 1 + 1 = 2
  → ok: 2 >= 3 → false
  → message: "第2次失败"
  → 条件边 → 返回 "retry" → 回到 attempt

  第 3 次进入 attempt:
  → tries: 2 + 1 = 3
  → ok: 3 >= 3 → true
  → message: "第3次成功"
  → 条件边 → 返回 "done" → END

  最终结果:
  { tries: 3, ok: true, message: '第3次成功' }
bash 复制代码
循环的本质:

  条件边指向自己 = 循环

  .addConditionalEdges('attempt', ..., {
    retry: 'attempt',  // 回到自己 → 循环!
    done: END
  })

  → 条件边不仅可以指向其他节点
  → 还可以指回自己
  → 这就形成了"循环"
  → 循环必须有出口(done → END)
  → 否则死循环

  实际应用:
  → API 调用失败重试
  → LLM 输出校验不过重试
  → 指数退避重试(间隔递增)

六、MemorySaver:状态持久化

6.1 为什么要持久化?

perl 复制代码
没有持久化的问题:

  → 每次 invoke 都是全新开始
  → 状态在调用结束后丢失
  → 无法跨会话记忆

  场景:访问计数
  → 第 1 次访问:显示"第1次进入"
  → 第 2 次访问:应该显示"第2次进入"
  → 但状态丢了,每次都显示"第1次"

  用 MemorySaver 保存状态:
  → 把 state 保存到内存里
  → 下次基于上次的 state 继续执行
  → 跨调用保持记忆

  更复杂的场景:
  → Agent 执行中断、失败、暂停、需要授权
  → MemorySaver 保存状态
  → 之后可以继续运行

6.2 完整代码

javascript 复制代码
// checkpointer-memory.mjs --- MemorySaver 状态持久化
import {
  Annotation,
  END,
  START,
  MemorySaver,  // 内存保存器
  StateGraph
} from '@langchain/langgraph';

const StateAnnotation = Annotation.Root({
  // session 相关:某人访问次数
  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
});

// 多用户:thread_id 区分会话
const user1 = { configurable: { thread_id: '用户-小张' } };
const user2 = { configurable: { thread_id: '用户-小李' } };

const res1 = await app.invoke({}, user1);
console.log(res1); // visitCount: 1, "第1次进入"
const res2 = await app.invoke({}, user1);
console.log(res2); // visitCount: 2, "第2次进入"
const res3 = await app.invoke({}, user1);
console.log(res3); // visitCount: 3, "第3次进入"
const res4 = await app.invoke({}, user2);
console.log(res4); // visitCount: 1, "第1次进入"(新会话)

6.3 MemorySaver 与 thread_id

python 复制代码
核心机制:

  checkpointer(检查点)
  → 在每次节点执行后保存状态快照
  → 相当于游戏里的"存档"
  → 下次可以从存档继续

  compile({ checkpointer })
  → 把 checkpointer 挂到图上
  → 图就具备持久化能力

  thread_id(线程 ID)
  → 区分不同的会话
  → 每个 thread_id 是独立的记忆空间
  → 小张的访问计数不影响小李

  ┌──────────────────────────────────────────────────────────┐
│  执行过程:                                               │
│                                                          │
│  user1 第 1 次 invoke                                    │
│  → 会话 "用户-小张" 状态为空                             │
│  → visitCount: 0 + 1 = 1                                 │
│  → 保存 → "第1次进入"                                    │
│                                                          │
│  user1 第 2 次 invoke                                    │
│  → 从内存读取上次状态                                    │
│  → visitCount: 1 + 1 = 2                                 │
│  → 保存 → "第2次进入"                                    │
│                                                          │
│  user1 第 3 次 invoke                                    │
│  → visitCount: 2 + 1 = 3                                 │
│  → "第3次进入"                                           │
│                                                          │
│  user2 第 1 次 invoke                                    │
│  → 新线程 "用户-小李"                                    │
│  → 状态独立,从 0 开始                                   │
│  → "第1次进入"                                           │
└──────────────────────────────────────────────────────────┘

6.4 持久化的进阶

arduino 复制代码
MemorySaver 的局限:

  → 保存在内存里
  → 进程重启就丢失
  → 只适合开发测试

  生产级持久化:

  ┌──────────────────────────────────────────────────────────┐
│  持久化方案:                                             │
│                                                          │
│  MemorySaver(内存)                                     │
│  → 开发测试用                                            │
│  → 进程重启丢失                                          │
│                                                          │
│  SQLite / Redis(数据库)                                │
│  → 生产环境用                                            │
│  → 重启不丢失                                            │
│  → 支持分布式共享                                        │
│                                                          │
│  用途:                                                   │
│  → Agent 执行中断、失败、暂停                            │
│  → 需要授权(human-in-the-loop)                         │
│  → 保存状态后下次继续运行                                │
│  → 这就是 harness 的中断、恢复机制                       │
└──────────────────────────────────────────────────────────┘

七、interrupt:中断与恢复

7.1 场景设计

vbnet 复制代码
场景:转账需要人工确认

  → AI 帮你转账,不能直接转!
  → 必须停下来等用户确认
  → 用户输入确认后才继续

  流程图:
  ┌──────────────────────────────────────────────────────────┐
│                                                          │
│  START ──→ showTransfer ──→ waitConfirm ──→ END          │
│                                │                          │
│                                │ interrupt(中断)        │
│                                ▼                          │
│                     等待用户输入确认...                    │
│                                │                          │
│                                │ Command({resume})        │
│                                ▼                          │
│                          继续执行                          │
│                                                          │
│  这就是 Human-in-the-Loop(人在回路)                     │
│  → AI 执行到关键步骤,停下来等人工批准                   │
│  → 这是 Agent 工程的安全底线                             │
└──────────────────────────────────────────────────────────┘

7.2 完整代码

javascript 复制代码
// graph-interrupt.mjs --- 中断与恢复
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() });

// 第一次 invoke:会中断在 waitConfirm
const config = { configurable: { thread_id: 'interrupt-demo' } };
const paused = await graph.invoke({}, config);
console.log('待你确认', paused.__interrupt__?.[0]?.value);
// 待你确认 { hint: '...', actionSummary: '向张三转账 $100' }

// 等用户输入(模拟人工确认)
const rl = createInterface({
  input: process.stdin,
  output: process.stdout
});
const line = (await rl.question('> ')).trim();
console.log('你输入了', line);
await rl.close();

// 第二次 invoke:用 Command({ resume }) 恢复执行
const done = await graph.invoke(new Command({ resume: line }), config);
console.log('done:', done);

7.3 中断机制详解

vbnet 复制代码
interrupt 的完整流程:

  第一次 invoke:
  → START → showTransfer(展示转账信息)
  → waitConfirm 执行到 interrupt(...)
  → 图在此"暂停"
  → 返回 paused 对象
  → paused.__interrupt__ 包含中断信息
    { hint: '...', actionSummary: '向张三转账 $100' }

  人工介入:
  → 用户看到转账信息
  → 在终端输入确认或备注
  → 这是"人在回路"(Human-in-the-Loop)

  第二次 invoke:
  → new Command({ resume: line })
  → resume 携带用户输入
  → 图从暂停处"恢复"
  → waitConfirm 收到用户输入
  → 继续执行到 END

  ┌──────────────────────────────────────────────────────────┐
│  为什么需要 checkpointer?                                │
│                                                          │
│  中断时:                                                 │
│  → 图执行到一半停止                                      │
│  → 状态必须保存!                                        │
│  → 否则恢复时不知道执行到哪了                             │
│                                                          │
│  MemorySaver 的作用:                                     │
│  → 保存中断时的状态快照                                  │
│  → 恢复时从快照继续                                      │
│  → 中断 + 持久化 = 可恢复的 Agent                        │
│                                                          │
│  场景:                                                   │
│  → 审批流程(AI 起草 → 人工批准 → 执行)                 │
│  → 支付确认(AI 生成订单 → 用户确认 → 扣款)             │
│  → 敏感操作(AI 生成代码 → 人工审核 → 合并)             │
└──────────────────────────────────────────────────────────┘

7.4 Command:控制图的指令

scss 复制代码
Command 是 LangGraph 的控制指令:

  new Command({ resume: line })
  → resume: 恢复中断时传给 interrupt 的返回值
  → waitConfirm 里 interrupt() 的返回值就是 line
  → 实现了"外部输入注入图中"

  Command 的其他用途:
  → Command({ goto: '节点名' }):指定跳转
  → Command({ update: {...} }):更新状态
  → Command({ resume: data }):恢复中断

  本质:
  → 图是"状态机"
  → 中断 = 停在某个状态
  → Command = 外部给状态机发指令
  → resume = 告诉它"继续,这是你要的数据"

八、总结

8.1 知识体系图

scss 复制代码
LangGraph 完整实战
│
├── 为什么需要多 Agent
│   ├── 单 Agent 上下文开销(冗余 prompt + 干扰)
│   ├── 每个 Agent 只带必要 prompt → 省 token + 高准确率
│   ├── Agent = LLM(大脑)+ Harness(tool/mcp/rag/skill)
│   ├── 主 Agent 下发任务,子 Agent 并行处理
│   └── 三大理由:省 token / 并行 / 互相纠错
│
├── LangChain → LangGraph
│   ├── LangChain:线性工作流 + 基础模块
│   ├── LangGraph:网状工作流编排
│   ├── 四大要素:START / 节点 / 边 / END
│   └── 场景:简单 Agent → 复杂多 Agent 协作
│
├── 基础图 StateGraph
│   ├── Annotation.Root 状态声明(reducer / default)
│   ├── 函数就是节点(state → new state)
│   ├── addNode / addEdge / compile / invoke
│   └── mermaid 可视化(getGraphAsync + drawMermaid)
│
├── 条件路由
│   ├── router 节点判断意图
│   ├── addConditionalEdges(节点, 决定函数, 路由表)
│   ├── 正则判断数学表达式
│   ├── eval() 教学演示(生产禁用!代码注入风险)
│   └── 分支:math / chat 两个出口
│
├── 循环重试
│   ├── 条件边指向自己 = 循环
│   ├── tries + 1 计数
│   ├── 成功 → done → END
│   ├── 失败 → retry → 回到 attempt
│   └── 应用:API 失败重试 / 输出校验重试
│
├── MemorySaver 持久化
│   ├── checkpointer 保存状态快照(存档)
│   ├── compile({ checkpointer })
│   ├── thread_id 区分多用户会话
│   ├── 跨调用保持记忆
│   └── 生产级:SQLite / Redis
│
├── interrupt 中断恢复
│   ├── interrupt({...}) 暂停图
│   ├── __interrupt__ 携带中断信息
│   ├── Command({ resume }) 恢复执行
│   ├── 必须配 checkpointer(保存中断状态)
│   └── 应用:审批 / 支付确认 / Human-in-the-Loop
│
└── 核心认知
    ├── 图 = 状态机(状态 + 节点 + 边)
    ├── 条件边 = 分支 + 循环
    ├── checkpointer = 存档机制
    ├── interrupt = 人在回路
    └── 单 Agent 串行思考 → 多 Agent 网状协作

8.2 一句话总结

LangGraph 是 LangChain 的网状升级:LangChain 的线性工作流只能 A→B→C 一条路走到底,LangGraph 的状态图支持分支、循环、中断、持久化------这正是复杂多 Agent 产品需要的编排能力。核心 API 只有四个要素:Annotation 声明状态(reducer 决定状态如何变)、函数即节点(state 进 state 出)、边连接节点(addEdge 固定走向 / addConditionalEdges 条件选路)、START/END 作为出入口。条件边指向自己是循环(失败重试),挂上 MemorySaver 是持久化(checkpointer 存档 + thread_id 分会话),interrupt 是中断(执行到关键步骤停下来等人工确认,Command({ resume }) 恢复)------这三个组合起来,就实现了 Agent 工程最核心的 Human-in-the-Loop 机制。多 Agent 为什么是趋势?单 Agent 把所有工具和 prompt 塞进一个大脑,token 浪费且互相干扰;多 Agent 各司其职,每个大脑只带最少 prompt,省 token、准确率高、还能并行和互相纠错。LangGraph 就是把这些"大脑"组织成网状工作流的编排框架。


如果这篇文章对你有帮助,欢迎点赞收藏

相关推荐
知无涯者1 小时前
Agent 008 - Permission
agent
Erishen2 小时前
从 SQLite 到 LLM 重排:一个 Rust RAG 服务的七层设计决策
架构·开源·agent
NineData2 小时前
NineData智能数据管理平台新功能发布|2026年8月
数据库·人工智能·oracle·中间件·agent·数据库开发·ninedata
DO_Community4 小时前
RAG 的 Embedding 模型需要微调吗?什么时候值得自己训练?
人工智能·llm·aigc·agent·ai编程
腾讯云开发者4 小时前
用 WorkBuddy 带你搭一支 7×24 小时待命的「AI 投研团队」
agent
moMo4 小时前
LangGraph 完全指南:从单 Agent 到多 Agent 工作流编排
agent
武子康5 小时前
小智的音频队列满了:丢旧帧、拒新包与播放延迟
人工智能·llm·agent
阿图灵5 小时前
LangGraph 实战 07:Event Streaming 事件流式——类型化投影与频道机制
agent
半糖程序员5 小时前
从零构建 Agent(3):接入阿里云百炼
agent