本系列教程将带你深入理解 LangGraph 的思维方式,学会用 LangGraph 的方式构建智能体。

本章主题:Step 1 - 把业务流程拆解成离散的节点 阅读时间:约 20 分钟


开篇:从需求到流程

为什么要先拆解流程?

在开始写代码之前,最重要的一步是理解需求,拆解流程

常见错误:
需求来了 → 直接写代码 → 边写边改 → 越来越乱

正确做法:
需求来了 → 理解需求 → 拆解流程 → 画流程图 → 再写代码

为什么这样更好?

1. 思路清晰:
   - 先想清楚再动手
   - 避免边写边改

2. 便于沟通:
   - 流程图可以和产品、业务确认
   - 确保理解一致

3. 易于修改:
   - 发现问题在流程图上改
   - 不影响代码

4. 便于分工:
   - 流程拆解后可以并行开发
   - 每个人负责几个节点

案例背景

假设产品团队找到你,说:

"我们需要一个 AI 客服,能自动处理客户邮件。要求:

  1. 读取客户邮件
  2. 按紧急程度和主题分类
  3. 搜索相关文档回答问题
  4. 起草回复
  5. 复杂问题转人工
  6. 必要时安排后续跟进"

听起来挺复杂?别急,我们一步步拆解。

这个需求的难点

1. 多种情况:
   - 问题、Bug、账单、功能请求...
   - 每种情况处理方式不同

2. 条件分支:
   - 紧急的要人工审核
   - Bug 要创建工单
   - 问题要搜索文档

3. 可能循环:
   - 人工审核不通过要重新起草
   - 可能需要多轮沟通

这正是 LangGraph 擅长的场景!

一、理解需求:典型场景分析

1.1 为什么要分析典型场景?

在拆解流程之前,先理解需求。最好的方法是分析典型场景

为什么分析典型场景?

1. 理解业务:
   - 看看实际会发生什么
   - 理解每种情况的处理方式

2. 发现边界:
   - 找出所有可能的情况
   - 发现特殊情况和边界条件

3. 验证理解:
   - 和产品、业务确认
   - 确保理解正确

4. 指导设计:
   - 场景直接映射到流程
   - 每个场景对应一条路径

1.2 典型场景分析

让我们来看看这个案例的典型场景:

场景一:简单问题
客户邮件:"如何重置密码?"

期望流程:
1. 读取邮件
2. 分类:问题 / 低紧急度
3. 搜索文档:找到密码重置指南
4. 起草回复:包含重置步骤
5. 发送回复

特点:
- 流程简单
- 不需要人工审核
- 可以完全自动化
场景二:Bug 报告
客户邮件:"导出功能选择 PDF 时崩溃"

期望流程:
1. 读取邮件
2. 分类:Bug / 中紧急度
3. 创建 Bug 工单
4. 起草回复:告知已记录
5. 发送回复

特点:
- 需要创建工单
- 不需要搜索文档
- 可以完全自动化
场景三:紧急账单问题
客户邮件:"我被重复扣款了!"

期望流程:
1. 读取邮件
2. 分类:账单 / 高紧急度
3. 起草回复
4. 人工审核(紧急问题必须人工确认)
5. 发送回复

特点:
- 紧急问题
- 必须人工审核
- 不能完全自动化
场景四:功能请求
客户邮件:"能添加深色模式吗?"

期望流程:
1. 读取邮件
2. 分类:功能请求 / 低紧急度
3. 搜索文档:查看是否已有相关功能
4. 起草回复
5. 发送回复

特点:
- 需要搜索文档
- 不需要人工审核
- 可以完全自动化
场景五:复杂技术问题
客户邮件:"API 集成间歇性出现 504 错误"

期望流程:
1. 读取邮件
2. 分类:复杂 / 高紧急度
3. 起草回复(可能需要更多信息)
4. 人工审核(复杂问题需要人工介入)
5. 发送回复

