跳到主要内容

文档索引

在以下地址获取完整的文档索引:https://docs.langchain.org.cn/llms.txt

在进一步探索之前,请使用此文件发现所有可用页面。

当你使用 LangGraph 构建智能体时,首先需要将其拆解为称为**节点 (nodes)** 的离散步骤。然后,描述每个节点的不同决策和转换。最后,通过一个共享的**状态 (state)** 将节点连接起来,每个节点都可以从中读取或写入数据。 在本演练中,我们将引导你完成构建客户支持邮件智能体的思路过程。

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

想象一下,你需要构建一个处理客户支持邮件的 AI 智能体。你的产品团队给出了这些要求:
The agent should:

- Read incoming customer emails
- Classify them by urgency and topic
- Search relevant documentation to answer questions
- Draft appropriate responses
- Escalate complex issues to human agents
- Schedule follow-ups when needed

Example scenarios to handle:

1. Simple product question: "How do I reset my password?"
2. Bug report: "The export feature crashes when I select PDF format"
3. Urgent billing issue: "I was charged twice for my subscription!"
4. Feature request: "Can you add dark mode to the mobile app?"
5. Complex technical issue: "Our API integration fails intermittently with 504 errors"
在 LangGraph 中实现智能体时,通常会遵循相同的五个步骤。

第一步:将工作流映射为离散步骤

首先识别流程中的不同步骤。每个步骤将成为一个**节点**(一个执行特定任务的函数)。然后,勾勒出这些步骤如何相互连接。 此图中的箭头显示了可能的路径,但实际采取哪条路径的决策发生在每个节点内部。 既然我们已经确定了工作流中的组件,让我们了解每个节点需要执行的操作:
  • Read Email (读取邮件):提取并解析邮件内容
  • Classify Intent (分类意图):使用 LLM 对紧迫性和主题进行分类,然后路由到相应的操作
  • Doc Search (文档搜索):查询知识库以获取相关信息
  • Bug Track (Bug 追踪):在追踪系统中创建或更新问题
  • Draft Reply (草拟回复):生成适当的回复
  • Human Review (人工审核):升级给人工客服进行审批或处理
  • Send Reply (发送回复):发送邮件回复
请注意,某些节点会决定下一步去哪里(Classify IntentDraft ReplyHuman Review),而其他节点总是进行到同一个下一步(Read Email 总是转到 Classify IntentDoc Search 总是转到 Draft Reply)。

第二步:确定每个步骤需要执行的操作

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

LLM 步骤

当需要理解、分析、生成文本或做出推理决策时使用

数据步骤

当需要从外部源检索信息时使用

操作步骤

当需要执行外部操作时使用

用户输入步骤

当需要人工干预时使用

LLM 步骤

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

数据步骤

当一个步骤需要从外部源检索信息时
  • 参数:来自状态的客户邮件或 ID
  • 重试策略:有,但如果不可用则回退到基本信息
  • 缓存:有,设置生存时间以平衡新鲜度和性能

操作步骤

当一个步骤需要执行外部操作时
  • 何时执行节点:审批后(人工或自动)
  • 重试策略:有,针对网络问题使用指数退避
  • 不应缓存:每次发送都是一次唯一的动作
  • 何时执行节点:意图为“bug”时始终执行
  • 重试策略:有,确保不丢失 Bug 报告至关重要
  • 返回:包含在回复中的工单 ID

用户输入步骤

当一个步骤需要人工干预时
  • 决策上下文:原始邮件、回复草稿、紧迫性、分类
  • 预期输入格式:审批布尔值加上可选的修改后的回复
  • 何时触发:高紧迫性、复杂问题或质量考量

第三步:设计状态

状态是智能体中所有节点都可以访问的共享内存。将其视为智能体在处理流程时,用来记录其学到的所有内容和所做决定的笔记本。

哪些内容属于状态?

针对每条数据问自己以下问题

包含在状态中

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

不存储

