【langgraph 从入门到精通】多步骤编排与条件分支 — 使用 Functional API 控制流程

文章目录

  • [第 4 章:多步骤编排与条件分支 --- 使用 Functional API 控制流程](#第 4 章:多步骤编排与条件分支 — 使用 Functional API 控制流程)
    • [4.1 本章目标](#4.1 本章目标)
    • [4.2 核心概念(图文讲解)](#4.2 核心概念(图文讲解))
      • [4.2.1 生动类比:做一道菜](#4.2.1 生动类比:做一道菜)
      • [4.2.2 多步骤流程图](#4.2.2 多步骤流程图)
      • [4.2.3 关键术语解释](#4.2.3 关键术语解释)
    • [4.3 实战:旅行规划助手 v0.3](#4.3 实战:旅行规划助手 v0.3)
      • [4.3.1 场景描述](#4.3.1 场景描述)
      • [4.3.2 完整代码](#4.3.2 完整代码)
      • [4.3.3 运行结果展示](#4.3.3 运行结果展示)
      • [4.3.4 代码逐段解析](#4.3.4 代码逐段解析)
    • [4.4 API 速查](#4.4 API 速查)
    • [4.5 常见错误与避坑指南](#4.5 常见错误与避坑指南)
      • [错误 1:在并行 Task 之间共享可变状态](#错误 1:在并行 Task 之间共享可变状态)
      • [错误 2:条件分支后忘记 return](#错误 2:条件分支后忘记 return)
      • [错误 3:混淆 `.result()` 和 `.aresult()`](#错误 3:混淆 .result().aresult())
      • [错误 4:并行 Task 之间存在隐式依赖](#错误 4:并行 Task 之间存在隐式依赖)
    • [4.6 最佳实践总结](#4.6 最佳实践总结)

第 4 章:多步骤编排与条件分支 --- 使用 Functional API 控制流程

4.1 本章目标

学完本章后,你将能够:

  • 使用多个 Task 编排复杂的多步骤工作流
  • 在 entrypoint 中使用 Python 条件判断(if/else)实现条件分支
  • 理解并实现 Task 的并行执行
  • 使用 asyncio.gather() 同时执行多个独立 Task
  • 构建旅行规划助手 v0.3:多步骤推理 + 条件分支 + 并行查询

4.2 核心概念(图文讲解)

4.2.1 生动类比:做一道菜

想象你在做一道「宫保鸡丁」:

  1. 备菜阶段 :切鸡肉、切葱姜蒜、调酱汁、炸花生米------这些可以同时进行(并行)
  2. 炒菜阶段 :热锅 → 爆香 → 炒鸡肉 → 加酱汁 → 加花生------这些必须按顺序进行(串行)
  3. 装盘阶段 :出锅、摆盘、点缀------最后一步(收尾)

LangGraph 的 Graph 构建就是编排这样的「做菜流程」:

  • 串行步骤:Task A → Task B → Task C,一个接一个执行
  • 并行步骤:Task A、Task B、Task C 同时执行,谁先完成谁先返回
  • 条件分支:根据口味选择「加辣」还是「不加辣」,走不同的执行路径

4.2.2 多步骤流程图

#mermaid-svg-Bte6xIsfNjllzPPW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Bte6xIsfNjllzPPW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Bte6xIsfNjllzPPW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Bte6xIsfNjllzPPW .error-icon{fill:#552222;}#mermaid-svg-Bte6xIsfNjllzPPW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Bte6xIsfNjllzPPW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Bte6xIsfNjllzPPW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Bte6xIsfNjllzPPW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Bte6xIsfNjllzPPW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Bte6xIsfNjllzPPW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Bte6xIsfNjllzPPW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Bte6xIsfNjllzPPW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Bte6xIsfNjllzPPW .marker.cross{stroke:#333333;}#mermaid-svg-Bte6xIsfNjllzPPW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Bte6xIsfNjllzPPW p{margin:0;}#mermaid-svg-Bte6xIsfNjllzPPW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Bte6xIsfNjllzPPW .cluster-label text{fill:#333;}#mermaid-svg-Bte6xIsfNjllzPPW .cluster-label span{color:#333;}#mermaid-svg-Bte6xIsfNjllzPPW .cluster-label span p{background-color:transparent;}#mermaid-svg-Bte6xIsfNjllzPPW .label text,#mermaid-svg-Bte6xIsfNjllzPPW span{fill:#333;color:#333;}#mermaid-svg-Bte6xIsfNjllzPPW .node rect,#mermaid-svg-Bte6xIsfNjllzPPW .node circle,#mermaid-svg-Bte6xIsfNjllzPPW .node ellipse,#mermaid-svg-Bte6xIsfNjllzPPW .node polygon,#mermaid-svg-Bte6xIsfNjllzPPW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Bte6xIsfNjllzPPW .rough-node .label text,#mermaid-svg-Bte6xIsfNjllzPPW .node .label text,#mermaid-svg-Bte6xIsfNjllzPPW .image-shape .label,#mermaid-svg-Bte6xIsfNjllzPPW .icon-shape .label{text-anchor:middle;}#mermaid-svg-Bte6xIsfNjllzPPW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Bte6xIsfNjllzPPW .rough-node .label,#mermaid-svg-Bte6xIsfNjllzPPW .node .label,#mermaid-svg-Bte6xIsfNjllzPPW .image-shape .label,#mermaid-svg-Bte6xIsfNjllzPPW .icon-shape .label{text-align:center;}#mermaid-svg-Bte6xIsfNjllzPPW .node.clickable{cursor:pointer;}#mermaid-svg-Bte6xIsfNjllzPPW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Bte6xIsfNjllzPPW .arrowheadPath{fill:#333333;}#mermaid-svg-Bte6xIsfNjllzPPW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Bte6xIsfNjllzPPW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Bte6xIsfNjllzPPW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Bte6xIsfNjllzPPW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Bte6xIsfNjllzPPW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Bte6xIsfNjllzPPW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Bte6xIsfNjllzPPW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Bte6xIsfNjllzPPW .cluster text{fill:#333;}#mermaid-svg-Bte6xIsfNjllzPPW .cluster span{color:#333;}#mermaid-svg-Bte6xIsfNjllzPPW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Bte6xIsfNjllzPPW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Bte6xIsfNjllzPPW rect.text{fill:none;stroke-width:0;}#mermaid-svg-Bte6xIsfNjllzPPW .icon-shape,#mermaid-svg-Bte6xIsfNjllzPPW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Bte6xIsfNjllzPPW .icon-shape p,#mermaid-svg-Bte6xIsfNjllzPPW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Bte6xIsfNjllzPPW .icon-shape .label rect,#mermaid-svg-Bte6xIsfNjllzPPW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Bte6xIsfNjllzPPW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Bte6xIsfNjllzPPW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Bte6xIsfNjllzPPW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 信息不完整
信息完整
用户输入
Task 1: analyze_requirements

分析需求
条件判断

信息是否完整?
Task 2: clarify_questions

生成追问
返回追问给用户
并行执行区域
Task 3a: search_attractions

搜索景点
Task 3b: search_weather

查询天气
Task 3c: estimate_budget

估算预算
Task 4: generate_itinerary

综合生成行程
Task 5: format_output

格式化输出
返回最终结果

4.2.3 关键术语解释

术语 含义 在做菜中的类比
串行执行 Task 依次执行,后一个依赖前一个的结果 爆香后才能炒鸡肉
并行执行 多个 Task 同时执行,互不依赖 同时切葱姜蒜和调酱汁
条件分支 根据中间结果决定走哪条执行路径 口味决定加不加辣
编排 在 entrypoint 中组织 Task 的执行顺序和逻辑 厨师长安排做菜流程
asyncio.gather() 异步并行执行多个 Task 多个帮厨同时干活

4.3 实战:旅行规划助手 v0.3

4.3.1 场景描述

v0.3 在 v0.2 的基础上增加了两个重要能力:(1) 条件分支 ------如果用户输入的信息不完整(缺少目的地、天数等),先生成追问让用户补充;(2) 并行查询------当信息完整后,同时查询景点、天气和预算,而不是一个接一个等。

4.3.2 完整代码

python 复制代码
# ============================================================
# 旅行规划助手 v0.3 --- 第 4 章完整可运行代码
# 保存为 ch04_graph_building.py 后运行:python ch04_graph_building.py
# 运行前请确保已设置:export OPENAI_API_KEY="your-key"
# 依赖:langgraph>=1.2.9, langchain-openai>=0.3.0, pydantic>=2.0
# ============================================================

import os  # 导入操作系统模块,读取环境变量
import asyncio  # 导入异步 IO 模块,用于并行执行多个 Task

from pydantic import BaseModel, Field  # 导入 Pydantic 基类和字段工具
from typing import Optional  # 导入 Optional 类型注解

from langgraph.func import entrypoint, task  # 导入 LangGraph Functional API 核心
from langchain_openai import ChatOpenAI  # 导入 OpenAI 聊天模型


# ============================================================
# 定义 State --- 比 v0.2 增加了更多字段
# ============================================================
class TravelPlanState(BaseModel):
    """旅行规划状态 v0.3:支持条件分支和并行查询"""

    # 用户输入
    raw_input: str = Field(default="", description="用户的原始输入")

    # 结构化信息
    destination: str = Field(default="", description="目的地")
    days: int = Field(default=0, description="旅行天数")
    budget: str = Field(default="", description="预算")
    preferences: str = Field(default="", description="偏好")

    # 信息完整性标志:用于条件分支判断
    is_complete: bool = Field(
        default=False,  # 默认信息不完整
        description="输入信息是否完整(destination 和 days 都有值)",
    )

    # 追问内容:当信息不完整时生成
    follow_up_questions: str = Field(default="", description="追问用户的问题")

    # 并行查询结果
    attractions_info: str = Field(default="", description="景点推荐信息")
    weather_info: str = Field(default="", description="天气信息")
    budget_estimate: str = Field(default="", description="预算估算")

    # 行程和最终输出
    itinerary: str = Field(default="", description="生成的行程")
    final_output: str = Field(default="", description="最终格式化输出")


# ============================================================
# 初始化 LLM(全局复用)
# ============================================================
llm = ChatOpenAI(
    model="gpt-4o-mini",  # 使用 gpt-4o-mini 模型
    temperature=0.5,  # 适中的温度,平衡准确性和创意
)


# ============================================================
# Task 1:分析需求并提取结构化信息
# ============================================================
@task  # 标记为 LangGraph 任务
def analyze_requirements(state: TravelPlanState) -> TravelPlanState:
    """分析用户需求:从原始输入中提取结构化信息,并判断信息是否完整。"""
    # 构建提示词:要求 LLM 提取信息并以 JSON 格式返回
    prompt = f"""请分析以下用户输入,提取旅行相关信息:

用户输入:{state.raw_input}

请以 JSON 格式返回:
{{"destination": "目的地", "days": 天数, "budget": "预算", "preferences": "偏好"}}

如果某个字段无法从输入中提取,请将其值设为 null。只返回 JSON。"""

    response = llm.invoke(prompt)  # 调用 LLM

    # 解析 JSON(带容错处理)
    import json  # 用于解析 JSON
    data = {}  # 默认空字典,确保后续 .get() 调用安全
    try:
        content = response.content  # 获取 LLM 返回的文本
        # 处理 LLM 可能用 Markdown 代码块包裹 JSON 的情况
        if "```json" in content:
            content = content.split("```json")[1].split("```")[0].strip()
        elif "```" in content:
            content = content.split("```")[1].split("```")[0].strip()
        data = json.loads(content)  # 解析 JSON 字符串为 Python 字典
    except (json.JSONDecodeError, IndexError):
        data = {}  # 解析失败时使用空字典

    # 提取各字段值,使用 .get() 提供默认值
    destination = data.get("destination") or ""  # 如果为 None 则用空字符串
    days = data.get("days") or 0  # 如果为 None 则用 0
    budget = data.get("budget") or "中等"  # 如果为 None 则用默认值
    preferences = data.get("preferences") or "综合体验"  # 如果为 None 则用默认值

    # 判断信息是否完整:目的地和天数都必须有有效值
    is_complete = bool(destination and days and days > 0)

    # 创建更新后的 state
    updated_state = state.model_copy(
        update={
            "destination": destination,  # 更新目的地
            "days": days,  # 更新天数
            "budget": budget,  # 更新预算
            "preferences": preferences,  # 更新偏好
            "is_complete": is_complete,  # 设置完整性标志
        }
    )
    return updated_state  # 返回更新后的 state


# ============================================================
# Task 2:生成追问(仅当信息不完整时执行)
# ============================================================
@task  # 标记为 LangGraph 任务
def generate_clarify_questions(state: TravelPlanState) -> TravelPlanState:
    """当用户输入信息不完整时,生成追问让用户补充必要信息。"""
    # 构建提示词:根据已有的信息生成针对性的追问
    prompt = f"""用户想规划旅行,但目前提供的信息不完整。

已知信息:
- 目的地:{state.destination or "未知"}
- 天数:{state.days if state.days > 0 else "未知"}
- 预算:{state.budget}
- 偏好:{state.preferences}

请生成 2-3 个友好的追问问题,帮助用户补充缺失的信息。问题要简洁自然。"""

    response = llm.invoke(prompt)  # 调用 LLM 生成追问

    # 更新 state:将追问内容存入 follow_up_questions 字段
    updated_state = state.model_copy(
        update={"follow_up_questions": response.content}  # 存入 LLM 生成的追问
    )
    return updated_state  # 返回更新后的 state


# ============================================================
# Task 3a:搜索景点(模拟并行查询之一)
# ============================================================
@task  # 标记为 LangGraph 任务
def search_attractions(state: TravelPlanState) -> TravelPlanState:
    """搜索目的地的热门景点(模拟数据,实际可接入真实 API)。"""
    # 构建提示词:根据目的地和偏好搜索景点
    prompt = f"""请为以下目的地推荐 5 个热门景点:

目的地:{state.destination}
偏好:{state.preferences}

请为每个景点提供名称和简短描述(为什么推荐)。"""

    response = llm.invoke(prompt)  # 调用 LLM 生成景点推荐

    # 更新 state:将景点信息存入 attractions_info 字段
    updated_state = state.model_copy(
        update={"attractions_info": response.content}  # 存入景点推荐
    )
    return updated_state  # 返回更新后的 state


# ============================================================
# Task 3b:查询天气(模拟并行查询之二)
# ============================================================
@task  # 标记为 LangGraph 任务
def search_weather(state: TravelPlanState) -> TravelPlanState:
    """查询目的地的天气信息(模拟数据,实际可接入天气 API)。"""
    # 构建提示词:让 LLM 模拟天气查询
    prompt = f"""请模拟查询 {state.destination} 未来 {state.days} 天的天气情况。
请提供每天的大致温度、天气状况和穿衣建议。"""

    response = llm.invoke(prompt)  # 调用 LLM 生成天气信息

    # 更新 state:将天气信息存入 weather_info 字段
    updated_state = state.model_copy(
        update={"weather_info": response.content}  # 存入天气信息
    )
    return updated_state  # 返回更新后的 state


# ============================================================
# Task 3c:估算预算(模拟并行查询之三)
# ============================================================
@task  # 标记为 LangGraph 任务
def estimate_budget_info(state: TravelPlanState) -> TravelPlanState:
    """估算旅行预算(模拟数据,实际可接入价格 API)。"""
    # 构建提示词:让 LLM 估算旅行费用
    prompt = f"""请为以下旅行估算预算明细:

目的地:{state.destination}
天数:{state.days} 天
预算水平:{state.budget}

请提供以下项目的估算费用(人民币):
- 机票往返
- 住宿(每晚)
- 餐饮(每日)
- 景点门票(每日)
- 交通(每日)
- 总计"""

    response = llm.invoke(prompt)  # 调用 LLM 生成预算估算

    # 更新 state:将预算估算存入 budget_estimate 字段
    updated_state = state.model_copy(
        update={"budget_estimate": response.content}  # 存入预算估算
    )
    return updated_state  # 返回更新后的 state


# ============================================================
# Task 4:综合生成行程
# ============================================================
@task  # 标记为 LangGraph 任务
def generate_itinerary(state: TravelPlanState) -> TravelPlanState:
    """综合所有信息(景点、天气、预算)生成完整的旅行行程。"""
    # 构建包含所有上下文信息的提示词
    prompt = f"""你是一个专业旅行规划师。请根据以下完整信息生成详细的旅行行程:

## 基本信息
- 目的地:{state.destination}
- 天数:{state.days} 天
- 预算:{state.budget}
- 偏好:{state.preferences}

## 景点推荐
{state.attractions_info}

## 天气信息
{state.weather_info}

## 预算参考
{state.budget_estimate}

请按天组织行程,确保行程合理、可行,并考虑天气因素。"""

    response = llm.invoke(prompt)  # 调用 LLM 生成综合行程

    # 更新 state:将行程存入 itinerary 字段
    updated_state = state.model_copy(
        update={"itinerary": response.content}  # 存入综合行程
    )
    return updated_state  # 返回更新后的 state


# ============================================================
# Task 5:格式化输出
# ============================================================
@task  # 标记为 LangGraph 任务
def format_output(state: TravelPlanState) -> TravelPlanState:
    """将所有信息格式化为美观的最终输出。"""
    # 构建 Markdown 格式的最终报告
    final = f"""# 🌍 旅行规划报告

## 基本信息
- **目的地**:{state.destination}
- **旅行天数**:{state.days} 天
- **预算**:{state.budget}
- **偏好**:{state.preferences}

## 天气概况
{state.weather_info}

## 预算估算
{state.budget_estimate}

## 详细行程
{state.itinerary}

---
*由 LangGraph 旅行规划助手 v0.3 生成 · 支持多步骤编排和并行查询*
"""

    # 更新 state:将格式化文本存入 final_output 字段
    updated_state = state.model_copy(
        update={"final_output": final}  # 存入最终输出
    )
    return updated_state  # 返回更新后的 state


# ============================================================
# Entrypoint:编排整个工作流(含条件分支和并行执行)
# ============================================================
@entrypoint()  # 标记为工作流入口
def travel_planner_v3(raw_input: str) -> TravelPlanState:
    """旅行规划助手 v0.3 入口函数。
    编排流程:分析需求 → 条件分支(追问 or 并行查询) → 生成行程 → 格式化输出。
    """

    # 第一步:创建初始 State
    state = TravelPlanState(raw_input=raw_input)

    # 第二步:分析需求,提取结构化信息
    state = analyze_requirements(state).result()

    # 第三步:条件分支 --- 根据信息完整性决定后续路径
    if not state.is_complete:  # 如果信息不完整
        # 走追问分支:生成追问让用户补充信息
        state = generate_clarify_questions(state).result()
        # 直接返回:包含追问内容
        return state

    # 第四步:并行执行 --- 三个独立查询同时进行
    # 使用 asyncio 的 gather 实现并行,三个 Task 互不依赖,可以同时执行
    async def run_parallel():
        """异步并行执行三个独立查询。"""
        # asyncio.gather 同时启动三个协程,等待全部完成
        results = await asyncio.gather(
            search_attractions(state).aresult(),  # 异步执行景点搜索
            search_weather(state).aresult(),  # 异步执行天气查询
            estimate_budget_info(state).aresult(),  # 异步执行预算估算
        )
        return results  # 返回三个结果组成的元组

    # 在当前事件循环中运行并行任务
    # 注意:此处的 state 是并行查询前的快照,三个 Task 各自返回更新后的 state
    results = asyncio.run(run_parallel())

    # 合并并行查询结果:将三个 Task 返回的 state 中的对应字段合并
    # 每个 Task 返回的 state 只更新了各自的字段,合并成一个完整的 state
    state = state.model_copy(
        update={
            "attractions_info": results[0].attractions_info,  # 合并景点信息
            "weather_info": results[1].weather_info,  # 合并天气信息
            "budget_estimate": results[2].budget_estimate,  # 合并预算估算
        }
    )

    # 第五步:综合所有信息生成行程
    state = generate_itinerary(state).result()

    # 第六步:格式化最终输出
    state = format_output(state).result()

    # 返回最终 state
    return state


# ============================================================
# 程序入口
# ============================================================
if __name__ == "__main__":
    # 检查 API Key
    if not os.environ.get("OPENAI_API_KEY"):
        print("错误:未设置 OPENAI_API_KEY 环境变量!")
        print("请先运行:export OPENAI_API_KEY='your-api-key-here'")
        exit(1)

    print("=" * 60)
    print("测试 1:信息完整的输入")
    print("=" * 60)

    # 测试 1:信息完整的输入
    complete_input = "我想去日本京都玩3天,预算中等,喜欢古迹和寺庙。"
    final_state = travel_planner_v3.invoke(complete_input)

    if final_state.is_complete:
        print(final_state.final_output)  # 打印完整报告
    else:
        print("追问:", final_state.follow_up_questions)  # 打印追问内容

    print("\n" + "=" * 60)
    print("测试 2:信息不完整的输入")
    print("=" * 60)

    # 测试 2:信息不完整的输入(缺少目的地和天数)
    incomplete_input = "我想出去玩,预算不要太高,喜欢自然风光。"
    final_state2 = travel_planner_v3.invoke(incomplete_input)

    if final_state2.is_complete:
        print(final_state2.final_output)
    else:
        print("追问:", final_state2.follow_up_questions)  # 打印追问内容

4.3.3 运行结果展示

测试 1(信息完整)

复制代码
============================================================
测试 1:信息完整的输入
============================================================
# 🌍 旅行规划报告

## 基本信息
- **目的地**:京都
- **旅行天数**:3 天
- **预算**:中等
- **偏好**:古迹、寺庙

## 天气概况
第1天:晴,15-22°C,适宜户外活动,建议穿薄外套
第2天:多云,14-20°C,可能有小雨,建议带伞
第3天:晴,16-23°C,温暖舒适,建议穿长袖T恤

## 预算估算
- 机票往返:约 3000-5000 元
- 住宿(每晚):约 400-800 元
- 餐饮(每日):约 200-400 元
- 景点门票(每日):约 100-200 元
- 交通(每日):约 50-100 元
- 总计:约 5000-8000 元

## 详细行程
...
============================================================

测试 2(信息不完整)

复制代码
============================================================
测试 2:信息不完整的输入
============================================================
追问:
1. 请问您想去哪个目的地呢?国内还是国外?
2. 您计划旅行几天呢?这样我可以更好地安排行程。
3. 您对住宿有什么偏好吗?比如民宿、经济型酒店还是度假村?

4.3.4 代码逐段解析

第一部分:条件分支

python 复制代码
if not state.is_complete:
    state = generate_clarify_questions(state).result()
    return state  # 直接返回追问,不继续后续流程

这是 Functional API 的核心优势------你可以使用标准 Python if/else 来控制 Agent 的执行路径。当信息不完整时,工作流在「追问」这一步终止,不再执行后续的并行查询和行程生成。

第二部分:并行执行

python 复制代码
async def run_parallel():
    results = await asyncio.gather(
        search_attractions(state).aresult(),
        search_weather(state).aresult(),
        estimate_budget_info(state).aresult(),
    )
    return results

results = asyncio.run(run_parallel())

三个 Task 互不依赖,使用 asyncio.gather() 同时启动它们。aresult() 是 Task 的异步版本(等价于 .result() 的 async 版)。三个查询并行执行可以将总耗时从「A+B+C」缩短到「max(A, B, C)」。

第三部分:结果合并

python 复制代码
state = state.model_copy(
    update={
        "attractions_info": results[0].attractions_info,
        "weather_info": results[1].weather_info,
        "budget_estimate": results[2].budget_estimate,
    }
)

并行执行后,每个 Task 返回各自的 state 副本。需要手动将三个结果合并到同一个 state 中。注意这里只更新了新增的字段,之前的状态(如 destination、days 等)保持不变。

4.4 API 速查

API 类型 说明 签名/导入路径
asyncio.gather() 函数 并行执行多个异步任务 import asyncio
.aresult() 方法 异步等待 Task 完成 result = await my_task(state).aresult()
asyncio.run() 函数 在当前线程中运行异步函数 result = asyncio.run(async_fn())
model_copy(update={...}) 方法 创建 State 副本并更新指定字段 new = state.model_copy(update={"f": v})
if/else 语句 Python 原生条件分支 在 entrypoint 中直接使用

4.5 常见错误与避坑指南

错误 1:在并行 Task 之间共享可变状态

错误代码

python 复制代码
# 错误:三个并行 Task 都修改同一个 state 对象
state = TravelPlanState(raw_input="...")
# 下面三个 Task 同时修改 state,可能导致数据不一致
results = await asyncio.gather(
    search_attractions(state).aresult(),  # 修改 state.attractions_info
    search_weather(state).aresult(),  # 修改 state.weather_info
    estimate_budget_info(state).aresult(),  # 修改 state.budget_estimate
)
# 期望:state 包含所有三个结果
# 实际:每个 Task 返回的是独立副本,results 才是正确结果

正确代码

python 复制代码
# 正确:并行执行后,显式合并三个 Task 返回的结果
state = TravelPlanState(raw_input="...")
results = await asyncio.gather(
    search_attractions(state).aresult(),  # 返回 state 副本 1
    search_weather(state).aresult(),  # 返回 state 副本 2
    estimate_budget_info(state).aresult(),  # 返回 state 副本 3
)
# 显式合并:从每个 Task 的返回值中提取对应字段
state = state.model_copy(
    update={
        "attractions_info": results[0].attractions_info,
        "weather_info": results[1].weather_info,
        "budget_estimate": results[2].budget_estimate,
    }
)

错误 2:条件分支后忘记 return

错误代码

python 复制代码
@entrypoint()
def my_workflow(raw_input: str) -> TravelPlanState:
    state = analyze_requirements(raw_input).result()
    if not state.is_complete:
        state = generate_clarify_questions(state).result()
        # 错误:忘记 return,程序会继续执行后续代码
    # 即使信息不完整,也会继续执行并行查询和行程生成
    state = generate_itinerary(state).result()
    return state

正确代码

python 复制代码
@entrypoint()
def my_workflow(raw_input: str) -> TravelPlanState:
    state = analyze_requirements(raw_input).result()
    if not state.is_complete:
        state = generate_clarify_questions(state).result()
        return state  # 正确:提前返回,不执行后续代码
    # 只有信息完整时才会执行到这里
    state = generate_itinerary(state).result()
    return state

错误 3:混淆 .result().aresult()

错误代码

python 复制代码
# 错误:在 async 函数中使用 .result()(同步方法)
async def run_parallel():
    results = await asyncio.gather(
        search_attractions(state).result(),  # 错误:.result() 是同步方法
        search_weather(state).result(),
    )

正确代码

python 复制代码
# 正确:在 async 上下文中使用 .aresult()(异步方法)
async def run_parallel():
    results = await asyncio.gather(
        search_attractions(state).aresult(),  # 正确:异步等待
        search_weather(state).aresult(),
    )

原因.result() 是同步阻塞方法,在 async 上下文中使用会阻塞事件循环,导致并行失效。必须使用 .aresult() 才能实现真正的异步并行。

错误 4:并行 Task 之间存在隐式依赖

错误代码

python 复制代码
@entrypoint()
def my_workflow(raw_input: str) -> TravelPlanState:
    state = TravelPlanState(raw_input=raw_input)
    # 错误:generate_itinerary 依赖 search_attractions 的结果
    # 但 search_attractions 和 generate_itinerary 被并行执行
    results = await asyncio.gather(
        search_attractions(state).aresult(),
        generate_itinerary(state).aresult(),  # 此时 attractions_info 还是空的!
    )

正确代码

python 复制代码
@entrypoint()
def my_workflow(raw_input: str) -> TravelPlanState:
    state = TravelPlanState(raw_input=raw_input)
    # 正确:先并行执行三个独立的查询
    results = await asyncio.gather(
        search_attractions(state).aresult(),
        search_weather(state).aresult(),
        estimate_budget_info(state).aresult(),
    )
    # 合并结果
    state = merge_results(state, results)
    # 然后串行执行依赖前序结果的 Task
    state = generate_itinerary(state).result()  # 此时 attractions_info 已填充

原因:并行执行的 Task 必须是真正独立的------它们之间不能有数据依赖。如果一个 Task 需要另一个 Task 的输出,它们必须串行执行。

4.6 最佳实践总结

  1. 条件分支保持简洁:在 entrypoint 中,条件分支的每个路径应该清晰明确。如果分支逻辑变得复杂(超过 3 层嵌套),考虑拆分为独立的 entrypoint 或使用子图。

  2. 并行 Task 必须是真正独立的:只有互不依赖的 Task 才适合并行执行。如果 Task B 需要 Task A 的结果,必须串行执行。并行不是银弹,滥用反而增加复杂度。

  3. 并行结果要显式合并:每个并行 Task 返回的是独立的 state 副本。必须显式地将各 Task 的结果合并到同一个 state 中,不能假设它们会自动合并。

  4. 异步上下文用 .aresult() :在 async def 函数中调用 Task 时,始终使用 .aresult() 而不是 .result()。前者是真正的异步非阻塞,后者会阻塞事件循环。

  5. 先验证再执行:在进入复杂流程之前,先做基础验证(如信息完整性检查)。这避免了在无效输入上浪费计算资源,也提升了用户体验。


相关推荐
可涵不会debug1 天前
从向量嵌入到复杂 Agent:LLM、LangChain、LangGraph 完整科普
langchain·嵌入模型·langgraph
大鹅同志6 天前
Agent Runtime Evaluation Platform — AI Agent 运行时全维度质量评估平台
ai·langchain·agent·rag·langgraph·vibecoding·llm-as-judge
新知图书7 天前
多模态智能体开发的核心挑战与解决方案
人工智能·agent·多模态·ai agent·智能体·langgraph
Esaka_Forever9 天前
LangGraph Edge(边)完整讲解
langgraph
Esaka_Forever9 天前
LangGraph add_conditional_edges 完整详解
langgraph
新知图书10 天前
RAG之生成技术
人工智能·agent·ai agent·智能体·langgraph
新知图书10 天前
智能体基础架构
人工智能·agent·ai agent·智能体·langgraph
伊布拉西莫1 个月前
LangGraph State / Config / Input / Output Schema 深度解析
langgraph
茉莉玫瑰花茶1 个月前
综合案例 - AI 智能租房助手 [ 4 ]
数据库·python·ai·langgraph