使用 Langchain 进行异步编程
基于 LLM 的应用程序通常涉及大量 I/O 密集型操作,例如向语言模型、数据库或其他服务发起 API 调用。异步编程(或 async 编程)是一种允许程序并发执行多个任务而不会阻塞其他任务执行的范例,从而提高效率和响应能力,尤其是在 I/O 密集型操作中。
在阅读本指南之前,您应该熟悉 Python 中的异步编程。如果您不熟悉,请在线查找适当的资源来学习如何在 Python 中进行异步编程。本指南专门侧重于您需要了解的,以便在异步环境中使用 LangChain,并假设您已经熟悉异步编程。
Langchain 异步 API
许多 LangChain API 都被设计为异步的,允许您构建高效且响应迅速的应用程序。
通常,任何可能执行 I/O 操作的方法(例如,发起 API 调用、读取文件)都将具有异步对应项。
在 LangChain 中,异步实现与它们的同步对应项位于相同的类中,异步方法具有 "a" 前缀。 例如,同步的 invoke
方法有一个名为 ainvoke
的异步对应项。
LangChain 的许多组件实现了Runnable 接口,其中包括对异步执行的支持。这意味着您可以在 Python 中使用 await
关键字异步运行 Runnables。
await some_runnable.ainvoke(some_input)
其他组件(如嵌入模型和VectorStore)不实现 Runnable 接口,但通常仍遵循相同的规则,并在同一类中包含带有 "a" 前缀的方法的异步版本。
例如,
await some_vectorstore.aadd_documents(documents)
使用LangChain 表达式语言 (LCEL)创建的 Runnables 也可以异步运行,因为它们实现了完整的 Runnable 接口。
有关更多信息,请查看您正在使用的特定组件的API 参考。
委托给同步方法
大多数流行的 LangChain 集成都实现了对其 API 的异步支持。例如,许多 ChatModel 实现的 ainvoke
方法使用 httpx.AsyncClient
向模型提供商的 API 发起异步 HTTP 请求。
当没有异步实现可用时,LangChain 会尝试提供默认实现,即使这会产生轻微的开销。
默认情况下,LangChain 会将未实现的异步方法的执行委托给同步的对应项。LangChain 几乎总是假设同步方法应被视为阻塞操作,并且应在单独的线程中运行。这是使用 asyncio
库提供的 asyncio.loop.run_in_executor 功能完成的。 LangChain 使用 asyncio
库提供的默认执行器,该执行器会懒惰地初始化一个线程池执行器,其中包含在给定事件循环中重用的默认线程数。虽然此策略会由于线程之间的上下文切换而产生轻微的开销,但它可以保证每个异步方法都有一个开箱即用的默认实现。
性能
LangChain 中的异步代码通常应该开箱即用且性能良好,并且在大多数应用程序中不太可能成为瓶颈。
主要的两个开销来源是:
- 在委托给同步方法时,线程之间进行上下文切换的成本。这可以通过提供原生的异步实现来解决。
- 在LCEL中,作为链一部分出现的任何“廉价函数”都将被安排为事件循环上的任务(如果它们是异步的)或在单独的线程中运行(如果它们是同步的),而不是只是内联运行。
您应该预期从这些操作产生的延迟开销在数十微秒到几毫秒之间。
更常见的性能问题来源是用户在异步上下文中调用同步代码(例如,调用 invoke
而不是 ainvoke
)时意外地阻塞了事件循环。
兼容性
LangChain 仅与作为 Python 标准库一部分分发的 asyncio
库兼容。它不适用于其他异步库,如 trio
或 curio
。
在 Python 3.9 和 3.10 中,asyncio 的任务不接受 context
参数。 由于此限制,LangChain 无法在某些情况下自动将 RunnableConfig
向下传播到调用链中。
如果您在使用异步代码中的流式传输、回调或跟踪时遇到问题,并且您使用的是 Python 3.9 或 3.10,这很可能就是原因。
请阅读传播 RunnableConfig 了解更多详细信息,了解如何手动将 RunnableConfig
向下传播到调用链中(或升级到 Python 3.11,其中不再存在此问题)。
如何在 ipython 和 jupyter 笔记本中使用
从 IPython 7.0 开始,IPython 支持异步 REPL。这意味着您可以在 IPython REPL 和 Jupyter Notebook 中使用 await
关键字,而无需任何其他设置。有关更多信息,请参阅IPython 博客文章。