第 01 章

认识 LangGraph

用状态图构建可靠、可调试、可暂停恢复的 AI 智能体

从"裸调 LLM"到"工程化智能体"的第一步

为什么需要它

你有没有试过直接写代码调 LLM 来做复杂任务?

裸调 LLM 的困境

  • 多轮对话的上下文散落在各处,没有统一的状态管理
  • 流程中途崩了?从头再来——没有持久化
  • 想让人审一下中间结果?只能复制粘贴——没有内置的人机交互
  • 出 bug 了只能 console.log 猜——没有可视化调试
  • LLM 调用、工具调用、条件分支全缠在一起,代码变成意大利面条

LangGraph 的解法

  • 所有节点共享一个 State 对象,状态一目了然
  • 每一步自动存快照,崩了从上次位置继续
  • 随时 interrupt() 暂停,等人工确认后恢复
  • LangSmith 可视化追踪每一次调用和状态变更
  • 节点 + 边 + 条件路由,流程即代码
问题核心:裸调 LLM 写 demo 够用,但一旦你的智能体需要"记住状态、处理异常、等人确认、中途恢复",你就会发现自己正在重新发明一个框架。LangGraph 就是那个你已经快发明出来的东西。

开始之前

你需要会什么

  • TypeScript / JavaScript 基础(函数、类型、async/await)
  • 了解 LLM 的基本概念(prompt、token、tool calling)
  • 会用 npm 初始化项目、安装依赖
  • 接触过至少一个 LLM API(OpenAI、Anthropic 都行)

读完后你应该能

  • 说清 LangGraph 是什么、解决什么问题
  • 写出第一个能跑的 StateGraph Hello World
  • 理解节点、边、状态这三大核心概念
  • 掌握"五步法"来设计一个完整智能体
  • 知道什么时候该用 LangGraph,什么时候不该用

本章不会深入

  • 具体的工作流模式(链式、并行、路由等)
  • 持久化与 checkpointer 的实现细节
  • 流式处理、中断、时间旅行等进阶功能
  • 生产环境部署方案
环境准备:确保你有一个 Node.js 项目。安装命令:$ npm install @langchain/langgraph @langchain/core

LangGraph 是什么

一个低级编排框架和运行时,用于构建长时间运行的、有状态的智能体

说白了,LangGraph 帮你把一个复杂的 AI 任务拆成一个个节点(函数),用边(条件分支)连起来,再给它们一个共享的状态笔记本。整个执行过程可追踪、可暂停、可恢复。已被 Klarna、Uber、J.P. Morgan 等公司投入生产使用。

图:Hello World StateGraph——最简有向无环图
思考题:你想让 AI 自动生成一个笑话:先随机选一个话题,再根据这个话题写笑话。这两个步骤有先后顺序,怎么用代码表达?
// 安装依赖
$ npm install @langchain/langgraph @langchain/core

// Hello World —— 最简 StateGraph
import { StateSchema, MessagesValue, GraphNode, StateGraph, START, END } from "@langchain/langgraph";

const State = new StateSchema({
  messages: MessagesValue,
});

// 定义一个节点:接收状态,返回更新
const mockLlm: GraphNode<typeof State> = (state) => {
  return { messages: [{ role: "ai", content: "hello world" }] };
};

// 构建图:起点 → 节点 → 终点
const graph = new StateGraph(State)
  .addNode("mock_llm", mockLlm)
  .addEdge(START, "mock_llm")
  .addEdge("mock_llm", END)
  .compile();

// 运行
const result = await graph.invoke({
  messages: [{ role: "user", content: "hi!" }]
});
// result.messages 最后一条: { role: "ai", content: "hello world" }
如果看到报错:
  • ERR_MODULE_NOT_FOUND → 先安装依赖:$ npm install @langchain/langgraph @langchain/core
  • TypeError: StateSchema is not a constructor → 检查是否使用 ESM("type": "module" in package.json)或用 .mjs 后缀

📄 完整代码:01_hello_world.ts

三个关键字:StateSchema(定义共享状态)、addNode(添加节点函数)、addEdge(连线)。这就是 LangGraph 的最小知识集。

核心优势

LangGraph 不抽象你的提示词或架构,它提供的是底层基础设施

优势说明什么时候需要
持久化执行 每步自动存快照,任务崩了能从上次位置恢复 长时间运行的任务、LLM 调用可能超时的场景
人机交互 在任意节点暂停执行,等人审批或修改后再继续 高风险操作(发邮件、改数据)需要人工确认时
全面记忆 短期记忆(线程内)+ 长期记忆(跨会话) 多轮对话、记住用户偏好
LangSmith 调试 可视化追踪执行路径、状态变化、token 消耗 调试复杂智能体行为、评估输出质量
生产部署 专为有状态、长时间运行工作流设计的扩展基础设施 从原型走向生产、需要稳定性和可观测性时