特点:
- 复杂问题
- 需要人工审核
- 可能需要更多信息

1.3 从场景中提取规律

分析完场景,我们可以提取一些规律:

规律一:所有场景都有这些步骤
- 读取邮件
- 分类意图
- 起草回复
- 发送回复

规律二:根据分类有不同分支
- 问题、功能请求 → 搜索文档
- Bug → 创建工单
- 账单、复杂 → 直接起草

规律三:根据紧急程度决定是否人工审核
- 高紧急度 → 人工审核
- 低紧急度 → 直接发送

规律四:人工审核可能循环
- 审核不通过 → 重新起草 → 再次审核

二、拆解流程:从场景到节点

现在我们有了典型场景,可以开始拆解流程了。

2.1 识别关键步骤

从上面的场景,我们可以识别出这些关键步骤:

步骤 说明 出现频率 为什么需要
读取邮件 获取邮件内容 每次都有 需要邮件内容才能处理
分类意图 判断邮件类型和紧急度 每次都有 决定后续处理方式
搜索文档 查找相关知识 问题、功能请求 需要知识来回答
Bug 跟踪 创建 Bug 工单 Bug 报告 需要记录 Bug
起草回复 生成回复内容 每次都有 需要回复内容
人工审核 等待人工确认 紧急、复杂 确保质量
发送回复 发送邮件 每次都有 完成处理

如何识别关键步骤?

方法一:从场景中提取
- 列出每个场景的步骤
- 找出共同步骤
- 找出特殊步骤

方法二:从需求中提取
- 需求中的每个"动词"可能是一个步骤
- "读取"、"分类"、"搜索"、"起草"、"发送"

方法三:从问题中提取
- 问自己:要完成这个任务,需要做什么?
- 按顺序列出所有必要的步骤

2.2 画出流程图

有了关键步骤,我们可以画出流程图:

┌─────────────────────────────────────────────────────────────┐
│                     邮件处理流程                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ┌─────────────┐                                           │
│   │  读取邮件   │                                           │
│   │  Read Email │                                           │
│   └──────┬──────┘                                           │
│          │                                                  │
│          ▼                                                  │
│   ┌─────────────┐                                           │
│   │  分类意图   │ ──────┬──────┬──────┬──────┐             │
│   │  Classify   │       │      │      │      │             │
│   └─────────────┘       │      │      │      │             │
│                         │      │      │      │             │
│          ┌──────────────┘      │      │      │             │
│          │                     │      │      │             │
│          ▼                     ▼      │      │             │
│   ┌─────────────┐       ┌──────────┐ │      │             │
│   │  文档搜索   │       │ Bug 跟踪 │ │      │             │
│   │  Doc Search │       │Bug Track │ │      │             │
│   └──────┬──────┘       └────┬─────┘ │      │             │
│          │                   │       │      │             │
│          └─────────┬─────────┘       │      │             │
│                    │                 │      │             │
│                    ▼                 │      │             │
│             ┌─────────────┐          │      │             │
│             │  起草回复   │ ◄────────┘      │             │
│             │Draft Reply  │                 │             │
│             └──────┬──────┘                 │             │
│                    │                        │             │
│          ┌─────────┴─────────┐              │             │
│          │                   │              │             │
│          ▼                   ▼              │             │
│   ┌─────────────┐     ┌───────────┐        │             │
│   │  人工审核   │     │  发送回复 │ ◄──────┘             │
│   │Human Review │     │Send Reply │                      │
│   └──────┬──────┘     └───────────┘                      │
│          │                                                  │
│          ▼                                                  │
│   ┌───────────┐                                            │
│   │ 发送回复  │                                            │
│   │Send Reply │                                            │
│   └───────────┘                                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

2.3 理解流程图

让我详细解释这个流程图的每个部分:

第一步:读取邮件
作用:
- 入口点,所有流程从这里开始
- 获取邮件内容、发送者信息等

