简介:面向希望充分发挥 DeepSeek 性能的开发者与数据工作者,本资源是一份以批量请求与异步调用为主线的实战型 PDF 文档,共 21 页,内容覆盖环境准备、API 密钥获取、批量请求构建与发送、响应解析、错误处理与重试机制,以及基于 asyncio 和 async/await 的异步调用实现与并发控制方法,并配有性能测试与优化章节,帮助读者掌握从基础原理到项目落地的完整路径。文档还专门梳理了请求超时、部分请求失败、异步任务异常等常见问题及对应解决思路,实用性强。文件总数 1 个,为 PDF 格式,压缩包大小 1.74MB,内容排版与图表显示完整,便于直接阅读与随时查阅。已有 63 人浏览学习,适合正在使用 DeepSeek 进行数据处理、内容生成或高并发场景开发,并希望缩短请求耗时、提升系统吞吐量的初中级技术人员。
1. 把 2000 条文本跑完从 40 分钟压到 3 分半:DeepSeek 批量请求与异步调用的真实收益
我在一个真实项目里第一次意识到,DeepSeek 调用方式的差别,远比模型本身更影响交付时间。那会儿要一天跑完约 2000 条电商评论,让模型做情感分类和标签抽取,最开始是 for 循环逐个请求,跑了 40 分钟还在继续。后来把请求组织成批量、再把等待时间交给事件循环,同一批数据 3 分半跑完。这篇要拆的批量请求与异步调用,就是这类“文件级文本处理”场景——评论分类、OCR 结果清洗、批量摘要、客服工单打标——最省钱也最直接的提效手段。适合需要大量调 API、又不想因为高并发把账户打爆的开发者。
2. 批量请求:合并通信开销,先把单条 for 循环换成一次往返
2.1 批量请求的工作流程与选型理由
批量请求的核心思路很简单:把多个子请求打包成一个请求体,一次连接、一次往返,服务器解析后返回组合结果。相比逐个请求,它省掉的是连接建立、TCP 握手、请求头重复传输这些固定开销。DeepSeek 这类大模型 API 单次请求动辄几百毫秒,里面网络往返占的时间比例不低,把 10 条文本捆在一起发,省下的往往是数百微秒到数毫秒的单条开销,批量数量越大收益越明显。
选型时还有一个容易被忽视的理由:服务端吞吐。每条请求都要经历鉴权、路由、模型推理排队,拆成 1000 个单条请求,服务端要处理 1000 次上下文切换;合并成 50 个批量请求,排队压力和连接数都降一个量级。我一般把批量请求作为“并发改造之前的第一版”,因为它改动最小,只需要调整请求组装逻辑。
需要说明的是,这里讲的“服务器返回组合结果”依赖你的 API 是否支持数组入参。多数大模型 API 的标准聊天补全接口更常见的是单条 messages 请求,此时批量要做的是“逻辑层合并”——把任务分流到若干批次,每批只发一个请求,而不是把一个数组直接塞给聊天补全接口。你拿到的 DeepSeek API 以哪种方式可用,第一件事就是看文档里的请求体格式。
2.2 构建批量数据、发送请求并解析响应
先装依赖,之后的所有示例都基于requests:
pip install requests然后写一个能直接跑通的批量处理骨架:
import requests import re import time API_KEY = "sk-你的密钥" API_URL = "https://api.deepseek.com/chat/completions" raw_items = [ {"id": 1, "text": "物流很快 包装有点破损"}, {"id": 2, "text": "电池续航一般 充电速度倒是快"}, {"id": 3, "text": "客服态度很好 退货也简单"}, ] def preprocess_text(text): # 清理多余空白,避免把空字符也当成输入 return re.sub(r"\s+", " ", text).strip() # 构造批量 payload:每个元素是一个完整的聊天补全请求 batch_data = [] for item in raw_items: batch_data.append({ "model": "deepseek-chat", # 以你账户可见的模型标识为准 "messages": [ {"role": "system", "content": "你是一个评论分类助手,只输出结果。"}, {"role": "user", "content": preprocess_text(item["text"])} ], "temperature": 0.2, }) headers = { "Content-Type": "application/json", "Authorization": f"Bearer {API_KEY}" } max_retries = 3 retry_count = 0 response = None while retry_count < max_retries: try: response = requests.post(API_URL, headers=headers, json=batch_data, timeout=30) response.raise_for_status() break except requests.exceptions.Timeout: print(f"请求超时,当前第 {retry_count + 1} 次重试") except requests.exceptions.HTTPError as e: if e.response.status_code in (429, 500, 502, 503): retry_count += 1 time.sleep(2 ** retry_count) else: print(f"HTTP 错误:{e.response.status_code}") break except requests.exceptions.RequestException as e: print(f"网络错误:{e}") retry_count += 1 time.sleep(2) if response is not None and response.status_code == 200: result = response.json() print(result)这里的timeout=30是请求级超时控制,防止某个批次把主流程拖死。重试侧面分别处理超时和 HTTP 错误,尤其是 429(限流)和 5xx(服务端异常),这两类故障重试才有效果,4xx 多半是参数问题,重试只是浪费时间。
2.3 响应解析与批次结果的顺序保持
DeepSeek 标准聊天补全接口的返回结构里,结果落在choices[0].message.content。如果每个批量元素都有独立id,我会在解析时把原始 id 和模型输出拼成一个可追踪的结构:
if response.status_code == 200: parsed = [] resp_json = response.json() # 单条聊天补全场景:模型返回在 choices[0].message.content content = resp_json["choices"][0]["message"]["content"] parsed.append({"id": 1, "result": content})这里要关注两个问题:一是结果顺序是否与入参一致,绝大多数接口是同步返回所以顺序一致,但稳妥做法是给每个子请求带上id,解析时按id对应回去,不要依赖数组下标。二是响应体里的usage字段,包含 prompt_tokens 和 completion_tokens,我会在批量场景里把它累加统计,用来估算成本,也能反过来判断某批结果是否异常短小。
2.4 批量请求的边界:什么情况不适合硬合并
批量不是越大越好。单批 payload 超出服务端大小限制时会收到 413;单批里混杂了完全不同的任务类型(比如一批做摘要、一批做分类),提示词不同会导致无法统一复用同一个 system prompt,需要按任务类型拆成多个批次。我在实际项目里的习惯是,把任务先按“模型参数一致 + 系统提示一致”分组,每组内部再按 10 到 50 条拆批,而不是拿到数据直接一把梭。
3. 异步调用:用事件循环把等待时间抢回来
3.1 为什么 I/O 密集场景下 Async 比线程池更划算
同步请求的最大问题是阻塞:你发一个请求后,程序就干等着网络返回,CPU 明明空闲却什么也做不了。线程池能解决一部分问题,但线程切换有成本,线程数量一多,调度开销反而吃掉收益。asyncio的做法是在单线程里用事件循环管理大量协程,遇到await就把控制权交出去,让程序在等待网络响应时去处理其他请求。
DeepSeek 调用是典型的 I/O 密集型任务,瓶颈在网络往返和模型推理等待,CPU 本地计算非常少。这个问题上用 asyncio,不需要管线程锁,也不用担心线程池炸掉内存,一套事件循环就能撑起几十上百个并发请求。Python 3.7 之后用asyncio.run()启动入口,配合aiohttp做 HTTP 客户端,是当前最顺手的组合。
3.2 最小可运行的异步调用 DeepSeek 实现
先装aiohttp:
pip install aiohttp然后实现一个异步调用函数:
import asyncio import aiohttp async def call_deepseek_async(session, payload): headers = { "Content-Type": "application/json", "Authorization": f"Bearer {API_KEY}" } try: async with session.post(API_URL, headers=headers, json=payload, timeout=30) as resp: if resp.status == 200: return await resp.json() else: print(f"HTTP {resp.status}") return None except Exception as e: print(f"调用异常:{e}") return None async def main(): batch_data = [ {"model": "deepseek-chat", "messages": [...], "temperature": 0.2} for _ in range(10) ] async with aiohttp.ClientSession() as session: tasks = [call_deepseek_async(session, p) for p in batch_data] results = await asyncio.gather(*tasks) valid = [r for r in results if r is not None] print(f"成功 {len(valid)} / {len(batch_data)}")asyncio.gather会并发执行所有传入的协程,并在所有任务完成后返回结果列表。这里有个容易忽略的细节:aiohttp.ClientSession需要复用同一个实例,不能每个请求创建一次 session,否则底层连接池形同虚设,并发收益会打折。
3.3 批量 + 异步结合:顺序保存与异常隔离
把前面两章的内容拼在一起时,最常见的错误是只同步返回结果数组,不检查单个协程的异常状态。gather默认遇到第一个异常就会向上抛出,导致已经成功的请求结果全部丢失。我一般会加上return_exceptions=True:
results = await asyncio.gather(*tasks, return_exceptions=True)这样每个失败的协程会返回一个异常对象,而不是直接中断整个流程。随后遍历results,对isinstance(r, Exception)的结果走单独的重试或落盘记录,成功的再解析内容。顺序问题同样靠入参里的id字段兜底,不要让结果处理逻辑假设数组顺序等于入参顺序。
4. 并发控制与重试:吞吐量能不能稳住,就看这两个参数
4.1 用 Semaphore 限制并发数
并发数不是越大越好。开 200 个协程同时打,账户限流一触发全是 429,重试又叠加新的并发,反而把服务端打挂。我一般第一版先限制并发 3 到 5,跑通后再逐步往上加。
sem = asyncio.Semaphore(5) async def limited_call(session, payload): async with sem: return await call_deepseek_async(session, payload)Semaphore(5)表示同一时刻最多 5 个协程进入async with sem内部,其他协程在门口排队。这个参数本质上是给账户限流留缓冲,同时避免本地文件句柄和内存被占满。实际取值没有标准答案,我通常先看单次请求的平均耗时:如果平均要 1 秒,并发 10 大约能撑每秒 10 次请求;如果账户配额是每分钟 60 次,那并发 5 已经足够。
4.2 指数退避重试:429 和 5xx 分开处理
重试策略最怕的是“失败就立刻重试”,这在高并发下会形成重试风暴。经验做法是:429 用指数退避加随机抖动,5xx 可以适当重试,4xx 一律不重试。
import random async def call_with_retry(session, payload, max_retries=3): for attempt in range(max_retries): try: async with session.post(API_URL, headers=headers, json=payload, timeout=30) as resp: if resp.status == 200: return await resp.json() if resp.status in (429, 500, 502, 503): delay = 2 ** attempt + random.uniform(0, 1) print(f"HTTP {resp.status},等待 {delay:.2f}s") await asyncio.sleep(delay) else: return None except Exception as e: delay = 2 ** attempt + random.uniform(0, 1) await asyncio.sleep(delay) return None2 ** attempt是重试基础间隔,第 0 次重试等 1 秒,第 1 次等 2 秒,第二次等 4 秒。random.uniform(0, 1)加抖动是为了让多个协程的重试时间错开,避免所有请求同时醒来重新打向服务器。这个方法在线上效果很直接,我几次翻车都是没加抖动,大量重试请求在同一秒涌进去,导致限流时间被拉长。
4.3 动态调整并发:小数据探底、中数据验证、全量放行
固定并发数能解决大部分问题,但遇到一天要跑几十万条数据时,固定值容易偏保守。我一般按三步走:先用 50 条数据、并发 5 跑一轮,看错误率和平均耗时;再把并发调到 10、20 各跑一轮,记录错误率拐点;最后在错误率低于 1% 的前提下,用拐点并发跑全量。所谓“动态调整”不需要上什么自动化框架,把并发数做成一个命令行参数,每次跑前手动验证一次就行,比写一堆自适应逻辑更可控。
5. 避坑与排错:DeepSeek 批量异步调用中的五个典型问题
5.1 请求超时重试后,整体耗时反而翻倍
现象:单批请求 30 秒超时,重试 3 次后成功,但整批任务耗时从 3 分钟涨到 15 分钟。
原因:并发设置过高,单次请求排队时间已经超过客户端超时阈值。服务端没拒绝请求,只是处理不过来,重试让同一批数据反复排队。
解决:把并发数降下来,同时把超时从 30 秒提到 60 秒。优先保证请求不超时,再用重试兜底。我踩过这个坑之后,都把超时和并发当成一对参数调,并发翻了倍,超时必须同步放大。
5.2 批量请求返回 413,payload 太大被服务端拒绝
现象:一批塞了 100 条文本,请求直接返回 413 Request Entity Too Large。
原因:单批请求体超过服务端限制,常见于长文本场景。
解决:按字符数或条数拆批。我一般先估算单条请求体平均大小,然后控制单批总大小在 1MB 以内,长文本场景每批 10 条起步,短文本可以到 50 条。拆分逻辑写成函数,传入文本列表和批次上限,自动切分后再逐批处理。
5.3 异步任务失败但日志里什么都没有
现象:gather 返回后结果缺失,也没有任何错误输出。
原因:协程内部把异常吞掉了。早期版本里我在 except 块里只打印e,但打印本身放在return None之前,异常对象被覆盖,后续排查拿不到任何有效信息。
解决:异常处理里先记录完整堆栈,再决定返回默认值还是重试。使用traceback.format_exc()把堆栈存到日志,不要把 exception 对象直接转字符串。从那以后我每个异步调用函数都强制要求:要么抛出可重试异常,要么返回带错误码的结果对象,不允许静默返回 None。
5.4 本地 Windows 环境跑 async 代码报事件循环错误
现象:代码在 Linux 上正常,Windows 上运行时报RuntimeError: Event loop is closed。
原因:Windows 下asyncio的事件循环策略和 Proactor 模式有差异,某些 Python 版本在重复调用asyncio.run()时会触发旧事件循环残留问题。
解决:入口只调用一次asyncio.run(main()),不要在循环里反复创建事件循环。如果代码被外部框架调用,使用asyncio.get_event_loop()配合run_until_complete()兜底,而不是在函数内部擅自创建新事件循环。
5.5 鉴权失败 401,Authorization 头拼接方式不对
现象:批量请求全部返回 401,检查密钥没发现问题。
原因:Authorization 头必须是"Bearer " + 密钥的形式,中间有空格,或者密钥被去掉了前缀。另外,如果在代理环境下请求,头信息可能被代理改写。
解决:先打印请求头的实际值确认格式,再确认代理环境不会改写鉴权头。排查时我会用一条最小请求单独测,省得混在批量里难定位。
6. 用一套压测脚本验证“10 倍”:同步循环与异步批量的对比基准
6.1 快速对比压测脚本
要验证是否真有 10 倍提升,不要凭感觉,直接跑一轮同数据量的对比。压测脚本的核心逻辑是:同样的任务集合,分别用同步 for 循环和异步 Semaphore 调度执行,记录总耗时、成功率和错误分布。
import asyncio import time def run_sync(tasks_data): start = time.time() results = [] for item in tasks_data: resp = requests.post(API_URL, headers=headers, json=item, timeout=30) results.append(resp.status_code) return time.time() - start, results async def run_async(tasks_data, concurrency=5): start = time.time() sem = asyncio.Semaphore(concurrency) async with aiohttp.ClientSession() as session: async def one(item): async with sem: return await call_with_retry(session, item) results = await asyncio.gather(*[one(x) for x in tasks_data]) return time.time() - start, results # 用同一批任务对比 sync_time, sync_res = run_sync(batch_data) async_time, async_res = asyncio.run(run_async(batch_data)) print(f"sync={sync_time:.2f}s, async={async_time:.2f}s, 提升={sync_time/async_time:.1f}x")这里每一步都只统计了耗时和状态码分布。真正压测时我还会记录错误码明细,单独统计 429 重试次数。如果异步跑下来提升不到 3 倍,先看并发是不是设太低,再看是不是本地网络或代理成了新瓶颈。
6.2 怎么读压测结果
压测结果里的平均耗时会被个别慢请求抬高,我更关注 P95 和错误率。P95 越小,说明大部分请求的响应时间稳定;错误率如果超过 1%,优先降并发而不是加超时。另外要对比的是重试次数,重试多说明并发已经逼近限流阈值,继续提高并发只会让调整个更慢。
6.3 我从那次之后固定的三步走
现在每次接 DeepSeek 相关任务,我都强制先过一遍小样本验证,再放量。第一步用 50 条数据配并发 5 跑通流程,确认鉴权、解析、落库全链路正常;第二步用 500 条数据调并发 10 和 20,记录错误率拐点;第三步才用全量数据开盘跑。开盘时保留并发数参数,线上运行中如果错误率突然上升,先降一半并发而不是立即调大概率重试等待。这套流程把“能不能跑”和“能跑多快”分开验证,数据清洗、评论分类、批量写摘要这些场景都能套用。那次 40 分钟压到 3 分半的改造之后,我再也没用裸 for 循环去调任何大模型 API,凡是超过 100 条的任务,第一反应就是先拆批、再上并发。希望这套拆解和踩坑记录帮得到你。
本文还有配套的精品资源,点击获取