工作流模式(上)
链式调用、并行执行、智能路由 —— 三种基础编排模式
当一次 LLM 调用不够用的时候,你需要的是模式,而不是更多的提示词一个不够用
你有没有遇到过这样的场景?
单次调用搞不定
想让 LLM 先生成内容,再改进内容,最后润色成稿——一步到位?结果往往是每一步都做不好。
串行太慢
明明三个子任务之间毫无依赖,但你还是一个个串着跑。等三分钟,其实十秒就够了。
不知道该走哪条路
用户可能要故事、要笑话、要诗歌——你不想把三个 prompt 塞进一次调用,质量会崩。
这些问题,本质上都是编排问题:不是 LLM 不够聪明,而是你没有给它安排好工作流。
开始之前
需要具备
- 已经读完第 01 章,了解 LangGraph 的核心概念(节点、边、状态)
- 熟悉 TypeScript 基本语法
- 有一个可用的 Anthropic API Key
- Node.js 18+ 环境
本章结束后你能
- 区分工作流(Workflow)和智能体(Agent)
- 写出链式调用、并行执行、路由三种模式的代码
- 理解
addConditionalEdges和质量门控 - 独立完成一个内容处理管道练习
本章不会涉及
- 编排者-工作者模式(Orchestrator-Worker)
- 评估者-优化者模式(Evaluator-Optimizer)
- 持久化、流式处理、人机交互
- 生产环境部署细节
基础设置
安装依赖
npm install @langchain/langgraph @langchain/core @langchain/anthropic
初始化 LLM
// 导入 DeepSeek 聊天模型
import { ChatDeepSeek } from "@langchain/deepseek";
const llm = new ChatDeepSeek({
model: "deepseek-chat",
apiKey: "<your_deepseek_key>"
});
LLM 增强能力
import * as z from "zod"; // Zod:运行时类型校验库,用来定义 Schema
import { tool } from "@langchain/core/tools"; // LangChain 工具定义函数
// ========== 1. 结构化输出 ==========
// 场景:你希望 LLM 不返回自由文本,而是返回严格符合 Schema 的 JSON 对象
// 原理:withStructuredOutput 内部会把 Zod Schema 转成 JSON Schema 发给模型,
// 模型会被强制按格式输出,返回结果会经过 Zod 校验
const SearchQuery = z.object({
search_query: z.string().describe("Optimized web search query."), // .describe() 的文字会作为字段说明传给模型
justification: z.string().describe("Why this query is relevant."),
});
const structuredLlm = llm.withStructuredOutput(SearchQuery);
// 用法:const result = await structuredLlm.invoke("...");
// result = { search_query: "...", justification: "..." } ← 保证是合法 JSON
// ========== 2. 工具调用 ==========
// 场景:你希望 LLM 能"调用"外部函数(如计算器、搜索、数据库查询)
// 原理:bindTools 把工具定义发给模型,模型决定是否需要调用工具、传什么参数;
// 返回的 aiMsg.tool_calls 里包含工具名和参数,由你来实际执行
const multiply = tool(
({ a, b }) => a * b, // 第 1 个参数:工具的实际执行函数
{
name: "multiply", // 工具名称(模型通过这个名字调用)
description: "Multiply two numbers", // 工具描述(模型根据描述判断何时使用)
schema: z.object({ // 参数 Schema(告诉模型需要传哪些参数)
a: z.number(),
b: z.number(),
}),
}
);
const llmWithTools = llm.bindTools([multiply]);
// 用法:const aiMsg = await llmWithTools.invoke("3乘以5等于几?");
// aiMsg.tool_calls = [{ name: "multiply", args: { a: 3, b: 5 } }]
withStructuredOutput 控制输出格式、bindTools 添加工具调用能力、MemorySaver 提供短期记忆。本章主要用到前两个。
工作流与智能体
工作流 Workflow
- 路径预定义——代码决定下一步去哪个节点
- 适合流程清晰、步骤确定的场景
- 可预测、可调试、可测试
- 本章的链式、并行、路由都是工作流
智能体 Agent
- 路径自主决定——LLM 自己判断下一步做什么
- 适合开放性、需要灵活判断的场景
- 更强大但更难调试
- 后续章节会深入讲解
LangGraph 的优势在于:工作流和智能体用同一套 API(StateGraph、addNode、addEdge),你随时可以把工作流的某个节点替换成智能体决策。
模式一:链式调用
每个 LLM 调用处理前一个调用的输出,像流水线一样一步步推进。适合可以分解为多个可验证子步骤的任务。

