LangGraph 深度解析:从增强型 LLM 到生产级 Agent

LangGraph 深度解析:从增强型 LLM 到生产级 Agent

你用过 withStructuredOutput,也写过 bindTools,但一旦业务变复杂------多轮工具调用、条件分支、并行请求、人在回路------光靠链式调用就开始力不从心。LangGraph 就是为这个而生的。


这篇文章怎么读

这不是那种"把文档翻译一遍"的教程。它会沿着一条真实的认知路径走下来:

普通 LLM 的局限 → 增强型 LLM 能做什么 → 为什么增强型 LLM 还不够 → Agent 循环的本质困境 → LangGraph 如何用"图"来解决这一切

建议按顺序读。 每一章都在为下一章打基础,跳着看容易在某个地方卡住------尤其是第二部分(LangGraph 核心机制),它是后面所有模式的基础,值得多花点时间。

读完你会得到什么:

  • 理解增强型 LLM 的底层原理,不只是会用 API
  • 理解 ReAct Agent 的本质------它到底是一个什么循环
  • 彻底搞懂 LangGraph 的 State / Node / Edge 和执行引擎
  • 掌握六大 Agent 设计模式的完整代码实现
  • 知道面试时怎么把 Agent 架构讲出层次感
  • 了解 LangGraph 的生产级特性:Memory、Human-in-the-Loop、Streaming、部署

第一部分:为什么需要 LangGraph

在学一个框架之前,先搞清楚"没有它的时候,我们是怎么熬过来的"。理解了痛苦,才能理解解决方案。


第一章:从普通 LLM 到增强型 LLM

1.1 裸调 LLM 的四个硬伤

typescript 复制代码
const msg = await llm.invoke("What is 2 times 3?");
console.log(msg.content);
// "2 times 3 is 6"

输入一段字,输出一段字------这是最原始的 LLM 调用。看起来没什么问题?在真实业务里,它有四个致命短板:

痛点 为什么它是问题 具体例子
输出不可控 你期望 JSON,它偏给你自然语言;你期望字符串数组,它给一个段落 你想把结果存数据库,LLM 返回了一句"好的,结果如下......"
没有行动能力 LLM 只能"说"它想做什么,不能真的去查数据库、调 API、发邮件 用户问"今天天气",LLM 只能说"你可以去天气网站查"
无法验证输入输出 LLM 返回的数据有没有缺字段、类型对不对,你完全没有保障 你期望 {name: string, age: number},LLM 返回 {name: "张三"} 漏了 age
无法做多步骤任务 要"先搜索→再阅读→再总结",你需要在外面写一大堆 if/else 和 while 每一步的结果要传给下一步,中间某步失败还要处理重试

1.2 第一重增强:结构化输出 --- 让 LLM 按你的格式填表

typescript 复制代码
import { z } from "zod";

// 用 Zod 定义"答题卡"的格式
const searchQuerySchema = z.object({
  searchQuery: z.string().describe("Query optimized for web search"),
  justification: z.string().describe("Why this query is relevant"),
});

// 给 LLM 戴上"紧箍咒"
const structuredLlm = llm.withStructuredOutput(searchQuerySchema, {
  name: "searchQuery",
});

// 调用------返回值是类型安全的对象,不是乱码的自然语言
const output = await structuredLlm.invoke(
  "How does Calcium CT score relate to high cholesterol?"
);

console.log(output.searchQuery);
// → "Calcium CT score high cholesterol correlation"
console.log(output.justification);
// → "Directly addresses the user's question about..."

这背后发生了什么? 很多人以为 Zod 直接"约束"了 LLM 的输出------这是误解。整个流程分四步:

swift 复制代码
第 1 步:翻译(Zod → JSON Schema)
  z.object({ searchQuery: z.string(), ... })
  ↓ LangChain 内部调用 zod-to-json-schema 库 ↓
  {
    "type": "object",
    "properties": {
      "searchQuery": { "type": "string", "description": "Query optimized..." },
      "justification": { "type": "string", "description": "Why..." }
    },
    "required": ["searchQuery", "justification"]
  }

第 2 步:传递给 LLM API
  LangChain 把这个 JSON Schema 塞进 OpenAI 的 response_format 或 tools 参数。
  → 相当于给 LLM 发了一张"必须按此格式填写的答题卡"。

第 3 步:LLM 按 Schema 生成 JSON 字符串
  → "{\"searchQuery\": \"...\", \"justification\": \"...\"}"

第 4 步:Zod 做"质检"(运行时验证)
  → schema.parse(jsonString)
  → 字段全不全?类型对不对?
  → 不对就抛 ZodError,阻止错误数据继续往下流

关键认知:真正"约束"LLM 输出格式的,不是 Zod,而是 OpenAI/Anthropic API 底层的 response_formattools 参数。Zod 扮演两个角色------调用前负责"画图纸"(生成 JSON Schema 给 LLM 看),调用后负责"验尺寸"(校验返回值是否达标)。

注意你代码里的 .describe() 非常重要------它会被翻译成 JSON Schema 里的 description 字段,LLM 会阅读这段描述 来理解每个字段应该填什么内容。如果你写成 z.string("描述")(这是错的),Zod 会把 "描述" 当作默认值而不是提示词,LLM 根本看不到。

1.3 第二重增强:工具调用 --- 让 LLM 能够"动手"

结构化输出解决了"说出来的话格式对不对"。但 LLM 还有一个更致命的局限------它什么也做不了

typescript 复制代码
import { tool } from "@langchain/core/tools";
import { z } from "zod";

// 定义一个真实的可执行函数
const multiply = tool(
  async ({ a, b }: { a: number; b: number }) => {
    // 这段代码真的会跑在你的服务器上!
    return a * b;
  },
  {
    name: "multiply",
    description: "multiplies two numbers together",
    schema: z.object({
      a: z.number().describe("the first number"),
      b: z.number().describe("the second number"),
    }),
  }
);

// 把工具"挂载"到 LLM 上
const llmWithTools = llm.bindTools([multiply]);

// 调用
const message = await llmWithTools.invoke("What is 2 times 3?");

// LLM 没有真的计算!它只生成了一个"调用请求"
console.log(message.tool_calls);
// [
//   {
//     name: "multiply",
//     args: { a: 2, b: 3 },
//     id: "call_abc123"
//   }
// ]

用个比喻帮你记住:

LLM 就像一个坐在办公室里的经理。你不会指望经理亲自去搬箱子。你指望的是:他听懂你的需求,写一张"派工单"(tool_calls),交给真正干活的人(你的 JS 函数)去执行。经理拿到执行结果后,再决定下一步。

这个比喻引出了一个自然而然的问题:谁来当"跑腿的"------负责"拿派工单→找工人→把结果送回经理→经理继续写派工单→..."这个循环?

这个"跑腿的",就是 Agent。而这个循环的编排,就是 LangGraph 要做的事。


第二章:从增强型 LLM 到 Agent------问题升级

2.1 一个自然的演化

上一步你学会了两个增强手段:

  • withStructuredOutput → LLM 说出来的话格式可控
  • bindTools → LLM 能写"派工单"

现在你的大脑会自动把它们串起来:

用户问一个问题 → LLM 写派工单 → 执行工具 → 把结果还给 LLM → LLM 再写派工单 → 执行 → ... → 最终回答

这就是 Agent 循环 。学术上最早系统化这个模式的,是 2022 年 Google 和 Princeton 的 ReAct(Reasoning + Acting) 论文。

2.2 ReAct:让 LLM"边想边做"

ReAct 的核心思想非常朴素:

不要一次性生成最终答案。像人一样:思考 → 行动 → 观察结果 → 再思考 → 再行动 → 直到满意。

yaml 复制代码
用户:"帮我查一下苹果市值,判断是否值得投资"

第 1 轮
  💭 Thought: 需要查 AAPL 当前市值
  🔧 Action: search_stock_price("AAPL")
  👁 Observation: 市值 = 3.2 万亿美元

第 2 轮
  💭 Thought: 光看市值不够,还需要市盈率 + 行业对比
  🔧 Action: get_financial_metrics("AAPL")
  👁 Observation: PE = 32, 行业平均 PE = 28

第 3 轮
  💭 Thought: PE 略高但有品牌护城河,中长期看好
  ✅ Final Answer: 苹果市值 3.2 万亿,PE 32 高于行业均值 28,
     但考虑到其现金储备和服务收入增长,仍具投资价值...

2.3 不用框架手写一个 ReAct------看看有多容易崩

在 LangGraph 出现之前,这个循环只能靠手写。下面是它的样子:

