跳到主要内容
LangGraph 可以改变您构建代理的思维方式。使用 LangGraph 构建代理时,您首先会将其分解为称为节点的离散步骤。然后,您将描述每个节点的各种决策和转换。最后,您将通过每个节点都可以读取和写入的共享状态将您的节点连接起来。在本教程中,我们将引导您完成使用 LangGraph 构建客户支持电子邮件代理的思维过程。

从您想要自动化的流程开始

想象一下,您需要构建一个处理客户支持电子邮件的 AI 代理。您的产品团队给您提出了以下要求: 该代理应:
  • 阅读收到的客户电子邮件
  • 按紧急程度和主题分类
  • 搜索相关文档以回答问题
  • 草拟适当的回复
  • 将复杂问题上报给人工代理
  • 在需要时安排跟进
要处理的示例场景
  1. 简单产品问题:“如何重置密码?”
  2. 错误报告:“当我选择 PDF 格式时,导出功能崩溃了”
  3. 紧急账单问题:“我的订阅被重复收费了!”
  4. 功能请求:“您能为移动应用程序添加深色模式吗?”
  5. 复杂技术问题:“我们的 API 集成间歇性地出现 504 错误”
要在 LangGraph 中实现代理,您通常会遵循相同的五个步骤。

步骤 1:将您的工作流程绘制为离散的步骤

首先确定流程中的不同步骤。每个步骤都将成为一个节点(一个执行特定功能的函数)。然后草绘这些步骤如何相互连接。 箭头显示了可能的路径,但实际选择哪条路径的决定发生在每个节点内部。 既然您已经确定了工作流程中的组件,现在让我们了解每个节点需要做什么:
  • 读取电子邮件:提取和解析电子邮件内容
  • 分类意图:使用 LLM 对紧急程度和主题进行分类,然后路由到适当的操作
  • 文档搜索:查询您的知识库以获取相关信息
  • 错误跟踪:在跟踪系统中创建或更新问题
  • 草拟回复:生成适当的回复
  • 人工审核:上报给人工代理进行批准或处理
  • 发送回复:发送电子邮件回复
请注意,有些节点会决定下一步去哪里(分类意图、草拟回复、人工审核),而另一些节点总是会进行到相同的下一步(读取电子邮件总是转到分类意图,文档搜索总是转到草拟回复)。

步骤 2:确定每个步骤需要做什么

对于图中的每个节点,确定它代表什么类型的操作以及它需要什么上下文才能正常工作。

LLM 步骤

当一个步骤需要理解、分析、生成文本或做出推理决策时
  • 静态上下文(提示):分类类别、紧急程度定义、响应格式
  • 动态上下文(来自状态):电子邮件内容、发件人信息
  • 期望结果:确定路由的结构化分类
  • 静态上下文(提示):语气指南、公司政策、回复模板
  • 动态上下文(来自状态):分类结果、搜索结果、客户历史记录
  • 期望结果:准备好供审核的专业电子邮件回复

数据步骤

当一个步骤需要从外部来源检索信息时
  • 参数:根据意图和主题构建的查询
  • 重试策略:是,对于瞬态故障采用指数退避
  • 缓存:可以缓存常见查询以减少 API 调用
  • 参数:来自状态的客户电子邮件或 ID
  • 重试策略:是,但如果不可用则回退到基本信息
  • 缓存:是,具有生存时间以平衡新鲜度和性能

操作步骤

当一个步骤需要执行外部操作时
  • 何时执行:经批准后(人工或自动)
  • 重试策略:是,对于网络问题采用指数退避
  • 不应缓存:每次发送都是一个独特的动作
  • 何时执行:当意图是“错误”时总是执行
  • 重试策略:是,不丢失错误报告至关重要
  • 返回:要包含在回复中的工单 ID

用户输入步骤

当一个步骤需要人工干预时
  • 决策上下文:原始电子邮件、回复草稿、紧急程度、分类
  • 预期输入格式:批准布尔值加上可选的编辑回复
  • 何时触发:高紧急程度、复杂问题或质量问题

步骤 3:设计您的状态

状态是您的代理中所有节点都可以访问的共享内存。将其视为您的代理用于跟踪它在整个过程中学习和决定的一切的笔记本。

什么属于状态?

就每条数据向自己提出这些问题

包含在状态中

它是否需要跨步骤持久化?如果是,则将其放入状态。

