首页 / 文章 / 一夜处理百万文档:端到端批量推理
← 返回
IT技术

一夜处理百万文档:端到端批量推理

✍️ zhirenhun 📅 2026/8/11 👁 236 阅读 ⏱ 55 分钟
一夜处理百万文档:端到端批量推理

假设你的对象存储中有一百万张支持工单,而明天早上之前你需要将每一张按问题类型分类并总结成一段话。第一时间想到的显然是通过实时聊天补全API逐个发送,但这是错误的方法。按照标准速率限制,仅请求本身就需要超过一天时间,每个token都要付全价,而且在凌晨三点的一次网络故障就可能让你的脚本半途而废。

本文要论证的观点是:大多数LLM工作负载并不是对话。将文档分类、总结、为旧记录添加标签或字段、运行评估、处理积压任务——这些是吞吐量问题,而人们第一时间想到的交互式API是一种面向延迟优化的工具,却被用在了吞吐量任务上。那些把实时端点视为唯一端点的团队,为了一个无人等待的答案,大约要付出双倍的代价。批量推理是一种执行模式,你将全部一百万请求打包成文件,交给平台,然后在任务完成时收集结果——这才是为这种工作形态而生的工具,它应该成为任何截止时间是某个时刻而非若干秒的任务的默认选择。

首先明确本文的内容:一种成本建模方法和一条可运行的流水线。批量成本模型基于文档化的限制和标价构建。故障模式和模型选择数据是实测得出的:我们针对真实文档(以新闻文章作为替代,并匹配类别列表)在无服务器实时API上运行了本文的提示结构,共两次,并报告了我们的发现。一百万工单的任务是一个经过精心设计的实例,旨在让每个数字都具体化,并且所有计算过程都已展示,因此你可以用自己文档数量、token大小和模型选择重新运行该模型。模型价格会有变化;在预算实际任务之前,请查阅DigitalOcean的推理定价页面

我们要处理的任务

为了让数字更具体,以下是这个项目:

  • 1,000,000个纯文本文档,每个平均约1,200个token(大约900个单词)
  • 每个文档:从八个类别中分配一个,并撰写3-4句话的摘要
  • 截止时间:第二天早上出结果
  • 模型:GPT-5 mini,通过DigitalOcean的无服务器推理平台提供,实时价格为每百万输入token $0.25,每百万输出token $2.00。批量请求按这些价格最多一半计费,原因在账单部分会介绍。

一个前置设计决策:分类和总结在每份文档的单个请求中完成,而不是两个。两次单独调用会使请求数翻倍,并且每个文档都要发送两次。相反,提示要求模型返回一个同时包含类别和摘要的JSON对象。这使账单减半并简化了流水线,代价是提示稍长一些。

加上提示指令后,每个请求携带约1,500个输入token,并产生约200个输出token。一百万文档就是15亿输入token和2亿输出token。

为什么实时推理在这里是错误的工具

在编写任何代码之前,值得先检查实时API是否能在截止时间前完成。

DigitalOcean的无服务器推理在Tier 3和Tier 4的速率限制是每分钟600个请求,以及大约每分钟800K到2M个token(请参阅推理限制页面上的账户层级表)。每分钟600个请求处理一百万个请求大约需要27.8小时,而且这还假设零重试且脚本永远不落后。token限制同样严格:17亿总token以每分钟2M的速度处理大约需要14小时稳定的、完美定时的请求。

你可以通过工程手段绕过这些问题:申请提高层级、调整速率限制器、设置检查点、分片处理工作。团队确实会这么做,但通常是白费力气,因为速率限制在这里并不是障碍,而是一个信号:延迟优化的API不适合吞吐量任务的执行模型。

批量推理避免了所有这些问题。批量作业使用独立的配额(默认情况下,每个账户每个模型最多可以提交100亿token),并在隔离容量上以较低调度优先级运行,因此正在运行的批量作业不会消耗你的实时配额,也不会降低生产应用程序的延迟。你也不需要编写速率限制器、带退避的重试循环或检查点文件。平台会自动以指数退避方式重试瞬时错误(429、408、5xx),最多重试两次,失败的请求会落入错误文件而不会导致任何崩溃(请参阅推理特性页面中的批量推理部分)。