typescript 复制代码
async function reactAgent(userInput: string) {
  const messages = [new HumanMessage(userInput)];
  const tools = [searchTool, calculatorTool, weatherTool];
  const MAX_LOOPS = 10;
  let loopCount = 0;

  while (loopCount < MAX_LOOPS) {
    loopCount++;
    const response = await llmWithTools.invoke(messages);

    // 情况 1:LLM 认为可以给最终答案了
    if (response.content && !response.tool_calls?.length) {
      return response.content;
    }

    // 情况 2:LLM 想调工具
    if (response.tool_calls?.length) {
      messages.push(response); // AI 的消息(包含 tool_calls)

      for (const toolCall of response.tool_calls) {
        const tool = tools.find(t => t.name === toolCall.name);

        // 😰 工具不存在怎么办?
        if (!tool) {
          messages.push(new ToolMessage({
            content: `Error: tool "${toolCall.name}" not found`,
            tool_call_id: toolCall.id,
          }));
          continue;
        }

        try {
          const result = await tool.invoke(toolCall.args);
          messages.push(new ToolMessage({
            content: JSON.stringify(result),
            tool_call_id: toolCall.id,
          }));
        } catch (error) {
          // 😰 工具执行失败怎么办?
          messages.push(new ToolMessage({
            content: `Error: ${error.message}`,
            tool_call_id: toolCall.id,
          }));
        }
      }
      continue;
    }

    // 情况 3:LLM 既不回答也不调工具------😰 卡死了?
    break;
  }

  return "Agent stopped without reaching a conclusion.";
}

这段代码看起来"能跑",但实际上藏着五个你迟早会踩到的大坑:

问题 具体表现 手工解决有多麻烦
无限循环 LLM 反复调同一个工具但得不到满意结果,MAX_LOOPS 用完才强制终止 需要设计智能的循环终止策略(不只是靠计数器)
状态失控 对话历史全塞在 messages 数组里,每一轮都在膨胀,Token 费用爆炸 需要自己实现消息摘要/压缩/选择性遗忘
崩溃即丢失 跑到第 8 轮时 API 超时,前面 7 轮的结果全没了,钱也花了 需要自己实现检查点/快照机制
无法注入人工判断 想在第 3 轮让人类审批一下再继续?while 循环不支持"暂停-恢复" 需要自己实现状态机 + 持久化 + 恢复逻辑
分支逻辑混乱 "如果搜索失败就换 Google,如果计算超时就降级"------if/else 嵌套炸了 缺乏结构化的控制流表达方式

2.4 我们到底需要一个什么样的框架

从上面的分析可以归纳出 Agent 框架的五个核心需求:

  1. 状态管理:每一步的状态要结构化、可追踪、可持久化
  2. 控制流编排:支持顺序、分支、循环,且能可视化
  3. 错误恢复:某一步失败了,能从失败点重试,而不是从头来
  4. 人在回路:能在任意步骤暂停,等人类审批后继续
  5. 可观测性:能追踪每一步的执行------状态、输入、输出、耗时

2.5 LCEL vs LangGraph:什么时候用管道,什么时候用图

在你进入 LangGraph 之前,先搞清楚一个问题:如果只是"链条",为什么不用 LangChain 的 LCEL?

typescript 复制代码
// LCEL 管道:适合线性串联
const chain = prompt | llm | parser;
const result = await chain.invoke({ topic: "AI" });

// LangGraph 图:适合有分支和循环的场景
const graph = new StateGraph(StateAnnotation)
  .addNode("step1", fn1)
  .addNode("step2", fn2)
  .addConditionalEdges("step1", router, { A: "step2", B: "step3" })
  .compile();