灵感来源

LangGraph 受 Pregel(Google 图计算模型)、Apache Beam(大数据处理)、NetworkX(图网络库)启发,由 LangChain Inc. 构建,但可以不依赖 LangChain 使用。

产品生态

产品做什么
LangSmith Observability追踪请求、评估输出、监控部署——一个地方看清全貌
LangSmith Deployment专为长时间运行、有状态工作流构建的部署和扩展平台
LangChain模型和工具集成层,包含基于 LangGraph 构建的预置智能体

三者都是可选的——LangGraph 本身完全独立,可以单独使用。

五个步骤

用 LangGraph 构建智能体的通用方法论

图:LangGraph 五步构建法——从需求到可运行智能体

1. 拆步骤

把工作流映射为离散节点

2. 分类型

LLM / 数据 / 操作 / 用户

3. 设计状态

定义共享 State 对象

4. 写节点

实现为函数 + 错误处理

5. 连线

边 + 条件边 + Command

这五步是通用的:不管你做邮件处理、代码审查还是客服机器人,流程都一样。后面的章节会反复用到这个方法论。

第一步:拆步骤

把工作流映射为离散步骤——每个节点只做一件事

假设你要构建一个邮件处理智能体,需求如下:

图:邮件处理智能体——工作流全景
智能体应该:
- 阅读收到的客户邮件
- 按紧急程度和主题进行分类
- 搜索相关文档以回答问题
- 起草适当的回复
- 将复杂问题升级给人工处理

拆成节点之后,每个节点就是一个只做一件事的函数:

节点职责下一步
Read Email提取并解析邮件内容→ Classify Intent(固定)
Classify Intent用 LLM 分类紧急程度和主题→ 视情况路由(条件)
Doc Search在知识库查询相关信息→ Draft Reply(固定)
Bug Track在跟踪系统中创建或更新 Bug→ Draft Reply(固定)
Draft Reply生成适当的回复→ 视情况路由(条件)
Human Review升级给人工审批或处理→ 视情况路由(条件)
Send Reply发送邮件回复→ END
常见误区:不要一个节点做太多事。"分类 + 搜索 + 起草"塞进一个函数,看起来省事,但后面加条件分支、错误处理、人工审核时会很痛苦。每个节点只做一件事。

第二步:分类型

判断每个节点属于哪种类型——决定了实现方式

拆完步骤之后,你需要搞清楚每个节点到底在做什么。不同的类型,实现方式完全不同。

LLM 步骤

需要理解、分析、生成文本或做推理决策。

示例:Classify Intent、Draft Reply

调用 LLM API

数据步骤

需要从外部源检索信息。

示例:Document Search、Customer History Lookup

调用数据库 / API

操作步骤

需要执行外部操作(发邮件、建 Bug)。

示例:Send Reply、Bug Track

执行副作用

用户输入步骤

需要人工干预或确认。

示例:Human Review

调用 interrupt()

为什么要分类型?因为不同类型节点的错误处理策略不同。LLM 步骤可能需要重试或调整提示词;数据步骤需要处理网络超时;操作步骤需要确保幂等性;用户输入步骤需要暂停等待。

第三步:设计状态

状态是所有节点共享的笔记本——存储原始数据,不存格式化文本

说白了,State 就是一个对象,每个节点都能读它、往里写东西。设计状态的核心原则是:

应该放入状态

  • 需要在步骤间持久化的数据
  • 多个下游节点都需要的信息
  • 重新获取代价高的数据
  • 调试时需要查看的中间结果

不应该放入状态

  • 可以从其他数据推导出来的(需要时再算)
  • 格式化后的提示词文本(在节点内按需格式化)
  • 临时变量(节点函数内部处理)
思考题:你在写一个「项目评审系统」,多个评审员会同时给项目打分、写评语。你需要记录当前状态、收集所有评审员的标签(不互相覆盖)、累加评审次数。怎么设计一个 State 能同时满足这些需求?
import { StateSchema } from "@langchain/langgraph";
import * as z from "zod";

// 定义邮件分类结构
const EmailClassificationSchema = z.object({
  intent: z.enum(["question", "bug", "billing", "feature", "complex"]),
  urgency: z.enum(["low", "medium", "high", "critical"]),
  topic: z.string(),
  summary: z.string(),
});

