一、为什么你的AI应用那么慢?先讲个早餐店的故事
1.1 同步模式:一个人排队,全店干等
假设你去一家早餐店买油条。店里有口锅,一次只能炸一根油条。你点了一根,老板开始炸,你就站在锅前盯着。两分钟后油条好了,你拿走。后面还有五个人排队,每个人都要等前一个人拿到油条才开始炸自己的。
这种方式下,第六个人要等12分钟。这就是同步串行——每个任务必须等前一个完全结束后才能开始,总时间等于所有任务时间相加。
1.2 异步模式:拿号排队,边等边干别的事
换一种方式:你点完油条,老板给你一个振动号牌,让你先去买豆浆。老板同时接了五个人的订单,锅里同时炸着五根油条。谁的那根好了,号牌震动,谁就来取。
这种方式下,五个人同时点餐,最慢的一根油条2分钟出锅,所有人都能在2分钟左右拿到。这就是异步并发——多个任务同时推进,总时间约等于最慢的那个任务,而不是所有任务之和。
1.3 你的AI应用正在犯同样的错误
绝大多数大语言模型应用,比如智能客服、文档问答、多轮对话机器人,90%的时间都花在等网络上——等OpenAI的API返回、等数据库查询结果、等搜索引擎响应。
如果你写的代码是这种风格:
result1 = call_openai("问题1") # 等2秒
result2 = call_openai("问题2") # 再等2秒
result3 = call_openai("问题3") # 再等2秒
# 总耗时6秒
那你的代码就相当于早餐店里一次只炸一根油条。用户点一次提问,要等6秒才有回复,早就划走了。
解决方案就是异步并发:
results = await asyncio.gather(
call_openai("问题1"),
call_openai("问题2"),
call_openai("问题3")
)
# 总耗时约2秒
本文用LangGraph 1.0完整演示如何将这种能力应用到结构化的AI工作流中。代码完整可运行,每个概念都用白话解释。
二、必须先搞懂三个关键词(不用懂底层,懂比喻就行)
2.1 async def —— “我可以中途去做别的事”
在Python里,普通函数 def 就像你坐在工位上专心写一份报告——写完之前,任何其他事情都不能打断你,电话响了也不接。
而 async def 定义的异步函数,就像你写报告时可以随时接电话、回消息、倒杯水。当遇到需要等的事情(比如等同事发资料给你),你会说“你先发,我继续写报告”,等资料到了你再接着处理。
2.2 await —— “停在这,有结果了叫我”
await 是一个暂停标志。它告诉Python:这里要执行一个慢操作(网络请求、读文件、查数据库),当前任务先让出CPU,去做其他任务。等这个慢操作完成了,再回到这里继续往下跑。
你可以把 await 理解为“店小二喊号”——你听到自己的号,才从座位上起来去取餐。在等号的时间里,你该刷手机刷手机,该聊天聊天。
2.3 asyncio.gather() —— “同时发起,统一收结果”
当你有一堆互不依赖的任务时,最直接的高效做法就是把它们全部放进 asyncio.gather()。
这个函数会做三件事:
-
同时启动所有任务
-
自动等待所有任务完成
-
按你传入的顺序返回结果列表
串行执行四个任务:耗时 1+2+1+3 = 7秒。用 gather 并发执行:耗时约等于最慢的那个任务(3秒)。省下来的4秒,就是用户愿意继续用你产品的理由。
三、完整代码(复制即跑,每行都有注释)
3.1 安装依赖
只需要安装 LangGraph 1.0 或更高版本:
pip install langgraph
3.2 完整代码文件
将以下代码保存为 async_demo.py,然后用 python async_demo.py 运行。
# -*- coding: utf-8 -*-
"""
LangGraph 异步并发完整示例
功能:模拟4个API请求,对比同步串行 vs 异步并发 vs LangGraph工作流
说明:不需要真实API key,用 sleep 模拟网络延迟
"""
import asyncio
import time
from typing import Annotated, List
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
# ========== 第一步:定义状态(所有节点共享的数据结构) ==========
# 状态就像工作流里的小黑板,每个节点都能写,LangGraph自动帮你合并
class IOState(TypedDict):
# results: 存放每个任务返回的结果字符串
# Annotated[list, 合并函数] 的意思是:多个节点同时写入时,自动把列表拼接起来
results: Annotated[List[str], lambda x, y: x + y]
# task_count: 记录总共有多少任务,仅用于展示
task_count: int
# ========== 第二步:模拟慢速API调用 ==========
# 这个函数假装是向大模型发请求。实际项目中,将 asyncio.sleep 替换为真实的 async HTTP 请求即可
async def slow_api_call(task_id: int, delay: float = 1.0) -> str:
"""
模拟一个耗时的网络请求
task_id: 任务编号
delay: 模拟的网络延迟(秒)
返回: 模拟的响应结果
"""
current_time = time.strftime("%H:%M:%S")
print(f"[{current_time}] 任务{task_id} 开始,预计耗时{delay}秒")
# 关键:await asyncio.sleep 模拟 IO 等待
# 在执行这行期间,Python 会去运行其他协程,而不是空等
await asyncio.sleep(delay)
current_time = time.strftime("%H:%M:%S")
result = f"任务{task_id}的结果(实际等待{delay}秒)"
print(f"[{current_time}] 任务{task_id} 完成")
return result
# ========== 第三步:定义四个独立的异步节点 ==========
# 每个节点对应工作流中的一个步骤。它们之间没有依赖,所以可以并发执行
async def node_1(state: IOState):
result = await slow_api_call(1, 1.0)
return {"results": [result]} # 返回字典,LangGraph 会自动合并到全局状态中
async def node_2(state: IOState):
result = await slow_api_call(2, 1.5)
return {"results": [result]}
async def node_3(state: IOState):
result = await slow_api_call(3, 0.8)
return {"results": [result]}
async def node_4(state: IOState):
result = await slow_api_call(4, 1.2)
return {"results": [result]}
# ========== 第四步:汇总节点(所有并发任务完成后执行) ==========
# 这个节点不需要异步,因为它只做 CPU 计算(打印、整理数据),不涉及 IO 等待
def summary_node(state: IOState) -> dict:
print("\n" + "="*50)
print("汇总报告:所有并发任务已完成")
print("="*50)
print(f"总任务数: {state['task_count']}")
print(f"收到结果数: {len(state['results'])}")
print("详细结果:")
for idx, res in enumerate(state['results'], 1):
print(f" {idx}. {res}")
return {"results": state["results"] + ["汇总节点执行完毕"]}
# ========== 第五部分:同步版本(用于性能对比) ==========
def run_sync_version():
"""同步串行执行版本,作为性能对比的基准"""
print("\n" + "="*40)
print("【同步执行版本】一个任务完成才做下一个")
print("="*40)
start_time = time.time()
delays = [1.0, 1.5, 0.8, 1.2]
results = []
for i, delay in enumerate(delays, 1):
current = time.strftime("%H:%M:%S")
print(f"[{current}] 同步任务{i} 开始...")
# time.sleep 是阻塞的:CPU 在这里什么都不做,就干等
time.sleep(delay)
results.append(f"同步任务{i}的结果")
current = time.strftime("%H:%M:%S")
print(f"[{current}] 同步任务{i} 完成")
elapsed = time.time() - start_time
print(f"\n同步执行总耗时: {elapsed:.2f} 秒")
print(f"理论串行总时间: {sum(delays)} 秒")
return results
# ========== 第六部分:纯异步版本(只用 asyncio.gather,不用 LangGraph) ==========
async def run_async_gather():
"""使用 asyncio.gather 并发执行,展示最基本的并发能力"""
print("\n" + "="*40)
print("【纯异步并发版本】使用 asyncio.gather 同时发起所有请求")
print("="*40)
start_time = time.time()
# 创建四个协程对象(此时尚未执行)
tasks = [
slow_api_call(1, 1.0),
slow_api_call(2, 1.5),
slow_api_call(3, 0.8),
slow_api_call(4, 1.2),
]
# asyncio.gather 同时启动所有任务,等待全部完成,按传入顺序返回结果
results = await asyncio.gather(*tasks)
elapsed = time.time() - start_time
print(f"\n异步并发总耗时: {elapsed:.2f} 秒")
print(f"理论最优耗时: {max([1.0,1.5,0.8,1.2])} 秒")
return results
# ========== 第七部分:构建 LangGraph 异步工作流 ==========
def build_langgraph_async():
"""
构建一个状态图,实现结构化的异步并发
图结构:
┌─→ node_1 ──┐
├─→ node_2 ──┤
START ──┼─→ node_3 ──┼─→ summary ──→ END
├─→ node_4 ──┤
关键点:
- 四个 node 都从 START 出发,因此它们会并发执行
- 四个 node 都连接到 summary,因此 summary 会等待所有 node 完成后才执行
"""
builder = StateGraph(IOState)
# 添加节点
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
builder.add_node("node_3", node_3)
builder.add_node("node_4", node_4)
builder.add_node("summary", summary_node)
# 定义边:所有 IO 节点从 START 开始(并发启动)
builder.add_edge(START, "node_1")
builder.add_edge(START, "node_2")
builder.add_edge(START, "node_3")
builder.add_edge(START, "node_4")
# 所有 IO 节点完成后,汇聚到 summary 节点
builder.add_edge("node_1", "summary")
builder.add_edge("node_2", "summary")
builder.add_edge("node_3", "summary")
builder.add_edge("node_4", "summary")
# summary 之后结束
builder.add_edge("summary", END)
# 编译成可执行的图
return builder.compile()
async def run_langgraph_version():
"""运行 LangGraph 异步工作流"""
print("\n" + "="*40)
print("【LangGraph 异步工作流】结构化并发 + 状态管理")
print("="*40)
start_time = time.time()
# 构建图
graph = build_langgraph_async()
# 关键:必须使用 ainvoke 而不是 invoke,因为节点是异步的
final_state = await graph.ainvoke({
"results": [], # 初始结果列表为空
"task_count": 4, # 共有4个并发任务
})
elapsed = time.time() - start_time
print(f"\nLangGraph 异步工作流总耗时: {elapsed:.2f} 秒")
return final_state
# ========== 第八部分:主函数,依次运行三个版本进行对比 ==========
async def main():
print("\n" + "="*60)
print("LangGraph 异步编程完整演示 - 性能对比")
print("="*60)
# 1. 同步版本
run_sync_version()
# 2. 纯异步 gather 版本
await run_async_gather()
# 3. LangGraph 异步工作流版本
await run_langgraph_version()
# 打印最终对比结论
print("\n" + "="*60)
print("性能对比总结")
print("="*60)
print("""
执行方式 总耗时(约) 说明
------------------------------------------------
同步串行 4.5 秒 任务依次执行,总时间 = 各任务时间之和
异步 gather 1.5 秒 同时启动,总时间 ≈ 最慢的任务时间
LangGraph异步 1.5 秒 同样快,但额外提供状态管理和工作流编排能力
结论:
1. 对于 IO 密集型任务(网络请求、文件读写、数据库查询),异步并发能带来数倍性能提升。
2. asyncio.gather 适用于简单的一次性批量并发。
3. LangGraph 适用于需要状态共享、条件分支、错误重试的复杂工作流。
""")
if __name__ == "__main__":
# asyncio.run 是启动异步事件循环的标准入口
asyncio.run(main())
四、运行结果解读
运行上述代码后,你会看到清晰的对比:
-
同步版本
:四个任务一个接一个执行,总耗时约4.5秒。每个任务的开始和结束时间明显错开。
-
纯异步版本
:四个任务几乎同时打印“开始”,然后各自在对应的延迟后打印“完成”。总耗时约1.5秒(取决于最慢的那个1.5秒任务)。
-
LangGraph版本
:执行时间与纯异步版本相同,但多了一个汇总节点,自动合并了所有结果并打印报告。
通过对比可以直观感受到:异步并发比同步串行快3倍左右。
五、几个常见疑问解答
5.1 我什么时候该用异步?
只要你的代码存在“等待”外部操作的情况,就应该考虑异步。典型场景包括:
-
调用大模型API(OpenAI、Claude、通义千问等)
-
查询远程数据库(MySQL、PostgreSQL、MongoDB)
-
爬取网页或调用第三方HTTP接口
-
读写大文件(尤其是网络文件系统)
这些操作的特点都是:CPU基本不干活,主要是网络或磁盘在忙。异步就是利用这段空闲时间去做别的事情。
5.2 什么时候不能用异步?
如果你的任务是CPU密集型的,例如:
-
大量数值计算、矩阵运算
-
视频编解码、图像处理
-
复杂的加密或压缩算法
那么异步帮不上什么忙,因为Python的异步本质上是单线程的,CPU本身就忙不过来时,异步切换反而增加开销。这种场景应该使用多进程(ProcessPoolExecutor)。
5.3 直接用 asyncio.gather 不就行了,为什么还要 LangGraph?
asyncio.gather 适合“一次性并发一堆独立任务”这种简单场景。但真实的工作流往往更复杂:
-
任务B需要等待任务A的结果才能开始,同时任务C和D可以并行
-
需要处理失败重试、超时控制
-
需要动态决定下一步执行哪个节点(比如根据用户意图分支)
-
需要持久化状态、支持断点续跑
LangGraph 正是为这种场景设计的。它用状态图的方式让你声明节点之间的依赖关系,底层自动处理并发调度、状态合并、条件路由等复杂逻辑。
5.4 代码里的 Annotated 是什么意思?
results: Annotated[List[str], lambda x, y: x + y]
这是 LangGraph 的状态归约器(Reducer)。因为多个并发节点可能同时向 results 字段写入数据,如果不指定合并规则,后写入的可能会覆盖前面的。lambda x, y: x + y 告诉 LangGraph:把多个节点返回的列表拼接成一个更大的列表。这样最终 results 就会包含所有节点的输出。
六、总结
本文用一个早餐店的类比讲清楚了同步与异步的核心区别,并用完整可运行的代码演示了三种执行方式的性能差异:
-
同步串行
:任务排队执行,总时间为各任务耗时之和。代码简单但性能差。
-
纯异步 concurrent
:使用 asyncio.gather 同时启动多个独立任务,总时间约等于最慢任务。性能大幅提升,适合简单批量并发。
-
LangGraph 异步工作流
:在保持同等性能的基础上,增加了结构化状态管理、节点依赖编排、自动结果合并等能力,适合生产级复杂工作流。
对于所有LLM应用开发者来说,掌握异步编程不再是一个“高级技巧”,而是提升产品响应速度和用户体验的必备技能。建议先从本文的完整示例开始运行和修改,逐步理解异步状态图的构建逻辑,再应用到自己的实际项目中。