5 步 · 5 段可复制提示词 · 约 11 分钟读完
站内精选
核心结论

批量调 API 崩掉的根因,不是并发开太大,而是重试和续跑没设计:500 条跑到 380 条报 429,结果全丢只能重来。四件套是——任务落 jsonl、ID 用内容哈希、只对 429/5xx 退避重试、结果逐行追加并按 ID 跳过。实测 3 并发 26 分钟跑完、429 不到 1%;8 并发 429 到 34%,反而慢 1 分钟。中断一次损失 0 条。

批量调 API 最容易出问题的地方,不是并发开多大,而是重试和续跑这两件事没设计。500 条任务跑到第 380 条报 429,脚本一崩,前面全白跑。本文给一套四件套:任务落文件、并发限死、退避重试、按 ID 跳过。实测 8 并发跑 31 分钟,3 并发跑 26 分钟——开得多反而更慢。

一句话结论

批量任务的稳定性来自把「跑什么」和「跑到哪了」都落到文件里,而不是靠把并发调小。500 条摘要任务的实测:8 并发触发 429 的比例 34%,重试把总耗时推到 31 分钟;3 并发 429 比例不到 1%,总耗时 26 分钟。任务清单用一行一条的 jsonl、ID 取内容哈希、结果追加写并每行落盘、启动时按 ID 跳过已完成的——中断一次损失 0 条。整套骨架 80 行代码,改一次所有批处理都能复用。

这篇解决什么问题

三种情况最常见。一是把 500 条任务塞进一个 for 循环、结果攒在列表里最后统一写文件,跑到中途报错,内存里的全丢。二是遇到 429 就 while 循环硬重试,越重试越堵,最后整段时间被重试请求占满。三是直接提交 500 个并发任务,前 10 秒就被限速,之后全是失败重试,你还以为是自己额度不够。

根子是把批量调用当成「发请求」,而它其实是「一个有状态的长任务」:要有清单、有进度、能从断点接着跑、有失败清单可单独补。五步做完,得到任务清单、结果文件、失败清单三份东西,以及一个换任务就能复用的脚本。

开始前要准备什么

  • 任务清单,先落成文件再开始跑:一行一个 JSON,带一个稳定 ID。别在代码里用列表硬编码 500 条——那样每次改任务都要动代码,也没法记录进度。
  • 一个能算出来的 ID:用任务内容的哈希(比如 MD5 前 12 位),不要用序号。序号会随筛选条件变化,你重跑一次顺序变了,就分不清哪条是完成的。
  • 模型的限速区间,先小规模探一次:用 10 条任务、2 并发跑一轮,看有没有 429、平均耗时多少。这个数字决定后面并发设几,比看文档准。
  • 结果字段先定死:ID、原文、模型输出、耗时、状态、错误信息。字段定不下来,后面汇总还得再洗一遍数据。

具体怎么做

第一步:任务落成 jsonl,一行一条、ID 用内容哈希

jsonl 能追加、能流式读、坏一行不影响其他行。每条固定四个字段:ID、输入、任务类型、期望输出长度。ID 用「输入 + 任务类型」的哈希——同一段文字换个任务类型不会被误判为已完成,重复输入也能自然去重。

我有 500 条待处理内容,结构是:每条一个 dict,字段为 text 和 category。

请帮我写一个生成任务清单的脚本,输出 tasks.jsonl:
1. 每行一个 JSON:{“id”: …, “text”: …, “category”: …, “type”: “summary”};
2. id = md5(text + type) 的前 12 位,不要用序号;
3. 生成时按 id 去重,重复的打印出来并跳过;
4. 打印统计:总条数、去重后条数、被去重的条数。

另外说明:为什么用内容哈希而不是自增序号,会带来什么代价(提示:如果原文改了一个字会怎样)。

第二步:先探吞吐,再把并发写死

