Skip to main content

12 · 并发与网络模型:LangChain 的调度机制拆解

本片目标:回答生产级三个必问题——100 个用户同时问扛得住吗?同步/异步/批量怎么选?底层发了什么 HTTP 请求?本片把 LangChain 的线程模型网络栈彻底拆开。 新增规定性:12(执行协议:invoke/batch/stream/ainvoke 四件套 + 并发配置) 数据字典:ContextThreadPoolExecutor / run_in_executor / RunnableConfig。 进程线程模型:本片主角——线程池、事件循环、async 并发。 网络模型:httpx Client / AsyncClient、SSE 流、超时重试。

1. 上集回顾

第 11 篇模型能调用工具了——但一次工具问答 = 多次模型调用(1 工具 = 2 次)。部署到生产前必须回答:
  1. 100 个用户同时提问,串行 invoke 一个占 1~3 秒,第 100 个用户要等 5 分钟——怎么扛?
  2. 同步、异步、批量,什么时候用哪个?为什么有 ainvoke 这种东西?
  3. 底层到底走什么 HTTP 客户端?超时、重试、连接池在哪配?
答案都在 LangChain 的执行层。本片用源码 + 实测说话。

2. 数据字典:Runnable 四件套(执行协议)

对照规则a 前缀 = 异步版;输入列表 = 批量版。

2.1 RunnableConfig(并发控制的关键参数)


3. 数据字典:LangChain 的线程池

3.1 ContextThreadPoolExecutor(langchain_core 的专用线程池)

它和标准 ThreadPoolExecutor 的区别submit 时用 copy_context() 复制当前线程的 contextvars 上下文到工作线程。为什么?——因为 LangChain 用 contextvars 传递 run_id、追踪信息,如果线程不复制上下文,异步调用里的追踪就断了

3.2 run_in_executor(同步↔异步的桥)

用途:在 async 代码里调用同步函数,避免阻塞事件循环。

4. 实测:并发行为的真相(2026-08 本机,智谱 glm-4.7)

我用 3 个问题做了一组对照实验:
⚠️ 最重要的发现:batch 比串行还慢! 为什么? 因为智谱 API 有速率限制(rate limit)——3 个请求同时打过去,部分触发 429 Too Many Requests,SDK 自动重试(默认最多 2 次),重试又要等。所以:
并发 ≠ 一定更快。并发是”潜力”,实际收益取决于 API 的限流策略。 生产环境要对 API 的 QPS 上限做压测,再定 max_concurrency
另一个实测确认RunnableParallel(第 09 篇的 {"context":..., "question":...})的同步 invoke 内部确实用 ThreadPoolExecutor——两条分支是真并行:

5. 进程线程模型(全图)

两条世界线的桥run_in_executor。同步调用会被包装成 Future 丢进线程池,事件循环等 Future 完成——这是 FastAPI(第 14 篇)里混用同步/异步的关键

6. 网络模型:httpx 全栈

LangChain 生态的 HTTP 客户端是 httpx(不是 requests、不是 urllib)。本机实测依赖:

6.1 同步栈(invoke 路径)

6.2 异步栈(ainvoke 路径)

异步为什么能扛高并发invoke 阻塞线程 1~3 秒,100 并发需要 100 个线程;ainvoke 在事件循环里等待时不占线程,100 并发只需要 1 个事件循环线程 + N 个协程。这是 FastAPI async 路由的核心优势(第 14 篇)。

6.3 流式网络模型(SSE)


7. 关键方法速查


8. 验证:跑起来

配套代码 code/12_async_bench.py
  1. 对照实验:串行 invoke vs batch vs abatch(3 问 × 3 种方式计时);
  2. 观察 RunnableParallel 内部线程名(证明线程池存在);
  3. abatch 限流演示:max_concurrency=1 时退化为串行;
  4. 打印 httpx 客户端信息。
预期输出(节选,数字随网络波动)

9. 边界

  • batch 不一定快——API 限流时并发触发 429 重试反而更慢。先压测再定并发数
  • 线程池每次 batch 新建——高频调用建议自己管理连接复用(或直接走异步)。
  • 同步阻塞代码里别混 async——asyncio.run() 在同一线程二次调用会报错。
  • 本地 Chroma 的 HNSW 索引是线程安全的吗——多线程并发检索建议用同一实例(第 08 篇已提)。写入要加锁。

推荐资料(延伸阅读)


10. 未完待续

并发模型懂了。但这一篇我们反复钻进 langchain_core.runnables.confighttpx 这些底层——你已经在用 langchain_core,但只用了它 36 个模块里的几个。 它到底还有多少模块?哪些是你以后的武器、哪些是必须避开的坑?——这就是第 13 篇:langchain_core 全地图。 13 · langchain_core 全地图