不存储

您可以从其他数据中推导出来吗?如果是,则在需要时计算它,而不是将其存储在状态中。
对于我们的电子邮件代理,我们需要跟踪
  • 原始电子邮件和发件人信息(无法重构这些)
  • 分类结果(多个下游节点需要)
  • 搜索结果和客户数据(重新获取成本高昂)
  • 回复草稿(需要通过审核才能持久化)
  • 执行元数据(用于调试和恢复)

保持状态原始,按需格式化提示

一个关键原则:您的状态应存储原始数据,而不是格式化的文本。在节点内部需要时格式化提示。
这种分离意味着
  • 不同的节点可以根据自己的需要以不同的方式格式化相同的数据
  • 您可以更改提示模板而无需修改状态模式
  • 调试更清晰——您可以看到每个节点收到了哪些数据
  • 您的代理可以在不破坏现有状态的情况下发展
让我们定义我们的状态
import * as z from "zod";

// Define the structure for email classification
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 = z.object({
  // Raw email data
  emailContent: z.string(),
  senderEmail: z.string(),
  emailId: z.string(),

  // Classification result
  classification: EmailClassificationSchema.optional(),

  // Raw search/API results
  searchResults: z.array(z.string()).optional(),  // List of raw document chunks
  customerHistory: z.record(z.any()).optional(),  // Raw customer data from CRM

  // Generated content
  responseText: z.string().optional(),
});

type EmailAgentStateType = z.infer<typeof EmailAgentState>;
type EmailClassificationType = z.infer<typeof EmailClassificationSchema>;
请注意,状态仅包含原始数据——没有提示模板、没有格式化字符串、没有指令。分类输出以单个字典的形式存储,直接来自 LLM。

步骤 4:构建您的节点

现在我们将每个步骤实现为一个函数。LangGraph 中的节点只是一个 JavaScript 函数,它接受当前状态并返回对其的更新。

适当地处理错误

不同的错误需要不同的处理策略
错误类型谁来修复策略何时使用
瞬态错误(网络问题、速率限制)系统(自动)重试策略通常在重试时解决的临时故障
LLM 可恢复错误(工具故障、解析问题)LLM将错误存储在状态中并循环返回LLM 可以看到错误并调整其方法
用户可修复错误(信息缺失、指令不明确)人工使用 interrupt() 暂停需要用户输入才能继续
意外错误开发者让它们冒泡需要调试的未知问题
  • 瞬态错误
  • LLM 可恢复
  • 用户可修复
  • 意外
添加重试策略以自动重试网络问题和速率限制
import type { RetryPolicy } from "@langchain/langgraph";

workflow.addNode(
"searchDocumentation",
searchDocumentation,
{
    retryPolicy: { maxAttempts: 3, initialInterval: 1.0 },
},
);

实现我们的电子邮件代理节点

我们将把每个节点实现为一个简单的函数。请记住:节点接受状态,执行工作,并返回更新。
import { StateGraph, START, END, Command } from "@langchain/langgraph";
import { HumanMessage } from "@langchain/core/messages";
import { ChatAnthropic } from "@langchain/anthropic";

const llm = new ChatAnthropic({ model: "claude-sonnet-4-5-20250929" });

async function readEmail(state: EmailAgentStateType) {
// Extract and parse email content
// In production, this would connect to your email service
console.log(`Processing email: ${state.emailContent}`);
return {};
}

async function classifyIntent(state: EmailAgentStateType) {
// Use LLM to classify email intent and urgency, then route accordingly

// Create structured LLM that returns EmailClassification object
const structuredLlm = llm.withStructuredOutput(EmailClassificationSchema);

// Format the prompt on-demand, not stored in state
const classificationPrompt = `
Analyze this customer email and classify it:

Email: ${state.emailContent}
From: ${state.senderEmail}

Provide classification including intent, urgency, topic, and summary.
`;

// Get structured response directly as object
const classification = await structuredLlm.invoke(classificationPrompt);

// Determine next node based on classification
let nextNode: "searchDocumentation" | "humanReview" | "draftResponse" | "bugTracking";

if (classification.intent === "billing" || classification.urgency === "critical") {
    nextNode = "humanReview";
} else if (classification.intent === "question" || classification.intent === "feature") {
    nextNode = "searchDocumentation";
} else if (classification.intent === "bug") {
    nextNode = "bugTracking";
} else {
    nextNode = "draftResponse";
}

// Store classification as a single object in state
return new Command({
    update: { classification },
    goto: nextNode,
});
}
async function searchDocumentation(state: EmailAgentStateType) {
// Search knowledge base for relevant information

// Build search query from classification
const classification = state.classification!;
const query = `${classification.intent} ${classification.topic}`;

let searchResults: string[];

try {
    // Implement your search logic here
    // Store raw search results, not formatted text
    searchResults = [
    "Reset password via Settings > Security > Change Password",
    "Password must be at least 12 characters",
    "Include uppercase, lowercase, numbers, and symbols",
    ];
} catch (error) {
    // For recoverable search errors, store error and continue
    searchResults = [`Search temporarily unavailable: ${error}`];
}

return new Command({
    update: { searchResults },  // Store raw results or error
    goto: "draftResponse",
});
}

