第 03 章

工作流模式(下)

编排者-工作者、评估优化、智能体 —— 三种高级编排模式

当任务的数量和路径都不可预知时,静态模式就不够用了

更复杂的场景

第 02 章的三种模式遇到了新问题

任务数量不确定

你想写一份报告,但不知道要分几个章节。用户说"写五节"就五节,说"三节"就三节。并行模式要求你在编码时就确定有几个并行节点,做不到动态分派。

质量不达标怎么办

链式调用有质量门控,但门控只能"通过/不通过"。如果 LLM 第一次生成的内容不好,你更想要的是带着反馈重试,而不是直接放弃。

流程本身就不可预知

有些任务需要根据中间结果自主决定下一步做什么——可能调工具,可能直接回复,可能需要多轮操作。这就是智能体(Agent)的场景。

这三个问题对应三种新模式:编排模式解决动态分派、评估优化解决反馈循环、智能体解决自主决策。

本章核心:掌握编排者-工作者(Orchestrator-Worker)、评估者-优化者(Evaluator-Optimizer)、智能体(Agents)三种高级模式。学完之后,你将拥有 LangGraph 全部六种工作流模式的完整工具箱。

模式四:编排模式

Orchestrator-Worker

编排者(Orchestrator)把大任务拆成若干子任务,分派给工作者(Worker),等所有工作者完成后综合结果。关键区别:子任务的数量在运行时才确定。

图:编排者拆分任务 → 动态工作者 → 综合报告
与并行模式的区别:并行模式在编码时就知道有几个并行节点(写死 3 条边)。编排模式用 Send 类在运行时动态创建任意数量的工作者节点,编排者 LLM 说几个就几个。

Send 动态分发

核心 API:Send 类

思考题:你的系统需要给每个员工生成绩效评估,但每次人数不同(3人或10人)。普通条件边只能选"A节点"或"B节点",怎么实现运行时动态创建 N 个处理节点?
import { StateGraph, StateSchema, ReducedValue, GraphNode, Send, START, END } from "@langchain/langgraph";

Send 让你从条件边函数里动态地把任务分发到指定节点。每次 new Send("节点名", { 数据 }) 就创建一个独立的工作者调用。

// 条件边路由函数 —— 返回 Send 数组,每个 Send 创建一个工作者
const assignWorkers: ConditionalEdgeRouter<typeof State, "llmCall"> = (state) => {
  // state.sections 是编排者 LLM 拆分出的子任务列表
  // 有几个 section,就创建几个工作者
  return state.sections.map(
    (section) => new Send("llmCall", { sections: [section] })
  );
};

为什么需要 ReducedValue?

多个工作者并行运行,每个都往同一个 state 字段写结果。如果用普通字段,后者会覆盖前者。ReducedValue 用 reducer 函数把所有工作者的结果合并到一个数组里。

// 用 ReducedValue 定义一个会"累积"的字段
const State = new StateSchema({
  topic: z.string(),
  sections: z.array(z.object({
    title: z.string(),
    description: z.string(),
  })),
  completedSections: new ReducedValue(
    z.array(z.object({ title: z.string(), content: z.string() })),
    // reducer: 把新结果追加到已有数组
    (existing, update) => [...(existing ?? []), ...update],
  ),
  finalReport: z.string(),
});
易错点:Send 只能在条件边函数中使用,不能在普通节点里创建。ReducedValue 的 reducer 函数第一个参数可能是 null(第一次调用时),别忘了处理。

📄 完整代码:03_send_api.ts

编排模式代码

思考题:你要写一份关于"AI Agent 发展"的报告,报告有多个章节需要独立研究,但章节数量在运行时才确定。怎么让一个编排者动态分配 N 个任务?
import { StateGraph, StateSchema, ReducedValue, GraphNode, Send, START, END } from "@langchain/langgraph";
import * as z from "zod";

const State = new StateSchema({
  topic: z.string(),
  sections: z.array(z.object({ title: z.string(), description: z.string() })),
  completedSections: new ReducedValue(
    z.array(z.object({ title: z.string(), content: z.string() })),
    (existing, update) => [...(existing ?? []), ...update],
  ),
  finalReport: z.string(),
});

// 编排者:分析任务,拆分成子任务列表
const orchestrator: GraphNode<typeof State> = async (state) => {
  const structLlm = llm.withStructuredOutput(
    z.object({ sections: z.array(z.object({
      title: z.string().describe("Section title"),
      description: z.string().describe("What to cover"),
    })) })
  );
  const result = await structLlm.invoke([
    { role: "system", content: "Plan a report sections." },
    { role: "user", content: state.topic },
  ]);
  return { sections: result.sections };
};