输入:
- email_id:邮件 ID

输出:
- email_content:邮件内容
- sender_email:发送者邮箱

为什么是第一步?
- 需要邮件内容才能进行后续处理
- 所有场景都需要这一步
第二步:分类意图
作用:
- 使用 LLM 分析邮件
- 输出:意图类型 + 紧急程度

输入:
- email_content:邮件内容

输出:
- classification:
  - intent:question / bug / billing / feature / complex
  - urgency:low / medium / high / critical
  - topic:主题

为什么是第二步?
- 需要邮件内容才能分类
- 分类结果决定后续处理方式
- **这是分支点**
第三步:根据分类分流

分类后有三条路:

分类结果 下一步 原因
问题、功能请求 文档搜索 需要查找相关知识来回答
Bug Bug 跟踪 需要创建工单记录
账单、复杂 直接起草回复 不需要搜索,直接处理

为什么这样分流?

问题、功能请求 → 文档搜索:
- 需要相关知识才能回答
- 例如:"如何重置密码?"需要密码重置指南

Bug → Bug 跟踪:
- 需要记录 Bug,分配给开发团队
- 例如:"导出 PDF 时崩溃"需要创建工单

账单、复杂 → 直接起草:
- 账单问题需要查询账单系统,不是文档
- 复杂问题可能需要更多信息,先起草再人工审核
第四步:搜索或跟踪
文档搜索:
输入:
  - classification.topic:搜索主题
输出:
  - search_results:搜索结果列表

Bug 跟踪:
输入:
  - email_content:Bug 描述
输出:
  - ticket_id:工单 ID

两者完成后都去"起草回复"。
第五步:起草回复
作用:
- 使用 LLM 生成回复
- 根据紧急程度决定是否需要人工审核

输入:
- email_content:邮件内容
- classification:分类结果
- search_results:搜索结果(如果有)
- ticket_id:工单 ID(如果有)

输出:
- response_text:回复内容

决策:
- 如果 urgency = high 或 critical → 人工审核
- 否则 → 直接发送
第六步:人工审核(可选)
作用:
- 紧急或复杂问题需要人工确认
- 审核通过后发送

输入:
- response_text:起草的回复

输出:
- approved:是否通过
- edited_response:修改后的回复(如果有修改)

可能的结果:
- 审核通过 → 发送回复
- 审核不通过 → 重新起草(循环)
第七步:发送回复
作用:
- 发送邮件
- 流程结束

输入:
- sender_email:收件人
- response_text:回复内容

输出:
- 无(流程结束)

三、节点设计原则

3.1 单一职责原则

每个节点应该只做一件事:

好的设计:
├── read_email:只读取邮件
├── classify_intent:只分类意图
├── search_documentation:只搜索文档
├── bug_tracking:只创建工单
├── draft_response:只起草回复
├── human_review:只等待人工审核
└── send_reply:只发送回复

不好的设计:
└── process_email:读取、分类、搜索、起草、发送全在一个函数

为什么单一职责很重要?

1. 可测试:
   - 每个节点可以单独测试
   - 测试用例简单
   - 问题容易定位

   示例:
   def test_classify_intent():
       state = {"email_content": "如何重置密码?"}
       result = classify_intent(state)
       assert result["classification"]["intent"] == "question"

2. 可复用:
   - 节点可以在其他流程中复用
   - 不需要重复代码

   示例:
   - classify_intent 可以用于邮件、聊天、工单等多种场景
   - search_documentation 可以用于问答、建议等多种场景

3. 可调试:
   - 出问题容易定位
   - 日志清晰

   示例:
   ERROR in classify_intent
   → 问题在分类节点
   → 检查分类逻辑

4. 可维护:
   - 修改一个节点不影响其他节点
   - 新功能容易添加

   示例:
   - 想改进分类逻辑?只改 classify_intent
   - 想增加新的搜索源?只改 search_documentation