async function bugTracking(state: EmailAgentStateType) {
// Create or update bug tracking ticket

// Create ticket in your bug tracking system
const ticketId = "BUG-12345";  // Would be created via API

return new Command({
    update: { searchResults: [`Bug ticket ${ticketId} created`] },
    goto: "draftResponse",
});
}
import { Command, interrupt } from "@langchain/langgraph";

async function draftResponse(state: EmailAgentStateType) {
// Generate response using context and route based on quality

const classification = state.classification!;

// Format context from raw state data on-demand
const contextSections: string[] = [];

if (state.searchResults) {
    // Format search results for the prompt
    const formattedDocs = state.searchResults.map(doc => `- ${doc}`).join("\n");
    contextSections.push(`Relevant documentation:\n${formattedDocs}`);
}

if (state.customerHistory) {
    // Format customer data for the prompt
    contextSections.push(`Customer tier: ${state.customerHistory.tier ?? "standard"}`);
}

// Build the prompt with formatted context
const draftPrompt = `
Draft a response to this customer email:
${state.emailContent}

Email intent: ${classification.intent}
Urgency level: ${classification.urgency}

${contextSections.join("\n\n")}

Guidelines:
- Be professional and helpful
- Address their specific concern
- Use the provided documentation when relevant
`;

const response = await llm.invoke([new HumanMessage(draftPrompt)]);

// Determine if human review needed based on urgency and intent
const needsReview = (
    classification.urgency === "high" ||
    classification.urgency === "critical" ||
    classification.intent === "complex"
);

// Route to appropriate next node
const nextNode = needsReview ? "humanReview" : "sendReply";

return new Command({
    update: { responseText: response.content.toString() },  // Store only the raw response
    goto: nextNode,
});
}

async function humanReview(state: EmailAgentStateType) {
// Pause for human review using interrupt and route based on decision
const classification = state.classification!;

// interrupt() must come first - any code before it will re-run on resume
const humanDecision = interrupt({
    emailId: state.emailId,
    originalEmail: state.emailContent,
    draftResponse: state.responseText,
    urgency: classification.urgency,
    intent: classification.intent,
    action: "Please review and approve/edit this response",
});

// Now process the human's decision
if (humanDecision.approved) {
    return new Command({
    update: { responseText: humanDecision.editedResponse || state.responseText },
    goto: "sendReply",
    });
} else {
    // Rejection means human will handle directly
    return new Command({ update: {}, goto: END });
}
}

async function sendReply(state: EmailAgentStateType): Promise<{}> {
// Send the email response
// Integrate with email service
console.log(`Sending reply: ${state.responseText!.substring(0, 100)}...`);
return {};
}

步骤 5:将其连接起来

现在我们将节点连接到工作图中。由于我们的节点处理自己的路由决策,我们只需要一些必要的边。 为了通过 interrupt() 实现人工干预,我们需要使用检查点进行编译,以便在运行之间保存状态:

图编译代码

import { MemorySaver, RetryPolicy } from "@langchain/langgraph";

// Create the graph
const workflow = new StateGraph(EmailAgentState)
  // Add nodes with appropriate error handling
  .addNode("readEmail", readEmail)
  .addNode("classifyIntent", classifyIntent)
  // Add retry policy for nodes that might have transient failures
  .addNode(
    "searchDocumentation",
    searchDocumentation,
    { retryPolicy: { maxAttempts: 3 } },
  )
  .addNode("bugTracking", bugTracking)
  .addNode("draftResponse", draftResponse)
  .addNode("humanReview", humanReview)
  .addNode("sendReply", sendReply)
  // Add only the essential edges
  .addEdge(START, "readEmail")
  .addEdge("readEmail", "classifyIntent")
  .addEdge("sendReply", END);