代价是速度。批量作业有24小时的完成窗口,你无法控制你的请求在该窗口内何时运行。如果你的工作负载中任何部分需要在几秒内得到答案,那部分应该留在实时API上。其他一切都适合批量处理。

开始之前你需要什么

三个前提条件,都是一次性设置:

  • DigitalOcean账户层级为Tier 3或更高。Tier 1和Tier 2账户无法使用Anthropic模型或OpenAI模型(开源的gpt-oss模型除外),而批量推理仅支持使用文本提示的OpenAI和Anthropic商业模型(请参阅推理限制页面了解层级模型访问权限,以及批量推理限制了解支持的模型)。
  • 正的预付费余额。无服务器推理(包括批量)是预付的:用量从你的余额中扣除,如果余额降至$0,访问将被暂停。对于这个任务,请确保余额足够覆盖账单部分中的估算金额,并留出余量。
  • 一个模型访问密钥,通过DigitalOcean控制面板创建。下面的每个API调用都使用它针对无服务器推理基础URL进行身份验证,https://inference.do-ai.run

一个需要规划的约束:每个批处理作业使用单个模型,不支持多模型批处理作业(参见批处理推理限制)。这与DigitalOcean的推理路由器不同,后者为每个请求挑选最合适的模型;该功能适用于实时推理,而非批处理作业。因此,如果您想将简单的文档发送到较便宜的模型,将困难的文档发送到更强大的模型,那是两个独立的批处理作业,而不是一个。

围绕限制的规划

批处理推理有三个限制,决定了你如何拆分一百万份文档:

  • 每个输入文件最多50,000个请求
  • 每个输入文件最大200 MB
  • 每个账户每个模型最多提交100亿个token(默认值;你可以申请提高限额)

乍一看,拆分似乎很简单:1,000,000 ÷ 50,000 = 20个文件。但每个文件还必须保持在200 MB以下,所以按三个步骤来计算这些数字。

步骤1:一行有多大? 每行包含一个文档、提示指令和一个小的JSON包装器。一个token大约相当于四个字符。

