跳转至

附录 1A:KV Cache 与 Prefill

第一章提到,输出 token 的单价是输入 token 的 3-4 倍。这不是商业策略上的定价歧视——背后是实实在在的计算成本差异。要理解这个差异,需要先搞清楚 Transformer 在推理阶段到底在做什么。


KV Cache 是什么

Transformer 的核心是 Self-Attention。每生成一个新 token,注意力机制需要拿当前 token 的 Query 去和所有前面 token 的 Key 做点积,得到注意力权重后再去加权求和所有前面 token 的 Value。

问题在于:生成第 100 个 token 的时候,需要前面 99 个 token 的 Key 和 Value;生成第 101 个 token 的时候,又需要前面 100 个 token 的 Key 和 Value。如果每生成一个 token 都重新算一遍所有 token 的 K 和 V,计算量是 O(n²) 级别的——这太浪费了,因为前面 token 的 K 和 V 根本没变过。

KV Cache 就是用来解决这个问题的:把已经算过的 K 和 V 存起来,后面直接用,不重复计算

graph LR
    subgraph 无 KV Cache
        A1["生成 token 3"] --> B1["重新计算 token 1,2,3 的 K,V"]
        A2["生成 token 4"] --> B2["重新计算 token 1,2,3,4 的 K,V"]
        A3["生成 token 5"] --> B3["重新计算 token 1,2,3,4,5 的 K,V"]
    end

    subgraph 有 KV Cache
        C1["生成 token 3"] --> D1["只计算 token 3 的 K,V
复用 token 1,2 的缓存"] C2["生成 token 4"] --> D2["只计算 token 4 的 K,V
复用 token 1,2,3 的缓存"] C3["生成 token 5"] --> D3["只计算 token 5 的 K,V
复用 token 1,2,3,4 的缓存"] end

说白了就是一个空间换时间的策略——多占一些 GPU 显存来存 KV Cache,换来每一步只需要做一个 token 的 K,V 计算。


Prefill 和 Decode 两个阶段

LLM 推理分成两个阶段,性质完全不同。

Prefill(预填充)

用户发来一段 prompt(比如"帮我规划一个轻松的杭州一日游"),这些 token 是一次性全部送进模型的。模型把所有 input token 并行处理,计算出每个 token 的 K 和 V,存入 KV Cache。

Prefill 阶段的特点:

  • 并行计算 — 所有 input token 同时处理,GPU 的算力被充分利用。
  • 计算密集(compute-bound) — 瓶颈在 GPU 的浮点运算能力,不在显存带宽。
  • 一次性完成 — 不管 prompt 有 100 个 token 还是 1000 个,都是一次前向传播。

Decode(解码)

KV Cache 准备好之后,模型开始一个 token 一个 token 地生成输出。每一步只有一个 token 进入模型,计算它的 K,V 后存入 Cache,然后用 Attention 算出下一个 token。

Decode 阶段的特点:

  • 串行计算 — 必须一个接一个,第 N+1 个 token 的生成依赖第 N 个 token 的结果。
  • 内存带宽密集(memory-bandwidth-bound) — 每一步都要从显存读出整个 KV Cache(所有历史 token 的 K 和 V),但实际计算量只有一个 token 的。GPU 大部分时间在等数据从显存搬过来。
  • 每个 token 都是一次前向传播 — 生成 300 个 token 就要做 300 次前向传播。
flowchart LR
    subgraph Prefill 阶段
        P["所有 input token
并行处理"] --> KV["生成 KV Cache"] end subgraph Decode 阶段 KV --> T1["token 1"] T1 --> T2["token 2"] T2 --> T3["token 3"] T3 --> T4["..."] T4 --> TN["token N"] end style P fill:#4a9eff,color:#fff style KV fill:#ffa64a,color:#fff style T1 fill:#5cb85c,color:#fff style T2 fill:#5cb85c,color:#fff style T3 fill:#5cb85c,color:#fff style TN fill:#5cb85c,color:#fff

为什么输出比输入贵

现在回到第一章的问题:为什么 completion_tokens 的单价是 prompt_tokens 的好几倍?

原因就在两个阶段的计算效率上:

Prefill 处理 input token —— 高效。1000 个 input token 可以一次性并行处理,GPU 利用率高,摊到每个 token 上的时间和电费很低。

Decode 生成 output token —— 低效。每个 output token 都要做一次单独的前向传播,GPU 利用率低(大量时间花在读显存),摊到每个 token 上的成本自然高。

用一个类比:Prefill 像是印刷厂一次印 1000 份报纸,单份成本很低;Decode 像是手抄报纸,一份一份地抄,效率差别就在这里。

具体到 OpenAI 的定价(以 gpt-4o-mini 为例):

单价 每百万 token
Input token(Prefill) $0.15 / 1M $0.15
Output token(Decode) $0.60 / 1M $0.60

输出是输入的 4 倍。这个倍率直接反映了 Decode 阶段的低效率。


Prompt Caching:复用 KV Cache

既然 Prefill 的主要工作是生成 KV Cache,那一个自然的想法是:如果 prompt 的前缀没变,能不能直接复用之前算好的 KV Cache?

这就是 Prompt Caching。

flowchart TD
    subgraph 第一次请求
        A1["系统提示 + 用户消息 A"] --> B1["完整 Prefill"]
        B1 --> C1["生成 KV Cache"]
        C1 --> D1["Decode 生成回复"]
    end

    subgraph 第二次请求(前缀相同)
        A2["系统提示 + 用户消息 B"] --> CHECK{"前缀匹配?"}
        CHECK -->|是| REUSE["复用已有 KV Cache"]
        CHECK -->|否| FULL["完整 Prefill"]
        REUSE --> NEW["只对新增 token 做 Prefill"]
        NEW --> D2["Decode 生成回复"]
        FULL --> D2
    end

    style REUSE fill:#5cb85c,color:#fff
    style FULL fill:#d9534f,color:#fff

举一个旅游 Agent 的场景。假设你的系统提示是 1500 token,包含了 Agent 的角色设定、工具描述、输出格式要求等。每次用户发消息,系统提示都会原封不动地放在最前面。

如果没有 Prompt Caching,每次请求都要重新处理这 1500 token 的系统提示——每次都做同样的 Prefill 计算,每次都生成一模一样的 KV Cache。

有了 Prompt Caching,第一次请求照常做完整 Prefill。从第二次请求开始,只要系统提示的前缀没变,就直接复用缓存的 KV Cache,只对新增的 token(用户的新消息)做 Prefill。

OpenAI 对 Prompt Caching 的定价是 input token 的 50% 折扣。也就是说,命中缓存的部分只收一半的钱:

每百万 token(gpt-4o-mini)
Input token(首次,无缓存) $0.15
Input token(命中缓存) $0.075
Output token $0.60

这意味着:系统提示越长、对话轮次越多,Prompt Caching 省下的钱越明显。

触发 Prompt Caching 有一个前提条件:前缀必须严格一致。如果你在系统提示里插入了当前时间戳、随机数、或者每次请求都不同的内容,前缀就不匹配,缓存就失效了。所以设计 prompt 时,把固定内容放前面、动态内容放后面,是一个实用的优化策略。


对 Agent 开发的影响

理解了 Prefill/Decode 和 KV Cache 之后,有几个直接的工程影响:

对话越长,Prefill 越慢越贵

每次 API 调用都要带上完整的对话历史(messages 数组),这些都是 input token。对话进行到第 20 轮,messages 可能已经有几千个 token 了——每次请求都要做一遍 Prefill。

graph LR
    R1["第 1 轮
input: 100 token"] --> R5["第 5 轮
input: 800 token"] R5 --> R10["第 10 轮
input: 2000 token"] R10 --> R20["第 20 轮
input: 5000 token"] style R1 fill:#5cb85c,color:#fff style R5 fill:#a8d08d,color:#000 style R10 fill:#ffa64a,color:#fff style R20 fill:#d9534f,color:#fff

这就是为什么后面会讲"对话历史管理"——不能无脑把所有历史消息都塞进去,需要做截断、摘要、或者选择性保留。

系统提示要放在最前面

为了最大化 Prompt Caching 的命中率,固定的系统提示应该放在 messages 数组的最前面。如果你把系统提示和动态内容交错排列,缓存就很难命中。

控制输出长度直接影响成本

因为 Decode 阶段每个 token 都贵,max_tokens 不要随便设得很大。如果你只需要一个 JSON 格式的行程摘要(大约 200 token),就没必要给模型 2000 token 的空间让它写散文。


小结

LLM 推理分 Prefill 和 Decode 两个阶段。Prefill 并行处理输入,生成 KV Cache;Decode 逐个生成输出 token,依赖 KV Cache 避免重复计算。Prefill 高效、Decode 低效,所以输出 token 比输入 token 贵。Prompt Caching 通过复用相同前缀的 KV Cache 来省钱省时间。

这些知识在第一章可能显得有点"背景知识",但到后面做 Agent 的时候——对话历史越来越长、系统提示越来越复杂、工具描述占的 token 越来越多——这些就是实打实影响成本和延迟的因素了。