先算成本再写循环
批量任务的第一行代码不该是 for 循环,是一张预算表。20 万条记录,每条 600 token 输入、200 token 输出,用 glm-5.3-flash(稳定国模分组输入 0.16、输出 0.56 美元每百万 token):输入 1.2 亿 token 计 19.2 美元,输出 4000 万 token 计 22.4 美元,合计 41.6 美元。加上 20% 重试余量,预算按 50 美元报。
这个数要在写代码前算出来。算完发现超预算,就换模型或者先跑一万条看效果,别等跑完了才发现。
单条也要算清:600 token 输入是 0.000096 美元,200 token 输出是 0.000112 美元,一条约 0.0002 美元。这个数乘以总量是总预算,乘以剩余条数就是还能跑多久。
任务表和状态机
用一张 SQLite 表存任务,字段至少五个:id、payload、status、attempts、result。状态只有 pending、running、done、failed 四种。跑之前先把输入全部写进表,跑的时候只改状态。
输入文件的格式尽量规整,一行一条 JSON,别用嵌套很深的 Excel。写不进表的数据,跑到一半会变成一堆手工例外。
表放在本地文件里就够,几十万行的读写压力很小,还能随时用 SQL 查进度。
建表时给 status 加索引,claim 的查询才不会全表扫描。payload 存原始输入,别存处理后的文本,重跑的时候才知道当时到底发了什么。
并发池骨架
import json
import sqlite3
import threading
from concurrent.futures import ThreadPoolExecutor
db = sqlite3.connect("jobs.db", check_same_thread=False)
lock = threading.Lock()
def claim(limit: int = 32):
with lock:
db.execute("BEGIN IMMEDIATE")
rows = db.execute(
"SELECT id, payload FROM jobs WHERE status='pending' ORDER BY id LIMIT ?",
(limit,),
).fetchall()
db.executemany(
"UPDATE jobs SET status='running', attempts=attempts+1 WHERE id=?",
[(r[0],) for r in rows],
)
db.commit()
return rows
def finish(row_id: int, status: str, result: str | None = None):
with lock:
db.execute(
"UPDATE jobs SET status=?, result=?, finished_at=CURRENT_TIMESTAMP WHERE id=?",
(status, result, row_id),
)
db.commit()
def run_one(row):
row_id, payload = row
try:
text = call_model(json.loads(payload)) # 换成你的请求函数
finish(row_id, "done", text)
except Exception as e:
finish(row_id, "failed", str(e)[:500])
def main():
with ThreadPoolExecutor(max_workers=8) as pool:
while True:
batch = claim(32)
if not batch:
break
list(pool.map(run_one, batch))
main()status='running' 的行在进程崩溃后会一直卡着。启动时先把超过十分钟的 running 改回 pending,或者加一个 started_at 字段按时间判断。
断点续跑与幂等
每条的输入算一个 hash(内容 + 模型 + 提示词版本),在表上建唯一索引。重跑时用 INSERT OR IGNORE,同一份输入不会算两次钱。模型升级或提示词改动就换版本号,老结果留在表里可以对比。
幂等还意味着可以放心重跑:失败的行重新置为 pending,done 的行永远跳过。整套流程中断三次也不怕。
hash 用 sha256,输入里带上提示词模板的版本号。模板改了一个字,hash 就变,旧结果不会污染新结果。
失败分类与重试预算
429 和 5xx 可以重试,401/403 重试没有意义,400 多半是输入本身有问题。给每条设 attempts 上限,三次失败就进 failed,别让一条坏数据拖住整批。
按失败类型分别统计:限流占多数就降并发,超时占多数就缩短输入或换模型,400 占多数就回头改提示词。三种情况的处理方式完全不同。
重试前先看这条的 attempts,第三次还失败就换模型,不要在同一条上反复试。失败样本单独导出一份,人工看二十条,通常能定位到同一类问题。
跑完要抽检
随机抽 100 条人工看,比相信模型自评有用。格式错误率超过 1% 就停下来改提示词,别硬跑完二十万条再回头。
抽检要覆盖不同来源、不同长度、不同语言。只看前 100 条,看到的都是最简单的那批,剩下的问题要等上线才暴露。
模型和价格在 模型总览 与 定价页 都能查到,glm-5.3-flash 的参数在 模型页。先跑 1000 条,把失败率和单条成本对一遍预算表,再开全量。
跑批的日志要留够:每条记录 id、模型、耗时、token 数和状态。出问题时不用猜是哪一步慢,直接按耗时排序就能找到异常样本。
预算表跑完要回填实际值,差得远说明估算模型有问题,下一次的预算就不可信了。