脚本里接 Agnes-2.5-Flash 批量跑文本 / 先补上超时、重试和失败落盘

文章导读
循环批量调用 Agnes-2.5-Flash,跑了几十条之后开始报错,或者整批停在一个请求上不动,通常不能先归因于模型侧限流;更多时候是脚本没有设置超时、重试没有上限,失败的那几条请求被直接跳过或吞掉。建议先把超时、重试和失败落盘这三层兜底补上,再去看并发和吞吐;否则报错信息不完整,排查会绕很多弯路。
📋 目录
  1. 壹 先用最小请求确认通路
  2. 贰 区分连接超时和读取超时
  3. 叁 给重试设上限和退避
  4. 肆 把失败请求完整落盘
  5. 伍 小批量验证后再放量
A A

循环批量调用 Agnes-2.5-Flash,跑了几十条之后开始报错,或者整批停在一个请求上不动,通常不能先归因于模型侧限流;更多时候是脚本没有设置超时、重试没有上限,失败的那几条请求被直接跳过或吞掉。建议先把超时、重试和失败落盘这三层兜底补上,再去看并发和吞吐;否则报错信息不完整,排查会绕很多弯路。

批量跑文本前,先确认单条请求确实能通,再把连接超时和读取超时分开设置;重试只针对 429、5xx 和传输层异常,并设次数上限与递增退避;每条失败请求连同状态码、耗时和响应体原文按行落盘;最后用二十条小批量看真实失败率再决定并发上限。适用场景是脚本侧自建调度;字段名、状态码语义和限流策略需要以随用文档和实际响应为准。

先用最小请求确认通路

把怀疑对象从脚本逻辑挪到请求本身:先发一条固定输入,确认鉴权、请求体字段、返回结构三件事。地址、密钥、模型名一律用占位符,路径和字段名以随用文档为准,不要凭记忆拼端点。

POST {BASE_URL}{CHAT_PATH}
Authorization: Bearer {API_KEY}
Content-Type: application/json

{
  "model": "{MODEL_NAME}",
  "messages": [
    {"role": "user", "content": "{PROMPT}"}
  ]
}

跑通之后把返回体原样打印一次,看清字段层级:顶层一般有请求标识和模型名,正文通常在 choices 数组的元素里,具体层级以你拿到的响应为准。这一步的验证方式是——能从响应里稳定取出文本内容,才算通路成立;取不到就说明路径或字段名对不上,先别进入批量循环。同时记下这次请求的状态码和响应体的顶层键名,后面写落盘字段时会用到。

区分连接超时和读取超时

连接超时是还没建立连接就放弃,通常在几秒内结束;读取超时是请求已经发出、在等模型生成返回的时间,长输入、长输出都会拉长这一段。两者混成一个超时值,最容易把「模型还在生成」误判成「网络故障」,然后触发一批无谓重试。

脚本里接 Agnes-2.5-Flash 批量跑文本 / 先补上超时、重试和失败落盘
import httpx

timeout = httpx.Timeout(connect=5.0, read=60.0, write=10.0, pool=5.0)
client = httpx.Client(timeout=timeout)

连接超时可以设得短一些,比如几秒;读取超时要按你最长的输入和输出预期来定,建议先按业务里最慢的一批留出余量,再从小批量结果里回调。用 requests 的话,超时参数可以写成 (connect, read) 元组,语义相同。

日志里的表现也要分开记:连接超时通常拿不到 HTTP 状态码,异常类型是连接类超时或连接错误;读取超时往往已经建立了连接,异常类型是读取类超时,状态码字段为空。所以日志里不能只写 status_code,要同时写 error_type,否则两类问题在事后看是一模一样的空白。

给重试设上限和退避

不加区分地重试,会把一次失败放大成几次调用量,在本来就限流的时段尤其明显。可以按状态码和异常类型分流:

脚本里接 Agnes-2.5-Flash 批量跑文本 / 先补上超时、重试和失败落盘
  • 可重试:408、429、500、502、503、504,以及连接超时、读取超时、连接被重置这类传输层异常。
  • 不建议重试:400、401、403、404、422。参数写错、密钥无效、模型名不对,重试只会重复失败,应当改请求体或配置。

最大重试次数建议 2 到 3 次;退避用指数递增加随机抖动,比如第一次等 1 秒、第二次 2 秒、第三次 4 秒,再叠加一点随机量,避免同一时刻的请求整齐地再打一遍。

import random

RETRYABLE_STATUS = {408, 429, 500, 502, 503, 504}

def should_retry(status_code, err):
    if status_code in RETRYABLE_STATUS:
        return True
    return isinstance(err, (httpx.ConnectTimeout, httpx.ReadTimeout, httpx.ConnectError))

def backoff(i, base=1.0):
    return base * (2 ** i) + random.uniform(0, 0.5)

重试前是否调参,按失败类型决定:429 优先降并发或拉长请求间隔;读取超时先调大读取超时再加次数;4xx 停下来改请求,不要重试。这条分流规则写进代码,比事后看日志猜要省事。

把失败请求完整落盘

失败记录要能让别人离线复现同一次调用。建议用 JSONL,一行一条,追加写入,字段至少包含下面这些:

脚本里接 Agnes-2.5-Flash 批量跑文本 / 先补上超时、重试和失败落盘
{"ts":"{ISO8601_TIME}","request_id":"{REQUEST_ID}","model":"{MODEL_NAME}","attempt":3,"status_code":503,"elapsed_ms":1234,"error_type":"ReadTimeout","input":"{原始输入}","response_body":"{响应体原文}"}
  • 时间:用带时区的 ISO8601,方便和其他日志对齐。
  • 输入:保留原始输入原文;输入很长时可以存哈希加截断快照,但要保证能还原。
  • 状态码与异常类型:没有状态码时写 null,同时必须有 error_type。
  • 耗时与重试次数:elapsed_ms 记这一次的,attempt 记这是第几次尝试。
  • 响应体原文:按合理长度截断,同时记录原始长度,避免单行过大。

文件命名建议 failures-YYYYMMDD-NN.jsonl,按天轮转,单个文件超过约定大小就换下一个序号;写完一条立即 flush,避免进程中断丢掉缓冲区里的最后几条。验证方式是随机挑一行,用同样的输入和参数重放,看能否复现相同的状态码或异常类型。

小批量验证后再放量

不要一上来就开高并发。先跑二十条,统计三样东西:成功与失败条数、失败按状态码和异常类型的分组、单条请求的耗时分布(均值或中位数都行,别只看自己手掐的总时长)。这二十条的意义是拿到你所在环境下的真实失败率,而不是估一个。

据此设初始并发,建议从 1 到 2 起步,每次翻倍,每档都跑同样条数的批次,对比失败率与耗时是否明显变差。判断条件可以这样用:失败集中在 429,说明是频率问题,回退到上一档并发或拉长间隔;失败集中在读取超时,先加大读取超时再看;失败集中在 4xx,说明请求或鉴权有问题,停下改代码,不放量。若失败率上升且新增失败主要是限流与超时,就回退到上一档,不继续加。可接受的失败阈值需要结合业务容忍度定,这里没有通用数字。