// 工作者:根据子任务描述生成内容(会被 Send 动态创建多次)
const llmCall: GraphNode<typeof State> = async (state) => {
  const section = state.sections[0]; // Send 传入 { sections: [section] },这里取第 0 个
  const msg = await llm.invoke(
    `Write a section: "${section.title}" covering: ${section.description}`
  );
  return { completedSections: [{ title: section.title, content: msg.content }] };
};

📄 完整代码:03_orchestrator_worker.ts

编排图构建

思考题:Send 创建了 N 个并行工作者,它们的结果如何汇总到一个节点?为什么 synthesizer 能读到所有 worker 的输出,而不会出现数据竞争?
// 综合者:把所有工作者生成的内容合成最终报告
const synthesizer: GraphNode<typeof State> = async (state) => {
  const parts = state.completedSections
    .map((s) => `## ${s.title}\n${s.content}`)
    .join("\n\n");
  const msg = await llm.invoke(
    `Format into a final report:\n\n${parts}`
  );
  return { finalReport: msg.content as string };
};

// 条件边:编排者 → 动态分派工作者
const assignWorkers = (state) => {
  return state.sections.map(
    (section) => new Send("llmCall", { sections: [section] })
  );
};

// 构建图
const orchestratorWorkflow = new StateGraph(State)
  .addNode("orchestrator", orchestrator)
  .addNode("llmCall", llmCall)
  .addNode("synthesizer", synthesizer)
  .addEdge(START, "orchestrator")
  // 关键:条件边用 Send 动态创建多个 llmCall 实例
  .addConditionalEdges("orchestrator", assignWorkers, ["llmCall"])
  // 所有工作者完成后汇入综合者
  .addEdge("llmCall", "synthesizer")
  .addEdge("synthesizer", END)
  .compile();

const result = await orchestratorWorkflow.invoke({
  topic: "Create a report on the history of AI"
});
执行流程:编排者拆出 N 个 section → Send 创建 N 个 llmCall 实例并行运行 → 每个实例的结果通过 ReducedValue 累积 → 全部完成后综合者读取累积结果并生成报告。

📄 完整代码:03_orchestrator_worker.ts

模式五:评估优化

Evaluator-Optimizer

一个 LLM 负责生成,另一个 LLM 负责评估。评估不合格就带着反馈回到生成者重新来过,直到合格为止。

图:生成 → 评估 → 循环直到合格

与链式调用的区别

链式调用是线性前进(A → B → C),质量门控只能"过/不过"。评估优化是循环迭代,不合格就修改再提交,直到满意。

核心设计

  • 评估者用 withStructuredOutput 返回结构化评价
  • 条件边根据评价结果决定是结束还是循环
  • 反馈信息写回 state,供下一轮生成参考

评估优化代码

思考题:你让 AI 写了一篇文章但质量不确定。你想让它反复修改直到质量达标,但又怕陷入死循环。怎么实现"写 → 评 → 不行就改 → 再评 → 最多改 N 次"?
import { StateGraph, StateSchema, GraphNode, ConditionalEdgeRouter, START, END } from "@langchain/langgraph";
import * as z from "zod";

// 评估结果 Schema
const FeedbackSchema = z.object({
  grade: z.enum(["funny", "not funny"]).describe("Evaluation grade"),
  feedback: z.string().describe("Feedback for improvement"),
});

const State = new StateSchema({
  joke: z.string(),
  topic: z.string(),
  feedback: z.string(),
  funnyOrNot: z.string(),
});

// 生成者:写笑话(如果之前有反馈,就参考反馈改进)
const llmCallGenerator: GraphNode<typeof State> = async (state) => {
  const prompt = state.feedback
    ? `Write a joke about ${state.topic}. Improve based on feedback: ${state.feedback}`
    : `Write a joke about ${state.topic}`;
  const msg = await llm.invoke(prompt);
  return { joke: msg.content as string };
};

// 评估者:判断笑话好不好笑
const llmCallEvaluator: GraphNode<typeof State> = async (state) => {
  const evaluator = llm.withStructuredOutput(FeedbackSchema);
  const result = await evaluator.invoke(
    `Evaluate this joke: "${state.joke}". Is it funny? Give feedback.`
  );
  return {
    funnyOrNot: result.grade,
    feedback: result.feedback,
  };
};

// 条件边:合格 → 结束,不合格 → 带反馈重来
const routeJoke: ConditionalEdgeRouter<typeof State, "llmCallGenerator"> = (state) => {
  return state.funnyOrNot === "funny" ? "Accepted" : "Rejected";
};

