14 · langchain_core 的并发与网络模型:线程、协程、和那个发请求的库
这是一篇”扒引擎盖”的深度篇:不教怎么用,教底层怎么跑。 你写model.invoke()时,背后是线程还是协程?batch是真并行还是假并行?HTTP 请求到底走哪个库? 所有结论来自本机源码逐行核对(langchain_core 1.5.1、langchain_openai 1.4.1、anthropic SDK 0.120.2)。
开头的现象
小林用 LangChain 写了半年,一直有个模糊的感觉:“有时候快、有时候慢,有时候并行、有时候排队,它到底怎么调度的?” 他问老王,老王甩给他一句话:“同步走线程,异步走协程,批量偷懒走线程池。你自己去翻源码。” 小林半信半疑,打开了langchain_core/runnables/config.py——结果被一行代码震住了:他以为很高深的并发调度,核心就藏在一个叫 run_in_executor 的函数里。
第一幕:两种并发原语——线程池 vs 事件循环
小林先搞清楚了 LangChain 的并发只有两条路:runnables/config.py:607 找到了第一个主角——ContextThreadPoolExecutor:
config 能穿透线程边界。
想验证吗?from langchain_core.runnables.config import ContextThreadPoolExecutor,看它的继承链——是 ThreadPoolExecutor 的子类,重写了submit用copy_context().run。
第二幕:run_in_executor——异步世界里”偷懒”的桥
小林翻到config.py:678 的 run_in_executor,这是理解一切的钥匙:
Runnable.ainvoke(基类默认,base.py:917)=await run_in_executor(config, self.invoke, ...)——异步调用时把同步 invoke 丢进线程池BaseChatModel._agenerate基类默认(chat_models.py:2218)=await run_in_executor(None, self._generate, ...)——只有同步实现的模型,异步时被搬到线程池BaseChatModel._astream基类默认(chat_models.py:2247)= 先在线程池取同步生成器,然后每个 chunk 的 next() 都丢线程池
想验证吗?inspect.getsource(langchain_core.runnables.config.run_in_executor)——你会看到那两行run_in_executor,传 None 走默认池,传 Executor 走指定池。
第三幕:batch 的真面目——同步用线程池,异步用 gather
小林最关心”批量是不是真并行”。答案让他意外——同步和异步的批量,用的并发原语完全不同: 同步Runnable.batch(base.py:919-967):
Runnable.abatch(base.py:1054-1100):
小林又发现一个反直觉的点:
RunnableParallel(并行分支)同步 invoke 反而用线程池(base.py:4169):
{"a": fn1, "b": fn2} 这种并行 dict,同步执行时 LangChain 用线程池让两个分支真并行——不是”看起来并行”。
想验证吗?inspect.getsource(Runnable.batch)看那两行 executor.map;inspect.getsource(Runnable.abatch)看 gather_with_concurrency。同步异步两条路,一目了然。
第四幕:模型层——generate 串行,agenerate 并发
小林继续看BaseChatModel(chat_models.py),发现模型层也有自己的调度:
同步 generate(chat_models.py:1653-1672)——是串行的!
agenerate(chat_models.py:1780-1791)——才并发:
model.generate([多个]) 同步是串行的(慢但省资源);await model.agenerate([多个]) 异步是并发的(快但耗配额)。
那 model.batch([多个]) 呢? batch 走的是 Runnable.batch(线程池),它会调 self.invoke,invoke 内部调 generate(串行)——但每个 invoke 跑在不同线程里,所以 batch 多输入是线程级并发。三层调度叠起来:
想验证吗?inspect.getsource(BaseChatModel.generate)——是 for 循环;inspect.getsource(BaseChatModel.agenerate)——是 asyncio.gather。
第五幕:线程池都藏在哪里——全包扫描
小林 grep 了整个 langchain_core,把所有ThreadPoolExecutor 揪出来了:
ProcessPoolExecutor:一个都没有。 LangChain 从不用多进程——因为 GIL 下多进程开销大,且模型调用是 IO 密集不是 CPU 密集,线程池足够。
三种线程池来源:
- 临时建:每次 batch/RunnableParallel 调用新建 ContextThreadPoolExecutor,用完即关
- 常驻共享:callbacks 的 10 线程池、LangSmith tracer 的全局 executor
- 借用:
run_in_executor(None, ...)用 asyncio 事件循环自带的默认线程池
想验证吗?grepThreadPoolExecutor在整个 langchain_core 目录——数数有几处;grepProcessPoolExecutor——0 处。
第六幕:网络模型——core 不发请求,请求在 partner 包里
小林最后搞清楚”到底谁在发 HTTP”。结论让他意外:langchain_core 自己几乎不发网络请求! langchain_core 里的网络 import(仅 3 处,且都不是模型调用):
小林的顿悟:“原来整个 LangChain 生态的网络层全是 httpx!” 它没有用老牌 requests 发模型请求(requests 不支持原生异步),而是统一走 httpx——因为 httpx 一个库同时提供
Client(同步)和 AsyncClient(异步),完美匹配 LangChain 的”同步方法 + 异步方法”双轨设计。
想验证吗?pip show anthropic看 Requires 里httpx<1,>=0.25.0;pip show openai看httpx<1,>=0.23.0;pip show chromadb看httpx>=0.27.0。全生态都是 httpx。
第七幕:同步栈 vs 异步栈——两条完整的路
小林把所有证据拼起来,画出了全链路:- 同步栈:当前线程 + 阻塞 HTTP——简单,但一个 invoke 卡住,整个线程卡住
- 异步栈:事件循环 + 非阻塞 HTTP——并发高,但代码要 async/await
- 兼容桥:
run_in_executor(None, ...)让”只有同步实现的模型”也能被ainvoke调用——只是背地里在事件循环的默认线程池里偷偷跑同步代码
结论(小林扒完引擎盖换来的)
LangChain 的并发模型 = 双轨制:同步方法(invoke/batch/stream)跑在当前线程 + 临时线程池;异步方法(ainvoke/abatch/astream)跑在 asyncio 事件循环 + 协程。批量:同步走 ThreadPoolExecutor(每次新建),异步走 asyncio.gather(Semaphore 限流)。模型层:同步 generate 串行、异步 agenerate 并发、run_in_executor(None, ...) 是同步模型与异步调用之间的”桥”。网络层:langchain_core 几乎不发请求,真正的 HTTP 全在 partner 包,且全生态统一用 httpx(Client 同步 / AsyncClient 异步)——这就是为什么 LangChain 能一个接口同时支持同步和异步。
复现信号:什么时候你会想起这一章
model.batch([多个])很慢——同步批量是线程池并发,但如果底层模型只实现了同步 generate,每个线程内还是串行发请求;真想要高并发,用await model.abatch(...)。ainvoke卡住主线程?——不会。事件循环里ainvoke要么走真协程(httpx.AsyncClient),要么把同步代码丢进默认线程池(run_in_executor),事件循环始终不被阻塞。RunnableParallel同步执行却并行——它内部用线程池让分支真并发(base.py:4169),不是假并行。- 想看网络层——去 partner 包找
openai.OpenAI/anthropic.SyncAPIClient,底层全是 httpx。 - 想调 max_concurrency——
config={"max_concurrency": 5},同步批量控制线程池大小,异步批量控制 Semaphore。
关联阅读
- 第 11 篇:从流水线到会转弯的图——LangGraph 的图执行器(Pregel)本身也是 Runnable,遵循同样的并发模型
- 第 14 篇:LangGraph 怎么利用 langchain_core——LangGraph 复用 core 的 Runnable 协议