图:链式调用 —— 生成 → 改进 → 润色
【AI 生图提示词】请生成一张中文技术流程图,风格简洁、配色柔和、适合教学: - 画面主体:一条从左到右的流程图,展示笑话工厂的流水线 - 节点依次为:START(绿色圆角矩形)→ 生成笑话(蓝色)→ 质量检查(橙色菱形判断)→ 改进笑话(蓝色)→ 润色笑话(蓝色)→ END(红色圆角矩形) - 各元素关系:质量检查有两个出口——"通过"走上方路径进入改进笑话,"不通过"直接跳到 END - 关键标注:质量检查节点旁标注"Pass/Fail 分支",不通过路径用虚线箭头直接连到 END,标注"跳过后续步骤" - 配色建议:浅米色背景,普通节点用蓝色圆角矩形,判断用橙色菱形,跳过路径用灰色虚线 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
链式调用代码
import { StateGraph, StateSchema, GraphNode, ConditionalEdgeRouter, START, END } from "@langchain/langgraph";
import * as z from "zod";
// 定义图状态 —— 每个字段对应流水线的一个阶段
const State = new StateSchema({
topic: z.string(), // 输入主题
joke: z.string(), // 原始笑话
improvedJoke: z.string(), // 改进后的笑话
finalJoke: z.string(), // 最终润色
});
// 节点 1:生成初始笑话
const generateJoke: GraphNode<typeof State> = async (state) => {
const msg = await llm.invoke(`Write a short joke about ${state.topic}`);
return { joke: msg.content };
};
// 节点 2:改进笑话(添加双关语)
const improveJoke: GraphNode<typeof State> = async (state) => {
const msg = await llm.invoke(
`Make this joke funnier by adding wordplay: ${state.joke}`
);
return { improvedJoke: msg.content };
};
// 节点 3:最终润色(添加反转)
const polishJoke: GraphNode<typeof State> = async (state) => {
const msg = await llm.invoke(
`Add a surprising twist to this joke: ${state.improvedJoke}`
);
return { finalJoke: msg.content };
};
质量门控
核心机制:addConditionalEdges + ConditionalEdgeRouter
// 质量门函数 —— 检查笑话是否包含 ? 或 !(简单判断是否有笑点)
const checkPunchline: ConditionalEdgeRouter<typeof State, "improveJoke"> = (state) => {
if (state.joke?.includes("?") || state.joke?.includes("!")) {
return "Pass"; // 有笑点 → 继续改进
}
return "Fail"; // 没笑点 → 直接结束
};
// 构建完整工作流
const chain = new StateGraph(State)
.addNode("generateJoke", generateJoke)
.addNode("improveJoke", improveJoke)
.addNode("polishJoke", polishJoke)
// 入口
.addEdge(START, "generateJoke")
// 条件边 —— generateJoke 之后根据质量门控决定去向
.addConditionalEdges("generateJoke", checkPunchline, {
Pass: "improveJoke", // 通过 → 改进
Fail: END // 不通过 → 直接结束
})
.addEdge("improveJoke", "polishJoke")
.addEdge("polishJoke", END)
.compile();
// 调用
const result = await chain.invoke({ topic: "cats" });
ConditionalEdgeRouter<typeof State, "improveJoke"> 末尾的泛型参数(此处为 "improveJoke")用于声明路由函数可能返回的节点名。返回字符串必须和 addConditionalEdges 的映射对象的 key 完全一致,否则运行时报错。
模式二:并行执行
多个 LLM 调用同时运行,互不等待。适合子任务之间没有依赖关系的场景。也可以对同一任务运行多次,取最优结果。

