跳转至

第十一章:缓存、批处理、吞吐与成本优化

11.1 成本优化的四个杠杆

对按用量计费的模型 API,token 用量、工具使用和调用次数直接影响费用;自托管还要算算力利用率与运维。常用的四个杠杆是:减少重复计算(缓存)、把可延迟任务交给异步批处理、换更便宜的模型(路由)、压缩上下文(减少 token)。这里先看前两个应用层杠杆,路由降本已在第 3 章讨论,推理引擎内部的量化、KV Cache 等降本手段见 LLM · 推理与部署。

%%{init: {"flowchart": {"nodeSpacing": 12, "rankSpacing": 18, "padding": 8}}}%%
flowchart TB
    COST["调用成本"] --> CACHE["缓存:"]
    COST --> BATCH["批处理:"]
    COST --> ROUTE["路由降本:"]
    COST --> CTX["上下文压缩:"]

图中各项的完整含义:

  • 缓存: 避免重复计算
  • 批处理: 摊薄单位开销
  • 路由降本: 见第3章
  • 上下文压缩: 减少输入token

11.2 语义缓存:应用层最直接的降本手段

语义缓存用向量相似度寻找可复用答案,机制见 Tools · LLM 网关。相似度不是答案等价概率,0.85–0.95 之类阈值只能作为特定嵌入模型下的实验值;「可退款」与「不可退款」、不同日期和不同订单可能非常相似却不能共用答案。

先做精确缓存,再评估语义复用是否划算。缓存键或过滤条件应覆盖租户、用户权限、Prompt/模型/知识库/策略版本和业务时效;命中后仍要验证资源权限。对付款、实时余额等请求通常不能复用旧答案。按场景同时统计命中率、错误命中率、节省费用和答案陈旧率,删除文档或撤销权限时联动失效,不能只追求更高命中率。

11.3 Prompt Caching:另一种缓存,作用层次不同

答案缓存命中后可以跳过整次生成;Prompt Caching 则仍然发起调用,只复用已计算的前缀状态。 后者减少重复的 prefill 计算,未命中的输入和新输出仍要计算,生成时也仍需读取历史 KV 做注意力运算。这是LLM · KV Cache机制在应用层的直接收益,不是整段上下文此后都不参与推理。

应用层能做的事是把 Prompt 结构设计成"缓存友好":

# 缓存友好的结构:固定不变的部分放前面,易变部分放后面
prompt = (
    SYSTEM_INSTRUCTIONS
    + FEW_SHOT_EXAMPLES
    + retrieved_context
    + user_question
)

易变内容提前会缩短可复用前缀。应让供应商实际渲染后的前缀保持一致,包括消息、工具定义、多模态内容和相关设置,而不是只比较某个字符串的字节。最低长度、保留期、缓存写入价格与隔离范围由供应商和模型决定;命中后仍会生成新答案,不代表复用旧输出,也不保证整体延迟按同一比例下降。

11.4 请求批处理:同步实时 vs 异步批量

场景 策略 收益
用户实时对话 通常不等待应用层长时间攒批;引擎内部仍可在延迟预算内连续批处理 是否获益取决于排队时间与吞吐,不等于实时请求不能批处理
后台批量任务(摘要、打标签、离线分析) 使用供应商异步批量 API OpenAI Batch 官方文档给出相对同步 API 的 50% 折扣和 24 小时处理窗口;这是产品条件,不是批处理通则
# 伪代码:把可以延迟处理的任务路由到批量 API
def submit_batch_job(tasks: list[dict]) -> str:
    batch_input = "\n".join(json.dumps(t) for t in tasks)
    return batch_api.create(input_file=batch_input, completion_window="24h")

批量任务需要唯一的 custom_id、逐条状态和结果对账,结果顺序不保证与提交顺序一致。到期或部分失败时只重试失败项,不能重放已成功的有副作用任务。上述代码是接口无关伪代码,实际 OpenAI Batch 需先上传 JSONL 文件并使用文件 ID。批量 API 和第 3 章讲的"实时路由"是完全不同的两条路径:实时对话走网关的低延迟路由,能够容忍延迟的后台任务应该在架构设计阶段就分流到批量 API,而不是和实时流量走同一条路径再事后优化。