📄 完整代码:03_evaluator_optimizer.ts

评估优化图构建

思考题:如果 LLM 始终无法通过评估,循环会永远转下去。怎么在条件边中加入"最大重试次数"保护,防止无限循环?
// 构建评估优化工作流
const optimizerWorkflow = new StateGraph(State)
  .addNode("llmCallGenerator", llmCallGenerator)
  .addNode("llmCallEvaluator", llmCallEvaluator)
  .addEdge(START, "llmCallGenerator")
  .addEdge("llmCallGenerator", "llmCallEvaluator")
  // 评估者之后:合格结束,不合格循环
  .addConditionalEdges("llmCallEvaluator", routeJoke, {
    Accepted: END,
    Rejected: "llmCallGenerator",    // 带反馈回到生成者
  })
  .compile();

// 测试
const result = await optimizerWorkflow.invoke({
  topic: "Cats",
  joke: "",
  feedback: "",
  funnyOrNot: "",
});
console.log(result.joke);
console.log("Feedback received:", result.feedback);
防止无限循环:如果 LLM 始终无法通过评估,这个循环会一直转下去。实际项目中应该加一个最大重试次数——在 state 里加一个 attempts 计数器,条件边里检查是否超过阈值。
// 安全改进版:限制最大重试 3 次
const MAX_ATTEMPTS = 3;

const routeJokeSafe: ConditionalEdgeRouter<typeof State, "llmCallGenerator"> = (state) => {
  if (state.funnyOrNot === "funny" || (state.attempts ?? 0) >= MAX_ATTEMPTS) {
    return "Accepted";   // 合格或超限 → 结束
  }
  return "Rejected";     // 不合格 → 重试
};

📄 完整代码:03_evaluator_optimizer.ts

模式六:智能体

Agents

智能体(Agent)是一种自主决策的循环结构:LLM 自己决定要调用什么工具、调几次、什么时候结束。不需要你预先定义流程。

图:工具调用循环 —— LLM 自主决定下一步
与前五种模式的本质区别:工作流的每一步都是你预先设计好的,智能体的每一步都是 LLM 在运行时自己决定的。模式一到五是"按图纸施工",模式六是"让 AI 自己想办法"。

工具调用循环

核心机制:shouldContinue + ToolNode

思考题:你想做一个"能算数学的计算器 Agent",用户说"算一下 3+5 乘以 2",Agent 需要理解意图、调用工具、如果还需计算就继续调工具。怎么实现"思考 → 调用 → 再思考"的循环?

ToolNode

LangGraph 预置的工具执行节点。它自动解析 LLM 输出中的 tool_calls,调用对应工具,把结果追加到 messages。

import { ToolNode } from "@langchain/langgraph/prebuilt";

shouldContinue

条件边函数,检查 LLM 最后一条消息是否包含 tool_calls。有就执行工具,没有就结束。

// 条件边:检查是否需要继续调用工具
const shouldContinue: ConditionalEdgeRouter<typeof State, "toolNode"> = (state) => {
  const lastMessage = state.messages.at(-1);
  // 最后一条消息有 tool_calls → 执行工具
  if (lastMessage?.tool_calls?.length) return "toolNode";
  // 没有 tool_calls → LLM 已经给出最终回答,结束
  return END;
};
为什么用 messages 数组?智能体的 state 通常只有一个 messages 字段。LLM 的输出、工具的返回值、用户的输入都是 message,追加到这个数组里。LangGraph 的 ToolNode 和 shouldContinue 都基于这个约定工作。

📄 完整代码:03_agent_tool_calling.ts

智能体代码

思考题:用户说"算一下 (3+5)*2",LLM 需要先调 add(3,5) 得到 8,再调 multiply(8,2) 得到 16。怎么让 LLM 自主决定调哪个工具、调几次?
import { StateGraph, StateSchema, GraphNode, ConditionalEdgeRouter, Messages, START, END } from "@langchain/langgraph";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { tool } from "@langchain/core/tools";
import * as z from "zod";

// 定义工具
const multiply = tool(({ a, b }) => a * b, {
  name: "multiply", description: "Multiply two numbers",
  schema: z.object({ a: z.number(), b: z.number() }),
});
const add = tool(({ a, b }) => a + b, {
  name: "add", description: "Add two numbers",
  schema: z.object({ a: z.number(), b: z.number() }),
});

// LLM 绑定工具
const llmWithTools = llm.bindTools([multiply, add]);

// State 只需要一个 messages 数组
const State = new StateSchema({
  messages: Messages({}),   // 内置 Messages 注解
});

