以编程方式使用文档

使用 LangGraph 构建智能体时,首先需要将其分解为离散的步骤,称为 **节点**。然后,描述每个节点的不同决策和转换。最后,通过共享的 **状态** 将节点连接在一起,每个节点都可以从中读取和写入。

在本指南中,我们将引导您完成使用 LangGraph 构建客户支持邮件智能体的思维过程。

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

假设您需要构建一个处理客户支持邮件的 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 中实现智能体,通常需要遵循相同的五个步骤。

步骤 1:将工作流程映射为离散步骤

首先识别流程中的不同步骤。每个步骤将成为一个 **节点** (执行特定单一功能的函数)。然后,勾勒这些步骤如何相互连接。

flowchart TD
    A[START] --> B[Read Email]
    B --> C[Classify Intent]

    C -.-> D[Doc Search]
    C -.-> E[Bug Track]
    C -.-> F[Human Review]

    D --> G[Draft Reply]
    E --> G
    F --> G

    G -.-> H[Human Review]
    G -.-> I[Send Reply]

    H --> J[END]
    I --> J[END]

    classDef process fill:#E5F4FF,stroke:#006DDD,stroke-width:2px,color:#030710
    class A,B,C,D,E,F,G,H,I,J process

此图中的箭头显示了可能的路径,但实际选择哪条路径的决策发生在每个节点内部。

现在我们已经确定了工作流程中的各个组件,接下来了解每个节点需要做什么:

  • - Read Email:提取并解析邮件内容
  • - Classify Intent:使用 LLM 对紧急程度和主题进行分类,然后路由到适当的操作
  • - Doc Search:在知识库中查询相关信息
  • - Bug Track:在跟踪系统中创建或更新问题
  • - Draft Reply:生成适当的回复
  • - Human Review:升级给人工代理进行审批或处理
  • - Send Reply:发送邮件回复

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

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

LLM steps

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

Data steps

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

Action steps

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

User input steps

当需要人工干预时使用

LLM 步骤

当某个步骤需要理解、分析、生成文本或进行推理决策时:

Classify intent

- 静态上下文(提示词):分类类别、紧急程度定义、回复格式 - 动态上下文(来自状态):邮件内容、发件人信息 - 期望结果:决定路由的结构化分类

Draft reply

- 静态上下文(提示词):语调指南、公司政策、回复模板 - 动态上下文(来自状态):分类结果、搜索结果、客户历史 - 期望结果:可供审阅的专业邮件回复

数据步骤

当某个步骤需要从外部来源检索信息时:

Document search

- 参数:从意图和主题构建的查询 - 重试策略:是,对于暂时性故障使用指数退避 - 缓存:可以缓存常见查询以减少 API 调用

Customer history lookup

- 参数:状态中的客户邮箱或 ID - 重试策略:是,但如不可用则回退到基本信息 - 缓存:是,使用生存时间在新鲜度和性能之间取得平衡

操作步骤

当某个步骤需要执行外部操作时:

Send reply

- 何时执行节点:批准后(人工或自动) - 重试策略:是,对于网络问题使用指数退避 - 不应缓存:每次发送都是唯一操作

Bug track

- 何时执行节点:当意图为"bug"时始终执行 - 重试策略:是,关键是不能丢失 bug 报告 - 返回:响应中包含工单 ID

用户输入步骤

当某个步骤需要人工干预时:

Human review node

- 决策上下文:原始邮件、回复草稿、紧急程度、分类 - 期望的输入格式:批准布尔值加上可选的编辑回复 - 触发时机:高紧急程度、复杂问题或质量问题

步骤 3:设计您的状态

状态是共享的 内存 可被代理中的所有节点访问。将其视为代理用来跟踪在处理过程中学习和决定的所有内容的笔记本。

什么应该放在状态中?

关于每条数据,问问自己以下问题:

Include in state

它需要在步骤之间持久化吗?如果是,则放入状态中。

Don't store

你能从其他数据中推导出来吗?如果是,需要时再计算而不是存储在状态中。