并发不是越大越好。先跑 10 条、2 并发,记下平均耗时和有没有 429,再乘以 3 试一次。实测这个模型单条平均 8.5 秒(输出 600 字左右),3 并发时 429 比例不到 1%,8 并发时飙到 34%——超过阈值后每多一个并发,都在给重试队列添活。把并发写成一个常量,别用自动伸缩,你控制不了对端的限速策略。

这是我要调用的接口信息:(地址、模型名、单条平均输出长度)
这是小规模探测结果:10 条 / 2 并发,平均耗时 ____ 秒,429 次数 ____。

请帮我判断:
1. 按这个结果,稳态并发设几比较合适,依据是什么;
2. 如果我要提速,应该加并发还是缩短单条输出,哪个风险更低;
3. 列出 3 个会掩盖真实吞吐的因素(提示:重试、超时、输出被截断)。

第三步:429 和 5xx 退避重试,4xx 一个都不重试

两类错误要分开处理。429(限速)和 5xx(服务端)值得重试,用指数退避加随机抖动:第 1 次等 2 秒、第 2 次 4 秒、第 3 次 8 秒,每次再加 0 到 1 秒随机量,避免所有线程同时醒来再撞一次墙。400 / 401 / 402 重试一百次也一样,直接记失败。

这是我的调用函数和当前的重试逻辑:(粘贴代码)

请改写重试部分,要求:
1. 只对 429 和 5xx 重试,最多 3 次;400/401/402 立即返回失败,不重试;
2. 退避时间 2/4/8 秒,每次叠加 0~1 秒随机抖动;
3. 每次重试打印:第几次、等待多久、错误码;
4. 3 次都失败时,把错误码与最后一次响应体记进结果行,不要抛异常中断整批。

然后告诉我:为什么必须加随机抖动,不加会发生什么。

第四步:结果追加写、每行落盘,启动时按 ID 跳过

这一步决定「中断一次损失多少」。结果写到 results.jsonl,每完成一条就 write 并 flush,不要攒批。启动时先读一遍 results.jsonl 收集已完成 ID,剩下的才跑。中断、Ctrl+C、重启都不怕,重跑就是接着跑。实测在 380/500 处强杀进程,重跑只补了 120 条。

这是我的结果文件写入逻辑:(描述或粘贴)

请帮我改成可续跑的结构:
1. 写入用追加模式,每条结果 write 之后立即 flush;
2. 启动时先扫描 results.jsonl,收集 status=ok 的 id 集合;
3. 遍历任务时,命中的直接跳过,并打印「已跳过 N 条」;
4. 如果 results.jsonl 最后一行是半截(进程在写的时候被杀),要能识别并丢弃,不能让下一行拼在它后面。

最后解释:为什么必须逐行 flush,攒 50 条写一次看起来更快,实际风险在哪。

第五步:失败单独出一份清单,重跑只跑失败的

跑完不要只看「成功 468 条」这个数字。把 status 不是 ok 的挑出来写成 failures.jsonl,它就是下一轮的输入清单——格式跟 tasks.jsonl 一致,补跑不用改代码,换个文件名就行。实测 500 条里 32 条失败:24 条输出超长被截断、8 条内容触发拒绝,补跑后剩 8 条人工写。

这是跑完之后的 results.jsonl(500 行)。

请帮我生成一份汇总,并产出补跑清单:
1. 按状态统计:成功 / 失败 / 输出被截断,各多少条;
2. 失败的按错误码归类,每类给 2 个例子;
3. 把可重试的失败(429、5xx、超时)写成 failures.jsonl,格式与 tasks.jsonl 完全一致;
4. 剩下不可重试的,单独列一张表,写明每条为什么失败(需要人看一下的那种)。

注意:不要按我印象里的数字报,一切以文件内容为准。

实测记录

任务是给 500 条产品描述写 200 字摘要,模型 deepseek-chat,单条平均输出 600 字、耗时 8.5 秒。第一版 8 并发、无退避、结果攒批写:跑到 380 条时连续 429,脚本抛异常退出,内存里的 380 条结果全丢,重跑一遍又花了 31 分钟,最终还是没跑完。