// 邮件智能体的状态——存原始数据,不存格式化后的提示词
const EmailAgentState = new StateSchema({
  // 原始邮件数据
  emailContent: z.string(),
  senderEmail: z.string(),
  emailId: z.string(),
  // 分类结果(下游多个节点需要)
  classification: EmailClassificationSchema.optional(),
  // 原始搜索/API 结果(重新获取代价高)
  searchResults: z.array(z.string()).optional(),
  // 生成的内容
  responseText: z.string().optional(),
});

📄 完整代码:01_state_design.ts

为什么存原始数据:不同节点可以用不同方式格式化同一份数据。你改提示词模板时不用改状态结构,调试也更清晰——每个节点接收的就是纯数据,不是掺了格式的文本。

第四步:写节点

节点就是一个函数——接收当前状态,返回对状态的更新

说白了,每个节点就是 (state) => { return { key: newValue } }。返回值会合并到状态里,传给下一个节点。

思考题:你的 Agent 运行中可能遇到各种错误——网络超时、LLM 返回格式不对、需要用户提供信息、服务器崩溃。每种错误的处理方式不同,怎么区分和应对?
// 分类节点:调用 LLM 分析邮件意图
const classifyIntent: GraphNode<typeof EmailAgentState> = async (state) => {
  const prompt = `请分析以下邮件的意图和紧急程度:
邮件内容:${state.emailContent}
发件人:${state.senderEmail}`;

  const result = await llm.invoke(prompt);
  return { classification: result };
};

// 文档搜索节点:从知识库查询
const searchDocs: GraphNode<typeof EmailAgentState> = async (state) => {
  const results = await knowledgeBase.search(state.classification?.topic);
  return { searchResults: results };
};

📄 完整代码:01_error_handling.ts

错误处理策略——不同错误,不同解法

错误类型谁来修策略LangGraph 怎么做
瞬态错误(网络抖动、限流) 系统自动 自动重试 retryPolicy: { maxAttempts: 3 }
LLM 可恢复(工具失败、解析出错) LLM 把错误写进状态,LLM 自行调整 循环回 LLM 节点重试
用户可修复(信息缺失、指令不清) 人类 暂停,等人输入 interrupt() 暂停等待
意外错误 开发者 向上冒泡 让错误抛出,检查日志
思考题:你需要调用一个不稳定的外部 API,它有时超时、有时限流。你不想一失败就放弃,但也不能无限重试。怎么设计一个"最多重试 N 次"的机制?
// 给容易出网络问题的节点加重试策略
workflow.addNode(
  "searchDocumentation",
  searchDocumentation,
  { retryPolicy: { maxAttempts: 3, initialInterval: 1.0 } }
);

📄 完整代码:01_retry_policy.ts

第五步:连线

用边、条件边和 Command 对象把节点连成完整的图

最后一步,把所有节点连起来。LangGraph 提供三种连线方式:

普通边 addEdge

A 执行完一定到 B,没有分支。

addEdge(START, "readEmail")

条件边 addConditionalEdges

A 执行完后,根据状态决定去哪个节点。

addConditionalEdges("classify", router)

Command 对象

节点内部直接指定下一个节点 + 状态更新。路由逻辑在节点内完成。

return new Command({ goto: "nodeX" })

思考题:你写了一个「审批流程」图,有三个出口:通过→进入 publish 节点、驳回→进入 reject 节点、需要修改→同时更新状态并跳回 review 节点。能不能在节点内部直接决定"下一步去哪"?
import { MemorySaver, StateGraph, START, END } from "@langchain/langgraph";

// 创建图并连线
const workflow = new StateGraph(EmailAgentState)
  // 添加节点
  .addNode("readEmail", readEmail)
  .addNode("classifyIntent", classifyIntent)
  .addNode("searchDocumentation", searchDocumentation, {
    retryPolicy: { maxAttempts: 3 }  // 瞬态故障自动重试
  })
  .addNode("draftResponse", draftResponse)
  .addNode("humanReview", humanReview)
  .addNode("sendReply", sendReply)
  // 连线——只连必要的边
  .addEdge(START, "readEmail")
  .addEdge("readEmail", "classifyIntent")
  .addEdge("sendReply", END);

// 用 checkpointer 编译,启用持久化
const memory = new MemorySaver();
const app = workflow.compile({ checkpointer: memory });

📄 完整代码:01_command_routing.ts

图结构非常精简:因为路由逻辑通过条件边和 Command 对象在节点内部完成。你只需要连"必要的边",其余的由节点自己决定去哪。这让控制流显式且可追踪。