3.2 节点粒度

节点应该多大?这是一个常见问题。

太细的例子
def read_email_header(state):
    """只读取邮件头部"""
    return {"header": get_header()}

def read_email_body(state):
    """只读取邮件正文"""
    return {"body": get_body()}

def read_email_attachments(state):
    """只读取邮件附件"""
    return {"attachments": get_attachments()}

问题

  • 过度拆分,增加复杂度
  • 每个步骤都太小,没有独立意义
  • 增加了节点数量,但没增加价值
太粗的例子
def process_email(state):
    """处理邮件:读取、分类、搜索、起草、发送"""
    email = read_email()
    category = classify(email)
    docs = search(category)
    draft = generate(email, docs)
    send(draft)
    return {}

问题

  • 失去 LangGraph 的所有优势
  • 无法单独测试每个步骤
  • 无法复用
  • 无法调试
合适的例子
def read_email(state):
    """读取完整邮件"""
    email = get_full_email()
    return {"email": email}

def classify_email(state):
    """分类邮件意图"""
    email = state["email"]
    category = classify(email)
    return {"category": category}

def search_docs(state):
    """搜索相关文档"""
    category = state["category"]
    docs = search(category)
    return {"docs": docs}

def draft_response(state):
    """起草回复"""
    email = state["email"]
    docs = state["docs"]
    draft = generate(email, docs)
    return {"draft": draft}

def send_email(state):
    """发送回复"""
    draft = state["draft"]
    send(draft)
    return {}

原则每个节点对应业务流程中的一个明确步骤

判断标准

  • 能否用一句话描述这个节点做什么?
  • 这个节点是否有独立的业务意义?
  • 这个节点是否可以单独测试?

3.3 输入输出清晰

每个节点应该有清晰的输入和输出:

def search_documentation(state: EmailState) -> dict:
    """
    搜索相关文档

    输入:
        - state["classification"]["topic"]: 搜索主题
        - state["classification"]["intent"]: 意图类型

    输出:
        - {"search_results": 搜索结果列表}

    说明:
        - 根据分类结果的主题搜索相关文档
        - 返回最多 5 条相关文档
    """
    topic = state["classification"]["topic"]
    intent = state["classification"]["intent"]

    # 根据意图调整搜索策略
    if intent == "feature":
        query = f"功能:{topic}"
    else:
        query = topic

    results = knowledge_base.search(query, top_k=5)
    return {"search_results": results}

为什么输入输出要清晰?

1. 文档作用:
   - 看注释就知道节点做什么
   - 不需要读代码

2. 类型检查:
   - 可以用类型检查器验证
   - 提前发现错误

3. 接口明确:
   - 知道需要什么数据
   - 知道会产生什么数据

4. 便于测试:
   - 知道如何构造测试输入
   - 知道如何验证输出

四、确定节点列表

4.1 节点列表

根据上面的分析,我们确定以下节点:

节点名称 功能 输入 输出 类型
read_email 读取邮件 email_id email_content, sender_email 数据节点
classify_intent 分类意图 email_content classification LLM 节点
search_documentation 搜索文档 classification search_results 数据节点
bug_tracking Bug 跟踪 email_content ticket_id 动作节点
draft_response 起草回复 email_content, classification, search_results response_text LLM 节点
human_review 人工审核 response_text approved, edited_response 用户节点
send_reply 发送回复 sender_email, response_text 动作节点

4.2 节点之间的跳转关系

当前节点 条件 下一个节点 说明
START - read_email 流程开始
read_email - classify_intent 读取后分类
classify_intent intent=question/feature search_documentation 需要搜索文档
classify_intent intent=bug bug_tracking 需要创建工单
classify_intent intent=billing/complex draft_response 直接起草
search_documentation - draft_response 搜索后起草
bug_tracking - draft_response 创建工单后起草
draft_response urgency=high/critical human_review 需要人工审核
draft_response urgency=low/medium send_reply 直接发送
human_review approved=True send_reply 审核通过
human_review approved=False draft_response 审核不通过,重新起草
send_reply - END 流程结束

