把一段单请求代码改成批量脚本,循环写起来不难,任务状态最容易漏掉。请求发出去以后,客户端可能超时,服务端却已经完成生成;程序崩溃以后,内存里的进度也会消失。脚本若只把成功结果追加到一个文件,中断后往往不知道该从哪里继续,也无法确认重新执行会不会生成重复数据。
先把任务和调用尝试分开
每条输入先分配稳定的item_id,任务表保存输入、目标模型、最终状态和已接受的结果。另建尝试记录,保存本次请求的开始时间、结束时间、HTTP状态、请求标识、错误类别和消耗。一次任务可能经历多次尝试,但最终只能接受一个结果。
这样的拆分能处理一个常见情况。客户端等待超时,并不能证明服务端没有完成请求。此时任务先进入待核对状态,程序根据供应商提供的请求标识、调用日志或本地结果判断,确认没有可用结果后再重试。直接把超时当作失败重新发送,可能产生重复内容和额外费用。
状态可以按项目设计为待处理、执行中、待核对、成功、可重试失败和永久失败。状态名不是行业标准,关键是每次变化都有原因,并且程序重启后仍能从持久化存储恢复。SQLite适合小规模本地实验,更大的任务可以换成团队已有数据库,设计原则不变。
下面这段代码只演示领取任务时的原子更新。它没有包含真实API调用,也没有把表名和状态名写成通用标准。
python
def claim_one(conn, worker_id, lease_until):
row = conn.execute(
"SELECT item_id FROM tasks WHERE status = ? ORDER BY item_id LIMIT 1",
("pending",),
).fetchone()
if row is None:
return None
changed = conn.execute(
"""UPDATE tasks
SET status = ?, lease_owner = ?, lease_expires_at = ?
WHERE item_id = ? AND status = ?""",
("running", worker_id, lease_until, row[0], "pending"),
).rowcount
conn.commit()
return row[0] if changed == 1 else None
多线程同时领取时,生产代码还需要明确事务模式,并处理数据库忙和进程崩溃。代码里的条件更新负责确认任务仍处于待处理状态。其他线程已经领取后,rowcount会是零,当前线程重新取下一条即可。
重试要看错误类型
网络临时故障、部分限流和部分服务端错误可能适合有限重试。认证失败、无效参数和明确的权限问题通常需要先修配置,持续重试只会重复失败。内容格式不合格属于验收失败,也不应与HTTP失败混在一起,它可能需要改提示词、换教师或进入人工处理。
遇到429时,可以优先遵守响应中实际提供的Retry-After。服务没有给等待时间,再使用带随机扰动的指数退避,避免许多任务在同一秒重新涌入。5xx也不应无限重试,客户端要设置最大次数、总等待时间和整批错误率停止条件。具体状态是否适合重试,还要结合供应商文档和请求能否安全重复判断。
并发控制最好从小档位开始。记录每个档位的完成率、吞吐、尾部延迟和限流情况,找到稳定区域后再提高。把工作线程开到机器允许的最大值,往往会让排队和重试同时增加,最后完成得更慢。
幂等和断点续跑要落到数据库
客户端可以用item_id加任务版本生成幂等键,本地数据库再为它建立唯一约束。工作线程领取任务时写入租约和过期时间,完成后以条件更新方式提交结果。进程异常退出,租约到期的任务可以被重新领取;已经成功的任务不会再次进入队列。
结果文件应由数据库中的已验收记录导出,不要让多个线程直接向同一个JSONL文件随意追加。这样即使程序中途停止,数据库仍保存任务状态;恢复后只处理待执行、可重试或租约过期的任务。导出时再检查唯一ID、必填字段和数据集版本,避免半行内容或重复记录进入训练集。
断点续跑还需要运行批次。一次启动记录run_id、配置哈希、代码版本和任务范围。下次恢复时先核对配置是否一致,教师模型或提示词已经变化,就应创建新批次,不要把两套条件的结果悄悄混在一起。
统一接口只减少接入工作
需要比较多个教师时,147AI可以作为统一API候选入口,帮助团队减少多套接口适配,并结合当前页面提供的调用记录核对模型与消耗。客户端仍要保存本地任务状态、验收结果和数据集版本。平台接入不能代替幂等、自动停止和断点恢复,也不能证明教师输出已经获得训练许可。
批量脚本真正完成的标准,是任何一条任务都能回答当前状态、经历过哪些尝试、为什么被重试以及最终结果从哪里来。并发只决定跑得多快,这些记录决定任务失败以后还能不能继续。