你是否可以从其他数据中推导出来?如果是,请在需要时计算,而不是存储在状态中。
对于我们的邮件智能体,我们需要追踪
  • 原始邮件和发件人信息(稍后无法重建这些内容)
  • 分类结果(多个后续/下游节点需要)
  • 搜索结果和客户数据(重新获取成本很高)
  • 回复草稿(需要在审核过程中持久化)
  • 执行元数据(用于调试和恢复)

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

一个关键原则:你的状态应该存储原始数据,而不是格式化后的文本。在需要时在节点内部格式化提示词。
这种分离意味着
  • 不同的节点可以根据自己的需求对同一数据进行不同的格式化
  • 你可以在不修改状态模式的情况下更改提示词模板
  • 调试更清晰——你可以确切地看到每个节点接收到了什么数据
  • 你的智能体可以进化而不会破坏现有状态
让我们定义我们的状态
from typing import TypedDict, Literal

# Define the structure for email classification
class EmailClassification(TypedDict):
    intent: Literal["question", "bug", "billing", "feature", "complex"]
    urgency: Literal["low", "medium", "high", "critical"]
    topic: str
    summary: str

class EmailAgentState(TypedDict):
    # Raw email data
    email_content: str
    sender_email: str
    email_id: str

    # Classification result
    classification: EmailClassification | None

    # Raw search/API results
    search_results: list[str] | None  # List of raw document chunks
    customer_history: dict | None  # Raw customer data from CRM

    # Generated content
    draft_response: str | None
    messages: list[str] | None
请注意,状态仅包含原始数据——没有提示词模板、没有格式化字符串、没有指令。分类输出直接以字典形式存储,直接来自 LLM。

第四步:构建节点

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

适当地处理错误

不同的错误需要不同的处理策略
错误类型谁来修复策略何时使用
瞬态错误(网络问题、速率限制)系统(自动)重试策略通常在重试后可以解决的临时故障
LLM 可恢复错误(工具失败、解析问题)LLM将错误存储在状态中并循环返回LLM 可以看到错误并调整其方法
用户可修复错误(信息缺失、指令不明确)人工使用 interrupt() 暂停需要用户输入才能继续
重试后仍无法恢复的故障开发者(声明式)error_handler在重试耗尽后运行补偿/恢复分支
意外错误开发者让它们抛出 (bubble up)需要调试的未知问题
添加重试策略以自动重试网络问题和速率限制。结合 timeout= 来限制每次尝试的时间。请参阅容错以了解完整的生命周期。
from langgraph.types import RetryPolicy

workflow.add_node(
    "search_documentation",
    search_documentation,
    retry_policy=RetryPolicy(max_attempts=3, initial_interval=1.0)
)

实现我们的邮件智能体节点

我们将每个节点实现为一个简单的函数。记住:节点接收状态、执行任务并返回更新。
from typing import Literal
from langgraph.graph import StateGraph, START, END
from langgraph.types import interrupt, Command, RetryPolicy
from langchain_openai import ChatOpenAI
from langchain.messages import HumanMessage

llm = ChatOpenAI(model="gpt-5-nano")

def read_email(state: EmailAgentState) -> dict:
    """Extract and parse email content"""
    # In production, this would connect to your email service
    return {
        "messages": [HumanMessage(content=f"Processing email: {state['email_content']}")]
    }

def classify_intent(state: EmailAgentState) -> Command[Literal["search_documentation", "human_review", "draft_response", "bug_tracking"]]:
    """Use LLM to classify email intent and urgency, then route accordingly"""

    # Create structured LLM that returns EmailClassification dict
    structured_llm = llm.with_structured_output(EmailClassification)

    # Format the prompt on-demand, not stored in state
    classification_prompt = f"""
    Analyze this customer email and classify it:

    Email: {state['email_content']}
    From: {state['sender_email']}

    Provide classification including intent, urgency, topic, and summary.
    """

    # Get structured response directly as dict
    classification = structured_llm.invoke(classification_prompt)

    # Determine next node based on classification
    if classification['intent'] == 'billing' or classification['urgency'] == 'critical':
        goto = "human_review"
    elif classification['intent'] in ['question', 'feature']:
        goto = "search_documentation"
    elif classification['intent'] == 'bug':
        goto = "bug_tracking"
    else:
        goto = "draft_response"

    # Store classification as a single dict in state
    return Command(
        update={"classification": classification},
        goto=goto
    )