// Compile with checkpointer for persistence
const memory = new MemorySaver();
const app = workflow.compile({ checkpointer: memory });
图结构是最小的,因为路由发生在节点内部通过 Command 对象。每个节点声明它可以去哪里,使流程明确且可追溯。

试用您的代理

让我们用一个需要人工审核的紧急账单问题来运行我们的代理
// Test with an urgent billing issue
const initialState: EmailAgentStateType = {
  emailContent: "I was charged twice for my subscription! This is urgent!",
  senderEmail: "customer@example.com",
  emailId: "email_123"
};

// Run with a thread_id for persistence
const config = { configurable: { thread_id: "customer_123" } };
const result = await app.invoke(initialState, config);
// The graph will pause at human_review
console.log(`Draft ready for review: ${result.responseText?.substring(0, 100)}...`);

// When ready, provide human input to resume
import { Command } from "@langchain/langgraph";

const humanResponse = new Command({
  resume: {
    approved: true,
    editedResponse: "We sincerely apologize for the double charge. I've initiated an immediate refund...",
  }
});

// Resume execution
const finalResult = await app.invoke(humanResponse, config);
console.log("Email sent successfully!");
图在遇到 interrupt() 时暂停,将所有内容保存到检查点,然后等待。它可以在几天后恢复,从上次中断的地方精确地继续。thread_id 确保此对话的所有状态都一起保留。

总结和后续步骤

关键洞察

构建此电子邮件代理向我们展示了 LangGraph 的思维方式

高级考虑

本节探讨节点粒度设计的权衡。大多数应用程序可以跳过此部分并使用上面显示的模式。
您可能会问:为什么不将 Read EmailClassify Intent 合并到一个节点中?或者为什么将文档搜索与草稿回复分开?答案涉及韧性和可观察性之间的权衡。韧性考虑:LangGraph 的持久执行在节点边界创建检查点。当工作流程在中断或故障后恢复时,它会从执行停止的节点开始。较小的节点意味着更频繁的检查点,这意味着如果出现问题,重复的工作量更少。如果您将多个操作组合成一个大型节点,那么接近尾声的故障意味着从该节点的开头重新执行所有操作。我们为电子邮件代理选择这种分解的原因:
  • 外部服务隔离:文档搜索和错误跟踪是独立的节点,因为它们调用外部 API。如果搜索服务很慢或失败,我们希望将其与 LLM 调用隔离开来。我们可以将重试策略添加到这些特定节点,而不会影响其他节点。
  • 中间可见性:Classify Intent 作为自己的节点,可以让我们在采取行动之前检查 LLM 决定了什么。这对于调试和监控很有价值——您可以准确地看到代理何时以及为何路由到人工审核。
  • 不同的故障模式:LLM 调用、数据库查找和电子邮件发送具有不同的重试策略。单独的节点允许您独立配置这些。
  • 可重用性和测试:较小的节点更容易单独测试并在其他工作流程中重用。
另一种有效的方法:您可以将 Read EmailClassify Intent 合并到一个节点中。您将失去在分类之前检查原始电子邮件的能力,并且在该节点中发生任何故障时将重复这两个操作。对于大多数应用程序来说,分离节点的可观察性和调试优势是值得权衡的。应用程序级问题:步骤 2 中的缓存讨论(是否缓存搜索结果)是应用程序级决策,而不是 LangGraph 框架功能。您根据特定要求在节点函数中实现缓存——LangGraph 不会规定这一点。性能考虑:节点越多并不意味着执行速度越慢。LangGraph 默认在后台写入检查点(异步持久性模式),因此您的图会继续运行,而无需等待检查点完成。这意味着您可以获得频繁的检查点,而对性能的影响最小。如果需要,您可以调整此行为——使用 "exit" 模式仅在完成时创建检查点,或使用 "sync" 模式阻塞执行,直到每个检查点写入完成。

从这里开始

这是对使用 LangGraph 构建代理的思维方式的介绍。您可以使用以下内容扩展此基础
以编程方式连接这些文档到 Claude、VSCode 等,通过 MCP 获取实时答案。
© . This site is unofficial and not affiliated with LangChain, Inc.