个人主页: > for_ever_love__ <(欢迎各位大佬莅临😊)
其他栏目: > 大模型开发从0到1 <
其他栏目: > iOS项目总结大全 <
其他栏目: > 我想学python了 <
其他栏目: > iOS UI <
文章目录
- [LangChain 批量调用与并发控制:batch、abatch 与速率限制](#LangChain 批量调用与并发控制:batch、abatch 与速率限制)
-
- 一、四种调用方式
- [二、batch 的参数](#二、batch 的参数)
-
- [2.1 max_concurrency 必设](#2.1 max_concurrency 必设)
- [2.2 return_exceptions=True 很重要](#2.2 return_exceptions=True 很重要)
- 三、异步为什么快
-
- [3.1 同步 batch 的真相](#3.1 同步 batch 的真相)
- [3.2 abatch 才是真并发](#3.2 abatch 才是真并发)
- [3.3 Web 服务里必须用异步](#3.3 Web 服务里必须用异步)
- [四、速率限制:按 RPM 控制](#四、速率限制:按 RPM 控制)
- 五、进度与断点
- [六、长任务别用同步 batch](#六、长任务别用同步 batch)
- 七、常见问题
- 八、一份生产级批处理模板
- 九、小结
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 请求挂几分钟。
下一篇讲缓存------同样的请求别反复花钱。