def search_documentation(state: EmailAgentState) -> Command[Literal["draft_response"]]:
    """Search knowledge base for relevant information"""

    # Build search query from classification
    classification = state.get('classification', {})
    query = f"{classification.get('intent', '')} {classification.get('topic', '')}"

    try:
        # Implement your search logic here
        # Store raw search results, not formatted text
        search_results = [
            "Reset password via Settings > Security > Change Password",
            "Password must be at least 12 characters",
            "Include uppercase, lowercase, numbers, and symbols"
        ]
    except SearchAPIError as e:
        # For recoverable search errors, store error and continue
        search_results = [f"Search temporarily unavailable: {str(e)}"]

    return Command(
        update={"search_results": search_results},  # Store raw results or error
        goto="draft_response"
    )

def bug_tracking(state: EmailAgentState) -> Command[Literal["draft_response"]]:
    """Create or update bug tracking ticket"""

    # Create ticket in your bug tracking system
    ticket_id = "BUG-12345"  # Would be created via API

    return Command(
        update={
            "search_results": [f"Bug ticket {ticket_id} created"],
            "current_step": "bug_tracked"
        },
        goto="draft_response"
    )
def draft_response(state: EmailAgentState) -> Command[Literal["human_review", "send_reply"]]:
    """Generate response using context and route based on quality"""

    classification = state.get('classification', {})

    # Format context from raw state data on-demand
    context_sections = []

    if state.get('search_results'):
        # Format search results for the prompt
        formatted_docs = "\n".join([f"- {doc}" for doc in state['search_results']])
        context_sections.append(f"Relevant documentation:\n{formatted_docs}")

    if state.get('customer_history'):
        # Format customer data for the prompt
        context_sections.append(f"Customer tier: {state['customer_history'].get('tier', 'standard')}")

    # Build the prompt with formatted context
    draft_prompt = f"""
    Draft a response to this customer email:
    {state['email_content']}

    Email intent: {classification.get('intent', 'unknown')}
    Urgency level: {classification.get('urgency', 'medium')}

    {chr(10).join(context_sections)}

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

    response = llm.invoke(draft_prompt)

    # Determine if human review needed based on urgency and intent
    needs_review = (
        classification.get('urgency') in ['high', 'critical'] or
        classification.get('intent') == 'complex'
    )

    # Route to appropriate next node
    goto = "human_review" if needs_review else "send_reply"

    return Command(
        update={"draft_response": response.content},  # Store only the raw response
        goto=goto
    )

def human_review(state: EmailAgentState) -> Command[Literal["send_reply", END]]:
    """Pause for human review using interrupt and route based on decision"""

    classification = state.get('classification', {})

    # interrupt() must come first - any code before it will re-run on resume
    human_decision = interrupt({
        "email_id": state.get('email_id',''),
        "original_email": state.get('email_content',''),
        "draft_response": state.get('draft_response',''),
        "urgency": classification.get('urgency'),
        "intent": classification.get('intent'),
        "action": "Please review and approve/edit this response"
    })

    # Now process the human's decision
    if human_decision.get("approved"):
        return Command(
            update={"draft_response": human_decision.get("edited_response", state.get('draft_response',''))},
            goto="send_reply"
        )
    else:
        # Rejection means human will handle directly
        return Command(update={}, goto=END)

def send_reply(state: EmailAgentState) -> dict:
    """Send the email response"""
    # Integrate with email service
    print(f"Sending reply: {state['draft_response'][:100]}...")
    return {}

第五步:串联流程

现在我们将节点连接到一个可运行的图中。由于我们的节点自行处理路由决策,我们只需要几个基本的边。 为了通过 interrupt() 启用人机回环,我们需要使用检查点保存器 (checkpointer) 进行编译,以在运行之间保存状态:

图编译代码

from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import RetryPolicy

# Create the graph
workflow = StateGraph(EmailAgentState)

# Add nodes with appropriate error handling
workflow.add_node("read_email", read_email)
workflow.add_node("classify_intent", classify_intent)

# Add retry policy for nodes that might have transient failures
workflow.add_node(
    "search_documentation",
    search_documentation,
    retry_policy=RetryPolicy(max_attempts=3)
)
workflow.add_node("bug_tracking", bug_tracking)
workflow.add_node("draft_response", draft_response)
workflow.add_node("human_review", human_review)
workflow.add_node("send_reply", send_reply)

# Add only the essential edges
workflow.add_edge(START, "read_email")
workflow.add_edge("read_email", "classify_intent")
workflow.add_edge("send_reply", END)

# Compile with checkpointer for persistence, in case run graph with Local_Server --> Please compile without checkpointer
memory = MemorySaver()
app = workflow.compile(checkpointer=memory)
图结构非常简洁,因为路由是通过节点内部的 Command 对象实现的。每个节点使用类似 Command[Literal["node1", "node2"]] 的类型提示声明它可以去向何处,使流程显式且可追溯。

尝试你的智能体

让我们运行我们的智能体来处理一个需要人工审核的紧急账单问题
# Test with an urgent billing issue
initial_state = {
    "email_content": "I was charged twice for my subscription! This is urgent!",
    "sender_email": "customer@example.com",
    "email_id": "email_123",
    "messages": []
}

# Run with a thread_id for persistence
config = {"configurable": {"thread_id": "customer_123"}}
result = app.invoke(initial_state, config)
# The graph will pause at human_review
print(f"human review interrupt:{result['__interrupt__']}")

# When ready, provide human input to resume
from langgraph.types import Command

human_response = Command(
    resume={
        "approved": True,
        "edited_response": "We sincerely apologize for the double charge. I've initiated an immediate refund..."
    }
)

# Resume execution
final_result = app.invoke(human_response, config)
print(f"Email sent successfully!")
当图运行到 interrupt() 时会暂停,将所有内容保存到检查点保存器并等待。它可以几天后恢复,从中断处精确继续。thread_id 确保此对话的所有状态都完整地保留在一起。

总结与后续步骤

关键见解

构建这个邮件智能体向我们展示了 LangGraph 的建模思路

拆解为离散步骤

每个节点只做一件好事。这种分解实现了流式进度更新、可以暂停和恢复的持久执行,以及清晰的调试,因为你可以检查步骤之间的状态。

状态是共享内存

存储原始数据,而非格式化文本。这允许不同节点以不同方式使用相同的信息。

节点即函数

它们接收状态、执行任务并返回更新。当它们需要做出路由决策时,会同时指定状态更新和下一个目的地。

错误是流程的一部分

瞬态故障进行重试,LLM 可恢复错误携带上下文循环返回,用户可修复问题暂停等待输入,意外错误则抛出以便调试。

用户输入是一等公民

interrupt() 函数可以无限期暂停执行,保存所有状态,并在你提供输入时从中断处精确恢复。当与节点中的其他操作结合使用时,它必须放在首位。

图结构自然呈现

你定义基本的连接,而节点处理自己的路由逻辑。这使控制流保持显式且可追溯——通过查看当前节点,你总能理解智能体下一步会做什么。

高级考量

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

后续方向

这是对使用 LangGraph 构建智能体建模思路的介绍。你可以在此基础上扩展:

人机回环模式

了解如何在执行前添加工具审批、批量审批及其他模式

子图

为复杂的多步操作创建子图

流式处理

添加流式输出,向用户展示实时进度

可观测性

使用 LangSmith 添加可观测性,用于调试和监控

工具集成

集成更多用于网页搜索、数据库查询和 API 调用的工具

重试逻辑

为失败的操作实现带有指数退避的重试逻辑

© . This site is unofficial and not affiliated with LangChain, Inc.