LangChain 批量调用与并发控制:batch、abatch 与速率限制

个人主页: > for_ever_love__ <(欢迎各位大佬莅临😊)
其他栏目: > 大模型开发从0到1 <
其他栏目: > iOS项目总结大全 <
其他栏目: > 我想学python了 <
其他栏目: > iOS UI <

文章目录

  • [LangChain 批量调用与并发控制:batch、abatch 与速率限制](#LangChain 批量调用与并发控制:batch、abatch 与速率限制)

LangChain 批量调用与并发控制:batch、abatch 与速率限制

有 1000 条数据要处理,一条条 invoke 要跑到天亮。

但直接 batch 全发出去,又会 429 限流。

这篇讲清楚怎么又快又不被限流,以及异步到底快在哪。

一、四种调用方式

python 复制代码
# 单个
chain.invoke({"q": "..."})

# 批量(同步)
chain.batch([{"q": "a"}, {"q": "b"}, {"q": "c"}])

# 单个异步
await chain.ainvoke({"q": "..."})

# 批量异步  ← 生产首选
await chain.abatch([{"q": "a"}, {"q": "b"}, ...])
方式 并发 适合
invoke 无 单条、交互式
batch 线程池 小批量、脚本
ainvoke 需配合 gather 并发单条
abatch 真并发 大批量、Web 服务

二、batch 的参数

python 复制代码
results = chain.batch(
    inputs_list,
    config={"max_concurrency": 5},    # 最多同时 5 个
    return_exceptions=True,           # 单条失败不中断整体
)

2.1 max_concurrency 必设

不设的话,1000 条输入会一次性发出 1000 个请求。

结果必然是:

复制代码
429 Too Many Requests
429 Too Many Requests
...

然后触发重试,反而比串行还慢。

python 复制代码
# ❌ 危险
chain.batch(one_thousand_inputs)

# ✅ 安全
chain.batch(one_thousand_inputs, config={"max_concurrency": 5})

2.2 return_exceptions=True 很重要

默认情况任何一条失败都会让整个 batch 抛异常 ,

前面已经算完的结果全丢了。

python 复制代码
results = chain.batch(inputs, config={"max_concurrency": 5}, return_exceptions=True)

# 逐个检查结果
ok, bad = [], []
for inp, res in zip(inputs, results):
    if isinstance(res, Exception):
        bad.append((inp, res))
    else:
        ok.append(res)
print(f"成功 {len(ok)},失败 {len(bad)}")
# 失败的可以单独重试

处理 1000 条数据时,因为 1 条失败而丢掉 999 条的结果,是很疼的。

三、异步为什么快

3.1 同步 batch 的真相

chain.batch() 用的是线程池 。

线程有开销,而且 Python 有 GIL,实际加速有限。

3.2 abatch 才是真并发

python 复制代码
import asyncio
import time

async def main():
    t0 = time.time()
    results = await chain.abatch(
        inputs_list,
        config={"max_concurrency": 20},
        return_exceptions=True,
    )
    print(f"耗时 {time.time() - t0:.1f}s")

asyncio.run(main())

LLM 调用是 IO 等待 (发出请求 → 等响应),

异步能在等待时去发下一个请求,并发 20 就意味着 20 倍加速。

实测对比(100 条,单条约 2 秒):

方式 耗时
循环 invoke ~200 秒
batch (max_concurrency=5) ~40 秒
abatch (max_concurrency=20) ~10 秒

3.3 Web 服务里必须用异步

python 复制代码
@app.post("/ask")
async def ask(req: AskRequest):
    # ❌ 同步调用会阻塞整个事件循环
    #    一个慢请求会让所有其他请求排队
    return chain.invoke({"q": req.question})

    # ✅
    return await chain.ainvoke({"q": req.question})

这是 FastAPI 里最容易犯的性能错误。

同步调用在 async 函数里会阻塞事件循环,

QPS 直接掉到个位数。

四、速率限制:按 RPM 控制

max_concurrency 控制"同时几个",

但 API 通常限制的是"每分钟多少个请求(RPM)"。

python 复制代码
import asyncio
import time
from collections import deque

class RateLimiter:
    """限制每分钟最多 n 个请求"""
    def __init__(self, max_per_minute: int):
        self.max_per_minute = max_per_minute
        self.calls = deque()

    async def acquire(self):
        now = time.time()
        # 清掉 60 秒之前的记录
        while self.calls and now - self.calls[0] > 60:
            self.calls.popleft()
        if len(self.calls) >= self.max_per_minute:
            wait = 60 - (now - self.calls[0])
            await asyncio.sleep(wait)
            return await self.acquire()
        self.calls.append(now)

limiter = RateLimiter(max_per_minute=500)

async def limited_call(item):
    await limiter.acquire()
    return await chain.ainvoke(item)

results = await asyncio.gather(*[limited_call(i) for i in inputs])

两个参数配合 :max_concurrency 控制瞬时并发(防打爆),

速率限制器控制长期速率(防超限流)。

五、进度与断点

处理 1000 条时,你一定想知道"跑到哪了"以及"崩了能不能续跑"。

python 复制代码
import json
from pathlib import Path

OUT = Path("results.jsonl")

async def process_all(inputs, limit=20):
    done = set()
    if OUT.exists():
        with OUT.open(encoding="utf-8") as f:
            for line in f:
                done.add(json.loads(line)["id"])

    todo = [i for i in inputs if i["id"] not in done]
    print(f"总共 {len(inputs)},已完成 {len(done)},待处理 {len(todo)}")

    sem = asyncio.Semaphore(limit)

    async def one(item):
        async with sem:
            try:
                res = await chain.ainvoke(item)
                return {"id": item["id"], "ok": True, "result": res}
            except Exception as e:
                return {"id": item["id"], "ok": False, "error": str(e)}

    with OUT.open("a", encoding="utf-8") as f:
        for i, coro in enumerate(asyncio.as_completed([one(t) for t in todo]), 1):
            rec = await coro
            f.write(json.dumps(rec, ensure_ascii=False) + "\n")
            f.flush()          # 立刻落盘
            if i % 50 == 0:
                print(f"进度 {i}/{len(todo)}")

    print("完成")

三个要点:

  • 每处理完一条就写盘 (flush),崩了能从断点继续;
  • 用 asyncio.Semaphore 控制并发;
  • 启动时读已有结果,跳过已完成的。

六、长任务别用同步 batch

一个常见错误:在 Web 请求里处理 1000 条数据。

python 复制代码
@app.post("/batch")
async def batch_endpoint(req):
    results = await chain.abatch(req.items, config={"max_concurrency": 20})
    return results     # 请求挂在这里几分钟,网关先超时了

正确做法:改成异步任务 + 轮询/回调。

python 复制代码
@app.post("/batch")
async def batch_endpoint(req, background_tasks: BackgroundTasks):
    job_id = str(uuid.uuid4())
    background_tasks.add_task(process_all, req.items, job_id)
    return {"job_id": job_id}

@app.get("/batch/{job_id}")
async def get_status(job_id: str):
    return {"progress": get_progress(job_id)}

七、常见问题

现象 原因 解法
大量 429 并发太高 降 max_concurrency;加速率限制
一条失败全批失败 没设 return_exceptions 设成 True
跑一半崩了要重来 没落盘 逐条写 jsonl,断点续跑
异步还是慢 中间有同步阻塞调用 检查链里是否有 requests 等同步库
内存爆了 一次性构造 1000 个任务 分块处理,每块 100 条
网关超时 长任务同步等 改后台任务 + 轮询

特别提醒第 4 条 :如果你的链里某个工具用的是同步 HTTP 库(如 requests),

abatch 的并发会被它卡住。工具要提供 async 实现

(前面自定义 Tool 那篇讲过 coroutine 参数)。

八、一份生产级批处理模板

python 复制代码
async def batch_process(chain, items, *, concurrency=10, rpm=None, out_path="out.jsonl"):
    sem = asyncio.Semaphore(concurrency)
    limiter = RateLimiter(rpm) if rpm else None

    async def one(item):
        async with sem:
            if limiter:
                await limiter.acquire()
            try:
                return {"id": item.get("id"), "ok": True,
                        "result": await chain.ainvoke(item)}
            except Exception as e:
                return {"id": item.get("id"), "ok": False, "error": str(e)}

    with open(out_path, "a", encoding="utf-8") as f:
        for coro in asyncio.as_completed([one(i) for i in items]):
            rec = await coro
            f.write(json.dumps(rec, ensure_ascii=False) + "\n")
            f.flush()
    return out_path

九、小结

  • 四种方式:invoke(单条)、batch(同步线程池)、
    ainvoke(异步单条)、abatch(真并发,生产首选)
    ;
  • max_concurrency 必设,否则一次打爆 API;
  • return_exceptions=True 必设,否则一条失败丢掉整批;
  • Web 服务里必须用 ainvoke/abatch,同步调用会阻塞事件循环;
  • max_concurrency 管瞬时并发,速率限制器管长期 RPM,两者配合;
  • 大批量任务逐条落盘 + 断点续跑,别等全部完成再写;
  • 长任务改后台任务 + 轮询,别让 HTTP 请求挂几分钟。

下一篇讲缓存------同样的请求别反复花钱。

相关推荐
码流子1 小时前
高速场景视频接入:GB/T 28181 级联与 RTSP 取流,一条链路讲清楚
大数据·数据库·人工智能·物联网·音视频
卿卿的产品经理日记1 小时前
【AI产品经理实战】Day 37|拆开 JSON 与 EXE:数据都有格式
人工智能·笔记·python·json·产品经理
yunni81 小时前
政企AI落地避坑:敏感数据不出域,安全需求优先本地化部署
人工智能·安全
广东帝工1 小时前
如何利用大数据和人工智能技术实现智慧航道
大数据·人工智能
有月牙1 小时前
2026企业AI办公工具选型指南:企业办公AI怎么评估与落地
大数据·人工智能
柯南46681 小时前
【AI工程师精讲】09:LoRA:几十兆的文件,凭什么能改变一个大模型
人工智能·ai编程
198******126341 小时前
Work Agent深度解读:AI长程任务如何重塑办公自动化
人工智能
自养Agent1 小时前
Agent Harness 从用到会造:上下文塞满了,它为什么反而变笨
人工智能
喵叔哟1 小时前
第 2 章 Spec Kit 到底是什么?
人工智能