Gemini 3.5 Flash-Lite 这类轻量档模型适合单词标签、单行 JSON、短分类这种输出很短的任务。一旦批量任务放量后变慢,先不要急着调整并发参数,而应分别确认三件事:单条请求本身要多久、长输出任务占了多少、并发开多大才开始出现超时或限流。建议的顺序是:先在无并发干扰下测出单条延迟基线,再按输出长度把任务分成两类,最后才在脚本里加并发上限和退避。跳过分层直接提高并发,常见结果是短答任务被长答任务拖慢、重试互相叠加,日志里只看到失败数上升,根因却在于任务结构混杂。
Gemini 3.5 Flash-Lite 跑批量分类,瓶颈通常不在模型单条速度,而在长短任务混跑。先用二十条左右真实样本记录请求体长度、输出长度、首包时间和总耗时,拿到无并发干扰的基线;再按输出长度把任务分成短答和长答两类;最后给脚本加并发上限和带抖动的退避。分层是否值得保留,要小流量灰度对比分层前后的总完成时间和失败重试次数,没有改善就说明瓶颈在并发或链路侧。
用二十条真实样本跑一轮单条调用,记下每次耗时
这一步只做一件事:拿到一个不存在并发争抢的单条耗时区间,作为后面所有对比的参照。执行方式是一条一条顺序发,不并发,每条之间留短暂间隔,避免前一条还在跑就开始下一条。如果环境里已经有别的批量在跑,最好换一个时间段做。
记录字段建议固定成下面几列,字段一旦确定就不要中途改统计口径,否则前后基线不可比。
| 字段 | 取值方式 | 用途 |
|---|---|---|
| 请求体长度 | 按字符数或 token 统计,二选一并固定 | 判断输入是否也影响耗时 |
| 输出长度 | 按返回内容实际长度统计 | 后面分短答/长答的依据 |
| 首包时间 | 从发出请求到收到第一个可见内容 | 区分“排队等”和“生成慢” |
| 总耗时 | 从发出到内容接收完毕 | 批量完成时间的核心变量 |
| 状态 | 成功 / 超时 / 限流 / 解析失败 | 判断基线里是否已混入异常 |
样本来源要说明清楚:从线上真实待处理池里随机抽取,覆盖你实际会遇到的分类类别,不要只挑短样本。短答和长答各抽一部分,并刻意包含一两条明显超长的样本,否则基线会偏乐观。二十条是下限,用来看出量级;如果你要拿来做容量估算,样本量需要相应放大。
样本编号,类别,请求体长度,输出长度,首包时间_ms,总耗时_ms,状态,备注
01,短答,320,6,,,成功,
02,短答,410,4,,,成功,
03,长答,860,240,,,成功,多字段JSON
04,长答,790,180,,,成功,
把待处理任务按输出长度分成短答和长答两摞
分摞依据用输出长度,不用输入长度。批量分类的成本和耗时主要由生成侧决定,输入再长,只输出一个标签也还是轻任务;输入很短却要求给出多字段 JSON 和理由,反而更重。
- 短答这一类:单个标签、枚举值、是/否、一行 JSON,输出通常在几十个字符以内。
- 长答这一类:多字段 JSON、需要给出理由、需要抽取多个实体、带解释的分类结果。
阈值不要凭感觉定,先看第一轮二十条样本的实际分布,取一个能覆盖大部分短答样本的长度作为界,比如以 100 字符或一个短句为界。这个界后面灰度时可以再调,但调整时要同步说明依据。
每一类建议至少十条。短答类可以略多,因为真实批量里短答占比通常更高。两类的耗时区间要并排放在一起看,不是看平均值,而是看最小值、中位数、最大值和首包时间中位数:如果长答的最大值远高于短答的中位数,而且长答在待处理队列里数量不少,那拖慢整体的就是长答这一类,分层的必要性就成立了。如果两类的耗时区间基本重合,分层能带来的收益有限,这时应把精力放回并发和链路排查上。
在脚本骨架里加并发上限和带抖动的退避
并发上限的取值不要看别人写多少,要自己走一遍:从并发 1 开始,按 2、4、8 递增,用同一批样本跑,观察失败数、超时次数和总完成时间的变化;取在失败数明显抬升之前的那一档,再往回收一档作为日常值。这个上限和账号配额、单请求长度、长答占比都相关,需要结合你的实际环境确认。短答和长答两类建议分开设上限,不要共用一个通道。
MAX_CONCURRENCY = 从配置读,不要写死在业务代码里
MAX_RETRY = 3
BASE_DELAY_MS = 从配置读
JITTER_MS = 从配置读
stats = {ok: 0, retry: 0, fail: 0, fail_by_type: {}, retry_hist: {}}
def handle(item):
for attempt in 0..MAX_RETRY:
try:
t0 = 记录开始时间
resp = 调用模型(item.prompt, timeout=按分层配置)
stats.ok += 1
记录(首包时间, 总耗时, 请求体长度, 输出长度)
return 解析(resp)
except 可重试错误 as e: # 超时、限流、服务端临时错误
stats.retry += 1
stats.retry_hist[attempt] += 1
if attempt == MAX_RETRY:
stats.fail += 1
stats.fail_by_type[错误类型] += 1
return None
sleep(BASE_DELAY_MS * (2 ** attempt) + 随机抖动)
except 不可重试错误 as e: # 参数错误、内容校验失败
stats.fail += 1
stats.fail_by_type[错误类型] += 1
return None
用线程池或异步队列跑,池大小 = MAX_CONCURRENCY
短答池和长答池各一个,互不挤占
失败计数要按错误类型分开记,不要只留一个总失败数,否则你分不清是限流还是内容解析失败。重试次数分布也要记,能看出是偶发抖动还是持续打满。退避一定带随机抖动,否则一批请求会在同一时刻重新涌上来,把刚恢复的通道再次打满。日志里每条至少留下样本编号、类别、尝试次数、耗时和错误类型,灰度对比时直接用这批日志聚合。
小流量灰度跑一轮,对比分层前后的完成时间
灰度样本量建议控制在五十到两百条之间,从线上同一分布里抽,必须包含长答样本。不要拿第一轮那二十条当灰度样本,它们已经被看过、被调过,不具代表性。
| 对比字段 | 分层前 | 分层后 |
|---|---|---|
| 样本数 | ||
| 总完成时间 | ||
| 单条耗时中位数 | ||
| 单条耗时 p95 | ||
| 超时次数 | ||
| 重试次数 | ||
| 最终失败数(按类型) |
判据可以这样定:分层后总完成时间下降、失败数没有上升,说明分层有效,可以按分层后的并发上限逐步放量;总完成时间持平但失败数下降,说明分层带来的主要是稳定性,仍值得保留,但不必再加大投入;总完成时间和失败数都没变化,说明瓶颈不在任务结构,回头查并发上限、超时设置和链路侧;分层后失败数反而上升,先怀疑长答那一类单独跑时并发给得太高,把长答池的上限降一档再跑一轮。整套判断都基于你自己日志里的数字,不建议直接套用别人的阈值。