4.3 跳转关系的可视化

START
  │
  ▼
read_email
  │
  ▼
classify_intent ─────┬──────────┬──────────┐
  │                  │          │          │
  │ (question/       │ (bug)    │(billing/ │
  │  feature)        │          │ complex) │
  ▼                  ▼          │          │
search_documentation  bug_tracking    │
  │                  │          │          │
  └────────┬─────────┘          │          │
           │                    │          │
           ▼                    │          │
      draft_reply ◄─────────────┘          │
           │                               │
     ┌─────┴─────┐                         │
     │           │                         │
(urgent)    (normal)                       │
     │           │                         │
     ▼           │                         │
human_review    │                         │
     │           │                         │
 ┌───┴───┐       │                         │
 │       │       │                         │
(pass)  (fail)  │                         │
 │       │       │                         │
 ▼       └───────┼─────────────────────────┘
send_reply ◄─────┘
  │
  ▼
 END

五、本章小结

5.1 核心要点

1. 理解需求

方法:
- 分析典型场景
- 提取规律
- 和产品、业务确认

目的:
- 确保理解正确
- 发现边界情况

2. 识别步骤

方法:
- 从场景中提取
- 从需求中提取
- 从问题中提取

结果:
- 关键步骤列表
- 每个步骤的说明

3. 画出流程图

目的:
- 可视化流程
- 理清跳转关系
- 便于沟通

工具:
- 纸和笔
- 流程图工具
- ASCII 图

4. 确定节点

原则:
- 单一职责:每个节点只做一件事
- 合适粒度:不要太细也不要太粗
- 输入输出清晰

结果:
- 节点列表
- 跳转关系表

5.2 拆解流程的检查清单

在完成流程拆解后,问自己:

完整性检查:
□ 是否覆盖了所有典型场景?
□ 是否有遗漏的步骤?
□ 是否有遗漏的分支?

合理性检查:
□ 每个节点是否只做一件事?
□ 节点粒度是否合适?
□ 输入输出是否清晰?

一致性检查:
□ 节点之间的跳转关系是否清晰?
□ 是否有死循环?
□ 是否有无法到达的节点?

可行性检查:
□ 每个节点是否可以实现?
□ 是否有技术难点?
□ 是否需要外部依赖?

5.3 常见错误

错误一:跳过流程拆解,直接写代码
→ 结果:边写边改,越来越乱

错误二:节点粒度太细
→ 结果:过度复杂,没有价值

错误三:节点粒度太粗
→ 结果:失去 LangGraph 的优势

错误四:忽略边界情况
→ 结果:运行时出错

错误五:没有和产品确认
→ 结果:理解偏差,返工

5.4 下一章预告

下一章,我们将深入讲解 Step 2:识别节点类型

我们会学习四种节点类型:

  • LLM 节点:调用大模型
  • 数据节点:查询数据
  • 动作节点:执行操作
  • 用户节点:等待人工输入

每种类型有不同的特点和实现方式,我们会在下一章详细讲解。


六、思考题

思考题一:增加节点

如果要增加一个"客户历史查询"节点,应该放在流程的哪个位置?

提示:
- 客户历史有什么用?
- 哪些场景需要客户历史?
- 放在哪个位置最合理?

思考题二:支持多轮对话

如果要支持"多轮对话"(客户可能回复),流程应该如何修改?

提示:
- 客户回复后应该怎么处理?
- 需要记住什么信息?
- 流程图应该如何修改?

思考题三:实际应用

想想你工作中的某个流程,试着:

  1. 分析典型场景
  2. 画出流程图
  3. 拆解成节点
  4. 确定跳转关系

系列导航

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