文章目录
- [第 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 生动类比:做一道菜
想象你在做一道「宫保鸡丁」:
- 备菜阶段 :切鸡肉、切葱姜蒜、调酱汁、炸花生米------这些可以同时进行(并行)
- 炒菜阶段 :热锅 → 爆香 → 炒鸡肉 → 加酱汁 → 加花生------这些必须按顺序进行(串行)
- 装盘阶段 :出锅、摆盘、点缀------最后一步(收尾)
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 最佳实践总结
-
条件分支保持简洁:在 entrypoint 中,条件分支的每个路径应该清晰明确。如果分支逻辑变得复杂(超过 3 层嵌套),考虑拆分为独立的 entrypoint 或使用子图。
-
并行 Task 必须是真正独立的:只有互不依赖的 Task 才适合并行执行。如果 Task B 需要 Task A 的结果,必须串行执行。并行不是银弹,滥用反而增加复杂度。
-
并行结果要显式合并:每个并行 Task 返回的是独立的 state 副本。必须显式地将各 Task 的结果合并到同一个 state 中,不能假设它们会自动合并。
-
异步上下文用
.aresult():在async def函数中调用 Task 时,始终使用.aresult()而不是.result()。前者是真正的异步非阻塞,后者会阻塞事件循环。 -
先验证再执行:在进入复杂流程之前,先做基础验证(如信息完整性检查)。这避免了在无效输入上浪费计算资源,也提升了用户体验。