图:并行执行 —— 同一主题,三路并行
【AI 生图提示词】请生成一张中文技术流程图,风格简洁、配色柔和、适合教学: - 画面主体:一个扇出-扇入的并行结构,START 同时触发三个工作者 - 顶部 START(绿色圆角矩形),向下扇出三条并行箭头到三个节点:写笑话、写故事、写诗歌(蓝色圆角矩形,并排排列) - 三个节点各自向下汇聚到聚合器(紫色圆角矩形),再向右到 END(红色圆角矩形) - 各元素关系:START 到三个节点是一对多关系(同时触发),三个节点到聚合器是多对一关系(全部完成后汇入) - 关键标注:扇出处标注"同时启动,互不等待",汇聚处标注"全部完成后才继续" - 配色建议:浅米色背景,工作者节点用蓝色,聚合器用紫色,扇出/扇入箭头用橙色 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
START 到多个节点各加一条边,LangGraph 就会自动并行执行这些节点。所有并行节点完成后,再汇入下游节点。
并行执行代码
import { StateGraph, StateSchema, GraphNode, START, END } from "@langchain/langgraph";
import * as z from "zod";
const State = new StateSchema({
topic: z.string(),
joke: z.string(),
story: z.string(),
poem: z.string(),
combinedOutput: z.string(),
});
// 三个并行的 LLM 节点 —— 互不依赖,同时运行
const callLlm1: GraphNode<typeof State> = async (state) => {
const msg = await llm.invoke(`Write a joke about ${state.topic}`);
return { joke: msg.content };
};
const callLlm2: GraphNode<typeof State> = async (state) => {
const msg = await llm.invoke(`Write a story about ${state.topic}`);
return { story: msg.content };
};
const callLlm3: GraphNode<typeof State> = async (state) => {
const msg = await llm.invoke(`Write a poem about ${state.topic}`);
return { poem: msg.content };
};
// 聚合节点 —— 等所有并行节点完成后执行
const aggregator: GraphNode<typeof State> = async (state) => {
const combined = `Story:\n${state.story}\n\nJoke:\n${state.joke}\n\nPoem:\n${state.poem}`;
return { combinedOutput: combined };
};
// 关键:START 连接到三个节点 → 自动并行
const parallelWorkflow = new StateGraph(State)
.addNode("callLlm1", callLlm1)
.addNode("callLlm2", callLlm2)
.addNode("callLlm3", callLlm3)
.addNode("aggregator", aggregator)
.addEdge(START, "callLlm1") // 并行入口
.addEdge(START, "callLlm2") // 并行入口
.addEdge(START, "callLlm3") // 并行入口
.addEdge("callLlm1", "aggregator") // 汇入聚合
.addEdge("callLlm2", "aggregator")
.addEdge("callLlm3", "aggregator")
.addEdge("aggregator", END)
.compile();
const result = await parallelWorkflow.invoke({ topic: "cats" });
模式三:智能路由
先分类输入,再路由到专门的处理节点。每个处理节点只做自己擅长的事,输出质量更高。