第二版加了三件事:jsonl 逐行写、429 退避加抖动、启动按 ID 跳过。3 并发跑完 26 分钟,429 比例不到 1%,成功 468 条。剩下的 32 条里 24 条是输出被 max_tokens 截断(摘要写到一半断了),把上限从 800 提到 1200 后补跑,剩 8 条是内容本身触发了拒绝。中途故意在 380 条处强杀进程一次,重跑只补了 120 条,损失 0。

顺带测了 8 并发加退避:429 比例 34%,总耗时 27 分钟,比 3 并发还慢 1 分钟,失败率高出十倍。超过阈值之后,多出来的并发只是在排队等重试。

最容易错的 3 个地方

第一,用序号当 ID。序号会随筛选条件变化,你今天按分类 A 跑一半,明天加上分类 B 再跑,序号全错位,跳过逻辑就失效——不是重复跑就是漏跑。用内容哈希,改一个字就是新任务,这是符合预期的行为。

第二,把超时当成失败去重试,却没去重计费。请求超时只代表没等到响应,不代表对端没处理,重试会产生重复结果,下游按条计数就对不上。做法是结果按 ID 去重只留最后一次,计费以对端账单为准。

第三,只看成功条数就收工。468/500 看起来不错,但剩下的 32 条里可能有 8 条是内容有问题——这类补跑一百次也不会成功。跑完必须按错误码分组看一遍,把「可重试」和「需要人处理」分开,否则下一轮还是同样的数字。

可复用的东西

四件套结构:tasks.jsonl(清单)+ results.jsonl(逐行追加)+ failures.jsonl(下一轮输入)+ 一个常量并发数。换任务内容不用改代码。

ID 用内容哈希:md5(内容 + 任务类型) 取前 12 位,天然去重、跨批次不冲突。

退避参数:2 / 4 / 8 秒加 0~1 秒抖动,最多 3 次,只对 429 和 5xx 生效。

失败分类两分法:可重试(限速、5xx、超时)与需人工(内容被拒、超长截断)。前者进 failures.jsonl 自动补跑,后者进表格人工处理。

常见问题解答

为什么 8 并发反而比 3 并发慢

超过阈值之后多出来的请求只是在排队等重试。实测 500 条任务:3 并发跑 26 分钟、429 比例不到 1%;8 并发 429 比例 34%,退避把总耗时推到 27 分钟,失败率还高十倍。任何批量任务先跑 10 条 2 并发探一次真实吞吐,再定并发,比查文档准。

哪些错误码应该重试,哪些不该

只重试 429 和 5xx。1000 次重试也解决不了 400 参数错、401 鉴权失败、402 余额不足,这三类直接记失败。退避用 2/4/8 秒并叠加 0 到 1 秒随机抖动,最多 3 次——不加抖动的话所有线程会同时醒来,再撞一次墙。

中断之后怎么做到不重跑

三个动作:任务清单落成一行一条的 jsonl;结果用追加模式写,每条写完立即 flush;启动时先扫 results.jsonl 收集已完成 ID,命中就跳过。实测在 380/500 处强杀进程,重跑只补了 120 条。另外要能识别结果文件最后一行是半截,丢掉它,否则下一条会拼在断行后面。

任务 ID 为什么不能用序号

序号会随筛选条件变化。今天按分类 A 跑一半,明天加上分类 B 再跑,序号整体错位,跳过逻辑就失效——不是重复跑就是漏跑。用 md5(内容 + 任务类型) 取前 12 位,天然去重,跨批次不冲突,原文改一个字就算新任务。

小程序阅读免费
微信扫码进入小程序,按岗位找 AI 实操步骤
微信扫码进小程序
按岗位找 AI 实操步骤 · 提示词可收藏

发表回复

后才能评论