20250914-02: Langchain概念:异步编程(Async)
概念比喻解释同步方法 (invoke)老式锅:必须专人盯着会阻塞主线程,效率低异步方法 (ainvoke)智能锅:会通知你不阻塞主线程,效率高没有异步方法可用厨房里有个老式锅问题:会拖累整个异步流程委托给同步方法雇个帮手去盯老式锅解决方案:把阻塞操作扔到新线程里帮手池(线程池)提供帮手的机制轻微开销管理帮手要费点口舌创建线程、传递消息的成本“委托给同步方法”是 LangChain 提供的一
20250914-02: Langchain概念:异步编程(Async)
任务
- 阅读 LCEL 官方文档
- 理解
|
操作符的底层是RunnableSequence
🎯 学习目标
理解 LangChain 的“运行时引擎”——所有组件如何被统一调度和编排。
🔗 核心概念
-
可运行接口
(Runnable) -
LangChain 表达式语言
(LCEL) -
回调
(Callbacks) -
追踪
(Tracing) -
流式传输
(Streaming) -
异步编程
(Async)
异步编程
(Async)
asyncio — 异步 I/O — Python 3.13.1 文档 - Python 编程语言
asyncio
— 异步 I/O
asyncio 是一个使用 async/await 语法编写** 并发 **代码的库。
asyncio 被用作多个 Python 异步框架 的基础,这些框架提供高性能
的网络和 Web 服务器、数据库连接库、分布式任务队列
等。
asyncio 通常非常适合 IO 密集型 和高层结构化 网络 代码。
asyncio 提供了一组高层 API,用于:
- 并发运行 Python 协程,并完全控制它们的执行;
- 执行 网络 IO 和 IPC;
- 控制子进程;
- 通过队列分发任务;
- 同步并发代码;
此外,还有供库和框架开发人员
使用的底层 API,用于:
可用性:非 WASI。
此模块在 WebAssembly 上不起作用或不可用。有关详细信息,请参阅WebAssembly 平台。
import asyncio
await asyncio.sleep(10, result='hello')
在 3.12.5 版本中更改:(以及 3.11.10、3.10.15、3.9.20 和 3.8.20)发出审计事件。
在 3.13 版本中更改:如果可能,则使用 PyREPL,在这种情况下,也会执行 PYTHONSTARTUP
。发出审计事件。
Langchain 异步编程
基于大型语言模型(LLM)的应用通常涉及大量 I/O 密集型操作,例如调用语言模型 API、访问数据库或其他服务。
异步编程(或简称 async 编程)是一种允许程序并发执行多个任务
而不阻塞其他任务执行
的范式,它能提高效率和响应能力
,尤其是在 I/O 密集型
操作中。
许多 LangChain API 都设计为异步,使您能够构建高效且响应迅速的应用程序。
通常,任何可能执行 I/O 操作(例如,进行 API 调用、读取文件)的方法都将拥有一个异步对应项。
在 LangChain 中,异步实现与其同步对应项位于相同的类中,异步方法带有“a”前缀。例如,同步的 invoke
方法有一个异步对应项,名为 ainvoke
。
LangChain 的许多组件都实现了 Runnable 接口,该接口支持异步执行。这意味着您可以使用 Python 中的 await
关键字异步运行 Runnables。
await some_runnable.ainvoke(some_input)
其他组件,例如未实现 Runnable 接口 的 嵌入模型 和 向量存储,通常仍遵循相同的规则,并在同一个类中包含带“a”前缀的异步版本方法。
await some_vectorstore.aadd_documents(documents)
使用 LangChain 表达式语言 (LCEL) 创建的 Runnables 也可以异步运行,因为它们实现了完整的 Runnable 接口。
委托给同步方法
大多数流行的 LangChain 集成都实现了其 API 的异步支持。例如,许多 ChatModel 实现的 ainvoke
方法使用 httpx.AsyncClient
向模型提供商的 API 发送异步 HTTP 请求。
当异步实现不可用时,LangChain 会尝试提供一个默认实现,即使这会带来轻微的开销。
默认情况下,LangChain 会将未实现的异步方法的执行委托给同步对应项。LangChain 几乎总是假定同步方法应被视为阻塞操作,并应在单独的线程中运行。这通过 asyncio
库提供的 asyncio.loop.run\_in\_executor
功能完成。LangChain 使用 asyncio
库提供的默认执行器,该执行器惰性地初始化一个线程池执行器,其线程数量为默认值,并在给定的事件循环中重用。虽然这种策略由于线程间的上下文切换会产生轻微开销,但它保证了每个异步方法都有一个开箱即用的默认实现。
总结
“委托给同步方法”是 LangChain 提供的一个安全网和兼容性保障。它确保即使某个第三方库没有提供先进的异步接口,你依然可以在你的异步应用程序中无缝地、不卡顿地使用它。虽然不如原生异步那么快,但远比直接使用同步方式阻塞整个程序要好得多。
它体现了 LangChain 作为一个优秀框架的鲁棒性和开发者友好性,让你不用为底层一些依赖的兼容性问题而头疼。
性能
LangChain 中的异步代码通常应该开箱即用,表现相对良好,开销极小,在大多数应用程序中不太可能成为瓶颈。
开销的两个主要来源是
- 当 委托给同步方法 时,线程间上下文切换的开销。这可以通过提供原生的异步实现来解决。
- 在 LCEL 中,链中出现的任何“廉价函数”将要么作为任务调度到事件循环中运行(如果它们是异步的),要么在单独的线程中运行(如果它们是同步的),而不是直接内联运行。
您应该预期的这些延迟开销在几十微秒到几毫秒之间。
更常见的性能问题
来源是用户在异步上下文中意外地调用同步代码
(例如,调用 invoke
而不是 ainvoke
)从而阻塞了事件循环。
总结
简单来说:
- 别担心:LangChain异步本身的开销很小,你不会感觉到。
- 要小心:你自己写的代码才是最大的风险点。一定要确保在
async def
函数里,调用LangChain对象时使用的是 ainvoke
, astream
, abatch
等异步方法,千万不要误写成同步的invoke
。只要你避免了最后一个错误,LangChain的异步性能对你来说就是“开箱即用”且非常高效的。
兼容性
LangChain 仅与作为 Python 标准库一部分发布的 asyncio
库兼容。它不适用于其他异步库,如 trio
或 curio
。
在 Python 3.9 和 3.10 中,asyncio 的任务 不接受 context
参数。由于此限制,LangChain 在某些情况下无法自动将 RunnableConfig
沿着调用链向下传播。
如果您在使用 Python 3.9 或 3.10 的异步代码中遇到流式传输、回调或追踪问题,这很可能就是原因。
请阅读 RunnableConfig 传播 以获取更多详情,了解如何手动将 RunnableConfig
沿着调用链向下传播(或升级到 Python 3.11,在该版本中这不再是问题)。
总结
简单来说:
这段内容是在告诉你一个重要的前提和可能遇到的坑:
- 别用其他异步库,LangChain 只认
asyncio
。- 如果你用 Python 3.9 或 3.10 跑异步,遇到了流、回调、追踪不生效的问题,别慌,这不是你的代码写错了,而是Python版本的限制。
- 最好的解决办法就是升级Python版本。如果不行,再去研究如何手动配置。
深化理解
委托同步方法如何理解?
核心概念:同步 vs. 异步
首先,我们打个比方:
- 同步 (Synchronous) :就像你在一个厨房里独自做饭。你必须先烧水(等待),水开了才能下面条(等待),面条好了才能炒菜(等待)。一件事做完才能做下一件,你(主线程)一直被“阻塞”在等待中。
- 异步 (Async) :就像你是个厨师长,你可以同时管理多个锅。你让一个锅烧水(不用盯着,让它自己烧),同时另一个锅炒菜(也不用盯着),然后你去切菜。当水烧开或菜需要翻面时,锅会“通知”你,你再去处理。这样你的时间利用率更高,不会傻等。
在编程中,异步(async/await) 就是为了高效处理这种“等待”操作(比如网络请求、读写文件)而设计的。
问题:不是所有“锅”都智能
现在,LangChain 这个“大厨房”里有很多“厨具”(第三方集成,比如OpenAI、Anthropic的模型)。LangChain 希望所有厨具都能用异步的方式工作(ainvoke
),这样厨师长可以高效管理。
但是,有些第三方提供的“厨具”(库)可能比较老,或者设计者没想那么多,它只提供了同步的用法(只有 invoke
,没有 ainvoke
)。就好比有一个老式的锅,你必须站在旁边盯着它,不能离开。
那怎么办?难道就不能在异步环境下使用这个锅了吗?
解决方案:雇个帮手去盯锅(委托给同步方法)
LangChain 的解决方案非常聪明和实用:雇一个帮手(新线程)去盯着那个老式的同步锅。
这个过程就是 “委托给同步方法” 。
具体步骤是这样的:
- 你(主线程,异步环境)遇到一个只有同步方法的组件。
- 你大喊一声:“来个帮手!” LangChain 就会通过
asyncio.loop.run_in_executor()
这个机制,从“帮手池”(线程池)里叫一个帮手(一个新的线程)过来。 - 你把这个麻烦的同步任务(
invoke
)交给这个帮手,并对他说:“你去盯着这个老式锅,煮好了告诉我一声。” - 然后你(主线程)就被解放了,可以继续去处理其他已经准备好的异步任务(比如处理另一个模型的响应或者响应Web请求)。
- 帮手在另一个线程里,老老实实地用同步的方式执行那个阻塞的任务,耐心等待它完成。
- 任务完成后,帮手会通知你:“老板,事儿办完了,这是结果。”
- 你收到结果,继续后面的工作。
关于“轻微的开销”
文中提到的“轻微开销”指的是:
- 创建和管理线程需要一点额外的电脑资源(CPU和内存)。
- 在主线程和帮手线程之间传递数据和结果也需要一点时间。
- 多个线程之间切换会有一点点效率损失(上下文切换)。
但是,这点开销和让整个程序傻等着(被阻塞) 比起来,是完全值得的。它用一点点性能代价,换来了整个程序的响应能力和高效性。
总结与比喻
概念 | 比喻 | 解释 |
---|---|---|
同步方法 (invoke) | 老式锅:必须专人盯着 | 会阻塞主线程,效率低 |
异步方法 (ainvoke) | 智能锅:会通知你 | 不阻塞主线程,效率高 |
没有异步方法可用 | 厨房里有个老式锅 | 问题:会拖累整个异步流程 |
委托给同步方法 | 雇个帮手去盯老式锅 | 解决方案:把阻塞操作扔到新线程里 |
run_in_executor |
帮手池(线程池) | 提供帮手的机制 |
轻微开销 | 管理帮手要费点口舌 | 创建线程、传递消息的成本 |
所以,简单理解:
“委托给同步方法”是 LangChain 提供的一个安全网和兼容性保障。它确保即使某个第三方库没有提供先进的异步接口,你依然可以在你的异步应用程序中无缝地、不卡顿地使用它。虽然不如原生异步那么快,但远比直接使用同步方式阻塞整个程序要好得多。
它体现了 LangChain 作为一个优秀框架的鲁棒性和开发者友好性,让你不用为底层一些依赖的兼容性问题而头疼。
性能开销发生在哪里?
核心比喻:异步厨房
想象一下,LangChain 是一个高效的中央厨房(事件循环) ,它的目标是同时处理很多份订单(用户请求),而不是一次只做一份。
- 主厨(主线程) :是这个厨房的核心。他非常忙,要协调所有工作。他绝对不能停下来傻等某一道菜做完。
- 异步方法 (
ainvoke
) :就像是厨房里的智能厨具。主厨只要下个指令(比如“把汤煮上”),智能厨具就会自己去工作,并且保证在完成后会通知主厨。在此期间,主厨可以自由地去处理其他事情。 - 同步方法 (
invoke
) :就像是老式厨具。主厨必须站在它旁边,一直盯着它直到它完成工作。在这期间,主厨什么都干不了。
两种“轻微开销”的来源
文中提到,LangChain的异步性能很好,开销主要来自两个方面:
1. 雇帮手盯锅的开销(委托给同步方法)
-
场景:厨房里有个老式厨具(只有同步方法的库),但主厨又想用异步的方式工作。
-
解决方案:主厨从“帮手池”(线程池)里叫一个帮手(新线程) 过来,对他说:“你去盯着这个老式烤箱,蛋糕烤好了告诉我。”
-
产生的开销:
- 沟通成本:主厨需要花时间告诉帮手要做什么(线程间上下文切换)。
- 管理成本:维护一个帮手池也需要一点资源。
-
开销大小:非常小,通常只有几十微秒到几毫秒。对于网络请求(通常要几百毫秒甚至几秒)来说,这点开销几乎可以忽略不计。
-
如何避免:如果厨具厂商提供了智能厨具(原生的异步实现
ainvoke
),那主厨就直接和智能厨具沟通,就不用雇帮手了,这点开销也就省了。
2. 处理“廉价小任务”的开销(LCEL链中的函数)
-
场景:LCEL链就像一条流水线,一个菜做完传给下一个。有些步骤非常快,比如“撒点盐”(廉价函数,例如一个简单的字符串处理函数)。
-
“问题” :为了绝对的公平和避免阻塞,LangChain的设计是:流水线上的每一个步骤,哪怕再小,都必须被当作一个独立任务来调度。
- 如果这个步骤是异步函数 (
async def
),就把它作为一个任务调度到主厨的事件循环里排队。 - 如果它是同步函数 (
def
),就叫一个帮手(新线程)来执行它。
- 如果这个步骤是异步函数 (
-
产生的开销:调度任务或者叫帮手所产生的管理成本。对于“撒盐”这种本身可能就几微秒的操作来说,这个调度它的管理成本反而显得有点高了。
-
开销大小:同样,非常小(几十微秒到几毫秒)。对于绝大多数应用,尤其是涉及网络请求的LLM应用,这点开销微不足道。
-
设计权衡:LangChain宁愿付出这点微小的开销,也要保证整个系统的稳定性和一致性,确保任何一个步骤都不会意外地阻塞整个厨房。
真正的性能杀手:让主厨去盯锅
文章最后指出的才是最常见、最严重的性能问题:
在异步上下文中意外地调用同步代码(例如,调用
invoke
而不是 ainvoke
)从而阻塞了事件循环。
- 比喻:主厨(主线程)本来应该指挥全局,但他却亲自跑去盯着一口老式锅(调用同步的
invoke
方法)。 - 后果:整个厨房停摆! 因为主厨被一个任务“阻塞”住了,他没法去照看其他正在煮的汤、没法和智能厨具沟通、没法处理新来的订单。所有其他任务都在傻等,系统的并发能力瞬间降为零。
- 这比前面两种“轻微开销”要严重几个数量级。
总结与行动指南
情况 | 比喻 | 对性能的影响 | 建议 |
---|---|---|---|
使用原生 ainvoke |
主厨使用智能厨具 | 最佳,无额外开销 | 首选,极力推荐 |
委托同步方法 | 主厨雇帮手去盯老式锅 | 极小开销 (微秒-毫秒级) | 可以接受,影响很小 |
LCEL调度小任务 | 给“撒盐”步骤也发号施令 | 极小开销 (微秒-毫秒级) | 无需担心,这是框架设计的代价 |
误用同步 invoke |
主亲自去盯老式锅 | 灾难性,阻塞整个事件循环 | 绝对禁止! 务必检查代码,确保在异步函数里只用 ainvoke /astream |
简单来说:
- 别担心:LangChain异步本身的开销很小,你不会感觉到。
- 要小心:你自己写的代码才是最大的风险点。一定要确保在
async def
函数里,调用LangChain对象时使用的是 ainvoke
, astream
, abatch
等异步方法,千万不要误写成同步的invoke
。
只要你避免了最后一个错误,LangChain的异步性能对你来说就是“开箱即用”且非常高效的。
更多推荐
所有评论(0)