对于我们的邮件代理,我们需要跟踪:

  • - 原始邮件和发送者信息(之后无法重建)
  • - Classification results (needed by multiple later/downstream nodes)
  • - 搜索结果和客户数据(重新获取成本高)
  • - 回复草稿(需要通过审核持久化)
  • - 执行元数据(用于调试和恢复)

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

这种分离意味着:

  • - 不同的节点可以根据自身需求以不同方式格式化相同数据
  • - 你可以修改提示模板而不改变状态模式
  • - 调试更清晰——你可以准确看到每个节点接收到的数据
  • - 你的代理可以演进而不破坏现有状态

让我们定义我们的状态:

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 的单个字典存储。

步骤 4:构建你的节点

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

适当处理错误

不同的错误需要不同的处理策略:

错误类型由谁修复策略何时使用
临时性错误(网络问题、速率限制)系统(自动)重试策略重试后通常会解决的临时性故障
LLM 可恢复的错误(工具失败、解析问题)LLM将错误存储在状态中并循环回退LLM 可以看到错误并调整其方法
用户可修复的错误(信息缺失、指令不清晰)人工暂停并 interrupt()需要用户输入才能继续
重试后仍可恢复的失败开发者(声明式)error_handlerRun a compensation/recovery branch after retry exhaustion
意外错误开发者让它们向上冒泡需要调试的未知问题

Transient errors

添加重试策略以自动重试网络问题和速率限制。

结合使用 timeout= 来限制每次尝试。参见 容错 了解完整生命周期。

    from langgraph.types import RetryPolicy

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

LLM-recoverable

将错误存储在状态中并循环回退,这样 LLM 可以看到出了什么问题并重试:

    from langgraph.types import Command


    def execute_tool(state: State) -> Command[Literal["agent", "execute_tool"]]:
        try:
            result = run_tool(state['tool_call'])
            return Command(update={"tool_result": result}, goto="agent")
        except ToolError as e:
            # Let the LLM see what went wrong and try again
            return Command(
                update={"tool_result": f"Tool error: {str(e)}"},
                goto="agent"
            )
    

User-fixable

在需要时暂停并收集用户信息(如账户 ID、订单号或澄清说明):

    from langgraph.types import Command


    def lookup_customer_history(state: State) -> Command[Literal["draft_response"]]:
        if not state.get('customer_id'):
            user_input = interrupt({
                "message": "Customer ID needed",
                "request": "Please provide the customer's account ID to look up their subscription history"
            })
            return Command(
                update={"customer_id": user_input['customer_id']},
                goto="lookup_customer_history"
            )
        # Now proceed with the lookup
        customer_data = fetch_customer_history(state['customer_id'])
        return Command(update={"customer_history": customer_data}, goto="draft_response")
    

Unexpected

让它们向上冒泡以便调试。不要捕获你无法处理的内容:

    def send_reply(state: EmailAgentState):
        try:
            email_service.send(state["draft_response"])
        except Exception:
            raise  # Surface unexpected errors
    

Saga / compensation

重试耗尽后,运行一个恢复函数来更新状态并将流程路由到补偿分支。

参见 容错 了解完整模式。

    from langgraph.errors import NodeError
    from langgraph.types import Command, RetryPolicy

    def payment_error_handler(state: State, error: NodeError) -> Command:
        return Command(
            update={"status": f"compensated: {error.error}"},
            goto="finalize",
        )

    workflow.add_node(
        "charge_payment",
        charge_payment,
        retry_policy=RetryPolicy(max_attempts=3, retry_on=ConnectionError),
        error_handler=payment_error_handler,
    )
    

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

我们将把每个节点实现为一个简单的函数。记住:节点接收状态、执行工作并返回更新。

Read and classify nodes

    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
        )
    

Search and tracking nodes

    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"
        )
    

Response nodes

    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 {}
    

第 5 步:将它们连接在一起

现在我们将节点连接成一个可工作的图。由于我们的节点处理自己的路由决策,我们只需要几个必要的边。

