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