有时候你会看到 Python 代码里突然出现 async def 和 await,感觉像在说外语。你是不是心里一紧:"完了,这回真跟不上了。" 别慌,我第一次看到也懵------其实它没那么复杂,只是 Python 在说:"这个函数可能会等一会儿,但它不会傻站着等,而是可以先去干点别的。"
我们用一个你肯定经历过的场景来比喻:点咖啡 。你走进咖啡馆,点了一杯拿铁。如果店员做好才让你付钱,你必须在柜台前傻站着等三分钟,什么也干不了------这就是 同步 。但如果店员给你一个叫号器,说"好了会震动",你就可以先去旁边刷手机、回消息,等叫号器震了再去取------这就是 异步 。Python 的 async 和 await 就是帮你实现"拿着叫号器边等边干别的事"的工具。
同步与异步的区别:一个办完再办下一个,还是同时排多个队
同步 :一件事做完,再做下一件事。就像你排队买票,必须等第一个人买完,第二个人才能上前。如果第一个人掏钱掏了半天,后面所有人都得干等。Python 普通的函数(没有 async)就是同步的------它调用一个耗时操作(比如等 AI 回复),整个程序就会卡在那里,直到得到结果才能往下走。
异步:你可以在等待某个慢操作的同时,去处理别的任务。就像你同时有好几个叫号器:咖啡好了、奶茶好了、汉堡好了,哪个先震动你就先去取哪个。Python 的异步代码允许你"同时"发起多个耗时操作,然后等它们各自完成时分别处理,而不是一个接一个地傻等。
python
# 同步版本(普通函数)------ 模拟耗时操作
import time
def slow_sync():
time.sleep(2) # 模拟等待2秒
print("完成")
print("开始")
slow_sync() # 程序会卡在这里2秒,什么也不能做
print("结束") # 2秒后才执行这一行
python
# 异步版本(需要特殊写法)------ 先别管细节,感受气质
import asyncio
async def slow_async():
await asyncio.sleep(2) # 等待2秒,但不阻塞其他任务
print("完成")
# 这段代码不能直接运行,只是让你对比
同步是"做完一件再下一件",异步是"同时等很多件,哪件先好处理哪件"。
在 LangGraph 里哪里会遇到它?
LangGraph 在处理多个节点(比如多个工具调用、多个模型并发请求)时,为了不浪费等待时间,内部大量使用异步。如果你要同时让 AI 分析三篇不同的文章,同步版本需要等第一篇回来再发第二篇,异步版本可以三篇一起发出去,谁先回来就先处理谁。理解了这个区别,你就知道为什么 LangGraph 的示例里经常出现 async def。
async def 定义一个"可以暂停等待"的函数
普通的函数用 def 定义,一调用就会从头跑到尾,中间不能停。而用 async def 定义的函数,叫做 异步函数。它可以在执行到某个"等一会儿"的地方(比如等网络返回)主动暂停,让 Python 去执行其他异步任务,等那个耗时的操作完成了再回来继续。
你可以把 async def 理解成 "带暂停按钮的函数"。它像一个可以中途去泡杯茶、回来继续干的员工,而不是必须钉在座位上一动不动。
python
# 定义一个异步函数
async def fetch_data():
print("开始获取数据...")
# 这里本该是 await 某个耗时操作
print("数据获取完成")
注意:如果你只是定义 async def 但里面没有 await,它其实和普通函数差不多。但它的真正威力在于配合 await 使用。
另外非常重要的一点:异步函数不能直接像普通函数那样调用 。你写 fetch_data() 不会真的执行它,而是得到一个"协程对象"(可以理解成"一个等待启动的任务")。你需要用特定的方式(比如 asyncio.run())来启动它。
在 LangChain/LangGraph 里哪里会遇到它?
当你看到 LangGraph 示例里写 async def my_node(state):,那就是在定义一个异步的图节点。这个节点在处理过程中可能需要调用多个模型或者等待外部 API,用 async def 可以让它在该等的时候"让出"控制权,让其他节点同时运行。
await 用来等待一个耗时的操作,但不阻塞其他任务
await 是"我就在这儿等,但我不死等"的关键字。它后面跟着一个"可等待"的东西(通常是另一个异步函数或操作)。当 Python 遇到 await 时,它会说:"好了,这个操作需要花时间,我先让 CPU 去执行别的异步任务,等这个操作完成了再回来继续往下走。"
回到点咖啡的例子:你拿着叫号器,await 就是"我把叫号器捏在手里,然后去刷手机"。咖啡好了,叫号器震动,你才回去取。在 await 期间,你没有阻塞------你同时还在刷手机(相当于 Python 在执行别的任务)。
python
import asyncio
async def make_coffee():
print("开始做咖啡...")
await asyncio.sleep(3) # 模拟耗时3秒的咖啡制作
print("咖啡做好了")
return "拿铁"
async def main():
print("点了一杯咖啡")
result = await make_coffee() # 等待咖啡做好,但期间可以做别的事
print(f"拿到{result}")
# 运行
asyncio.run(main())
输出:
点了一杯咖啡
开始做咖啡...
(等待3秒,但这里没有其他任务,所以实际还是等了3秒)
咖啡做好了
拿到拿铁
注意:上面的例子只有一个任务,所以 await 看起来就像普通等待一样。但如果你有两个异步任务,就可以"同时"等待它们:
python
async def main():
task1 = asyncio.create_task(make_coffee()) # 开始做咖啡,不等待
task2 = asyncio.create_task(make_tea()) # 开始泡茶,也不等待
coffee = await task1 # 等咖啡完成
tea = await task2 # 等茶完成(可能已经好了)
这样总耗时大约等于最慢的那个任务,而不是两者之和。
在 LangGraph 里哪里会遇到它?
你会经常看到类似这种模式:response = await model.ainvoke(input)。这里的 ainvoke 是模型的异步调用方法(a 代表 async)。await model.ainvoke(...) 会发起请求,然后在等待网络返回的同时,LangGraph 可以去执行其他可并行的节点。如果你不用 await,而是用普通的 model.invoke,整个程序就会卡住直到模型返回,浪费了等待时间。
简单认识 asyncio.run(),怎么把异步函数跑起来
前面说了,异步函数不能直接调用。你需要一个"启动器"来运行异步代码。Python 标准库提供了 asyncio 这个工具箱,其中的 asyncio.run() 就是最常用的启动器。
asyncio.run() 接收一个异步函数(通常叫 main()),它会创建一套异步运行环境,执行这个异步函数,然后清理环境。你只需要记住:所有异步代码的入口都要用 asyncio.run() 启动。
python
import asyncio
async def say_hello():
await asyncio.sleep(1)
print("Hello async!")
# 启动异步函数
asyncio.run(say_hello())
这就是最简模式。如果你有一个 async def main(),就写 asyncio.run(main())。
注意:asyncio.run() 只能在程序的主线程调用一次。在 Jupyter Notebook 等特殊环境里可能需要别的方法,但普通 .py 脚本中就这么用。
在 LangChain/LangGraph 里哪里会遇到它?
当你写一个完全异步的 LangGraph 应用时,你的主程序最后会有一行 asyncio.run(main())。在官方示例中,你有时会看到 if __name__ == "__main__": asyncio.run(main()) 这种结构。你不需要自己设计复杂的异步逻辑,但至少要能认出:这就是"把异步函数跑起来"的开关。
另外,很多 LangChain 组件同时提供了同步方法(invoke)和异步方法(ainvoke)。如果你想在自己的代码里调用异步方法,就要用 await,并且整个调用链都要是异步函数,最终用 asyncio.run() 启动。但如果你觉得麻烦,完全可以只用同步版本(invoke)------LangChain 会帮你处理好内部转换。不过,识别异步代码 的能力还是很重要的,因为在 LangGraph 的复杂示例里你会频繁见到它。
本篇小结与下集预告
小结 :今天我们认识了异步的世界。同步是一个接一个做事情,异步是同时等很多事情,哪个先好了就处理哪个。async def 定义了一个可以暂停等待的函数,await 用来等待耗时操作但不阻塞其他任务,而 asyncio.run() 是整个异步程序的启动按钮。现在你再看到 LangGraph 示例里的 async def my_node(state): 和 await some_async_operation(),应该能翻译出来:"这个节点可以在等待时让位给别的节点,等结果回来再继续,这样总速度更快。"
下集预告 :下一篇文章,我们会聊一聊"整理你的房间"------也就是模块、包和文件组织。当你开始动手写一个稍微大一点的 LangChain 小项目时,把所有代码都塞进一个 .py 文件会非常混乱。我们会教你如何把不同功能的代码拆分到不同的文件,然后用 import 把它们组织起来。你还会明白 if __name__ == "__main__": 这行"咒语"到底是干什么的。放心,整理房间这件事你一定擅长。我们明天见~