图:智能路由 —— 先分类,再分发
【AI 生图提示词】请生成一张中文技术流程图,风格简洁、配色柔和、适合教学: - 画面主体:一个路由分发结构,先分类后处理 - 顶部 START(绿色圆角矩形)→ 路由分类器(橙色圆角矩形,稍大突出) - 路由分类器向下扇出三条路径到三个专家节点:故事专家(蓝色)、笑话专家(绿色)、诗歌专家(紫色),并排排列 - 每个专家节点各自向下到独立的 END(红色圆角矩形),共三个 END - 各元素关系:路由分类器与专家之间是条件一对多——根据分类结果只走其中一条路径,而非全部触发 - 关键标注:三条路径旁分别标注"story"、"joke"、"poem",路由分类器旁标注"LLM withStructuredOutput 做分类" - 配色建议:浅米色背景,分类器用橙色,三个专家用不同柔和色(蓝/绿/紫)以示区分 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
withStructuredOutput 让 LLM 做出二选一/三选一的决策,然后用 addConditionalEdges 把请求送到正确的节点。
路由代码实现
import { StateGraph, StateSchema, GraphNode, ConditionalEdgeRouter, START, END } from "@langchain/langgraph";
import * as z from "zod";
// 路由决策 Schema —— 用结构化输出让 LLM 做选择题
const routeSchema = z.object({
step: z.enum(["poem", "story", "joke"]).describe("The next step in the routing process"),
});
const router = llm.withStructuredOutput(routeSchema);
const State = new StateSchema({
input: z.string(),
decision: z.string(),
output: z.string(),
});
// 三个专门的处理节点
const llmCall1: GraphNode<typeof State> = async (state) => {
const result = await llm.invoke([
{ role: "system", content: "You are an expert storyteller." },
{ role: "user", content: state.input },
]);
return { output: result.content };
};
// llmCall2(喜剧专家)、llmCall3(诗歌专家)结构相同,省略……
// 路由节点 —— 调用结构化输出做分类
const llmCallRouter: GraphNode<typeof State> = async (state) => {
const decision = await router.invoke([
{ role: "system", content: "Route to story, joke, or poem based on the user's request." },
{ role: "user", content: state.input },
]);
return { decision: decision.step };
};
// 条件边 —— 根据路由决策分发
const routeDecision: ConditionalEdgeRouter<typeof State, "llmCall1" | "llmCall2" | "llmCall3"> = (state) => {
if (state.decision === "story") return "llmCall1";
else if (state.decision === "joke") return "llmCall2";
else return "llmCall3";
};
路由图构建
// 构建路由工作流
const routerWorkflow = new StateGraph(State)
.addNode("llmCall1", llmCall1)
.addNode("llmCall2", llmCall2)
.addNode("llmCall3", llmCall3)
.addNode("llmCallRouter", llmCallRouter)
// 入口 → 路由器
.addEdge(START, "llmCallRouter")
// 路由器 → 条件分发(三选一)
.addConditionalEdges("llmCallRouter", routeDecision, ["llmCall1", "llmCall2", "llmCall3"])
// 三个处理节点各自通向终点
.addEdge("llmCall1", END)
.addEdge("llmCall2", END)
.addEdge("llmCall3", END)
.compile();
// 测试路由 —— 输入 "写个关于猫的笑话" 会走 llmCall2
const result = await routerWorkflow.invoke({
input: "Write me a joke about cats"
});
console.log(result.output);
addConditionalEdges 两种写法
对象映射:第三个参数传对象 { Pass: "nodeA", Fail: END }
数组列表:第三个参数传数组 ["llmCall1", "llmCall2", "llmCall3"],路由函数直接返回节点名
选择哪种?
返回值是固定枚举(Pass/Fail)→ 用对象映射,更安全
返回值本身就是节点名 → 用数组列表,更简洁
排错指南
常见错误 1
边名拼错:addConditionalEdges 里的映射 key 和 ConditionalEdgeRouter 返回值不一致。
比如 Router 返回 "pass" 但映射写的是 Pass: "improveJoke"——大小写不匹配直接报错。
console.log("routing to:", result) 确认返回值。
常见错误 2
忘了 START 或 END:每个图必须有至少一个 START 入口和 END 出口。
缺少出口会导致图编译失败:"No path to END node found"。
END。
常见错误 3
并行节点写同一个字段:两个并行节点返回相同的 state key,后到的会覆盖先到的。
比如两个节点都 return { result: ... },最终只剩一个。
ReducedValue 做合并。
LANGSMITH_TRACING=true 和 LANGSMITH_API_KEY=...,可以在 LangSmith 里看到每一步的输入输出和路由决策,比 console.log 高效得多。
动手练习
构建一个内容处理管道
需求:对一篇英文文章做三步处理。
Step 1:摘要
节点 summarize:输入文章原文,输出 50 词以内的摘要。
Step 2:翻译
节点 translate:把摘要翻译成中文。
Step 3:格式化
节点 format:加上标题和分隔线,输出最终结果。
summarize 和 translate 之间加一个质量门控——如果摘要超过 80 个词,直接跳到 END 并返回原始摘要(说明摘要太长,翻译没意义)。
提示
- State 需要:
article、summary、translation、finalOutput - 质量门控检查
summary.split(" ").length - 参考链式调用的
addConditionalEdges写法
参考答案
import { StateGraph, StateSchema, GraphNode, ConditionalEdgeRouter, START, END } from "@langchain/langgraph";
import * as z from "zod";
const State = new StateSchema({
article: z.string(),
summary: z.string(),
translation: z.string(),
finalOutput: z.string(),
});
// Step 1:生成摘要
const summarize: GraphNode<typeof State> = async (state) => {
const msg = await llm.invoke(
`Summarize this article in under 50 words:\n\n${state.article}`
);
return { summary: msg.content as string };
};
// Step 2:翻译为中文
const translate: GraphNode<typeof State> = async (state) => {
const msg = await llm.invoke(
`Translate the following summary to Chinese:\n\n${state.summary}`
);
return { translation: msg.content as string };
};
// Step 3:格式化输出
const format: GraphNode<typeof State> = async (state) => {
const output = `=== 文章摘要 ===\n${state.translation}\n${"—".repeat(20)}`;
return { finalOutput: output };
};
// 质量门控:摘要太长(超过 80 词)则跳过翻译
const checkSummaryLength: ConditionalEdgeRouter<typeof State, "translate"> = (state) => {
const wordCount = state.summary.split(" ").length;
return wordCount <= 80 ? "Pass" : "Fail";
};
// 构建图
const pipeline = new StateGraph(State)
.addNode("summarize", summarize)
.addNode("translate", translate)
.addNode("format", format)
.addEdge(START, "summarize")
.addConditionalEdges("summarize", checkSummaryLength, {
Pass: "translate",
Fail: END,
})
.addEdge("translate", "format")
.addEdge("format", END)
.compile();
本章回顾
| 模式 | 特点 | 关键 API | 适用场景 |
|---|---|---|---|
| 链式调用 | 串行处理,每步处理上一步输出 | addEdge + addConditionalEdges |
多步加工、质量门控 |
| 并行执行 | 同时运行,互不等待 | START 连多条边 |
独立子任务、多角度生成 |
| 智能路由 | 先分类,再分发到专门节点 | withStructuredOutput + addConditionalEdges |
多类型请求、专门化处理 |
核心概念回顾
GraphNode:节点就是一个 async 函数,接收 state,返回 state 更新ConditionalEdgeRouter:条件边函数,决定下一步去哪个节点withStructuredOutput:让 LLM 输出符合 Schema 的结构化数据
模式选择指南
- 任务有先后顺序 → 链式调用
- 任务之间无依赖 → 并行执行
- 需要根据输入选不同处理 → 智能路由
延伸阅读
下一章
编排者-工作者模式(Orchestrator-Worker)、评估者-优化者模式(Evaluator-Optimizer)—— 用 Send 动态创建并行任务,用循环反馈不断改进输出。