📌 痛点:跑几十条没问题,跑 14,000 条就崩了
很多 LLM 应用的 demo 跑得很顺畅,一到生产环境处理大量数据就各种问题:
- 跑到一半触发 API 限流,程序中断
- 网络抖动导致某次请求失败,之前的结果全部丢失
- 数据里有脏值(空文本、超长文本),程序直接报错退出
我在处理 14,000 条地铁微博时,把这些坑全踩了一遍。这篇文章分享一套经过实战检验的批量调用 LLM API 的工程化方案。
🛠️ 问题一:API 限速——如何优雅地“慢下来”
免费或低成本的 API 都有速率限制(比如每分钟 60 次)。如果不加控制,程序会在短时间内疯狂请求,然后被 API 返回 429 错误。
解决方案:在循环中加入固定间隔
import time
for idx, row in tqdm(df.iterrows(), total=len(df)):
category = call_deepseek_api(text)
results.append(category)
time.sleep(1.2) # 每次请求后等待 1.2 秒
- 为什么是 1.2 秒? 每分钟 60 次 = 每秒 1 次。1.2 秒的间隔保证了每分钟约 50 次请求,留出安全余量。
tqdm进度条:让你知道程序还在跑,以及预计剩余时间。
💾 问题二:程序中断——如何避免结果全部丢失?
假设你跑了 8,000 条,突然电脑死机或 API 报错,如果没保存进度,之前的结果全部作废。
解决方案:每 N 条保存一次临时文件
if (idx + 1) % 1000 == 0:
temp_df = pd.DataFrame({
'微博原文': df['微博正文'].iloc[:idx + 1],
'需求层次': results
})
temp_df.to_csv('classified_results_temp.csv', index=False, encoding='utf_8_sig')
关键点:
(idx + 1) % 1000 == 0:每处理 1000 条就保存一次。- 保存的是当前已处理的所有数据,而不是增量。
- 程序重启后,可以从临时文件读取已处理的数据,跳过前 N 条继续跑。
🛡️ 问题三:异常处理——如何让单次失败不影响整体?
网络抖动、API 暂时不可用、某条数据格式异常……这些都会导致单次请求失败。如果不用 try-except 包裹,整个程序就会崩溃。
解决方案:捕获异常并返回默认值
def call_deepseek_api(text):
try:
response = requests.post(url, headers=headers, json=payload, timeout=20)
response.raise_for_status()
result = response.json()['choices'][0]['message']['content'].strip()
return result if result in CATEGORIES else "其他"
except Exception as e:
print(f"API调用出错: {str(e)[:50]}") # 打印简略错误信息
return "其他" # 返回默认值,让程序继续
关键点:
timeout=20:防止某个请求卡死,20 秒后自动超时。- 返回默认值:失败时返回“其他”或“API错误”,而不是让程序中断。
- 错误日志:打印错误信息但截断长度,避免刷屏。
📁 问题四:数据质量——如何应对空文本和异常格式?
真实数据里总有脏东西:空字符串、全是空格、None 值、超长文本……
解决方案:在循环入口做防御性处理
text = str(row['微博正文']).strip()
if not text:
results.append("其他") # 空文本直接归类
continue
# 截断超长文本,控制 token 消耗
category = call_deepseek_api(text[:500])
关键点:
str().strip()确保文本是字符串且去除了首尾空格。- 空文本直接跳过,不浪费 API 调用。
text[:500]截断长文本,对于分类任务,前 500 字已经足够判断。
📊 完整的主循环代码
def process_csv(input_path, output_path):
df = pd.read_csv(input_path, encoding='utf-8-sig')
results = []
for idx, row in tqdm(df.iterrows(), total=len(df)):
text = str(row['微博正文']).strip()
if not text:
results.append("其他")
continue
category = call_deepseek_api(text[:500])
results.append(category)
# 每1000条保存进度
if (idx + 1) % 1000 == 0:
pd.DataFrame({
'原文': df['微博正文'].iloc[:idx + 1],
'需求层次': results
}).to_csv('temp.csv', index=False)
time.sleep(1.2) # 限速
# 最终保存
pd.DataFrame({
'微博原文': df['微博正文'],
'需求层次': results
}).to_csv(output_path, index=False)
💡 总结:批量调用的四道保险
| 保险 | 方法 | 作用 |
|---|---|---|
| 限速 | time.sleep(1.2) | 避免触发 API 限流 |
| 断点续传 | 每 1000 条保存临时文件 | 防止中断导致数据丢失 |
| 异常处理 | try-except + 默认值 | 单次失败不影响整体 |
| 数据清洗 | 空值检查 + 截断 | 减少无效调用和 token 消耗 |
这套方案让我的 14,000 条数据处理稳定运行了约 5 个小时,中间网络波动了 3 次,但没有丢失任何已处理的数据。
🔗 完整代码
👉 nanjing-metro-analysis/scripts/01_demand_classification/classify_demand.py