异步与并发入门
昨天(Day 07)你会调 API 了,但每个请求都要干等服务器回话。今天解决"等"的问题——用 async/await 让多个请求几乎同时进行,把"排队等 10 次"变成"一起等 1 次"。明天(Day 09)你会把自己的功能做成一个 API 服务,那正是异步大显身手的地方。
痛点:串着等,一个 Agent 慢到想砸键盘
这类"大部分时间在等外部(网络、磁盘、数据库)"的任务,叫 I/O 密集型。Agent 干的活几乎全是这种——调大模型、查向量库、访问工具 API,无一不是"发出去、等回来"。所以异步对 Agent不是锦上添花,是刚需。
同步 vs 异步:一张时间线看懂
同一个服务员招呼 3 桌,两种干法的时间线对比:
| 同步(sync) | 异步(async) | |
|---|---|---|
| 做事方式 | 一件做完再做下一件 | 发起后不干等,去做别的,回头收结果 |
| 适合 | 纯计算(CPU 忙个不停) | 大量"等外部"的 I/O 任务 |
| 3 个 1 秒请求 | ≈ 3 秒 | ≈ 1 秒 |
async / await:两个关键字就够用
async def 定义一个"可以中途让出的函数"(叫协程),await 表示"这里要等一下,等的时候把控制权让出去,让别人先干活"。await 就是服务员说"这桌的菜我先挂着等,趁这空儿我去招呼别桌"的那个动作:
import asyncio # Python 自带的异步库
async def fetch_weather(city): # async def = 这是一个协程(可让出的函数)
print(f"开始查 {city} 天气…")
await asyncio.sleep(1) # await = 等 1 秒(模拟网络),等的时候让出控制权
print(f"{city} 天气拿到了")
return f"{city}: 晴 28℃" # 和普通函数一样能 return
await 只能写在 async def 里面;② 调用一个 async 函数时前面必须加 await(或交给下节的工具去跑),否则它根本不会执行,只得到一个"协程对象"——这是新手最常见的坑。👶 小白:为什么调 fetch_weather("北京") 没加 await,程序不报错也不干活?
👨🏫 老师:因为 async 函数被调用时只是造出一份"待办任务单"(协程对象),并不立刻开工。await 才是"把这张单子交出去、真正执行并等它完成"的动作。你可以把 async def 想成"写好了菜谱",await 才是"下令开火做这道菜"。菜谱写了不做,当然桌上没菜。
用 asyncio.run 把异步"启动"起来
await 只能在 async def 里用,但程序最外层的入口是普通代码——那第一个 async 函数由谁来"启动"?asyncio.run(...)。它就是"开门营业"的开关:启动一个后台调度器(事件循环),把你的顶层协程跑到结束,再关门。整个程序通常只在最外层调用它一次。import asyncio
async def main(): # 通常把顶层逻辑放进一个叫 main 的协程
result = await fetch_weather("北京") # 这里能用 await,因为在 async def 里
print("结果:", result)
asyncio.run(main()) # ← 唯一的“启动”入口,从普通代码进入异步世界
# 输出:
# 开始查 北京 天气…
# (等约1秒)
# 北京 天气拿到了
# 结果: 北京: 晴 28℃
async def main(): ... 写逻辑,最后一行 asyncio.run(main()) 点火。几乎所有异步 Python 脚本都长这样。asyncio.gather:让它们真正"一起等"
await 三次——await a; await b; await c——那不还是一个等完再等下一个,又变回 3 秒了吗?await 依然是串行。要真正并发,用 asyncio.gather(...):把多个协程一次性全部发起,然后统一等它们一起完成。这才是服务员"同时给三桌下单"。import asyncio, time
async def fetch_weather(city):
await asyncio.sleep(1) # 每个都“等”1 秒
return f"{city}: 晴"
async def main():
t = time.time()
# gather 把 3 个协程同时发起,一起等 → 总共约 1 秒而不是 3 秒
results = await asyncio.gather(
fetch_weather("北京"),
fetch_weather("上海"),
fetch_weather("广州"),
)
print(results) # ['北京: 晴', '上海: 晴', '广州: 晴'](顺序和传入一致)
print(f"耗时 {time.time()-t:.1f} 秒") # 耗时 1.0 秒 ← 三个等待重叠了!
asyncio.run(main())
await 三次:1 + 1 + 1 = 3.0 秒。asyncio.gather 三个一起:max(1, 1, 1) = 1.0 秒。请求越多差距越夸张:100 个各 1 秒的请求,串行要 100 秒,gather 仍约 1 秒。这就是异步的威力。
gather 返回的结果列表顺序和你传入的顺序一致,不会因为谁先完成就乱序,取值很安心。为什么 Agent 离不开异步
把今天的知识对到 Agent 的真实场景,你会发现异步无处不在:
- 并行取证:一个"研究 Agent"要同时查 5 个资料源,
gather一起查,5 秒变 1 秒; - 多智能体协作:主管同时派 3 个专家 Agent 干活,它们并发跑,最后汇总(后面阶段 8 会讲);
- 流式输出:大模型一个字一个字往外吐,你要一边收一边处理,天生就是异步的活(明天/Day 12 会用到);
- 高并发服务:你的 Agent 做成 API 后要同时接待很多用户——异步能用一个进程扛住大量"都在等模型回话"的请求。
async 的——今天这一课是它们的入场券。三个新手大坑(提前躲开)
| 坑 | 现象 | 正确姿势 |
|---|---|---|
① 忘了 await | 函数"没执行",得到 <coroutine object> 警告 | 调用 async 函数前一定加 await |
| ② 在异步里用了"阻塞"调用 | 用 time.sleep() 或昨天的 requests,整个程序还是卡死 | 睡用 asyncio.sleep();HTTP 用异步库 httpx/aiohttp |
③ 连续 await 当并发 | 以为快了,其实还是串行 | 要并发就用 asyncio.gather() |
第②坑最隐蔽也最重要:异步世界里混进一个"阻塞"调用,会把整条流水线全堵死——就像服务员本该去招呼别桌,却站在后厨前发呆。所以昨天的同步 requests 在异步代码里要换成它的异步兄弟 httpx:
import httpx # pip install httpx(requests 的异步版)
async def get_json(url):
async with httpx.AsyncClient() as client: # async with = 异步版的 with
resp = await client.get(url, timeout=10) # await 发请求,等的时候让出控制权
return resp.json()
# 之后就能 await asyncio.gather(get_json(u1), get_json(u2), ...) 真正并发拉取
👶 小白:那我昨天刚学的 requests 白学了?
👨🏫 老师:完全没有!写小脚本、一次只调一个接口时,同步 requests 又简单又够用,仍是最常用的库。只有当你要同时发很多请求、或身处 async 框架(FastAPI/LangGraph)里时,才换 httpx。两者 API 几乎一模一样,你昨天的功夫直接迁移过来,只是多加了 async/await。
今日小结 + 动手 10 分钟
🧠 今天你应该能回答
- 异步到底省了什么时间?(省"干等外部 I/O"的时间,把多段等待重叠)
- 异步和多线程一样吗?(不一样,异步还是单线程,只是不干等;专治 I/O,不加速纯计算)
async/await各是什么?(定义可让出的协程 / 标记"这里等一下并让出控制权")- 怎么启动?怎么真正并发?(
asyncio.run(main())启动;asyncio.gather(...)并发) - 为什么 Agent 离不开它?(Agent 全是"发出去等回来"的 I/O,谁不干等谁就快)
✋ 动手:亲手把 3 秒压成 1 秒
# 无需任何第三方库,asyncio 是 Python 自带的
python async_demo.py
# 存成 async_demo.py
import asyncio, time
async def task(name, seconds):
print(f"{name} 开始…")
await asyncio.sleep(seconds) # 模拟等网络
print(f"{name} 完成")
return name
async def main():
# —— 对照 1:串行(连续 await),看它花约 3 秒 ——
t = time.time()
await task("串行A", 1); await task("串行B", 1); await task("串行C", 1)
print(f"串行耗时 {time.time()-t:.1f} 秒\n")
# —— 对照 2:并发(gather),看它只花约 1 秒 ——
t = time.time()
await asyncio.gather(task("并发A", 1), task("并发B", 1), task("并发C", 1))
print(f"并发耗时 {time.time()-t:.1f} 秒")
asyncio.run(main())
进阶挑战:把 gather 里的任务改成 5 个、每个睡 2 秒,先猜总耗时,再运行验证。(答案:约 2 秒,因为 5 个 2 秒的等待全重叠了。)
async,写 @app.get/@app.post,用 pydantic 校验数据,还能白拿一个自动生成的接口文档页 /docs。这是把 Agent 交付上线的第一步。