长上下文的成本结构:为什么 128K 不等于能用 128K
前言
上下文窗口扩大是这两年最直观的进步。8K 到 32K 到 128K 甚至更长,看起来只是"能装更多"。
但把它当成"可以少做检索"的理由,通常会在两个地方吃亏:账单和效果。
账单的问题在于长上下文不是线性变贵的,效果的问题在于"装得下"和"用得上"是两件事。这篇文章把这两点拆开算。
一、成本的三个来源
一次大模型推理的成本,由三部分组成,它们的增长规律完全不同。
1.1 Prefill:平方项
Prefill 是把输入的 prompt 编码成 KV Cache 的过程。这里面有两块计算:
- 线性部分:每个 token 都要过一遍所有参数,成本正比于
序列长度 × 参数量 - 平方部分:自注意力里每个 token 要和其他所有 token 算相关性,成本正比于
序列长度²
短上下文时线性部分占主导,所以感觉上"成本随长度线性增长"。但到了长上下文,平方项开始吃掉主要成本。
粗略地说:
| 序列长度 | 注意力相对成本 |
|---|---|
| 1K | 1 |
| 8K | 64 |
| 32K | 1024 |
| 128K | 16384 |
注意这只是注意力部分的相对增长,不是端到端成本。但因为线性部分同时也在涨,总成本的增长曲线比线性陡得多。
这就是为什么长上下文的输入价格通常有明显的分档——不是定价策略,是成本结构决定的。
1.2 Decode:线性但累积
生成阶段,每生成一个 token 都要和所有已存在的 token 做一次注意力。所以单步成本正比于当前总长度,而生成长度累积起来,总成本是 n × m(n 是输入长度,m 是输出长度)。
这带来一个反直觉的结论:长输入 + 长输出是最贵的组合。
如果你的场景是"塞 100K 上下文,让模型输出一份详细的 5K 报告",成本会是"塞 10K 上下文,输出 5K 报告"的数倍到十几倍——远不止 10 倍输入量的差距。
1.3 KV Cache:内存账
这是最容易被忽略的一笔。
模型在生成时要把每一步的 Key 和 Value 缓存下来,避免重复计算。这部分占用的显存可以这么算:
每 token 占用 = 2 (K 和 V) × 层数 × KV 头数 × 每个头的维度 × 字节数
以一个常见的 8B 级别、采用 GQA 的模型为例(32 层、8 个 KV 头、每头 128 维、fp16):
2 × 32 × 8 × 128 × 2 字节 = 131,072 字节 ≈ 128 KB / token
那么 128K 上下文需要:
131,072 token × 128 KB ≈ 16 GB
光 KV Cache 就要 16GB 显存,还不算模型权重本身(8B 模型 fp16 约 16GB)。
对比一下:如果这个模型用的是传统多头注意力(32 个头而不是 8 个),每 token 占用会变成 512KB,128K 就要 64GB。这就是 GQA 这类技术存在的意义——它不是为了效果,是为了让长上下文跑得动。
二、为什么"装得下"不等于"用得上"
2.1 位置偏差
模型对上下文不同位置的利用效率不均匀。有一个被反复验证的现象:开头和结尾的信息被利用得更好,中间的信息容易被忽略。
这个现象和上下文长度有关——短上下文时差异不明显,长上下文时中间区域几乎成了"盲区"。
对你的应用意味着:把关键信息塞在 100K 上下文的中间位置,模型很可能看不见它。 这不是模型偷懒,是注意力机制在长序列上的固有特性。
2.2 名义长度 vs 有效长度
现在很多评测会区分这两个概念:
- 名义长度:模型声明支持的最大窗口
- 有效长度:在这个长度下,模型的实际任务表现仍能维持在可接受水平
这两者往往有明显差距。一些针对长上下文的评测(比如在大海捞针类任务上做多针、多跳的变体)显示,很多模型在名义长度的一半甚至三分之一处,准确率就开始明显下降。
也就是说,一个标称 128K 的模型,在处理 128K 输入时可能远不如它处理 32K 时可靠。"能读进去"和"能理解好"不是一回事。
2.3 干扰项效应
长上下文里必然包含大量不相关内容。这些内容不只是"浪费 token",它们会主动干扰。
典型表现:你问 A 相关的问题,上下文里同时有 B、C、D 的描述,模型有时会把 B 的属性套到 A 上。信息越密集、越相似,干扰越强。
这一点在 RAG 场景里特别明显:召回 20 条文档塞进去,效果经常不如召回 5 条精排后的。
三、工程上的应对
3.1 上下文压缩
不是所有内容都值得原样塞进去。常见的压缩手段:
| 手段 | 做法 | 适用 |
|---|---|---|
| 摘要压缩 | 先让模型把长文档摘要成要点 | 文档型输入 |
| 结构化提取 | 只抽需要的字段,丢掉原文 | 表单、合同、日志 |
| 段落筛选 | 先做一轮粗筛,只保留相关段落 | 多文档问答 |
| 去重合并 | 相似内容只留一份 | 检索结果 |
其中"结构化提取"收益最大——因为它是任务导向的。如果下游只需要 5 个字段,把 20 页原文塞进去是纯粹的浪费。
3.2 分层处理
对于超出窗口的文档,不要试图一次塞完。两种做法:
Map-Reduce 式:分块处理,每块产出一个中间结果,再汇总。适合"总结全文"类任务。
滑动窗口 + 状态累积:逐块处理,每块带着之前累积的状态。适合流式输入、长对话。
这两种都会增加调用次数,但每次的上下文长度可控——总成本经常比单次超长调用更低,效果也更稳。
3.3 缓存复用
如果多次请求共享同一段前缀(比如固定的系统提示 + 知识库内容),用 prompt caching 能显著降低成本。命中缓存的部分通常按正常输入价格的百分之十几计费。
但要注意缓存的两个约束:
- 前缀必须完全一致。中间插入一个字符,后面全失效
- 有存活时间。超过了就要重新计算
所以设计提示结构时,把稳定不变的部分放前面,变化的部分放后面。这个顺序调整是免费的,但能显著提高缓存命中率。
3.4 用检索替代全量塞
回到最初的问题:长上下文是否意味着可以不做检索?
答案基本是否定的。原因不是"塞不下",而是:
- 检索能降低干扰项
- 检索能把最相关的放在开头(利用位置优势)
- 检索的成本远低于把全量内容送进 prefill
长上下文更适合解决"检索精度不够导致漏掉关键信息"的问题,而不是替代检索。
一个合理的组合是:检索取出 10-20 条候选,用 reranker 精排到 3-5 条,再给它们配上足够的周边上下文。总长度控制在几千 token,效果好于硬塞 100K。
四、成本优化清单
按收益从高到低排:
| 动作 | 收益 | 代价 |
|---|---|---|
| 减少输出长度(明确要求简洁) | 高 | 可能丢细节 |
| 结构化提取替代原文塞入 | 高 | 需要写抽取逻辑 |
| 提示前缀稳定化,提高缓存命中 | 高 | 结构调整 |
| 检索精排替代全量塞 | 高 | 需要检索链路 |
| 分块处理超长文档 | 中 | 调用次数增加 |
| 换更小的模型做粗加工 | 中 | 需要任务拆分 |
| 调低 max_tokens 上限 | 中 | 效果风险 |
第一条经常被忽略。输出 token 比输入 token 贵得多(因为 decode 是逐 token 串行生成的),如果你的提示里没有明确"简洁回答",模型倾向于展开,这部分成本是白花的。
做了什么
- 把长上下文成本拆成 prefill 的平方项、decode 的线性累积、KV Cache 的内存三部分
- 以 8B 级别 GQA 模型为例算出 KV Cache:每 token 约 128KB,128K 上下文约 16GB
- 指出传统多头注意力下同样的上下文要 64GB,说明 GQA 是为长上下文服务的工程手段
- 区分名义长度与有效长度,指出长上下文存在位置偏差与干扰项效应
- 给出四种应对:上下文压缩、分层处理、缓存复用、检索替代
- 列出七项成本优化动作,按收益排序
一些体会
长上下文是"减少信息损失"的工具,不是"省掉检索"的工具。 这两个定位差别很大。前者解决的是"检索漏了一条关键信息",后者要面对干扰项、位置偏差、成本三座山。
算成本要算输出。 大家习惯关注输入有多长,但 decode 是逐 token 串行的,单位成本更高。一份啰嗦的长回答,账单可能比长输入更吓人。
KV Cache 决定了长上下文能不能落地。 128K 在纸面上是个数字,落到显存上就是十几 GB。做私有化部署时,这个数字直接决定了你要什么卡、能开多少并发。算清楚再谈功能。
"有效长度"这个概念值得记住。 厂商宣传的是名义窗口,你实际能依赖的是有效长度。做设计时按有效长度的一半去规划,通常比较稳。
代码
KV Cache 占用的估算函数:
def kv_cache_bytes(seq_len, n_layers, n_kv_heads, head_dim, dtype_bytes=2, batch=1):
"""返回 KV Cache 字节数"""
return 2 * n_layers * n_kv_heads * head_dim * dtype_bytes * seq_len * batch
def human(n):
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if n < 1024:
return f"{n:.1f} {unit}"
n /= 1024
return f"{n:.1f} PB"
# 8B 级别,GQA(8 个 KV 头)
cfg = dict(n_layers=32, n_kv_heads=8, head_dim=128)
for L in [8_000, 32_000, 128_000]:
size = kv_cache_bytes(L, **cfg)
print(f"{L:>7,} tokens → {human(size):>10}")
# 对比:传统多头注意力(32 个头)
for L in [32_000, 128_000]:
size = kv_cache_bytes(L, n_layers=32, n_kv_heads=32, head_dim=128)
print(f"[MHA] {L:>7,} tokens → {human(size):>10}")
跑出来的量级大致是:GQA 下 8K 约 1GB、32K 约 4GB、128K 约 16GB;换成 MHA 后 128K 要 64GB。这个对比最能说明为什么现在的模型普遍用 GQA。
提示结构对缓存命中率的影响:
# ❌ 变化的内容在前,缓存永远不命中
prompt_bad = f"""当前时间:{now}
用户问题:{question}
[以下为固定知识库内容...]"""
# ✅ 稳定内容在前,变化内容在后
prompt_good = f"""[以下为固定知识库内容...]
当前时间:{now}
用户问题:{question}"""
这个改动只有几行,但它是长上下文应用里性价比最高的优化之一。
- 点赞
- 收藏
- 关注作者
评论(0)