单 Agent 是直线代码,多 Agent 是一张会分岔、循环、暂停的图:LangGraph 工作流编排

单 Agent 是直线代码,多 Agent 是一张会分岔、循环、暂停的图:LangGraph 工作流编排

如果把复杂 Agent 比作一家公司,LangChain 是"流水线",LangGraph 是"组织架构图"。 本文用最小可运行的代码,把 LangGraph 最值钱的分岔、循环、暂停恢复三件事讲透。


1. 先说人话:为什么需要"图"而不是"一条线"

上一个项目里,我用 LangChain 写 Agent,本质是线性工作流:A 做完 → B 做 → C 做,一条直线走到底。

可真实的 Agent 不是流水线:

  • 用户问"1+2 等于几",和问"你好",走的根本不是同一条路------该分岔;
  • 调模型失败、校验不过,要重试 ------该循环;
  • 转账要用户确认 才能继续------该暂停,等批准再走。

直线写得出来这些,但代码会变成一坨 if/else 套循环套回调,自己都嫌乱。

LangGraph 换个思路:把流程本身画成一张图。

复制代码
开始
  │
  ▼
 step1 ──► step2 ──► 结束
  • 节点 = 一个函数(做一件事)
  • 边 = 节点怎么连(路由怎么走)
  • state = 整张图的"全局血液",节点读它、改它、传给下一个

我最开始理解 LangGraph,就是这句话:节点是函数,边决定走向,State 是血液。 抓准这三个词,后面全通了。


2. 最小图:State = 图的血液,reducer 决定"改"的方式

先看最基础的一张图,两个节点串起来:

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

// ① 声明状态的"形状":只有 text 一个字段
const StateAnnotation = Annotation.Root({
  text: Annotation({
    // 关键:reducer 决定"这个字段更新时怎么变",而不是无脑覆盖
    reducer: (_prev, next) => next,
    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();

const result = await graph.invoke({ text: "hello" });
console.log(result); // { text: "hello -> step1 -> step2" }

全图只有三件事:

节点 = 函数。 addNode("step1", step1),函数入参是当前 state,返回的是更新后的状态。

边 = 走向。 addEdge(START, "step1") 从入口进 step1,最后 addEdge("step2", END) 收在结束。

reducer 决定状态怎么变。 这是最容易忽略、也最值钱的一句。reducer: (_prev, next) => next 意思是:新来的覆盖旧的。但如果你想让字段追加而不是覆盖,把 reducer 改成数组拼接即可------状态怎么合并,是你说了算,而不是框架一刀切。

用 console.log(state.result.text) 能看到图里所有中间 state 流转,调试必备。


3. 分岔:让图自己挑路走(条件边)

同一个入口,问题不同走不同节点------这就是 addConditionalEdges。

js 复制代码
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: () => "" })
});

// 路由节点:看 query 决定 next 走哪
const router = (state) => {
  const isMath = /[+\-*/]/.test(state.query);
  return { route: isMath ? "math" : "chat" };
};
const mathNode = (state) => ({ answer: String(eval(state.query)) });
const chatNode = (state) => ({ answer: `你说的是:${state.query}` });

const graph = new StateGraph(StateAnnotation)
  .addNode("router", router)
  .addNode("math", mathNode)
  .addNode("chat", chatNode)
  .addEdge(START, "router")
  // 关键:条件边!返回值 "math" / "chat" 决定走哪条
  .addConditionalEdges("router", (state) => state.route, {
    math: "math",
    chat: "chat"
  })
  .addEdge("math", END)
  .addEdge("chat", END)
  .compile();

console.log(await graph.invoke({ query: "你好" })); // { route:'chat', answer:'你说的是:你好' }
console.log(await graph.invoke({ query: "1+2" }));  // { route:'math', answer:'3' }
lua 复制代码
            ┌─ math ─ 算结果 ─┐
开始 → router                END
            └─ chat ─ 回一句 ─┘

addConditionalEdges("router", fn, map) 的机制:fn(state) 返回一个 key,map 里 key → 节点名,图就跳到对应节点。把"决策"从节点里抽到边上,是整个框架最灵活的地方。

⚠️ 踩坑(readme 里的原话,最常见的坑):eval() 会把字符串当 JS 代码 执行。这里 eval("1+2") 是 3,但如果 query 是恶意字符串(比如 process.exit()),你的进程就没了 。demo 里图方便用它,生产环境绝不能用 eval 跑用户输入,代码里 dogfood 时尤其要警惕。


4. 循环:条件边指向自己 = 重试

分岔是边指到另一个节点;如果条件边指回自己,就是循环重试。