11.5 上下文压缩:减少输入侧 token

手段 说明
检索结果精简 RAG 场景下只把真正相关的片段放入上下文,而非整篇文档,详见 RAG · 检索
历史对话摘要 多轮对话中把较早的轮次压缩成摘要而非保留全文,详见 Agent · 记忆与上下文
精简系统 Prompt 定期审查系统 Prompt 是否存在冗余指令,过长的系统 Prompt 会在每次调用中重复计费

11.6 吞吐:用并发和排队策略平衡延迟与成本

在自建推理服务的场景下(见 LLM · 部署框架),连续批处理(continuous batching)等技术由推理引擎负责;应用层能控制的是并发请求数的准入策略——过多并发请求同时涌入会推高排队延迟,进而影响 SLO(见第 12 章):

import asyncio

semaphore = asyncio.Semaphore(MAX_CONCURRENT_REQUESTS)

async def call_model_with_admission_control(prompt: str):
    async with asyncio.timeout(REQUEST_TIMEOUT_SECONDS):  # Python 3.11+
        async with semaphore:
            return await model_client.generate(prompt)

Semaphore 只限制单进程活跃调用,不限制等待队列长度,也不约束多副本的总 RPM/TPM。入口还需要有界队列、按租户配额和超额拒绝;等待时间应计入总超时。多副本扩容之前确认供应商配额,否则只是更快触发限流。

11.7 成本可观测性:没有度量就没有优化依据

第 8 章提到的 Trace 数据应该聚合出按业务场景、按团队维度的成本报表:

报表维度 回答的问题
按接口/场景 哪个功能最烧钱,是否有优化空间
按团队/租户 成本如何分摊
按模型版本 换模型前后的成本变化是否符合预期

至少区分未缓存输入、缓存读写、输出、工具、检索、重试和评测费用;自托管还要计入 GPU 空闲、存储和运维。优化目标最好是「每个成功完成任务的总成本」,否则低价模型多轮重试、错误缓存和降级模板都可能让每请求价格更低、实际业务成本更高。

11.8 常见错误

11.8.1 只统计全局缓存命中率,不按场景拆分

会掩盖"该开缓存的场景没开、不该开的场景瞎开"的差异化问题,见 11.2 节。

11.8.2 拼接 Prompt 时把易变内容放在前面

这会缩短可复用的共同前缀;若低于供应商要求的最低长度,就无法命中。不能把任何一次内容变化都说成整段缓存必然失效。

11.8.3 把可以异步处理的任务和实时流量混在一起同步处理

可能错失供应商批量定价优惠,还可能因为攒批逻辑拖慢实时请求;折扣与完成窗口要按所选 API 核算。

11.8.4 历史对话不做任何压缩,无限累加上下文

多轮对话越往后,输入 token 越多,成本和延迟同步上升,且可能超出上下文窗口限制。

11.8.5 没有成本报表,优化决策靠猜测

不知道哪个场景真正烧钱,优化精力容易投入到影响不大的地方。

11.9 本章总结

  1. 成本优化有四个杠杆:缓存、批处理、路由降本、上下文压缩,这里先看前两个应用层手段;
  2. 语义缓存命中率要按业务场景拆分统计,而非只看全局数字;
  3. Prompt Caching 复用供应商实际处理的相同前缀,需满足具体模型与接口的缓存条件;
  4. 异步批量接口可能以较长完成窗口换取优惠,需处理部分失败和逐项对账;
  5. 上下文压缩(检索精简、对话摘要)直接减少输入 token,是最容易被忽视的降本手段;
  6. 成本可观测性是优化的前提,应按场景、团队、模型版本建立报表。

参考资料

本章的参考资料、阅读建议与来源说明见集中参考资料章节。