维度 LCEL 管道 LangGraph 图
控制流 纯线性(`A B
状态 无持久状态 全程持久化 State
错误恢复 不支持 支持断点恢复
人在回路 不支持 支持 interrupt/resume
调试 打印日志 可视化 Studio + checkpoint 回放
适合场景 单次转换(翻译、摘要) Agent、多步骤工作流

一句话总结:你写一个 prompt | llm | outputParser,不需要 LangGraph。你需要 LLM 主动决定"下一步做什么"的时候,LangGraph 的优势才体现出来。


第二部分:LangGraph 核心机制深入

警告:接下来的四章会有点"硬"。但它们是全文的脊梁------搞懂了,后面的六大模式四行代码你就能看懂本质;搞不懂,看什么都是魔咒。建议一口气读完,不要跳。


第三章:State --- Agent 的共享记忆

3.1 什么是 State

LangGraph 里,State 就是一张跟着数据一起跑的便签本 。图上每个节点(Node)都能读这张便签,也能往上贴新内容。所有节点看到的 State 是同一份

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

// 定义便签本上有哪些字段、各自是什么类型
const StateAnnotation = Annotation.Root({
  topic: Annotation<string>,          // 用户输入
  searchResults: Annotation<string>,  // 搜索结果
  summary: Annotation<string>,        // 总结
  finalAnswer: Annotation<string>,    // 最终答案
});

Annotation.Root({...}) 做了三件事:

  • 定义结构:便签本上允许有哪些字段
  • 定义类型:每个字段是 string、number、还是复杂对象
  • 提供类型推断 :后面的 Node 函数里,state.xxx 有完整的 TypeScript 类型提示

3.2 State 是"累积"的,不是"替换"的

这是新手最容易搞错的地方。看这段代码:

typescript 复制代码
async function stepOne(state: typeof StateAnnotation.State) {
  return { searchResults: "找到了 10 篇相关文章" };
  // 返回的是一个"部分 State",不是完整 State!
}

async function stepTwo(state: typeof StateAnnotation.State) {
  console.log(state.topic);         // ✅ 还在!初始值 "AI safety"
  console.log(state.searchResults); // ✅ 也在!stepOne 贴上去的
  return { summary: "总结一下..." };
  // 这次返回另一个字段
}

// 调用
const result = await graph.invoke({ topic: "AI safety" });

// 最终 State =
// {
//   topic: "AI safety",             ← 输入的
//   searchResults: "找到了 10 篇...",  ← stepOne 贴的
//   summary: "总结一下...",          ← stepTwo 贴的
//   finalAnswer: undefined           ← 没被赋值,保持空
// }

核心规则:Node 返回的是"增量更新",LangGraph 自动把它合并到全局 State 上。旧字段保留,新字段覆盖。就像便签本上贴便利贴------旧的还在,新的盖在对应位置上。

默认情况下,同名字段是直接覆盖 。如果你需要更复杂的合并逻辑(比如追加到数组而不是覆盖),可以用 reducer

typescript 复制代码
const StateAnnotation = Annotation.Root({
  // 默认 reducer:新值覆盖旧值(适合字符串、数字)
  topic: Annotation<string>,

  // 自定义 reducer:追加到数组(适合消息历史)
  messages: Annotation<BaseMessage[]>({
    reducer: (current, update) => current.concat(update),
    default: () => [],
  }),
});

3.3 State 为什么是 LangGraph 最核心的设计

和手写 Agent 循环对比一下就清楚了:

手写 while 循环 LangGraph State
状态在哪 散落在 messages 数组里 结构化 State 对象
中间状态能看吗 只能靠 console.log 每个 checkpoint 都有完整 State 快照
能从中间恢复吗 不能 从任意 checkpoint 继续执行
不同分支的状态隔离 全局变量,容易污染 每次 invoke 独立 State
调试 打断点,看局部变量 LangGraph Studio 可视化 State 变化

第四章:Node --- Agent 的执行单元

4.1 Node 是什么

Node 是 LangGraph 里真正"干活"的地方。它的签名非常统一:

typescript 复制代码
async function myNode(state: StateType): Promise<Partial<StateType>> {
  // 1. 从 state 里读你需要的数据
  // 2. 做点什么(调 LLM、调 API、算个东西......)
  // 3. 返回一个"部分 State",LangGraph 会自动合并
  return { fieldA: "新值" };
}

一个 Node 可以干任何事情:

  • 调用 LLM(最常见)
  • 调用工具/API
  • 读写数据库
  • 纯计算逻辑(不涉及 LLM)
  • 什么也不干,只是记录日志

LangGraph 不在乎你 Node 里面做什么,它只在乎你的入参(完整的 State)和出参(部分 State)。

4.2 Node 的返回值如何影响 State

yaml 复制代码
执行 myNode 之前 State = { A: 1, B: 2, C: 3 }
myNode 返回 { B: 20, D: 4 }
执行 myNode 之后 State = { A: 1, B: 20, C: 3, D: 4 }
                            ↑       ↑        ↑
                          不变    覆盖    新增

4.3 这个函数是 Node 还是路由?

看一个最常见的混淆点:

typescript 复制代码
// ✅ 这是 Node:消费 State、返回部分 State
async function generateJoke(state: typeof StateAnnotation.State) {
  const msg = await llm.invoke(`Write a joke about ${state.topic}`);
  return { joke: msg.content };  // 返回 State 更新
}

// ❌ 这不是 Node,这是路由函数:只读 State,返回目标节点名
function checkPunchline(state: typeof StateAnnotation.State) {
  if (state.joke?.includes("?") || state.joke?.includes("!")) {
    return "Pass";   // 这是一个"路标",不是 State 字段
  }
  return "Fail";
}

generateJoke.addNode() 注册;checkPunchline 直接作为参数传给 .addConditionalEdges() 路由函数不往 State 里写任何东西,它只负责"指路"。


第五章:Edge --- Agent 的控制流

5.1 两种边

LangGraph 里只有两种边:

固定边(Normal Edge): 从 A 到 B,无条件,A 执行完就去 B。

typescript 复制代码
.addEdge("generateJoke", "improveJoke")
//        源节点              目标节点

条件边(Conditional Edge): 从 A 出发,根据一个路由函数的返回值,决定去 B、C、还是 D。

typescript 复制代码
.addConditionalEdges("generateJoke", checkPunchline, {
  Pass: "improveJoke",  // checkPunchline 返回 "Pass" → 去 improveJoke
  Fail: "__end__",       // checkPunchline 返回 "Fail" → 去终点
})

5.2 __start____end__:图的天头地脚

  • __start__(或导入 START 常量):图的入口。你不需要定义它,它是一个隐式的"起点节点"。
  • __end__(或导入 END 常量):图的出口。当执行流到达 __end__invoke() 返回最终 State。
typescript 复制代码
.addEdge("__start__", "firstNode")   // 图一启动,先去 firstNode
.addEdge("lastNode", "__end__")      // lastNode 执行完,图结束

规则: 每个图必须至少有一条从 __start__ 出发的边;每个最终会终止的分支必须有一条到 __end__ 的边。

5.3 边的 Fan-out 和 Fan-in 语义

同一个源节点可以有多条出边:

typescript 复制代码
// Fan-out:__start__ 同时连接到三个节点
.addEdge("__start__", "worker1")
.addEdge("__start__", "worker2")
.addEdge("__start__", "worker3")
// → worker1、worker2、worker3 并行执行!

多个源节点可以指向同一个目标节点:

typescript 复制代码
// Fan-in:三个 worker 都指向聚合节点
.addEdge("worker1", "aggregator")
.addEdge("worker2", "aggregator")
.addEdge("worker3", "aggregator")
// → aggregator 会等待三个 worker 全部完成才启动!

5.4 边的可视化

css 复制代码
Fan-out(发散)                    Fan-in(汇聚)
  __start__                        worker1 ──┐
     │                                     │
  ┌──┼──┐                          worker2 ──┼──→ aggregator
  │  │  │                                     │
  ▼  ▼  ▼                            worker3 ──┘
  A  B  C

第六章:Pregel 执行引擎 --- LangGraph 到底怎么"跑"的

这是全文最重要的技术章节。前面讲的 State/Node/Edge 是积木,这章讲的是积木是怎么被"驱动"的。

6.1 图计算和 Pregel

LangGraph 的执行引擎基于 Google 2010 年发表的 Pregel 图计算模型。Pregel 的核心思想很简单------每个"超级步"(Superstep)里:

  1. 找出所有"就该在这一步执行"的节点
  2. 并行执行它们
  3. 收集它们的输出,更新全局状态
  4. 重复,直到没有更多节点需要执行

把它翻译成 LangGraph 的语境:

markdown 复制代码
超级步 1:哪些节点该执行?
  → 遍历所有边,找到源节点是 __start__ 的边
  → __start__ → firstNode
  → 执行 firstNode

超级步 2:firstNode 执行完了,接下来呢?
  → 遍历所有边,找到以 firstNode 为源的边
  → firstNode → secondNode(固定边)
  → firstNode → router → A 或 B(条件边)
  → 评估条件边,得到目标
  → 执行目标节点

超级步 3:...重复,直到所有路径都到达 __end__

6.2 完整执行追踪:追踪一只"猫"走完整条流水线

下面这个例子会贯穿我们后面的讨论。先看代码:

typescript 复制代码
const StateAnnotation = Annotation.Root({
  topic: Annotation<string>,
  joke: Annotation<string>,
  improvedJoke: Annotation<string>,
  finalJoke: Annotation<string>,
});

async function generateJoke(state: typeof StateAnnotation.State) {
  const msg = await llm.invoke(`Write a short joke about ${state.topic}`);
  return { joke: msg.content };
}

function checkPunchline(state: typeof StateAnnotation.State) {
  if (state.joke?.includes("?") || state.joke?.includes("!")) {
    return "Pass";
  }
  return "Fail";
}

async function improveJoke(state: typeof StateAnnotation.State) {
  const msg = await llm.invoke(
    `Make this joke funnier by adding wordplay: ${state.joke}`
  );
  return { improvedJoke: msg.content };
}

async function polishJoke(state: typeof StateAnnotation.State) {
  const msg = await llm.invoke(
    `Add a surprising twist to this joke: ${state.improvedJoke}`
  );
  return { finalJoke: msg.content };
}

const jokeGraph = new StateGraph(StateAnnotation)
  .addNode("generateJoke", generateJoke)
  .addNode("improveJoke", improveJoke)
  .addNode("polishJoke", polishJoke)
  .addEdge("__start__", "generateJoke")
  .addConditionalEdges("generateJoke", checkPunchline, {
    Pass: "improveJoke",
    Fail: "__end__",
  })
  .addEdge("improveJoke", "polishJoke")
  .addEdge("polishJoke", "__end__")
  .compile();

// 启动!
const result = await jokeGraph.invoke({ topic: "cats" });

流程图:

scss 复制代码
__start__
    │
    ▼
[generateJoke]    ← 工位 1:根据 topic 生成笑话
    │
    ▼ (条件边:checkPunchline)
   ╱ ╲
 Pass  Fail
  │      │
  ▼      ▼
[improveJoke]   __end__     ← 没笑点?直接结束
  │
  ▼
[polishJoke]     ← 工位 3:加反转结局
  │
  ▼
__end__

现在,追踪一次执行(假设 LLM 生成了一个包含 ? 的好笑话):

步骤 引擎在干什么 触发原因 执行哪个 Node 执行后的 State
初始 用户传入 { topic: "cats" } invoke() --- { topic: "cats" }
Step 1 找到源为 __start__ 的边 → __start__generateJoke 固定边 generateJoke { topic: "cats", joke: "Why did the cat sit on the computer? Because it wanted to keep an eye on the mouse!" }
Step 2 generateJoke 完成 → 评估条件边 checkPunchline(state) → 返回 "Pass" 条件边路由 improveJoke { topic: "cats", joke: "Why did the cat...", improvedJoke: "Why did the feline programmer sit on the laptop? To keep an eye on the cursor---and its mouse!" }
Step 3 improveJoke 完成 → 固定边 → polishJoke 固定边 polishJoke { topic: "cats", joke: "Why...", improvedJoke: "Why...", finalJoke: "...and then the cat realized the mouse was wireless!" }
Step 4 polishJoke 完成 → 固定边 → __end__ 到达终点 --- 图结束,返回最终 State

如果 LLM 生成的是一句平淡的 "Cats are cute"(没有 ?!):

步骤 引擎在干什么 执行哪个 Node 执行后的 State
Step 1 固定边 → generateJoke generateJoke { topic: "cats", joke: "Cats are cute" }
Step 2 checkPunchline(state) → 返回 "Fail" → 去 __end__ ---(直接结束) 最终 State:{ topic: "cats", joke: "Cats are cute" }(只有 joke,没有 improvedJoke 和 finalJoke)

6.3 隐式屏障是什么,什么时候生效

当多条边的目标指向同一个 Node 时,LangGraph 自动形成隐式屏障(Implicit Barrier)

typescript 复制代码
// 三条边都指向 aggregator
.addEdge("worker1", "aggregator")
.addEdge("worker2", "aggregator")
.addEdge("worker3", "aggregator")

引擎的行为:

arduino 复制代码
worker1 完成 → 检查:"以 worker1 为源的边 → aggregator,但指向 aggregator 的还有 worker2 和 worker3 的边"
            → worker2 还没完成?那 aggregator 再等等
worker2 完成 → 同上,worker3 还没完
worker3 完成 → 所有指向 aggregator 的源节点都完成了
            → 启动 aggregator!

这个行为是自动的,你不需要写任何 await Promise.all 之类的代码。 这是 Pregel 模型的原生语义------所有消息到达后,顶点才激活。

6.4 并行执行是什么,什么时候发生

反过来,当多条边从同一个节点发出、指向不同目标时,LangGraph 自动并行执行:

typescript 复制代码
.addEdge("__start__", "worker1")
.addEdge("__start__", "worker2")
.addEdge("__start__", "worker3")

引擎的行为:

markdown 复制代码
超级步 1:__start__ 完成了
  → 以 __start__ 为源的边:→ worker1、→ worker2、→ worker3
  → 这些目标节点是独立的,互相不依赖
  → 🔥 并行执行 worker1、worker2、worker3

超级步 2:所有 worker 都完成了
  → 寻找下一批可执行节点...

注意:并行执行是在同一个 Node 内部是 async 的。LangGraph 会在同一超级步内同时启动所有就绪的节点,Node 之间的执行顺序不确定。

6.5 .compile().invoke() 到底做了什么

arduino 复制代码
.compile()
  → 验证图的结构合法性(有没有孤立节点?每个分支是否都有终点?)
  → 预计算每个节点的"入度"(有多少条边指向它)
  → 返回一个 CompiledStateGraph 实例

.invoke(initialState)
  → 创建初始 State
  → 进入 Pregel 执行循环:
      while (有节点等待执行) {
        超级步:
          1. 收集所有"前置依赖已满足"的节点
          2. 并行执行它们
          3. 应用它们的返回值更新 State
          4. 评估条件边,确定下一步
      }
  → 返回最终 State

6.6 总结:这一章你带走的三个关键认知

  1. LangGraph 不是"一次执行所有节点" --- 它按 Pregel 超级步逐批执行,每一步只执行"前置条件已满足"的节点。

  2. Fan-out = 自动并行,Fan-in = 自动等待 --- 你不需要手写 Promise.all,引擎的入度计数器帮你做。

  3. 条件边的路由函数是在每个超级步里被调用的 --- 它读到的是最新的 State,所以 State 的变化会影响后续的路由决策。

现在你知道了 LangGraph 的底层执行逻辑。接下来的六大模式,本质上就是 State + Node + Edge 的不同组合方式。你不需要"背"模式------你只需要看懂每种模式是怎么用这三样东西搭出来的。


第三部分:六大 Agent 设计模式

从简单到复杂,每种模式都是在前面模式的基础上加新东西。建议按顺序读,每读完一种就想一下"这个和上一个有什么不同"。


第七章:模式一 ------ Prompt Chaining(提示链)

什么时候用

任务可以拆成明确的、有先后顺序的步骤,上一步的输出是下一步的输入。

典型场景:

  • 写一篇文章:生成大纲 → 撰写正文 → 润色 → 翻译
  • 代码审查:读取代码 → 分析问题 → 生成修复建议 → 验证修复
  • 数据分析:提取数据 → 清洗 → 分析 → 生成报告

流程图

css 复制代码
__start__ → [Step 1] → [Step 2] → [Step 3] → [Step 4] → __end__
  输入       输出1      输出2      输出3      最终结果

代码实现

typescript 复制代码
const ChainState = Annotation.Root({
  topic: Annotation<string>,
  outline: Annotation<string>,
  draft: Annotation<string>,
  polished: Annotation<string>,
  translated: Annotation<string>,
});

async function makeOutline(state: typeof ChainState.State) {
  const msg = await llm.invoke(
    `Create a detailed outline for a technical article about: ${state.topic}`
  );
  return { outline: msg.content };
}

async function writeDraft(state: typeof ChainState.State) {
  const msg = await llm.invoke(
    `Write a full draft based on this outline:\n\n${state.outline}`
  );
  return { draft: msg.content };
}

async function polishDraft(state: typeof ChainState.State) {
  const msg = await llm.invoke(
    `Polish this draft for clarity, conciseness, and flow:\n\n${state.draft}`
  );
  return { polished: msg.content };
}

async function translateToEnglish(state: typeof ChainState.State) {
  const msg = await llm.invoke(
    `Translate the following polished article to English:\n\n${state.polished}`
  );
  return { translated: msg.content };
}

const chainWorkflow = new StateGraph(ChainState)
  .addNode("outline", makeOutline)
  .addNode("draft", writeDraft)
  .addNode("polish", polishDraft)
  .addNode("translate", translateToEnglish)
  .addEdge("__start__", "outline")
  .addEdge("outline", "draft")
  .addEdge("draft", "polish")
  .addEdge("polish", "translate")
  .addEdge("translate", "__end__")
  .compile();

const result = await chainWorkflow.invoke({
  topic: "How Transformers Changed NLP",
});
console.log(result.translated);

State 变化追踪

步骤 执行 Node State 新增字段 其他字段
输入 --- topic = "How Transformers..." ---
Step 1 outline outline = "...I. Introduction\nII. Attention...\n..." topic 保留
Step 2 draft draft = "The Transformer architecture..." topic, outline 保留
Step 3 polish polished = "A polished version...:" topic, outline, draft 保留
Step 4 translate translated = "The Transformer architecture..." topic, outline, draft, polished 保留

关键洞察:每一步的 Node 都可以读到前面所有步骤产生的 State 字段。比如 translate 可以读 topicoutlinedraftpolished------但你只在 Prompt 里用了 polished,这是你自己的选择。

对比:Prompt Chaining vs 单次 LLM 调用

维度 单次 LLM 调用 Prompt Chaining
质量 一次生成,LLM 容易遗漏和跑偏 每一步聚焦一个任务,质量更高
可控性 无法干预中间过程 每一步输出可检查、可修改、可缓存
成本 一个长 Prompt,Token 多 每步 Prompt 更短,但调多次
延迟 一次 API 调用 T1 + T2 + T3 + T4
可调试 只能看最终输出 每步输出都存 State,容易定位问题
复杂度 极低

第八章:模式二 ------ Routing(路由)

什么时候用

输入的类型差异很大,需要先"分类"再交给不同的处理器处理。

和 Prompt Chaining 的关键区别:Chaining 是一条线 ,Routing 是分叉树------不是每一步都走,而是根据条件选一条路走。

典型场景:

  • 客服系统:用户问题 → 分类(退货/技术/投诉/一般) → 走不同处理流程
  • 内容平台:帖子 → 审核分类(通过/需修改/违规) → 不同处理
  • 代码助手:用户请求 → 分类(写代码/改Bug/解释代码/Code Review)→ 不同 Prompt

流程图

css 复制代码
                    ┌→ [codeHandler] ──┐
                    │                  │
__start__ → [classifier] ─┼→ [writingHandler] ─┼→ __end__
                    │                  │
                    ├→ [translateHandler] ┘
                    │
                    └→ [generalHandler] ──┘

代码实现

typescript 复制代码
const RouterState = Annotation.Root({
  input: Annotation<string>,
  category: Annotation<string>,
  result: Annotation<string>,
});

// 分类器 Node:判断输入属于什么类型
async function classifier(state: typeof RouterState.State) {
  const msg = await llm.withStructuredOutput(
    z.object({ category: z.enum(["code", "writing", "translation", "general"]) })
  ).invoke(
    `Classify this user request: "${state.input}"`
  );
  return { category: msg.category };
}

// 四个专用处理器
async function handleCode(state: typeof RouterState.State) {
  const msg = await llm.invoke(
    `You are a senior software engineer. ${state.input}`
  );
  return { result: msg.content };
}

async function handleWriting(state: typeof RouterState.State) {
  const msg = await llm.invoke(
    `You are a professional content writer. ${state.input}`
  );
  return { result: msg.content };
}

async function handleTranslation(state: typeof RouterState.State) {
  const msg = await llm.invoke(
    `Translate: ${state.input}`
  );
  return { result: msg.content };
}

async function handleGeneral(state: typeof RouterState.State) {
  const msg = await llm.invoke(`Answer: ${state.input}`);
  return { result: msg.content };
}

// 路由函数:根据 category 决定下一站
function routeByCategory(state: typeof RouterState.State) {
  return state.category; // "code" | "writing" | "translation" | "general"
}

const routerWorkflow = new StateGraph(RouterState)
  .addNode("classifier", classifier)
  .addNode("code", handleCode)
  .addNode("writing", handleWriting)
  .addNode("translation", handleTranslation)
  .addNode("general", handleGeneral)
  .addEdge("__start__", "classifier")
  .addConditionalEdges("classifier", routeByCategory, {
    code: "code",
    writing: "writing",
    translation: "translation",
    general: "general",
  })
  .addEdge("code", "__end__")
  .addEdge("writing", "__end__")
  .addEdge("translation", "__end__")
  .addEdge("general", "__end__")
  .compile();

State 变化追踪

假设输入 "帮我写一个 Python 的快速排序",分类器判断为 "code"

步骤 执行 Node 触发 State
输入 --- --- { input: "帮我写...", category: <空>, result: <空> }
Step 1 classifier 固定边 __start__classifier { input: "帮我写...", category: "code", result: <空> }
Step 2 code(不是 writing!) routeByCategory 返回 "code" { input: "帮我写...", category: "code", result: "def quick_sort(arr):..." }
Step 3 --- 固定边 code__end__ 图结束,writing/translation/general 从未执行

注意:只有一条分支被执行。其他三个处理器完全没有运行。这是 Routing 的核心------选一条路走,而不是走遍所有路。

关键细节:分类器用了 withStructuredOutput

注意 classifier 内部用了 withStructuredOutput + z.enum(),强制 LLM 只返回 "code" | "writing" | "translation" | "general" 中的一个。这保证了路由函数收到的 state.category 一定是这四个值之一,不会出现"路由到不存在的节点"的运行时错误。

对比:Routing vs Prompt Chaining

维度 Prompt Chaining Routing
控制流 直线:A → B → C → D 分叉:A → (B 或 C 或 D)
执行节点数 所有节点都执行 只有被路由到的节点执行
延迟 所有步骤延迟之和 分类器延迟 + 被选中节点的延迟
成本 所有步骤的 Token 之和 只有分类器 + 一个处理器
复杂性来源 步骤间数据依赖 分类准确性
适合场景 所有步骤都必须做 只有一个分支需要做

第九章:模式三 ------ Parallelization(并行化)

什么时候用

多个子任务互相独立------你不等我、我不等你------可以同时执行,最后汇总结果。

和 Routing 的关键区别:Routing 是选一条路 ,Parallelization 是所有路一起走,然后在终点等齐了再汇总。

典型场景:

  • 内容生成:同一主题 → 同时生成文章/视频脚本/社交媒体帖子
  • 多维度分析:同一数据 → 同时做技术分析/财务分析/市场分析
  • 多源搜索:同一问题 → 同时搜 Google/Bing/内部知识库
  • 多模型投票:同一问题 → 同时发给 GPT-4/Claude/Gemini → 投票最佳答案

流程图

css 复制代码
              ┌──[worker1]──┐
              │              │
__start__ ────┼──[worker2]──┼──→ [aggregator] → __end__
              │              │
              └──[worker3]──┘

  Fan-out(发散)             Fan-in(汇聚,隐式屏障)

代码实现

typescript 复制代码
const ParallelState = Annotation.Root({
  topic: Annotation<string>,
  joke: Annotation<string>,
  story: Annotation<string>,
  poem: Annotation<string>,
  combinedOutput: Annotation<string>,
});

// 三个独立 Worker------互不依赖,各干各的
async function generateJoke(state: typeof ParallelState.State) {
  const msg = await llm.invoke(`Write a joke about ${state.topic}`);
  return { joke: msg.content };
}

async function generateStory(state: typeof ParallelState.State) {
  const msg = await llm.invoke(`Write a short story about ${state.topic}`);
  return { story: msg.content };
}

async function generatePoem(state: typeof ParallelState.State) {
  const msg = await llm.invoke(`Write a poem about ${state.topic}`);
  return { poem: msg.content };
}

// 汇聚节点------等三个 Worker 全部完成才启动
async function aggregator(state: typeof ParallelState.State) {
  const combined = [
    `📖 STORY:\n${state.story}\n`,
    `😂 JOKE:\n${state.joke}\n`,
    `🎵 POEM:\n${state.poem}`,
  ].join("\n---\n");
  return { combinedOutput: combined };
}

const parallelWorkflow = new StateGraph(ParallelState)
  .addNode("genJoke", generateJoke)
  .addNode("genStory", generateStory)
  .addNode("genPoem", generatePoem)
  .addNode("aggregator", aggregator)
  // Fan-out:__start__ 同时连接三个 Worker
  .addEdge("__start__", "genJoke")
  .addEdge("__start__", "genStory")
  .addEdge("__start__", "genPoem")
  // Fan-in:三个 Worker 都指向 aggregator
  .addEdge("genJoke", "aggregator")
  .addEdge("genStory", "aggregator")
  .addEdge("genPoem", "aggregator")
  .addEdge("aggregator", "__end__")
  .compile();

执行时间线

scss 复制代码
时间 →
genJoke   ████████████████░░░░ (2.1s)  ← 同时启动
genStory  ██████████░░░░░░░░░░ (1.5s)  ← 同时启动
genPoem   ████████████████████ (2.8s)  ← 同时启动(最慢的)
aggregator                      ████ (0.3s)  ← 等最慢的那个完成后才启动

总耗时 ≈ max(2.1, 1.5, 2.8) + 0.3 = 3.1s
串行耗时 = 2.1 + 1.5 + 2.8 + 0.3 = 6.7s
加速比 ≈ 2.2x

隐式屏障详细解释

你可能注意到 .addEdge("genJoke", "aggregator") 等三条边都是固定边,没有提到"等待"。那 LangGraph 怎么知道 aggregator 要等三个都完成?

这就是第六章讲的 Pregel 引擎的隐式屏障机制:

  1. 编译时,LangGraph 统计每个节点的入度(指向它的边的数量)
  2. aggregator 的入度 = 3(三条边指向它)
  3. 运行时,内部有一个计数器:genJoke 完成 → 计数器 +1;genStory 完成 → 计数器 +1;genPoem 完成 → 计数器 +1
  4. 只有当计数器 = 入度 = 3 时,aggregator 才被激活执行

你不需要手写任何等待逻辑。这是图结构的天然语义。

常见疑问

Q: 如果 genJoke 报错了,aggregator 会一直等下去吗?

不会。LangGraph 有错误处理机制------如果某个节点抛出未捕获的异常,整个图的执行会终止(不会让 aggregator 傻等)。

Q: 如果某个 Worker 特别慢,我能设超时吗?

可以在 Node 函数内部用 Promise.race 或其他超时机制,但 LangGraph 本身不提供"节点级超时"------这是一种有意为之的设计选择,因为"超时后做什么"是业务逻辑,应该由你决定。

对比:Parallelization vs Routing

维度 Routing Parallelization
分支选择 只走一条路 所有路一起走
节点执行 只执行被选中的分支 所有分支都执行
延迟 分类器延迟 + 一个处理器 max(所有 Worker) + 聚合器
成本 分类器 + 一个处理器的 Token 所有 Worker + 聚合器的 Token
适用场景 任务互斥(只需要一个答案) 任务互补(需要多维度结果)
结果结构 单一输出 多源输出 → 合并为一个

第十章:模式四 ------ Evaluator-Optimizer(评估器-优化器)

什么时候用

你有一个"生成"节点和一个"评估"节点。生成的结果如果不达标,就带着评估反馈回到生成节点重新来。循环到达标为止。

和 Parallelization 的关键区别:Parallelization 是一次性并行 ,Evaluator-Optimizer 是迭代式串行循环------可能跑 1 轮就过,也可能跑 5 轮。

典型场景:

  • 内容生成:写文案 → 评分(吸引力、CTA、创意) → 不达标就带着反馈重写
  • 代码生成:写代码 → 跑测试 → 测试不通过就修复
  • 翻译质量:翻译 → 校对评分 → 不够地道就润色

流程图

scss 复制代码
__start__ → [generator] → [evaluator] ──(不达标)──┐
                              │                    │
                           (达标)                   │
                              │                    │
                              ▼                    │
                          __end__    ←─────────────┘ (循环回去)

代码实现

typescript 复制代码
const EvalState = Annotation.Root({
  task: Annotation<string>,
  draft: Annotation<string>,
  score: Annotation<number>,
  feedback: Annotation<string>,
  iteration: Annotation<number>,
});

async function generator(state: typeof EvalState.State) {
  const iter = (state.iteration || 0) + 1;

  // 第一轮:根据 task 生成
  // 后续轮次:根据 task + 上一轮的反馈重新生成
  const context = state.feedback
    ? `Previous attempt score: ${state.score}/10.\nFeedback: ${state.feedback}\n\nPlease rewrite based on the feedback.`
    : "Write the first version.";

  const msg = await llm.invoke(
    `Task: ${state.task}\n\n${context}`
  );
  return { draft: msg.content, iteration: iter };
}

async function evaluator(state: typeof EvalState.State) {
  const msg = await llm.withStructuredOutput(
    z.object({
      score: z.number().min(1).max(10).describe("Quality score 1-10"),
      feedback: z.string().describe("Specific improvement suggestions"),
    })
  ).invoke(
    `Evaluate this marketing copy on engagement, clarity, call-to-action, and creativity.
    Rate 1-10 and give SPECIFIC suggestions:\n\n${state.draft}`
  );

  return { score: msg.score, feedback: msg.feedback };
}

// 质量门:决定继续还是结束
function qualityGate(state: typeof EvalState.State) {
  const MAX = 5; // 最多迭代 5 轮

  if (state.score >= 8) {
    return "pass";     // 达标,结束
  }
  if (state.iteration >= MAX) {
    return "maxed";    // 达到上限,勉强结束
  }
  return "retry";      // 不达标,回炉重造
}

const evalWorkflow = new StateGraph(EvalState)
  .addNode("generator", generator)
  .addNode("evaluator", evaluator)
  .addEdge("__start__", "generator")
  .addEdge("generator", "evaluator")
  .addConditionalEdges("evaluator", qualityGate, {
    pass: "__end__",
    maxed: "__end__",
    retry: "generator",  // ← 回退到 generator,形成循环
  })
  .compile();

State 变化追踪(假设迭代了 3 轮才达标)

轮次 步骤 执行 Node 关键 State
输入 --- --- { task: "写一个AI课程的营销文案" }
第 1 轮 Step 1 generator { draft: "想学AI吗?来报名吧...", iteration: 1 }
Step 2 evaluator { score: 5, feedback: "太平淡,缺少情感共鸣..." }
路由 qualityGate score=5 < 8, iter=1 < 5 → "retry"
第 2 轮 Step 3 generator { draft: "还在为加班写重复代码烦恼吗?...", iteration: 2 }
Step 4 evaluator { score: 7, feedback: "好了很多,但CTA还不够紧迫..." }
路由 qualityGate score=7 < 8, iter=2 < 5 → "retry"
第 3 轮 Step 5 generator { draft: "...限时优惠,立即报名!", iteration: 3 }
Step 6 evaluator { score: 9, feedback: "非常棒!" }
路由 qualityGate score=9 ≥ 8 → "pass" → 结束

关键设计:generator 在迭代时能读到上一轮的 feedbackscore。这不是框架做的------是你自己在 generator 的 Prompt 里引用了 state.feedbackstate.score。State 的历史累积能力让你可以在任何时候引用任何之前步骤产生的数据。

循环终止策略

qualityGate 里有双重保护:

  • 质量保护:score ≥ 8 直接通过
  • 兜底保护:iteration ≥ 5 强制结束(避免永远不达标导致的无限循环)

这个双重保护是生产级 Agent 必须考虑的设计模式------永远不要让循环只有一个终止条件。

对比:Evaluator-Optimizer vs Parallelization

维度 Parallelization Evaluator-Optimizer
执行模式 一次性并行 迭代式串行循环
延迟 max(Worker) N × (生成 + 评估),N 不确定
成本 固定(所有 Worker) 变数(取决于迭代次数)
输出质量 取决于单次生成 逐步提升,有质量保证
复杂度 中高(需要设计评估标准和循环终止策略)
适合场景 独立子任务,结果互补 对输出质量有严格要求

第十一章:模式五 ------ Agent / ReAct 循环

什么时候用

你有一个 LLM + 一堆工具。你需要 LLM 自己决定"什么时候调工具、调哪个工具、调几次、调完工具后结果是否满意、要不要再调一次"。

这是 LangGraph 最经典的模式------你在 ChatGPT 里用的联网搜索、代码执行,底层就是这个循环。

和 Evaluator-Optimizer 的关键区别:

  • Evaluator-Optimizer 的循环是固定的(总是 generator → evaluator → 可能回退)
  • Agent 循环的路径是LLM 动态决定的(LLM 决定调用哪个工具,甚至决定现在是不是该结束了)

流程图

scss 复制代码
           ┌──────────────────────────────┐
           │                              │
           ▼                              │
__start__ → [agent] ──(LLM决定调工具)──→ [tools]
                         │
                      (LLM决定结束)
                         │
                         ▼
                      __end__

代码实现

typescript 复制代码
import { AIMessage, HumanMessage, ToolMessage } from "@langchain/core/messages";
import { ToolNode } from "@langchain/langgraph/prebuilt";

// ===== 定义工具 =====
const searchTool = tool(
  async ({ query }: { query: string }) => {
    // 实际项目中这里调 Google/Bing API
    return `Search results for "${query}": [result1, result2, ...]`;
  },
  {
    name: "web_search",
    description: "Search the web for current information",
    schema: z.object({
      query: z.string().describe("The search query"),
    }),
  }
);

const calculatorTool = tool(
  async ({ expression }: { expression: string }) => {
    // 实际项目中用安全的数学求值库
    return `Result: ${eval(expression)}`;
  },
  {
    name: "calculator",
    description: "Evaluate a mathematical expression",
    schema: z.object({
      expression: z.string().describe("Math expression to evaluate"),
    }),
  }
);

// ===== 定义 Agent State =====
const AgentState = Annotation.Root({
  messages: Annotation<BaseMessage[]>({
    reducer: (current, update) => current.concat(update),
    default: () => [],
  }),
});

// ===== 绑定工具到 LLM =====
const tools = [searchTool, calculatorTool];
const llmWithTools = llm.bindTools(tools);

// ===== 定义 Node =====
// Agent 节点:调用 LLM
async function callAgent(state: typeof AgentState.State) {
  const response = await llmWithTools.invoke(state.messages);
  return { messages: [response] };
}

// ToolNode 是 LangGraph 预置的工具执行节点
const toolNode = new ToolNode(tools);

// ===== 路由函数 =====
function shouldContinue(state: typeof AgentState.State) {
  const lastMessage = state.messages[state.messages.length - 1];

  // 如果 LLM 返回了 tool_calls → 执行工具
  if (lastMessage instanceof AIMessage && lastMessage.tool_calls?.length) {
    return "tools";
  }
  // 否则 → 结束
  return "__end__";
}

// ===== 构建图 =====
const agentWorkflow = new StateGraph(AgentState)
  .addNode("agent", callAgent)
  .addNode("tools", toolNode)
  .addEdge("__start__", "agent")
  .addConditionalEdges("agent", shouldContinue, {
    tools: "tools",
    __end__: "__end__",
  })
  .addEdge("tools", "agent")  // ← 这条边形成循环!
  .compile();

执行追踪:一个完整的 Agent 对话

arduino 复制代码
用户: "东京现在天气多少度?把摄氏度换算成华氏度。"
轮次 步骤 执行 Node messages 数组变化 LLM 在做什么
初始 --- --- [HumanMessage("东京现在天气...")] ---
第 1 轮 Step 1 agent [..., AIMessage(tool_calls=[{name:"web_search", args:{query:"东京天气"}}])] 🤔 "我不知道东京天气,得搜一下" → 写搜索派工单
路由 shouldContinue --- 检测到 tool_calls → "tools"
Step 2 tools [..., ToolMessage("Search results: 东京当前 25°C")] 🔧 执行 web_search,返回搜索结果
tools → agent --- 回到 agent(形成循环)
第 2 轮 Step 3 agent [..., AIMessage(tool_calls=[{name:"calculator", args:{expression:"25*9/5+32"}}])] 🤔 "25°C 转华氏度 = 25×9÷5+32,得算一下" → 写计算派工单
路由 shouldContinue --- 检测到 tool_calls → "tools"
Step 4 tools [..., ToolMessage("Result: 77")] 🔧 执行 calculator
tools → agent --- 回到 agent
第 3 轮 Step 5 agent [..., AIMessage("东京当前 25°C,约合 77°F。")] ✅ "信息足够了" → 直接给最终答案,无 tool_calls
路由 shouldContinue --- 无 tool_calls → "__end__" → 结束

ToolNode 做了什么

ToolNode 是 LangGraph 提供的一个预置组件。你不需要自己写"遍历 tool_calls → 找到对应工具 → 执行 → 返回 ToolMessage"的逻辑。它自动:

  1. 从最后一条 AIMessage 中提取 tool_calls
  2. 并行执行所有工具调用(如果 LLM 同时要求调两个工具)
  3. 返回一个 ToolMessage 数组,包含每个工具的执行结果
typescript 复制代码
// 你不需要写这些:
for (const toolCall of lastMessage.tool_calls) {
  const tool = tools.find(t => t.name === toolCall.name);
  const result = await tool.invoke(toolCall.args);
  messages.push(new ToolMessage({ content: result, tool_call_id: toolCall.id }));
}

// 只需要:
.addNode("tools", new ToolNode(tools))  // 一行搞定

Agent 模式和其他模式的核心区别

Agent 循环看起来只是一个 agent + tools 之间的二节点循环------但它是最灵活的模式,因为:

  • LLM 决定循环次数(不像 Evaluator 有固定的 generator→evaluator 路径)
  • LLM 决定调用哪个工具(不像 Routing 由分类器决定)
  • LLM 可以根据工具返回结果改变策略(比如搜索没结果,换一个搜索词重试)

对比:Agent vs Evaluator-Optimizer

维度 Evaluator-Optimizer Agent / ReAct
循环决策者 评估函数(规则/LLM评分) LLM 自主决定
路径 固定(generator↔evaluator) 动态(agent→任意工具→agent→任意工具→...)
工具数量 通常 0-1 个(评估可能也是 LLM) 任意多个,LLM 自由选择
循环终止 评分达标 或 次数上限 LLM 决定不再需要工具
适合场景 同一任务的反复优化 多步信息检索和处理

第十二章:模式六 ------ Coordinator / Supervisor(多 Agent 协作)

什么时候用

一个 Agent 不够------任务太复杂,需要多个不同专业的 Agent 协同工作。一个"总指挥"(Supervisor)负责分析任务、分配给合适的子 Agent、评估结果、决定是继续分配还是汇总结束。

这是最复杂的模式,它本质上是 Routing + Agent 循环 + Evaluator 的组合。

典型场景:

  • 全栈开发助手:Supervisor → 前端专家 / 后端专家 / DBA / DevOps
  • 科研助手:Supervisor → 文献检索 / 实验设计 / 数据分析 / 论文润色
  • 企业决策支持:Supervisor → 市场分析 / 财务分析 / 风险评估

流程图

scss 复制代码
                       ┌──[frontend_expert]──┐
                       │                     │
__start__ → [supervisor] ─┼──[backend_expert]───┼──→ 回到 supervisor
                       │                     │
                       └──(FINISH)───────────┘
                              │
                              ▼
                       [compileReport] → __end__

代码实现

typescript 复制代码
const TeamState = Annotation.Root({
  task: Annotation<string>,
  messages: Annotation<{ role: string; content: string }[]>({
    reducer: (current, update) => current.concat(update),
    default: () => [],
  }),
  next: Annotation<string>,
  finalReport: Annotation<string>,
});

// ===== Supervisor =====
async function supervisor(state: typeof TeamState.State) {
  const systemPrompt = `You are a supervisor managing a development team:

  Team members:
  - frontend_expert: UI/UX, React, CSS, component design
  - backend_expert: API design, database, server logic, auth
  - devops_expert: deployment, CI/CD, infrastructure, scaling

  Your job:
  1. Analyze the user's task
  2. Decide which expert should work next
  3. If enough information has been gathered, respond "FINISH"

  Reply with ONLY one word:
  frontend_expert, backend_expert, devops_expert, or FINISH`;

  const msg = await llm.invoke([
    { role: "system", content: systemPrompt },
    ...state.messages.map(m => ({ role: m.role, content: m.content })),
    { role: "user", content: `[CURRENT TASK]: ${state.task}\n\n[YOUR INSTRUCTION]: Who should act next? Reply with ONE word.` },
  ]);

  const decision = msg.content.trim().toLowerCase();
  return {
    next: decision,
    messages: [{ role: "supervisor", content: `→ Assigning to: ${decision}` }],
  };
}

// ===== 子 Agent =====
async function frontendExpert(state: typeof TeamState.State) {
  const msg = await llm.invoke([
    { role: "system", content: "You are a senior frontend engineer. Provide detailed, production-ready frontend solutions with code examples." },
    { role: "user", content: state.task },
  ]);
  return {
    messages: [{ role: "frontend_expert", content: msg.content }],
  };
}

async function backendExpert(state: typeof TeamState.State) {
  const msg = await llm.invoke([
    { role: "system", content: "You are a senior backend engineer. Provide detailed API designs, database schemas, and server architecture with code examples." },
    { role: "user", content: state.task },
  ]);
  return {
    messages: [{ role: "backend_expert", content: msg.content }],
  };
}

async function devopsExpert(state: typeof TeamState.State) {
  const msg = await llm.invoke([
    { role: "system", content: "You are a senior DevOps engineer. Provide deployment strategies, CI/CD configurations, and infrastructure designs." },
    { role: "user", content: state.task },
  ]);
  return {
    messages: [{ role: "devops_expert", content: msg.content }],
  };
}

// ===== 报告编译 =====
async function compileReport(state: typeof TeamState.State) {
  const msg = await llm.invoke([
    { role: "system", content: "Synthesize all expert inputs into a comprehensive final report. Structure it clearly with sections." },
    ...state.messages.map(m => ({ role: m.role, content: m.content })),
  ]);
  return { finalReport: msg.content };
}

// ===== 路由 =====
function supervisorRouter(state: typeof TeamState.State) {
  if (state.next === "finish") return "compileReport";
  // 必须是已注册的节点名
  if (["frontend_expert", "backend_expert", "devops_expert"].includes(state.next)) {
    return state.next;
  }
  return "compileReport"; // fallback
}

// 子 Agent 干完活 → 回到 Supervisor
function expertRouter(state: typeof TeamState.State) {
  return "supervisor";
}

// ===== 构建图 =====
const teamWorkflow = new StateGraph(TeamState)
  .addNode("supervisor", supervisor)
  .addNode("frontend_expert", frontendExpert)
  .addNode("backend_expert", backendExpert)
  .addNode("devops_expert", devopsExpert)
  .addNode("compileReport", compileReport)
  .addEdge("__start__", "supervisor")
  .addConditionalEdges("supervisor", supervisorRouter, {
    frontend_expert: "frontend_expert",
    backend_expert: "backend_expert",
    devops_expert: "devops_expert",
    compileReport: "compileReport",
  })
  // 子 Agent 执行完 → 总是回到 Supervisor
  .addEdge("frontend_expert", "supervisor")
  .addEdge("backend_expert", "supervisor")
  .addEdge("devops_expert", "supervisor")
  .addEdge("compileReport", "__end__")
  .compile();

执行追踪

vbnet 复制代码
用户: "帮我设计一个用户认证系统,包括前端登录页面和后端JWT实现"

Step 1: supervisor → 分析任务 → "这个任务涉及前后端,先让 backend_expert 设计 API"
         → next = "backend_expert"

Step 2: backend_expert → 输出完整的 JWT API 设计和数据库 schema

Step 3: 回到 supervisor → "后端设计已就绪,现在需要前端实现"
         → next = "frontend_expert"

Step 4: frontend_expert → 输出登录页面 React 组件和 Token 管理方案

Step 5: 回到 supervisor → "前后端方案都已就绪,交付最终报告"
         → next = "finish"

Step 6: compileReport → 整合所有专家的输出,生成最终报告

Coordinator 模式的三个关键设计决策

1. 为什么子 Agent 总是回到 Supervisor?

这是 Coordinator 和 Agent 循环的最大区别。Agent 循环里是 agent↔tools 之间反复;Coordinator 里是 supervisor→expert→supervisor→expert→...。

原因: Supervisor 需要评估每个专家的输出质量,决定"这个专家给的信息够不够?要不要换另一个专家?还是可以汇总了?" 如果不回到 Supervisor,就无法实现这种评估。

2. 子 Agent 之间怎么"通信"?

它们不直接通信。所有信息通过共享 State 传递------frontend_expert 写入的 messages,backend_expert 和 supervisor 都能读到。这是一种"共享白板"式的通信模型。

3. 怎么防止 Supervisor 不停地分配任务?

和 Evaluator 一样,需要双重终止条件。Supervisor 的 Prompt 里明确要求"如果信息够了就 FINISH",同时在 supervisorRouter 里对未知的 next 值有 fallback 到 compileReport

六大模式总结对比

模式 图结构 LLM 调用次数 延迟特征 复杂度 典型场景
Prompt Chaining 直线 N(固定) 串行延迟之和 大纲→正文→润色→翻译
Routing 分叉树 2(分类+处理) 分类+一枝 客服分流转接
Parallelization 散聚 N+1(N个Worker+聚合) max(Worker) 多维度分析/多源搜索
Evaluator-Optimizer 循环 2K(K轮迭代) K×(生成+评估) 中高 文案改写直到达标
Agent / ReAct 循环+工具 动态(取决于任务) 动态 联网搜索+计算+总结
Coordinator 多Agent+总控 动态(取决于任务) 动态 最高 跨专业复杂项目

怎么选模式------决策树

markdown 复制代码
你的任务能拆成明确的步骤吗?
│
├── 能,且步骤有先后依赖
│   └── 用 Prompt Chaining
│
├── 能,但只需要做其中一个分支
│   └── 用 Routing
│
├── 能,且各步骤互相独立
│   └── 用 Parallelization
│
├── 能,但需要迭代优化到一定标准
│   └── 用 Evaluator-Optimizer
│
└── 不能预先确定步骤------需要 LLM 自己决定
    │
    ├── 单个 LLM + 工具就够了
    │   └── 用 Agent / ReAct 循环
    │
    └── 需要多个专业 Agent 协作
        └── 用 Coordinator / Supervisor

第四部分:LangGraph 生产级特性

前面六种模式解决的是"Agent 怎么写"的问题。但"写出来"和"能上线"之间还隔着一大段距离。下面四个特性是 LangGraph 把 Agent 从原型推向生产的关键。


第十三章:Memory & Checkpointer --- 状态持久化

为什么需要持久化

考虑一个典型场景:你的 Agent 正在执行工具调用循环,已经跑了 5 轮,调了 3 次搜索、2 次计算,累计花了 1.5 美元 API 费。结果第 6 轮 API 超时了。如果没有持久化------钱花了,结果全丢了。

Checkpointer 是 LangGraph 的答案:每一步执行后自动保存 State 快照。

开发 vs 生产

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

// 开发环境:内存存储(重启丢失,但零配置)
const memorySaver = new MemorySaver();
const devGraph = workflow.compile({ checkpointer: memorySaver });

// 生产环境:SQLite(单机)或 PostgreSQL(分布式)
import { SqliteSaver } from "@langchain/langgraph-checkpoint-sqlite";
const db = SqliteSaver.fromConnString("./checkpoints.db");
const prodGraph = workflow.compile({ checkpointer: db });

多会话隔离

typescript 复制代码
// 会话 A:用户 Alice
await graph.invoke(input, {
  configurable: { thread_id: "alice-session-001" }
});

// 会话 B:用户 Bob------完全独立,互不影响
await graph.invoke(input, {
  configurable: { thread_id: "bob-session-002" }
});

LangGraph 用 thread_id 隔离不同用户/会话的 State。同一个图实例可以同时服务数千个用户,每个用户的 State 完全独立。

断点恢复------这个能力改变了一切

typescript 复制代码
// 第一次调用,执行到一半------API 出错了
try {
  await graph.invoke(
    { messages: [new HumanMessage("查找所有用户最近的订单")] },
    { configurable: { thread_id: "task-42" } }
  );
} catch (error) {
  console.log("Agent 在第 N 步崩溃了");
}

// 不用重新输入!直接 resume------
// LangGraph 自动从最后一个 checkpoint 继续执行
const result = await graph.invoke(null, {  // ← 注意:传入 null!
  configurable: { thread_id: "task-42" },
});

传入 null 作为输入时,LangGraph 会从最新的 checkpoint 恢复 State 并继续执行未完成的步骤。这在长任务(比如自动代码审查跑了 10 分钟)中非常关键。


第十四章:Human-in-the-Loop --- 人在回路

什么时候需要

不是所有操作都能让 Agent 自己决定。以下场景需要人工审批:

  • 发送重要邮件/通知:Agent 起草了,但需要人确认
  • 执行数据库变更:Agent 生成的 SQL,但需要 DBA 审核
  • 大额交易/支付:额度超过阈值
  • 敏感内容发布:AI 生成的文章需编辑审核

interrupt() --- 让图在任意节点暂停

typescript 复制代码
import { interrupt } from "@langchain/langgraph";

async function humanApproval(state: typeof State.State) {
  // 图在这里暂停!等待外部输入
  const approval = interrupt({
    action: "review_and_approve",
    draft: state.generatedContent,
    instruction: "请审核 AI 生成的内容,批准或拒绝",
  });

  if (approval === "approved") {
    return { status: "published" };
  }
  return { status: "rejected", reason: approval };
}

调用端的交互

typescript 复制代码
const config = { configurable: { thread_id: "content-123" } };

// 第一次调用:跑到 interrupt 处暂停
const partialResult = await graph.invoke(
  { task: "写一篇产品发布公告" },
  config
);
// → 返回暂停时的 State,包含 generatedContent

// 人类审核后,发送 Command 恢复执行
import { Command } from "@langchain/langgraph";

const finalResult = await graph.invoke(
  new Command({ resume: "approved" }),  // 人类的选择
  config
);
// → 从暂停点继续:humanApproval 收到 "approved" → 发布 → 结束

关键点: interrupt() 暂停时,State 已经被 Checkpointer 持久化了。审核人不需要立刻审批------几分钟后、几小时后都可以,State 不会丢。


第十五章:Streaming 与 部署

三种 Streaming 模式

typescript 复制代码
// Mode 1: values ------ 每个 Node 完成后推一次完整 State
for await (const chunk of graph.stream(input, {
  streamMode: "values",
})) {
  // chunk = 当前完整 State 快照
  // 适合:调试、记录完整状态
}

// Mode 2: updates ------ 只推本次 Node 的增量
for await (const chunk of graph.stream(input, {
  streamMode: "updates",
})) {
  // chunk = { "nodeName": { "field": "newValue" } }
  // 适合:前端显示 "正在搜索..."、"正在分析..." 等步骤状态
}

// Mode 3: messages ------ LLM 逐 token 输出
for await (const chunk of graph.stream(input, {
  streamMode: "messages",
})) {
  // chunk = 单个 token
  // 适合:ChatGPT 那样的打字机效果
}
模式 粒度 触发时机 前端体验
values 完整 State 每个 Node 完成 状态面板
updates Node 增量 每个 Node 完成 步骤进度条
messages LLM Token 实时逐字 打字机效果

三种模式可以组合使用 ,比如同时用 updates 显示步骤进度 + messages 显示最终答案的打字机效果。

部署

bash 复制代码
# 一行命令部署到 LangGraph Platform
npx @langchain/langgraph-cli deploy

# 自动获得:
# - REST API 端点
# - 内置 Streaming
# - Checkpointer 管理
# - 并发处理
# - 监控和日志

第五部分:面试指南

下面是面试时关于 Agent 架构和 LangGraph 的完整答题思路。关键不是"背答案",而是理解这个递进逻辑。


第十六章:面试语术体系

经典开场问题

"你们项目的 Agent 是怎么设计的?"

错误答法: "我们用 LangGraph,定义了 StateGraph,加了几个 Node 和 Edge......"(一上来就讲技术细节,面试官跟不上的)

正确答法: 按四个层次递进讲。

层次一:从问题出发(30 秒)

"我们首先要搞清楚------普通 LLM 能做什么、不能做什么。单次 LLM 调用只能处理'一问一答'的场景。但我们的业务需要 LLM 能主动调用工具 (查数据库、搜网页)、多步推理 (先查 A 再根据 A 的结果决定 B)、处理分支逻辑 (不同类型的请求走不同流程)。这些需求不是靠写几行 await llm.invoke() 能解决的。"

目的: 展示你理解 LLM 的局限,不是"为了用框架而用框架"。

层次二:Agent 循环的本质(30 秒)

"Agent 本质上是一个循环:LLM 决定下一步做什么 → 执行(调工具或给答案)→ 观察结果 → 再决定下一步。学术上这叫 ReAct 模式。手写这个循环会遇到五个问题------无限循环、状态丢失、不可恢复、无法注入人工判断、分支逻辑混乱。所以我们需要一个框架来管理这个循环。"

目的: 展示你理解 Agent 的底层运行方式,而且知道手写的痛点。

层次三:LangGraph 的解决方案(60 秒)

"LangGraph 用有向图来管理 Agent 的执行流程。三个核心概念:

  • State:全局共享的状态字典。每一个 Node 的执行结果会自动合并到 State 里,后面的 Node 能看到前面所有 Node 产生的数据。
  • Node :执行单元。可以是 LLM 调用、工具执行、或者纯逻辑。Node 签名为 (state) => Partial<State>
  • Edge:流转规则。固定边表示无条件跳转,条件边表示根据路由函数的返回值决定下一站。

LangGraph 底层是一个 Pregel 式的图计算引擎。它在每个"超级步"中找出所有"前置条件已满足"的 Node 并行执行,通过入度计数器实现 Fan-in 的隐式屏障。这和手写 Promise.all 的本质区别是------图的结构本身就编码了依赖关系,引擎自动推导执行顺序。"

目的: 展示你对执行模型有深入理解,不只是"会用 API"。

层次四:具体模式和选型(40 秒)

"在我们的项目中,根据任务复杂度我们用了不同的 Agent 模式。简单的链式任务用 Prompt Chaining;需要分类分发的用 Routing;多个独立分析维度用 Parallelization;对质量要求高的内容生成用 Evaluator-Optimizer,LLM 评分不达标就自动重写;需要 LLM 自主决定工具调用时用 ReAct 循环;跨专业复杂任务用 Coordinator 模式------一个 Supervisor 协调前端、后端、数据库多个专家 Agent。"

目的: 展示你知道什么时候用什么模式,不是在乱堆技术。

层次五:生产级考量(20 秒)

"另外我们还用到了 LangGraph 的生产级特性:Checkpointer 做状态持久化支持断点恢复,interrupt 做人工审批节点,Streaming 做前端实时反馈。这些是把 Agent 从'跑得通'到'能上线'的关键。"

目的: 展示你有生产环境经验,不只是玩具项目。

常见追问与回答

追问 1:"LangGraph 的 Fan-in 是怎么知道要等的?"

"底层是 Pregel 图计算引擎。编译时统计每个 Node 的入度(有多少条边指向它)。运行时维护一个计数器,每个上游 Node 完成时计数器 +1。只有当计数器等于入度时,该 Node 才被激活执行。这是图结构的天然语义,不需要手写等待逻辑。"

追问 2:"如果 Agent 一直在循环不停止怎么办?"

"需要双重终止条件。第一层是 LLM 自主决定------当它认为信息足够时不再返回 tool_calls。第二层是兜底------设置最大循环次数,超过就强制终止。另外在 Evaluator 模式里还可以设质量阈值和迭代上限。"

追问 3:"多个子 Agent 之间怎么通信?"

"它们通过共享 State 通信------这就是为什么 State 设计是 LangGraph 最核心的概念。frontend_expert 写的内容,backend_expert 和 supervisor 都能读到。这是一种'共享白板'模式,避免了点对点通信的复杂度。"

追问 4:"LangGraph 和其他 Agent 框架(CrewAI、AutoGPT)有什么区别?"

"CrewAI 和 AutoGPT 的优势是上手快、概念少,适合快速原型。但它们的执行模型是黑盒的------你很难精确控制执行流程,也很难做持久化和断点恢复。LangGraph 的优势是精确的控制流编排、透明的状态管理、完整的生产级支持(Checkpointer、Streaming、Human-in-the-Loop)。选型上:原型验证阶段可以用 CrewAI/AutoGPT,但要上生产建议 LangGraph。"


第六部分:附录

第十七章:学习路径建议

javascript 复制代码
第 1 步:裸调 LLM API 写几个简单应用
  → 理解 messages、temperature、system prompt
  → 感受"裸调"的局限

第 2 步:学 withStructuredOutput + bindTools
  → 理解增强型 LLM 的两个核心能力
  → 理解 Zod → JSON Schema 的底层转换

第 3 步:手写一个 ReAct 循环
  → 不用任何框架,纯 while + tool_calls
  → 感受手写方案的痛点(为 LangGraph 辩护)

第 4 步:用 LangGraph 重写上面的 ReAct
  → 理解 StateGraph.addNode.addEdge.addConditionalEdges
  → 理解为什么图比 while 循环好

第 5 步:实现六大模式(按本文顺序)
  → 从简单到复杂,感受每种模式解决了什么问题
  → 重点是理解模式之间的递进关系,不是背代码

第 6 步:加上生产级特性
  → Checkpointer → Human-in-the-Loop → Streaming

第十八章:参考资源


最后想说: Agent 不是银弹。大部分场景下,一个精心设计的 Prompt Chain 可能比一个"全能 Agent"更可靠、更便宜、更可预测。LangGraph 给了你搭建复杂系统的能力,但用不用这种能力,是你作为架构师的判断。先问自己"这个任务真的需要循环和动态决策吗",再决定是否引入 Agent。


相关推荐
小林ixn1 小时前
一文搞懂AI Agent核心概念:从LLM、Tools到记忆体,手把手带你实现一个能查股价的智能体
agent·ai编程
Artech2 小时前
[MAF预定义的AIContextProvider-02]AgentSkillsProvider——将Agent Skills引入MAF
ai·c#·agent·agent skills·maf
冬奇Lab14 小时前
Agent 系列(21):Harness 测试工程——45 个测试怎么设计,以及它发现了什么 bug
人工智能·llm·agent
harykali20 小时前
Hello-ROCm:Gemma4微调 #Datawhale #AMDev
人工智能·llm
weiwin12320 小时前
MAF 入门(5):多 Agent 编排全解
人工智能·agent
DigitalOcean20 小时前
砍掉 60% AI 推理成本:深度解构 DigitalOcean 推理路由器的 MoE 门控与智能分流机制
llm·aigc·agent
羞儿20 小时前
llm-algo-1
llm·调试·显存·构建
AndrewHZ20 小时前
【LLM技术全景】大模型能力探秘:In-Context Learning与思维链(CoT)
人工智能·语言模型·大模型·llm·cot·思维链·icl
Vergelight21 小时前
实战拆解|三类RAG架构差异:朴素、进阶、多轮RAG落地选型指南
架构·大模型·aigc·agent·ai产品经理·转行·ai后台设计