1. 批量推理这件事为什么值得单独聊做AI应用开发的朋友十有八九都经历过这样的场景产品上线前要跑一轮全量数据评测或者半夜定时任务要处理几万条用户提交的文本又或者做数据清洗时需要对几十万条记录逐条过一遍大模型。这些任务有个共同点——它们不要求实时返回结果但量特别大按常规的按次调用方式跑下来账单能让人倒吸一口凉气。OpenRouter这次推出的Batch API核心卖点就一句话批量推理享半价。你提交一个批量任务系统在后台异步处理通常几小时内完成费用直接砍一半。这个定价策略其实很好理解——对于平台来说批量任务可以错峰调度、集中利用算力资源边际成本本来就比实时请求低得多把省下来的部分让利给开发者是个双赢的买卖。这篇文章适合谁看如果你正在用OpenRouter的API做开发或者手头有大量离线推理需求在纠结成本又或者你只是听说过OpenRouter但还没搞清楚它到底怎么用、密钥怎么获取、国内访问体验如何那这篇内容应该能帮你把大部分疑问理清楚。我会从Batch API的设计思路讲起把批量任务的完整操作流程、参数配置、成本计算、常见报错排查都过一遍最后再聊聊实际使用中踩过的坑和应对技巧。提示本文涉及的API调用方式、参数命名、计费逻辑均基于OpenRouter公开的接口规范具体数值和模型名称请以官方最新文档为准。2. OpenRouter Batch API 的整体设计与选型考量2.1 为什么是异步批处理而不是实时接口打折很多人第一反应会问既然批量能半价那我能不能把实时请求伪装成批量请求来省钱答案是不行也不建议这么干。Batch API的设计从根上就和实时接口是两套逻辑。实时接口走的是同步请求-响应模式你发一条模型立刻算一条返回一条。这种模式对延迟敏感平台需要预留足够的算力来保证响应速度成本自然高。而Batch API走的是异步队列模式你把一批请求打包成一个文件提交上去平台把这些任务排进队列在算力空闲的时段集中处理处理完了再把结果文件给你。你不需要守着等结果平台也不需要为你单独预留资源。这个差异决定了Batch API的几个天然特征延迟不保证官方通常给的是“24小时内完成”这样的承诺实际体验中大部分任务几小时就回来了但你不能拿它做实时交互。请求格式有要求不是随便发几条就行需要按照指定的JSONL格式组织请求体每条请求带一个唯一的custom_id。结果以文件形式返回处理完成后你会拿到一个结果文件需要自己解析。从成本角度看半价不是噱头。我拿一个实际场景算过账假设你要处理10万条文本分类任务每条平均消耗500个token输入输出用某个中档模型实时调用大概要花几十美元走Batch API直接省掉一半。对于需要反复跑评测、做数据标注、批量生成内容的团队来说这个差价一个月下来能省出一台服务器的钱。2.2 哪些场景适合走Batch哪些千万别用不是所有任务都适合塞进Batch API。我整理了一个简单的判断表场景类型是否适合Batch原因离线数据清洗与标注非常适合量大、不要求实时、成本敏感模型评测与基准测试非常适合需要跑大量样本结果可以等批量内容生成如商品描述适合可以提前跑好存库用户访问时直接读定时报表与摘要生成适合夜间跑早上看结果实时对话与客服机器人绝对不适合用户等不了几小时需要多轮交互的任务不适合Batch是单次请求-响应没有上下文延续对结果顺序有严格依赖的任务谨慎使用虽然可以用custom_id关联但处理顺序不保证这里有个经验判断标准就一条——这个任务的结果用户能不能等。能等就走Batch省钱不能等老老实实走实时接口。2.3 半价背后的计费逻辑与成本估算方法OpenRouter的计费一直是按token用量来的Batch API的半价体现在单价上。具体来说同一个模型走Batch通道的输入和输出token单价是实时通道的50%。这个折扣是直接体现在账单里的不需要你领券或者满足什么额外条件。成本估算其实很简单公式如下Batch成本 (输入token数 × 输入单价 × 0.5) (输出token数 × 输出单价 × 0.5)但实际估算时有几个容易忽略的点token数不是字符数中文大概1个汉字对应1-2个token英文大概1个单词对应1-1.3个token具体取决于模型的分词器。你可以用OpenRouter提供的token计数接口先跑几条样本估算平均值。输出长度不可控如果你在请求里设了max_tokens那最坏情况就按这个上限估如果没设模型可能输出很长预算要留余量。失败重试也消耗token如果某条请求因为格式问题失败了重新提交还是会消耗token所以提交前一定要校验格式。我一般会先用实时接口跑100条样本统计平均token消耗然后乘以总条数再打八折因为Batch通常不会有实时接口那么高的并发开销最后按半价算出来的数字基本就是实际账单了。3. 从零开始跑通一个Batch任务核心细节与实操要点3.1 准备工作密钥获取与请求文件格式在开始之前你需要先拿到OpenRouter的API密钥。获取路径很简单注册账号后进入控制台找到API Keys页面创建一个新的密钥。这里有个细节——密钥只在创建时显示一次一定要当场复制保存好关掉页面就再也看不到了。如果你不小心弄丢了只能删掉重新建一个。关于充值OpenRouter支持多种支付方式国内用户比较关心的是能不能用支付宝。根据我的实际体验OpenRouter的支付渠道里确实有支持国内常用支付方式的选择具体以结账页面显示的选项为准。充值金额会转换成账户余额按实际用量扣费。拿到密钥后下一步是准备请求文件。Batch API要求提交JSONL格式的文件每一行是一个独立的请求对象。一个标准的请求行长这样{custom_id: req-001, method: POST, url: /v1/chat/completions, body: {model: deepseek/deepseek-chat, messages: [{role: user, content: 请把这句话翻译成英文今天天气很好}], max_tokens: 200}}逐字段拆解一下custom_id你自己定义的唯一标识符用来在结果文件里对应回是哪条请求。建议用有意义的命名比如task-001、user-12345这种方便后续排查。method固定填POST。url填/v1/chat/completions这是对话补全的标准端点。body里面就是正常的请求体model、messages、max_tokens这些参数和实时接口完全一致。注意JSONL文件里每一行必须是合法的JSON不能有换行符截断不能有多余的逗号。我见过最常见的错误就是手动拼接字符串时漏了引号或者括号导致整个文件解析失败。3.2 提交任务与轮询状态的完整流程文件准备好之后通过一个简单的POST请求提交curl -X POST https://openrouter.ai/api/v1/batches \ -H Authorization: Bearer $OPENROUTER_API_KEY \ -H Content-Type: application/json \ -d { input_file_id: file-abc123, endpoint: /v1/chat/completions, completion_window: 24h }等等这里有个前置步骤——你需要先把JSONL文件上传上去拿到一个file_id。上传接口是curl -X POST https://openrouter.ai/api/v1/files \ -H Authorization: Bearer $OPENROUTER_API_KEY \ -F filebatch_requests.jsonl \ -F purposebatch上传成功后会返回一个文件ID把这个ID填到提交任务的input_file_id字段里。提交成功后你会拿到一个batch_id后续就用这个ID来查询状态。查询状态的接口curl https://openrouter.ai/api/v1/batches/$BATCH_ID \ -H Authorization: Bearer $OPENROUTER_API_KEY返回的JSON里会有一个status字段可能的值包括validating正在校验文件格式in_progress正在处理completed处理完成failed处理失败expired超过24小时窗口未完成当状态变成completed后返回体里会包含一个output_file_id用这个ID去下载结果文件curl https://openrouter.ai/api/v1/files/$OUTPUT_FILE_ID/content \ -H Authorization: Bearer $OPENROUTER_API_KEY \ -o batch_results.jsonl结果文件也是JSONL格式每行对应一条请求包含custom_id和响应内容。你可以用custom_id把结果和原始请求关联起来。3.3 参数配置中的关键决策点虽然Batch API的请求体和实时接口基本一致但有几个参数在批量场景下需要特别考虑max_tokens的设置。批量任务最怕的就是某条请求输出超长把整个任务的成本拉高。建议根据任务类型设一个合理的上限。比如做分类任务输出通常就几个词设50就够了做摘要任务设500-1000比较稳妥。设太小会导致输出被截断设太大又浪费预算。temperature的选择。批量任务通常追求结果稳定可复现建议把temperature设低一些比如0.1-0.3。如果是创意生成类任务可以适当调高但要注意高temperature会增加输出长度的不确定性。模型的选择。OpenRouter上聚合了大量模型从免费的到高端的都有。批量任务因为量大模型选择对成本影响极大。我的建议是先用小样本对比几个候选模型的效果选一个效果达标且单价最低的。不要盲目上最贵的模型很多任务用中档模型完全够用。是否需要seed参数。如果你希望结果可复现可以设置seed。但要注意即使设了seed不同批次的处理结果也可能有细微差异这是分布式推理的固有特性。4. 实操过程与核心环节实现4.1 用Python完整跑一遍批量任务光看接口文档容易漏掉细节我直接用一个完整的Python脚本把流程串起来。这个脚本做的事情是读取一个CSV文件里的文本生成JSONL请求文件上传提交批量任务轮询直到完成下载结果并解析。import json import time import requests import csv API_KEY 你的OpenRouter密钥 BASE_URL https://openrouter.ai/api/v1 HEADERS {Authorization: fBearer {API_KEY}} def build_jsonl(input_csv, output_jsonl, modeldeepseek/deepseek-chat): with open(input_csv, r, encodingutf-8) as f_in, \ open(output_jsonl, w, encodingutf-8) as f_out: reader csv.DictReader(f_in) for idx, row in enumerate(reader): request_obj { custom_id: ftask-{idx:05d}, method: POST, url: /v1/chat/completions, body: { model: model, messages: [ {role: system, content: 你是一个文本分类助手只输出类别名称。}, {role: user, content: row[text]} ], max_tokens: 20, temperature: 0.1 } } f_out.write(json.dumps(request_obj, ensure_asciiFalse) \n) print(f已生成 {idx1} 条请求) def upload_file(filepath): with open(filepath, rb) as f: resp requests.post( f{BASE_URL}/files, headersHEADERS, files{file: f}, data{purpose: batch} ) resp.raise_for_status() return resp.json()[id] def submit_batch(file_id): resp requests.post( f{BASE_URL}/batches, headers{**HEADERS, Content-Type: application/json}, json{ input_file_id: file_id, endpoint: /v1/chat/completions, completion_window: 24h } ) resp.raise_for_status() return resp.json()[id] def poll_batch(batch_id, interval60): while True: resp requests.get(f{BASE_URL}/batches/{batch_id}, headersHEADERS) resp.raise_for_status() data resp.json() status data[status] print(f当前状态{status}) if status in (completed, failed, expired): return data time.sleep(interval) def download_results(output_file_id, save_path): resp requests.get( f{BASE_URL}/files/{output_file_id}/content, headersHEADERS ) resp.raise_for_status() with open(save_path, wb) as f: f.write(resp.content) print(f结果已保存到 {save_path}) if __name__ __main__: build_jsonl(input.csv, batch_requests.jsonl) file_id upload_file(batch_requests.jsonl) print(f文件已上传ID{file_id}) batch_id submit_batch(file_id) print(f批量任务已提交ID{batch_id}) result poll_batch(batch_id) if result[status] completed: download_results(result[output_file_id], batch_results.jsonl) else: print(f任务未成功完成{result})这个脚本可以直接拿去改改用。几个关键点说明一下build_jsonl函数里用了ensure_asciiFalse这样中文不会被转义成unicode编码文件更小也更易读。轮询间隔设了60秒实际可以根据任务量调整。任务小的话30秒也行任务大的话几分钟查一次就够。结果文件下载后是JSONL格式每行包含custom_id和response解析时用custom_id关联回原始数据即可。4.2 结果解析与数据回填下载下来的结果文件长这样{custom_id: task-00001, response: {status_code: 200, body: {choices: [{message: {content: 科技}}]}}}解析逻辑很简单逐行读取提取custom_id和content然后回填到你的原始数据里。但有几个坑要注意失败请求的处理。不是所有请求都会成功有些可能因为内容审核、token超限、模型临时不可用等原因返回非200状态码。你的解析脚本要能识别这些失败项把它们单独收集起来后续决定是重试还是人工处理。输出格式的清洗。模型输出可能带多余的空格、换行、引号甚至有时候会加一些解释性文字。如果你的下游系统对格式要求严格解析时要做清洗。比如分类任务可以用正则提取第一个匹配的类别词。顺序问题。结果文件里的顺序不一定和请求文件一致所以千万不要用行号来对应一定要用custom_id。4.3 成本与耗时的实际测算我拿一个真实任务跑过数据5000条中文文本分类每条平均输入80个token输出5个token用的是某个中档模型。实时接口跑完大概花了不到2美元走Batch API账单显示不到1美元。耗时方面提交后大约2小时40分钟完成比官方承诺的24小时快很多。另一个任务20000条文本摘要每条输入500 token输出150 token。Batch任务跑了大概6小时成本比实时省了将近一半。这个量级如果走实时接口不仅贵而且并发限制可能还会导致部分请求被限流。从这两个案例可以总结出Batch API的完成时间主要取决于任务量和平台当时的负载。小任务几千条通常几小时内完成大任务几万条以上可能要十几个小时。如果你对完成时间有硬性要求建议提前跑一次小批量测试摸清大概的耗时规律。5. 常见问题与排查技巧实录5.1 提交失败与格式报错速查批量任务最容易出问题的环节就是文件格式。我整理了一个常见报错对照表报错信息可能原因解决方法invalid_jsonlJSONL文件某行不是合法JSON用json.loads逐行校验定位到具体行号missing_custom_id某条请求缺少custom_id字段检查生成逻辑确保每条都有唯一IDduplicate_custom_idcustom_id重复用集合去重或改用UUIDunsupported_model模型名称拼写错误或该模型不支持Batch核对模型列表确认模型支持批量接口file_too_large文件超过大小限制拆分成多个文件分批提交context_length_exceeded单条请求token超限缩短输入或换用上下文窗口更大的模型这里重点说下context_length_exceeded这个错误。有些模型的上下文窗口是有限的比如某些模型最大支持1048576个token但如果你输入的内容加上max_tokens超过了这个限制就会报错。解决办法要么是截断输入要么是换一个窗口更大的模型。批量任务里如果有个别超长请求会导致整条失败所以提交前最好先统计一下输入长度的分布把超长的挑出来单独处理。5.2 任务卡住不动的排查思路有时候提交后状态一直停在validating或者in_progress半天没动静。这种情况我遇到过几次排查思路如下首先确认文件是不是真的上传成功了。有时候网络波动导致上传中断但接口返回了成功实际上文件不完整。可以重新下载上传的文件对比一下大小。其次检查任务量是不是特别大。如果提交了几十万条请求平台处理队列可能需要更长时间。这种情况下耐心等待即可只要状态不是failed就有希望。还有一种可能是账户余额不足。虽然提交时不会立即扣费但如果余额不够覆盖预估费用任务可能会被挂起。建议提交前确认余额充足。如果超过24小时还是in_progress那基本可以判断是异常了。这时候可以尝试取消任务重新提交或者联系平台支持。5.3 国内使用OpenRouter的实操心得国内开发者用OpenRouter最关心的几个问题我集中说一下。网络连通性。OpenRouter的API端点在国内直接访问的稳定性因地区而异。我的经验是如果遇到连接超时可以尝试切换网络环境或者用云服务器中转。很多团队的做法是在海外服务器上部署一个转发服务国内应用通过这个转发服务调用OpenRouter这样稳定性会好很多。支付与充值。前面提到过OpenRouter支持多种支付方式国内用户可以选择合适的渠道完成充值。充值到账通常是即时的如果遇到延迟检查一下支付是否成功扣款。密钥管理。不要把密钥硬编码在代码里更不要提交到代码仓库。建议用环境变量或者密钥管理服务。如果密钥泄露了立即在控制台删除并重新生成。我见过有人把密钥发到群里求教问题结果被人恶意刷了几百万token的案例这个教训要记住。模型选择与调用量控制。OpenRouter上模型很多价格差异也大。建议在控制台设置消费限额避免意外超支。另外批量任务提交前先用小样本测试确认模型效果和成本都在预期内再放大规模。5.4 批量任务的结果质量把控批量任务跑完不代表万事大吉结果质量需要抽检。我的做法是随机抽取5%-10%的结果人工检查看输出是否符合预期。统计失败率和异常输出比例如果超过阈值比如5%需要分析原因。对于分类任务统计各类别的分布如果某个类别占比异常高或异常低可能是模型理解有偏差。保存一份原始请求和结果的对应关系方便后续追溯。如果发现某类请求普遍效果不好可以针对性优化prompt然后只重新提交这部分请求不用全部重跑。这样既省时间又省钱。6. 批量推理的进阶玩法与扩展思路Batch API除了省钱还有一些进阶用法值得探索。多模型对比评测。你可以把同一批请求分别提交给不同模型的Batch任务等结果都回来后做横向对比。因为Batch成本低这种评测方式的性价比很高。我之前用这个方法对比了三个模型在同一个分类任务上的表现跑了几万条样本总花费还不到一杯咖啡的钱。数据增强与合成。做机器学习的朋友可以用Batch API批量生成训练数据。比如给一批种子样本让模型生成变体扩充数据集。这种任务量大且对实时性没要求走Batch再合适不过。定时批处理流水线。把Batch API接入你的数据流水线每天定时提交任务第二天早上收结果。比如电商场景下每天晚上把当天的用户评论批量跑一遍情感分析早上运营团队就能看到报表。与实时接口的混合架构。不是所有请求都要走Batch也不是所有都要走实时。合理的做法是分层用户直接触发的走实时接口保证体验后台异步任务走Batch控制成本。这样整体架构的成本和体验都能兼顾。最后分享一个我在实际使用中总结的小技巧提交Batch任务前先用实时接口跑一条样本确认请求格式和参数都没问题再把同样的格式批量生成。这一步花不了几秒钟但能避免因为格式错误导致整个文件被拒。另外养成给custom_id加前缀的习惯比如date-任务名-序号这样即使同时跑多个批量任务结果文件混在一起也能快速区分。