js 复制代码
const attempt = (state) => {
  const tries = state.tries + 1;
  const ok = tries >= 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();
ini 复制代码
             ┌────────── retry ─────────┐
             ▼                          │
开始 → attempt(tries=1,2,3) ── done ──► END

第 1、2 次 ok=false → 走 retry 回到自己,tries 累加;第 3 次 ok=true → 走 done 结束。重试 3 次的逻辑,在 LangGraph 里就是"一条边指回自己",循环越界全靠 state 里的 tries 条件收敛,优雅到有点不真实。


5. 暂停与恢复:把"人工确认"做进图里

前面三招已经能画公司架构图了,但还缺最关键的一环------机器算得再好,转账要不要真的打? 得人点头。

LangGraph 用 interrupt + Command({ resume }) + MemorySaver 三件套解决"图走一半停下来等人"。

js 复制代码
const StateAnnotation = Annotation.Root({
  actionSummary: Annotation({ reducer: (_prev, n) => n, default: () => "" }),
  userInput:     Annotation({ reducer: (_prev, n) => n, 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() }); // ⚠️ 必须有 checkpointer 才能暂停

const config = { configurable: { thread_id: "interrupt-demo" } };
const paused = await graph.invoke({}, config);
console.log("待你确认:", paused.__interrupt__?.[0]?.value); // → 提示确认

// ...你(人)确认了,把结果喂回去,图从打断处继续
const done = await graph.invoke(new Command({ resume: "确认, 金额正确" }), config);
console.log("done:", done); // → userInput: "确认, 金额正确"
arduino 复制代码
开始 → 显示转账概要 → 【interrupt 停下来,等人确认】
                                │  用户输入"确认"
                                ▼
                         继续 → 结束(拿到 userInput)

这下"转账前必须人工确认"这种需求,不再是堆锁和状态机,而是图上一个会暂停的节点 。interrupt 暂停、Command.resume 续走、thread_id 记住是哪一段中断,天然支持审批流、人工兜底、失败恢复。


6. 持久化 + 多用户隔离:thread_id 是"谁的记忆"

上面 interrupt 用到的 MemorySaver,同时解决一个大问题:默认每次 invoke 状态都从零开始,重启全丢。 checkpointer 把 state 存起来,同 thread_id 下次继续。

js 复制代码
const checkpointer = new MemorySaver();
const app = graph.compile({ checkpointer });

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

await app.invoke({}, user1); // 小张 visitCount=1
await app.invoke({}, user1); // 小张 visitCount=2(记住了!)
await app.invoke({}, user2); // 小李 visitCount=1(隔离,不串)
  • 同一个 thread_id → 状态在上一次基础上继续(记忆)
  • 不同 thread_id → 互不干扰,天然多用户隔离

到项目里,把 MemorySaver 换成 SQLite / Redis / Postgres 的 checkpointer,state 就能跨进程、跨重启持久化------Agent 从"每次失忆"变成"有记忆的个体"。


7. 一句话收尾

LangChain 教会我串 (线性流水线);LangGraph 教会我画(一张状态图)。

真正的分水岭不是"多一个库",而是想法的转变:

别再把流程写成代码里的一串命令,把它画成一幅图------节点是函数、边是走向、State 是血液。 这样,决策放边上(分岔)、失败指回自己(循环)、拿不准就停在半路等批准(暂停恢复),全都成了图的"艺术",而不再是你手工维护的 if/else 地狱。

多 Agent 的协作结构(主管下发、子 Agent 并行、评审纠错),本质就是一张更大的、会分岔会循环的图。先学会画这张图,多 Agent 才真正落地。

相关推荐
搬砖的小码农_Sky9 小时前
AI Agent:如何处理Claude Code 最近版本(2026年更新)引入的模型上下文限制
人工智能·windows·ai·ai编程
XLYcmy10 小时前
Dify 本地部署、Ollama 与 Xinference 集成:踩坑与最佳实践指南
llm·agent·dify·rag·ollama·rerank·harness
YOLO数据集集合11 小时前
EvoAgent:面向PR研发治理的自进化Multi-Agent Harness系统
java·开发语言·目标检测·agent·自进化
liuchangng11 小时前
Agent的state注入
人工智能·agent·jev·决策模型
全栈弄潮儿12 小时前
一次真实失败:AI 代码看起来没问题,为什么还是翻车了?
aigc·openai·ai编程
李航198312 小时前
AI定制柜建模,需要详细的建模规范和标准流程
人工智能·python·计算机视觉·ai·ai编程
不懂59814 小时前
如何用 pheromone-network 管理智能体上下文
agent
长弓三石14 小时前
用纯 Java 做一个企业级 Agent Harness 平台:BizBuddy 的设计与取舍
开源·agent·ai编程
长弓三石14 小时前
把 AgentScope Harness 装进 RuoYi-Vue-Plus:纯 Java AI 平台的集成实践
java·人工智能·agent
dora14 小时前
LangChain4j 新手入门实战教程(Java版)
后端·langchain·agent