动手练习

设计一个简单的文档处理智能体

现在轮到你了。停下来,用刚学的五步法设计一个文档处理智能体。需求如下:

智能体应该:
1. 接收用户上传的文档(纯文本)
2. 分析文档类型(合同、报告、邮件、其他)
3. 提取关键信息(日期、金额、人名等)
4. 生成结构化摘要
5. 如果信息不确定,暂停询问用户
6. 输出最终的 JSON 结果

任务 1:拆节点

  • 这个流程需要几个节点?
  • 每个节点的职责是什么?
  • 哪些是 LLM 步骤?哪些是用户输入步骤?

任务 2:设计状态

  • State 里需要哪些字段?
  • 哪些字段用 .optional()?
  • 想想"原始数据 vs 格式化文本"原则

任务 3:画连线

  • 哪些边是固定的?
  • 哪些需要条件边?
  • 在哪里需要 interrupt?
提示:先在纸上或文本编辑器里画出节点图和状态字段,不要急着写代码。想清楚了再动键盘,会快得多。

参考答案

一种可行的设计方案——不是唯一答案,但逻辑合理

节点类型职责下一步
receiveDoc数据接收并预处理文档文本→ classifyDoc(固定)
classifyDocLLM分析文档类型→ extractInfo(固定)
extractInfoLLM提取关键信息→ 视置信度(条件)
confirmInfo用户输入不确定时询问用户→ generateSummary(固定)
generateSummaryLLM生成结构化摘要→ END
import { StateSchema, StateGraph, START, END, MemorySaver } from "@langchain/langgraph";
import * as z from "zod";

// 状态设计——只存原始数据
const DocState = new StateSchema({
  rawText: z.string(),                         // 原始文档文本
  docType: z.string().optional(),              // 文档类型
  extractedInfo: z.record(z.any()).optional(),  // 提取的关键信息
  needsConfirmation: z.boolean().optional(),    // 是否需要用户确认
  finalJson: z.string().optional(),            // 最终 JSON 输出
});

// 构建图
const docAgent = new StateGraph(DocState)
  .addNode("receiveDoc", receiveDoc)
  .addNode("classifyDoc", classifyDoc)
  .addNode("extractInfo", extractInfo)
  .addNode("confirmInfo", confirmInfo)
  .addNode("generateSummary", generateSummary)
  .addEdge(START, "receiveDoc")
  .addEdge("receiveDoc", "classifyDoc")
  .addEdge("classifyDoc", "extractInfo")
  .addConditionalEdges("extractInfo", (state) => {
    return state.needsConfirmation ? "confirmInfo" : "generateSummary";
  })
  .addEdge("confirmInfo", "generateSummary")
  .addEdge("generateSummary", END)
  .compile({ checkpointer: new MemorySaver() });
你的方案可能不一样——这很正常。关键不是"答案是否一致",而是你能不能解释清楚:为什么这么拆节点、为什么状态里放这些字段、为什么这里用条件边。五步法是一种思维方式,不是唯一标准答案。

本章回顾

到这里,你已经掌握了 LangGraph 的全貌

你学到了什么

  • LangGraph 是一个低级编排框架,用于构建有状态的智能体
  • 三大核心概念:节点(函数)、边(流程)、状态(共享记忆)
  • 五步构建法:拆步骤 → 分类型 → 设计状态 → 写节点 → 连线
  • 状态存原始数据,提示词在节点内按需格式化
  • 四种错误处理策略:自动重试 / LLM 自愈 / 人工介入 / 向上冒泡
  • 图结构精简,路由逻辑在节点内完成

后续章节预告

  • 基础模式:提示词链、并行化、路由、编排者-工作者、评估者-优化者
  • 快速上手:完整实现一个带工具调用的计算器智能体
  • 持久化:checkpointer、线程、检查点的深入理解
  • 流式处理:token 级流式输出、自定义数据流
  • 中断与人机交互:interrupt 和 Command 的完整用法
  • 记忆管理:短期记忆 + 长期记忆 + 语义搜索
核心结论:LangGraph 不是让你"写得更快"的工具,而是让你"写得可控"的框架。当你的智能体需要记住上下文、处理异常、等人确认、中途恢复时,LangGraph 提供的就是这些"工程化"能力的底层支撑。

延伸阅读

继续本课程

官方文档

背景知识

  • Pregel 论文 — Google 图计算模型,LangGraph 的灵感来源
  • NetworkX — Python 图网络库,LangGraph 公共接口的设计参考