要启用 human-in-the-loop 配合 interrupt()使用,我们需要使用 检查点器 编译以在运行之间保存状态:

Graph compilation code

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"]])声明它可以去哪里,使流程明确且可追踪。

尝试你的代理

让我们用一个需要人工审核的紧急计费问题来运行我们的代理:

Testing the agent

图在遇到 interrupt()时会暂停,将所有内容保存到检查点器,然后等待。它可以在几天后恢复,从中断的地方精确地继续。 thread_id 确保此对话的所有状态都保存在一起。

总结和后续步骤

关键洞察

构建这个邮件代理向我们展示了 LangGraph 的思维方式:

Break into discrete steps

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

State is shared memory

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

Nodes are functions

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

Errors are part of the flow

暂时性故障会重试,LLM 可恢复的错误会带着上下文返回,用户可修复的问题会暂停等待输入,而意外错误会向上冒泡以便调试。

Human input is first-class

interrupt() 函数会无限期暂停执行,保存所有状态,并在您提供输入时从中断处准确恢复。当与其他操作结合在一个节点中时,必须放在最前面。

Graph structure emerges naturally

您定义必要的连接,您的节点处理它们自己的路由逻辑。这保持控制流清晰和可追踪——您始终可以通过查看当前节点来理解代理的下一步操作。

高级注意事项

Node granularity trade-offs

您可能想知道:为什么不合并 Read EmailClassify Intent 到一个节点中?

或者为什么要将文档搜索与草稿回复分开?

答案涉及弹性和可观察性之间的权衡。

弹性考虑: LangGraph 的 持久化层 在节点边界创建检查点。当工作流在中断或失败后恢复时,它会从执行停止的节点开始重新启动。较小的节点意味着更频繁的检查点,这意味着如果出现问题,需要重复的工作更少。如果您将多个操作组合到一个大节点中,靠近末尾的故障意味着从该节点的开头重新执行所有内容。

为什么我们为邮件代理选择这种分解:

  • 外部服务隔离: 文档搜索和缺陷跟踪是独立的节点,因为它们调用外部 API。如果搜索服务缓慢或失败,我们希望将其与 LLM 调用隔离开来。我们可以向这些特定节点添加重试策略,而不影响其他节点。
  • 中间可见性:Classify Intent 作为一个单独的节点,让我们能够在采取行动之前检查 LLM 的决定。这对于调试和监控很有价值——您可以准确看到代理何时以及为何路由到人工审核。
  • 不同的失败模式: LLM 调用、数据库查询和电子邮件发送具有不同的重试策略。单独的节点让您能够独立配置这些。
  • 可重用性和测试: 较小的节点更容易独立测试,并在其他工作流中重用。

另一种有效的方法:您可以将 Read EmailClassify Intent 合并到单个节点中。您将失去在分类之前检查原始电子邮件的能力,并且在该节点中的任何故障时都会重复两个操作。对于大多数应用程序来说,单独节点的可见性和调试好处值得这个权衡。

应用层关注点:第2步中的缓存讨论(是否缓存搜索结果)是一个应用层决策,而非 LangGraph 框架的功能。你根据具体需求在节点函数中实现缓存——LangGraph 不规定这一点。

性能考虑:更多节点并不意味着更慢的执行。LangGraph 默认在后台写入检查点(异步持久化模式),因此你的图会继续运行,无需等待检查点完成。这意味着你可以在最小性能影响下获得频繁的检查点。如有需要,你可以调整此行为——使用 "exit" 模式仅在完成时创建检查点,或使用 "sync" 模式阻塞执行直到每个检查点写入完成。

下一步去哪里

这是关于使用 LangGraph 构建智能体的思维方式的介绍。你可以在此基础上进行扩展:

Human-in-the-loop patterns

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

Subgraphs

创建子图用于复杂的多步骤操作

Streaming

添加流式输出以向用户显示实时进度

Observability

使用 LangSmith 添加可观测性以进行调试和监控

Tool Integration

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

Retry Logic

使用指数退避实现失败操作的自动重试逻辑