// LLM 节点:调用带工具的 LLM
const llmCall: GraphNode<typeof State> = async (state) => {
  const response = await llmWithTools.invoke(state.messages);
  return { messages: [response] };
};

// 条件边:检查是否继续调用工具
const shouldContinue: ConditionalEdgeRouter<typeof State, "toolNode"> = (state) => {
  const lastMessage = state.messages.at(-1);
  if (lastMessage?.tool_calls?.length) return "toolNode";
  return END;
};

📄 完整代码:03_agent_tool_calling.ts

智能体图构建

思考题:代码里只有 addEdge("toolNode", "llmCall") 这一条回边。为什么 LLM 不会一直循环——什么时候它会主动停止调用工具?
// 工具执行节点
const toolNode = new ToolNode([multiply, add]);

// 构建智能体工作流
const agentWorkflow = new StateGraph(State)
  .addNode("llmCall", llmCall)
  .addNode("toolNode", toolNode)
  .addEdge(START, "llmCall")
  // LLM 之后检查是否有工具调用
  .addConditionalEdges("llmCall", shouldContinue, ["toolNode", END])
  // 工具执行完后回到 LLM 继续判断
  .addEdge("toolNode", "llmCall")
  .compile();

// 测试:让智能体计算 (3 + 5) * 2
const result = await agentWorkflow.invoke({
  messages: [{ role: "user", content: "What is (3 + 5) * 2?" }],
});
console.log(result.messages.at(-1).content);
// 智能体会自动:调用 add(3,5) → 得到 8 → 调用 multiply(8,2) → 得到 16 → 输出结果

执行过程

  1. 用户发问 "(3+5)*2=?"
  2. LLM 决定先算 3+5,调用 add
  3. 工具返回 8,LLM 继续判断
  4. LLM 决定再算 8*2,调用 multiply
  5. 工具返回 16,LLM 继续判断
  6. LLM 给出最终回答,无工具调用 → 结束

智能体的"智能"

你没有写"先加后乘"的逻辑。LLM 自己读懂了算式,自己决定调哪个工具、调几次。这就是自主决策——流程路径完全由 LLM 运行时决定。

📄 完整代码:03_agent_tool_calling.ts

六种模式对比

模式 核心思想 关键 API 流程类型
链式调用 串行加工,每步处理上一步输出 addEdge + addConditionalEdges 线性 / 带跳过
并行执行 多个任务同时运行,互不等待 START 连多条边 扇出 → 扇入
智能路由 先分类输入,再分发到专门节点 withStructuredOutput 分类 → 分发
编排模式 动态拆分任务,按需创建工作者 Send + ReducedValue 动态扇出 → 聚合
评估优化 生成 + 评估循环,直到质量达标 withStructuredOutput + 循环边 循环迭代
智能体 LLM 自主决定调用工具和流程 ToolNode + bindTools 自主循环
模式选择速查: 任务有固定步骤 → 链式调用; 子任务互不依赖 → 并行执行; 需要分类处理 → 智能路由; 子任务数量运行时才知 → 编排模式; 需要迭代改进 → 评估优化; 让 AI 自己想办法 → 智能体。

排错指南

编排模式

Send 传入的 state 不匹配:new Send("llmCall", { section }) 传入的对象会作为该工作者节点的 state。如果节点里访问 state.sections[0],但 Send 传的是 { section },就会读到 undefined。

排查方法:在工作者节点里打印 console.log(state),确认 Send 传入的字段名和节点访问的字段名一致。

评估优化

循环停不下来:评估者 LLM 总是给出"不合格",生成者永远在重试。最常见的原因是评估标准太严,或者 withStructuredOutput 的 Schema 描述不够清晰。

排查方法:加 attempts 计数器限制最大重试次数。调整评估 Schema 的描述文字,降低评判标准。

智能体

工具没被调用:LLM 直接回答而不是调用工具。通常是工具描述不够清晰,或者 LLM 觉得自己就能回答。

排查方法:检查 bindTools 是否正确传入。在 system prompt 里明确要求"必须使用工具来计算"。检查 tool_calls 字段是否存在。
通用调试技巧:三种高级模式都涉及循环或动态分支,console.log 可能打印一大堆。推荐使用 LangSmith(设置 LANGSMITH_TRACING=true),可以图形化查看每一步的输入输出和路由决策。

动手练习

构建一个研究助手

需求:用户输入一个研究主题,助手自动搜索、评估搜索质量、综合成报告。

Step 1:搜索

节点 search:根据主题生成搜索关键词。用 withStructuredOutput 返回关键词列表。