项目 大小
文档(1,200个token × 约4个字符) ~4,800字符
提示指令(约300个token) ~1,200字符
JSON包装器(custom_idmethodurlbody ~500字符
每行总计 ~6,500字符 ≈ 6.5 KB

步骤2:哪个限制最先达到上限?

限制 计算 每个文件的文档数
请求上限 每个文件最多50,000个 50,000
文件大小上限 200 MB ÷ 每行6.5 KB ~31,500

文件大小上限先达到:一个文件只能容纳约31,500个文档,远低于50,000个请求的上限。在这个项目中,文件大小决定了拆分方式。

步骤3:选择拆分方案,留出余量。

决策
每个文件的请求数 25,000
文件大小(25,000 × 6.5 KB) ~160 MB,远低于200 MB
1,000,000个文档所需的文件数 40

四十个文件意味着四十个批处理作业,听起来很多,但代码几乎无需改动,因为提交和轮询逻辑无论如何都是一个循环。

最后,检查token配额。该作业需要15亿个输入token,外加约2亿个输出token,总计17亿。这远低于默认的100亿,因此所有40个作业可以同时提交。

构建输入文件

批处理输入文件的每一行都是一个独立的请求。对于DigitalOcean上的OpenAI提供商作业,该行遵循OpenAI Batch API的结构:一个custom_id、一个方法、一个URL,以及你本来会发送到实时端点的请求体(参见DigitalOcean的批处理推理指南中的输入文件格式)。

有两个细节比表面上更重要:

首先,custom_id是将结果关联回其源文档的唯一键。结果不会按输入顺序返回。请使用真实的文档ID,绝不要使用在列表重新排序后毫无意义的数组索引。文件内重复的custom_id值会导致验证失败,因此一个稳定的唯一ID可以同时解决这两个问题。

其次,限制输出长度。摘要应为3-4句话,所以500个token绰绰有余。注意参数名称:GPT-5模型在聊天补全端点上使用max_completion_tokens,而不是旧的max_tokens,并且它们不接受自定义的temperature。该上限还包括模型的内部推理token,因此对于此类简单任务,将reasoning_effort设置为minimal;这使推理token接近零,并使输出费用可预测。如果没有上限,一次过长的补全会浪费输出token,并且有多少文档触发这种情况,就会浪费多少倍。

以下脚本读取文档记录(ID加文本),构建请求行,并每25,000个请求滚动到新文件:

import json

SYSTEM_PROMPT = (
    "You classify and summarize documents. Respond with a single JSON object: "
    '{"category": "
    'security, performance, documentation, other>", '
    '"summary": "<3-4 sentence summary>"} '
    "The category value must be exactly one of the eight listed strings, "
    "lowercase. Never invent another category; if unsure, use \"other\"."
)

CHUNK_SIZE = 25_000

def write_batch_files(documents, prefix="batch_input"):
    """documents yields (doc_id, text) tuples. Returns list of file paths."""
    paths, out, count, part = [], None, 0, 0
    for doc_id, text in documents:
        if count % CHUNK_SIZE == 0:
            if out:
                out.close()
            part += 1
            path = f"{prefix}_{part:03d}.jsonl"
            out = open(path, "w", encoding="utf-8")
            paths.append(path)
        line = {
            "custom_id": doc_id,          # your real document ID
            "method": "POST",
            "url": "/v1/chat/completions",
            "body": {
                "model": "gpt-5-mini",
                "messages": [
                    {"role": "system", "content": SYSTEM_PROMPT},
                    {"role": "user", "content": text},
                ],
                "max_completion_tokens": 500,
                "reasoning_effort": "minimal",
            },
        }
        out.write(json.dumps(line, ensure_ascii=False) + "\n")
        count += 1
    if out:
        out.close()
    return paths

在上传任何内容之前,先在本地进行验证。在验证阶段,任何一行的错误或重复的 custom_id 都会导致整个文件验证失败,而等到上传之后才发现这种情况会浪费一轮操作。一个十行检查脚本,解析每一行,验证必需的键,并确认 custom_id 唯一性,就能帮你省去这轮浪费。

上传文件并创建任务

每个文件的提交都是一个三步流程,且顺序很重要。完整流程,以及 Python、JavaScript 和 cURL 的官方示例,都记录在 DigitalOcean 的 批量推理指南 中。

第一步:请求一个文件意图。使用以 .jsonl 结尾的文件名调用 POST /v1/batches/files,会返回一个 file_id 和一个预签名上传 URL。file_id 在最多 30 天内有效,并且可以在多个任务中重复使用;上传 URL 大约在 15 分钟后过期,所以请及时上传。如果你错过了这个时间窗口,请重新请求一个意图。

第二步:将原始 JSONL 字节通过 PUT 请求发送到预签名 URL。使用 Content-Type: application/octet-stream 或完全省略该请求头。预签名 URL 对签名很敏感,像 application/jsonl 这样的非标准内容类型可能会导致签名不匹配。

第三步:使用 file_id 创建批量任务。此步骤会对对象存储进行检查,如果上传尚未完成则会失败,因此切勿颠倒第二步和第三步的顺序。创建调用需要提供提供商(openaianthropic)、完成时间窗口(目前仅接受 24h)、一个 endpoint(对于 OpenAI 任务,它必须与每一行上的 url 匹配;对于 Anthropic 任务则省略),以及你生成的 request_id

这个 request_id 是一把防止重复任务的安全钥匙,对于夜间运行 40 个任务来说至关重要。如果你的提交脚本遇到网络错误并重试,相同的 request_id 会返回已有任务,而不会创建重复任务,从而避免对 25,000 份文档重复计费。请根据文件名来构建它,而不是每次生成随机的 UUID,这样整个脚本的重跑也是安全的:

import hashlib
import os
import uuid

import requests
from pydo import Client

client = Client(token=os.environ["DIGITALOCEAN_TOKEN"])

def submit_file(path):
    # 1. Reserve a file_id and presigned upload URL.
    intent = client.batches.files.create(file_name=os.path.basename(path))
    file_id, upload_url = intent["file_id"], intent["upload_url"]

    # 2. PUT the raw JSONL bytes. octet-stream keeps the signature valid.
    with open(path, "rb") as fh:
        put = requests.put(
            upload_url,
            data=fh,
            headers={"Content-Type": "application/octet-stream"},
            timeout=300,
        )
    put.raise_for_status()

    # 3. Create the job. request_id derived from the file name makes
    #    the whole script safe to rerun without duplicating jobs.
    request_id = str(uuid.UUID(
        hashlib.md5(f"doc-pipeline-2026-08:{path}".encode()).hexdigest()
    ))
    batch = client.batches.create(
        file_id=file_id,
        provider="openai",
        endpoint="/v1/chat/completions",
        completion_window="24h",
        request_id=request_id,
    )
    return batch["batch_id"]

batch_ids = {}
for path in sorted(paths):           # paths from write_batch_files()
    batch_ids[path] = submit_file(path)
    print(f"submitted {path} -> {batch_ids[path]}")

batch_ids 映射持久化到磁盘(JSON 文件即可)。这是你的流水线在重启后需要保留的唯一状态。

监控 40 个任务

每个任务都会经历一组固定的状态:validating(文件结构、唯一 ID、token 数量)、queued(等待容量)、in_progress,然后是四个最终状态之一。completed 表示所有请求都已处理,即使其中某些单独请求失败;这些失败记录在错误文件中,不会反映在任务状态中。failed 表示系统性问题,通常是整体验证失败。expired 表示 24 小时窗口已用完。cancelled 不言自明。重要的是,expiredcancelled 并不代表全部丢失:任务结束前已完成的所有内容都会被保留、可下载并计费。只有未处理的请求会被丢弃,且不会向你收费。

轮询是指按重复的时间表检查任务状态:你的脚本向 API 请求当前状态,等待,然后再次请求,直到任务完成。批处理 API 在任务完成时不会发送通知,因此你需要通过这种循环来了解任务是否完成。没有必要频繁检查;对于隔夜运行的任务,所有任务每分钟检查一次就足够了:

import time

def wait_for_jobs(batch_ids, poll_seconds=60):
    pending = set(batch_ids.values())
    terminal = {"completed", "failed", "expired", "cancelled"}
    states = {}
    while pending:
        for bid in list(pending):
            b = client.batches.retrieve(bid)
            status = b["status"]
            counts = b.get("request_counts", {})
            print(f"{bid}  {status:12}  "
                  f"{counts.get('completed', 0)}/{counts.get('total', 0)}")
            if status in terminal:
                states[bid] = status
                pending.discard(bid)
        if pending:
            time.sleep(poll_seconds)
    return states

运行它,然后去睡觉。如果你想要仪表盘,request_counts字段会提供每个任务的进度,但对大多数团队来说,早上阅读日志就足够了。

处理失败

失败发生在三个层面,每个层面有不同的解决方法。

请求级失败是指无法处理的单行内容:例如文档超过模型的上下文窗口、内容策略拒绝、一个漏过验证的格式错误提示词。这些不会导致任务失败。每条失败记录都会写入一个错误文件,其中包含其custom_id和错误代码:

{"custom_id": "doc-88213", "error": {"code": "context_length_exceeded", "message": "Request exceeded maximum context length."}}
{"custom_id": "doc-90142", "error": {"code": "content_policy_violation", "message": "Request was blocked by content moderation."}}

错误代码会告诉你该怎么做。context_length_exceeded 文档在重新提交前需要截断或拆分。内容政策拒绝需要人工查看。任何临时性问题在到达这里之前,平台已经重试了两次,所以将这些ID作为最后一个小批量作业重新提交是合理的。如果你的输入文件是干净的,错误文件应该很小;在我们对实时API的200次完成评估中,没有请求在API级别失败。出现的条目大多是你在验证时就应该发现的超大型文档。

作业级故障较为罕见。如果作业过期时仍有未完成的工作,你可以创建一个延续作业,只处理失败或未触及的请求,而无需重新运行(或重新支付)已完成的工作。来自request_id的相同重复保护覆盖提交端:网络错误后重试的创建调用会返回原始批次作业。

计费规则很简单:你只需为已完成的请求付费。过期、取消或被护栏阻止且从未产生输出的请求不产生费用。

检索并合并结果

当作业达到终止状态时,GET /v1/batches/{batch_id}/results 会返回输出文件的预签名URL,如果存在错误文件,也会返回错误文件的URL。这里有两个操作细节:预签名URL是短期的,因此获取后应立即下载,而不是存储URL供以后使用;输出文件在完成后最多保留30天,之后将被永久删除。将结果下载并归档到自己的存储中应该是流程的一部分,而不是事后才想到的。

每一行输出都带有custom_id、完整的API响应(包括每个请求的token使用量),以及一个error字段,成功时为null。与源文档的关联就是字典查找,逐项累加usage字段可以得到确切的token数量,用于与账单核对:

import json
import requests as http

CATEGORIES = {"billing", "bug_report", "feature_request", "account",
              "security", "performance", "documentation", "other"}

def collect_results(batch_ids, out_path="results.jsonl"):
    total_in = total_out = failures = 0
    with open(out_path, "w", encoding="utf-8") as out:
        for path, bid in batch_ids.items():
            links = client.batches.results.retrieve(bid)
            if not links.get("result_available"):
                print(f"{bid}: results not ready, poll again later")
                continue
            resp = http.get(links["output_file_url"], timeout=300)
            resp.raise_for_status()
            for line in resp.text.splitlines():
                rec = json.loads(line)
                if rec.get("error"):
                    failures += 1
                    continue
                usage = rec["response"]["usage"]
                total_in += usage["prompt_tokens"]
                total_out += usage["completion_tokens"]
                content = rec["response"]["choices"][0]["message"]["content"]
                try:
                    parsed = json.loads(content)
                except json.JSONDecodeError:
                    failures += 1   # model returned non-JSON; queue for retry
                    continue
                if parsed.get("category") not in CATEGORIES:
                    failures += 1   # valid JSON, invalid category value
                    continue
                out.write(json.dumps({
                    "doc_id": rec["custom_id"],
                    "category": parsed["category"],
                    "summary": parsed["summary"],
                }, ensure_ascii=False) + "\n")
    print(f"input tokens: {total_in:,}  output tokens: {total_out:,}  "
          f"failures: {failures:,}")

注意这里处理的第二种失败模式:请求成功但模型返回了不是有效 JSON 的内容。在严格的系统提示下这很少见(我们在 200 次评估补全中测量为零),但在一百万次补全中“很少见”仍会发生,而检查不需要任何成本。将这些 custom_id 与错误文件 ID 一起加入清理批次的队列。

第三个检查 category not in CATEGORIES 之所以存在,是因为我们针对真实文档(50 篇新闻文章,用八个值的新闻类别列表代替工单类别)运行了这种提示结构,发现了一个建模流水线遗漏的失败情况:模型返回了完全有效的 JSON,但其中的类别不在列表上。在实时 API 上的 50 文档评估中,GPT-5 nano 在 50 次中编造了 4 次列表外的类别,GPT-5 mini 编造了 2 次,产生了诸如“science”和“humanitarian”之类的标签,这些标签没有任何下游连接会识别。有效的 JSON 并非有效的数据;要验证值,而不仅仅是结构。

然后我们收紧了提示(上面那句“绝不发明其他类别”),并重新运行了同样的 50 个文档。在你大规模信任提示修复之前,结果值得了解一下:mini 的违规次数从 2 次降到 0 次,而 nano 仍保持在 50 次中 4 次。提示层面的修复取决于模型;代码层面的检查则不然。如果你选择更便宜的模型,请为其测量到的违规率(我们的测试中为 8%)规划重试或归一化处理。重试 8% 的请求会增加 nano 约 8% 的费用,这仍使其比 mini 便宜约 5 倍。

逐项账单

DigitalOcean 上的批处理 token 按 OpenAI 和 Anthropic 模型的无服务器(实时)费率最高半价计费(参见 推理定价页面 的批处理推理部分)。较低的费率不是促销;而是调度经济学。批处理作业以较低优先级运行,并共享非高峰 GPU 容量(根据 批处理推理限制),因此可以等待 24 小时的工作会填充那些否则在实时高峰之间闲置的硬件。必须在两秒内回答的请求比可以在凌晨 4 点运行的请求服务成本更高,定价也反映了这一点。

Token 使用量是 定价页面 列出的唯一批处理费用;文件上传、存储、作业创建或轮询没有单独的费用。按完整批处理费率,GPT-5 mini 每百万输入 token 花费 $0.125,每百万输出 token 花费 $1.00。

以下是所指定作业的完整账单。基础费率是来自 推理定价页面 的 GPT-5 mini 无服务器价格(每百万 token 输入 $0.25,输出 $2.00),批处理减半;然后每个成本是 token 数乘以费率:

明细项 数量 费率 成本
输入 token(100 万文档 × ~1,500) 1.5B tokens $0.125 / 1M $187.50
输出 token(100 万文档 × ~200) 200M tokens $1.00 / 1M $200.00
文件上传(40 个文件) 40 $0 $0.00
批处理作业创建和轮询 40 jobs $0 $0.00
结果存储(30 天) ~1 GB $0 $0.00
总计 $387.50

这大约是每份文档 $0.0004。相同的工作负载按实时费率计算,输入 $375.00 加上输出 $400.00,共计 $775.00。$387.50 的差额就是紧急性的代价:当实际截止时间是明天早上时,这个作业却要为秒级答案支付费用。大多数流水线从不问这个问题,这就是为什么大多数流水线多付了钱。有用的框架不是“我们能否负担实时”,而是“截止时间到底是什么时候。”

模型选择对这个数字的影响超过流水线中的任何其他因素。在其他符合批处理条件的模型上,以完整批处理费率运行同一作业:

模型 每百万 token 的批处理输入/输出 作业总计
GPT-5 nano $0.025 / $0.20 $77.50
GPT-5 mini $0.125 / $1.00 $387.50
Claude Haiku 4.5 $0.50 / $2.50 $1,250.00

对于直接了当的分类任务,GPT-5 nano 整个百万份文档仅需 $77.50,值得首先评估。正确的流程是:以实时费率将代表性文档分别通过每个候选模型,并排比较输出打分,然后才投入那百万份文档。我们在本文中正是这样做的:以这种提示结构将 50 份文档(用新闻文章作为替代)分别通过 GPT-5 nano 和 GPT-5 mini 运行两次(一次在之前所述的提示收紧之前,一次在之后),总花费不到一美元。我们测量到的结果:

  • JSON 有效性完美:两个模型和两次运行共 200 次补全,零解析失败。该层级下的失败模式不是损坏的 JSON。
  • 类别纪律则不然。nano 在两次运行中都在 50 份文档中的 4 份上返回了列表外的类别;mini 在第一次运行中有 2 次,在提示修复后为 0 次。nano 的违规率并没有因为更严格的提示而改善。
  • 在摘要质量方面,逐文档评判,mini 在 50 份中的 14 到 23 份上明显或略好(取决于你打分的严格程度),其余为平局,nano 从未在任何单一文档上更好。模式是一致的:mini 保留了 nano 丢失的细节,例如姓名、美元数字和案例细节。

结论来自数字本身。对于大规模以分类为主的工作,nano 加上验证与重试机制,以五分之一的价格成为理性选择。当摘要需要人类阅读时,使用 mini 是合适的。无论哪种方式,一次花费不到一美元的评估,用测量而非直觉解决了一个四位数金额的决定;跳过这一步,团队最终会为 GPT-5 nano 的工作支付 Claude Haiku 的价格,或者以任何价格交付一百万个粗糙的摘要。

有两个计费注意事项。无服务器推理是预付费的,因此余额必须在任务运行前充值;批量定价被标注为“最高”为实时费率的一半,因此在提交工作量之前,请在定价页面确认您的模型的实际费率。

批处理何时胜过实时,以及何时自行托管

决定归结为几个在编写任何代码之前就能回答的问题。

当以下所有条件都成立时,批量推理是合适的工具:

  • 没有人等待单个响应。结果需要在24小时内获得,但不是几秒内。
  • 工作量足够大,以至于速率限制或成本很重要。在几千个请求以下,实时API更简单,节省的成本也太小而不重要。
  • 请求是独立的。每一行都是自包含的;批量推理没有让一个请求依赖于另一个请求输出的机制。多步骤链需要在批量API之外进行编排,通常每个步骤一个批量作业。
  • 文本进,文本出。DigitalOcean 的批量推理不支持多模态请求和图像生成。
  • 每个作业一个模型适合您的路由。混合模型管道意味着多个作业。

当延迟在任何程度上都很重要、请求量很小,或者您需要批量推理不支持的功能(如流式传输,或特定于提供商的功能如扩展思考)时,应继续使用实时推理。

在专用 GPU 上自行托管是第三种选择,其成本计算不同,而非自动更优。DigitalOcean 的专用推理以每小时 4.41 美元运行 H100,以每小时 30.32 美元运行 8× H100 节点。自行托管在三种情况下开始胜出。首先,开源模型:批量推理仅支持 OpenAI 和 Anthropic 的商业模型,因此每晚在 Llama 3.3 或 Qwen 上处理百万文档的工作应使用专用 GPU 或无服务器按 token 的开源定价。其次,稳定且大量的使用:如果夜间作业每晚运行并基本填满硬件,那么每晚数百美元 GPU 时间的专用端点可能比按 token 计费的大量 token 更便宜,并且缩放到零意味着队列为空时您停止付费。第三,数据控制:当文档不能离开您控制的基础设施时,按 token 计费的商业 API 无论价格如何都不能考虑。自行托管重新带来的成本是工程时间(服务栈、批处理逻辑、监控、重试:这些都是批量 API 为您完成的事情)以及空闲 GPU 的风险。如果您的流量不均匀或作业是偶发的,即使原始的 GPU 计算看起来接近,按 token 的批量定价在总成本上仍会胜出。

结语

上述流水线大约有 150 行 Python,其中大部分是常规数据处理而非机器学习:分割输入以遵守文件大小限制,使用真实文档 ID 作为 custom_id,根据文件名构建请求 ID 以便重跑安全,在 URL 和 30 天保留期到期前下载结果,并根据账单检查 token 使用量。平台处理在此规模下真正困难的部分:重试、容量调度以及与生产流量的隔离。

值得培养的习惯是,用一个问题来分类每个 LLM 工作负载:是否有人在等待这个答案?当有人在等待时,为实时行为支付实时价格。当没有人在等待时,以及对于积压任务、评估、标记作业和报告,没有人等待,批量应成为默认选项,实时则应成为需要说明理由的例外。大多数团队恰恰相反:他们将交互式 API 视为唯一的 API,将延迟容忍度视为可以忽略的东西,而不是设计输入。在这项任务中,这种思路在 775.00 美元的总费用中节省了 387.50 美元,而且不需要任何技巧、基础设施或模型更改。这项工作本来就要通宵运行;它就应该按这种定价方式计费。

参考

DigitalOcean 文档

  • 推理定价:本文使用的所有模型费率(GPT-5 mini 每百万 token 0.25 美元/2.00 美元,GPT-5 nano 每百万 token 0.05 美元/0.40 美元,Claude Haiku 4.5 每百万 token 1.00 美元/5.00 美元)、“最高 50%” 的批量折扣、专用 GPU 定价(H100 每小时 4.41 美元,8× H100 每小时 30.32 美元)、预付费计费,以及仅对已完成的请求收费的规则。
  • 推理限制:账户层级速率限制(第 3-4 层每分钟 600 个请求和 80万-200万 token)、批量限制(每个文件 50,000 个请求和 200 MB,每个账户每个模型 100 亿 token,24 小时窗口)、层级模型访问权限,以及批量流量隔离。
  • 推理功能:瞬态错误的自动重试、错误文件、续跑任务,以及防重复的任务创建。
  • 如何使用批量推理:输入文件格式、三步上传流程、预签名 URL 有效期(约 15 分钟)、文件 ID 有效性(最长 30 天)、任务状态、结果检索、输出保留期(最长 30 天)以及取消计费。
  • 使用无服务器推理管理模型访问密钥,以及管理无服务器推理预付款:设置前提条件。

提供商和竞争对手文档

测量数据:失败率、类别违规次数、标记数量和模型质量比较均来自作者自己的评估运行(2026年8月,通过 DigitalOcean 的无服务器实时 API 对 GPT-5 nano 和 GPT-5 mini 各运行两轮,每轮 50 篇文档),并非来自任何文档。

——

🧑‍💻

zhirenhun

一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。

← 上一篇
为推理用例选择正确的模型:生产环境中的推理系列
下一篇 →
AI评估工程:从零开始构建生产级LLM评估平台 [完整手册]

📌 相关推荐

GraphRAG 是推理问题,而非数据库问题
2026/8/30
构建市场时光机:使用 Python 和 WebSocket 重放交易会话
2026/8/30
如何自行基准测试LLM推理:值得信赖的数字设计标准
2026/8/30
← 返回文章列表