Step 2:评估

节点 evaluate:评估搜索结果是否充分。返回 sufficient: boolean 和 feedback: string。

Step 3:综合

节点 synthesize:把搜索结果综合成一份简短的研究摘要。

要求:
  • 使用评估优化模式:评估不通过时带反馈回到搜索节点重新搜索
  • 加上最大重试 3 次的限制
  • State 包含:topic、searchResults、feedback、sufficient、attempts、finalReport

提示

  • 搜索节点不需要真的调用搜索引擎——模拟一下,用 LLM 生成搜索结果即可
  • 参考评估优化模式的 addConditionalEdges 写法
  • 在条件边里检查 attempts 是否超过 3

参考答案

import { StateGraph, StateSchema, GraphNode, ConditionalEdgeRouter, START, END } from "@langchain/langgraph";
import * as z from "zod";

const MAX_ATTEMPTS = 3;

const EvalResult = z.object({
  sufficient: z.boolean().describe("Are the search results sufficient?"),
  feedback: z.string().describe("Feedback for improvement if insufficient."),
});

const State = new StateSchema({
  topic: z.string(),
  searchResults: z.string(),
  feedback: z.string(),
  sufficient: z.boolean(),
  attempts: z.number(),
  finalReport: z.string(),
});

// 搜索节点:模拟搜索(实际项目中替换为真实搜索 API)
const search: GraphNode<typeof State> = async (state) => {
  const prompt = state.feedback
    ? `Research "${state.topic}" more thoroughly. Previous feedback: ${state.feedback}`
    : `Research "${state.topic}" and summarize key findings.`;
  const msg = await llm.invoke(prompt);
  return {
    searchResults: msg.content as string,
    attempts: (state.attempts ?? 0) + 1,
  };
};

// 评估节点:判断搜索结果是否充分
const evaluate: GraphNode<typeof State> = async (state) => {
  const evaluator = llm.withStructuredOutput(EvalResult);
  const result = await evaluator.invoke(
    `Evaluate these research results for "${state.topic}":\n${state.searchResults}`
  );
  return { sufficient: result.sufficient, feedback: result.feedback };
};

// 综合节点:生成最终报告
const synthesize: GraphNode<typeof State> = async (state) => {
  const msg = await llm.invoke(
    `Write a research summary based on:\n${state.searchResults}`
  );
  return { finalReport: msg.content as string };
};

// 条件边:充分或超限 → 综合,否则 → 重试
const routeEvaluation: ConditionalEdgeRouter<typeof State, "search"> = (state) => {
  if (state.sufficient || (state.attempts ?? 0) >= MAX_ATTEMPTS) return "Pass";
  return "Fail";
};

const workflow = new StateGraph(State)
  .addNode("search", search)
  .addNode("evaluate", evaluate)
  .addNode("synthesize", synthesize)
  .addEdge(START, "search")
  .addEdge("search", "evaluate")
  .addConditionalEdges("evaluate", routeEvaluation, {
    Pass: "synthesize",
    Fail: "search",
  })
  .addEdge("synthesize", END)
  .compile();

本章回顾

模式 特点 关键 API 适用场景
编排模式 运行时动态创建工作者,结果累积合并 Send + ReducedValue 动态子任务、报告生成
评估优化 生成 → 评估循环,带反馈重试 withStructuredOutput + 循环条件边 质量迭代、内容改进
智能体 LLM 自主决策工具调用,循环直到完成 ToolNode + shouldContinue 工具调用、自主推理

核心 API 回顾

  • Send:在条件边中动态创建节点实例
  • ReducedValue:用 reducer 累积多个工作者的输出
  • ToolNode:预置的工具执行节点
  • shouldContinue:检查 tool_calls 决定是否继续

安全意识

  • 循环模式一定要加最大重试次数
  • 智能体的工具调用也可能无限循环——限制最大步数
  • ReducedValue 的 reducer 首次参数可能是 null

延伸阅读

下一章

第 04 章:快速上手实战

把六种工作流模式综合运用,构建一个完整的 AI 应用——从需求分析到代码实现到调试优化。

回顾上一章

第 02 章:工作流模式(上)

如果你对链式调用、并行执行、智能路由这三种基础模式还不够熟悉,建议先回去巩固一下——本章的高级模式建立在它们之上。

恭喜完成第 03 章!现在你已经掌握了 LangGraph 全部六种工作流模式。链式、并行、路由、编排、评估优化、智能体——它们是构建任何复杂 AI 应用的完整积木箱。接下来的章节将带你进入实战。
1 / 19