AI 工程基础体系 · 第 83/100 篇。内容覆盖机器学习、深度学习与生成式 AI;模型、数据、评测、权限和成本会作为同一生产系统处理。
LLM 长上下文工程:位置外推、注意力成本、检索与有效上下文
“长上下文”不是单一能力,而是四个相互约束的问题:
- 位置表示是否能覆盖训练时没有见过的序列长度;
- 注意力计算和 KV Cache 是否承担得起更长的序列;
- 系统是否能把真正相关的信息检索并放入上下文;
- 模型是否真的使用了这些信息,而不是仅仅接受更多 Token。
因此,模型配置中的 context_length=128k 不能直接等价为“模型能可靠利用 128k Token”。它至少同时受到训练分布、位置编码、注意力机制、显存、推理延迟、检索质量、数据权限和评测方式的限制。
一、先区分三个长度:窗口、训练长度与有效长度
上下文窗口是一次调用中,模型允许处理的输入 Token 与输出 Token 总数上限。若窗口为 ,输入长度为 ,预留输出长度为 ,通常需要满足:
这里的 是接口或模型实现允许的上限,不一定代表模型在这个长度上经过充分训练。
训练长度是模型预训练或长上下文微调阶段实际见过的最大序列长度。假设模型主要在 Token 上训练,即使推理代码允许传入 32,768 Token,也不能据此推出模型能可靠处理 32,768 Token。
有效上下文长度是一个任务相关的概念:在给定长度、位置、噪声和检索策略下,模型仍能以可接受准确率使用的信息范围。它往往小于名义窗口:
其中 是上下文长度, 是业务可接受的准确率阈值。对于“找出文档中的一个精确事实”和“总结整份长文”,这个有效长度可能完全不同。
例如,一个模型可以在 100k Token 输入中成功复述开头和结尾的内容,却在中间区域找不到一条唯一的事实。这说明它拥有较大的可接受输入窗口,但不一定拥有相同大小的可靠信息访问范围。
二、Transformer 为什么会受到长上下文影响
2.1 自注意力的计算过程
给定输入表示矩阵:
通过线性投影得到:
其中 是序列长度, 是隐藏维度。单头缩放点积注意力为:
含义如下:
- :当前位置要查询什么;
- :每个位置可以被匹配的索引;
- :匹配成功后取出的内容;
- :每个 Query 与每个 Key 的相似度;
- :避免点积数值随维度增大而过大;
softmax:把相似度转成权重;- 最后的矩阵乘法:按权重聚合 Value。
关键成本来自:
序列长度从 增长到 时,成对交互数量约增长四倍。因此,全注意力的时间和中间激活通常近似为:
这不是说所有实现都会实际分配一个完整的 矩阵。FlashAttention 等实现会通过分块和在线 Softmax 降低显存峰值,但它们并没有消除注意力需要处理大量 Query-Key 对这一事实。
2.2 因果注意力的精确数量
生成式语言模型使用因果掩码:第 个 Token 只能看到位置 到 。因此有效的注意力位置数量是:
当 时:
如果实现把完整分数矩阵以 FP16 保存,仅这一块就约占:
因果三角区域约为 64 MiB。但这只是一个注意力头或一个抽象矩阵的量级示例,实际模型还要乘上层数、批大小、头数,并包含 Q/K/V、激活和临时缓冲区。采用 FlashAttention 后,完整分数矩阵可以不落地,但计算量和内存带宽压力仍然存在。
2.3 预填充与逐 Token 解码是两种不同成本
推理通常分为两个阶段:
- Prefill(预填充):一次性处理用户输入和检索文档;
- Decode(解码):每次生成一个新 Token。
在 Prefill 阶段,输入中的 Token 彼此进行注意力交互,成本随输入长度近似二次增长。
在 Decode 阶段,历史 Token 的 Key 和 Value 通常被保存到 KV Cache 中。生成一个新 Token 时,只需用新 Query 与已有 K、V 交互,所以单步计算对历史长度近似线性,但生成越长,KV Cache 和每步读取的数据越大。
标准多头注意力下,每层 KV Cache 的元素数量近似为:
其中:
- 第一个 2 表示 K 和 V;
- 是已缓存的 Token 数;
- 是保存 K/V 的头数;
- 是每个头的维度。
全部层、FP16 下的字节数约为:
例如,32 层、32 个 KV 头、每头 128 维、上下文 128k、FP16:
这还没有计算模型权重、临时激活、批处理和运行时开销。
**Grouped-Query Attention(GQA)**和 **Multi-Query Attention(MQA)**通过减少 KV 头数降低缓存大小。它们通常保留较多 Query 头,但让多个 Query 头共享较少的 K/V 头。因此 KV Cache 规模由 决定,而不是简单由 Query 头数决定。
三、位置编码与“位置外推”
注意力本身只接收向量集合。若不额外提供位置信息,序列:
A B C
与:
C B A
可能无法区分顺序。因此 Transformer 需要把位置注入输入或注意力计算,这就是位置编码或更广义的位置表示。
3.1 绝对位置编码的边界
早期 Transformer 使用正弦位置编码,将位置 和维度 映射为:
这种方法可以通过公式计算训练范围之外的位置,但“可以计算”不等于“模型能够正确泛化”。模型训练时只在有限位置分布上学习了参数和模式,超出该范围后,注意力模式可能发生变化。
另一类是学习型绝对位置嵌入。它通常为每个位置维护一个向量表:
当 时,直接查表失败或需要截断。因此,它的外推能力通常更受限。
3.2 RoPE:把位置变成 Query-Key 的相对旋转
现代生成式模型大量使用 Rotary Position Embedding(RoPE,旋转位置编码)。它不是简单把位置向量加到 Token 表示上,而是对 Query 和 Key 的二维子空间进行位置相关的旋转。
对每个二维分量,位置 的旋转可写为:
然后:
由于旋转矩阵满足:
所以旋转后的点积可以表达为与相对距离 有关的形式:
这解释了 RoPE 的重要性质:注意力分数能够感知相对位置,而不只是两个绝对位置编号。
但 RoPE 仍然有频率和相位边界。不同维度使用不同角频率:
当位置越来越大时,低维高频分量会快速旋转。模型若只在较短长度训练,超出训练范围后可能遇到:
- 相位变化过快;
- 训练时没有出现的距离模式;
- 远距离注意力分数失真;
- 局部顺序能力与长距离定位能力互相影响。
因此,“RoPE 是相对位置编码”不能推出“RoPE 可以无限外推”。
3.3 位置外推与位置插值的区别
设训练长度为 ,目标长度为 ,其中:
位置外推通常指直接把位置编号扩展到训练范围之外,例如让 RoPE 继续使用更大的 。模型没有见过这些相位范围,风险较高。
位置插值则把目标位置压缩回训练范围:
例如训练长度 4k、目标长度 16k,原始位置 会映射为:
这样所有位置都落在训练范围内,但相邻 Token 的位置间隔被压缩了四倍。模型更容易适应整体长度,却可能损失细粒度位置分辨率。
实际长上下文扩展可能结合:
- RoPE 频率缩放;
- 位置插值;
- 持续预训练;
- 长序列监督微调;
- 特定的注意力缩放或偏置方法。
这些方法的具体配置依赖模型架构和 Transformers 版本。Hugging Face Transformers 提供统一的模型加载与生成接口,但并不存在一个对所有模型都等价、无需验证的“通用长上下文开关”。应检查具体模型配置、建模代码和训练说明,而不是只修改 max_position_embeddings 就认为模型完成了扩展。
3.4 一个必要的反例
假设模型在训练中只见过长度 4k。现在把 4k 文档直接拼接到 32k,并设置:
inputs = tokenizer(text, return_tensors="pt")
outputs = model.generate(**inputs, max_new_tokens=256)
这段代码可能出现三种情况:
- Tokenizer 或模型在输入阶段直接报长度错误;
- 实现接受输入,但位置索引、RoPE 配置或显存不足导致失败;
- 调用成功,但模型在 32k 位置上的检索准确率显著下降。
只有第一种是明显的接口错误。后两种是更危险的静默失效:程序成功运行,但模型质量已经超出训练支持范围。
四、长上下文不是“把所有内容塞进去”
4.1 检索增强生成的基本数据流
当知识库规模超过单次上下文窗口时,常见方案是 Retrieval-Augmented Generation(RAG,检索增强生成)。其基本过程如下:
flowchart LR
A[原始文档] --> B[解析与清洗]
B --> C[分块]
C --> D[生成向量与元数据]
D --> E[(向量索引)]
Q[用户问题] --> F[查询改写或嵌入]
F --> G[候选召回]
G --> H[权限过滤]
H --> I[重排序]
I --> J[上下文压缩与拼接]
J --> K[LLM 生成]
K --> L[引用、审计与评测]
离线阶段把文档转换为若干块,并为每块保存:
- 原文;
- 文档 ID;
- 块 ID;
- 标题和层级;
- 时间、租户、数据分类等元数据;
- 访问控制标签;
- 向量表示。
在线阶段不能只做向量相似度搜索。一个安全且可诊断的路径通常是:
- 解析用户身份和权限;
- 生成查询表示;
- 在权限允许的候选空间内召回;
- 使用关键词、向量或混合检索;
- 对候选块重排序;
- 去重、压缩并按 Token 预算拼接;
- 让模型生成答案;
- 返回引用并记录检索证据。
4.2 分块不是按固定字符数简单切割
若一个语义单元被切成两个块,检索到其中一块时可能缺少条件、结论或变量定义。若块过大,则一个块中包含大量无关内容,降低检索精度并增加上下文成本。
分块需要在以下因素之间取舍:
- 结构边界:标题、段落、列表、代码函数、SQL 语句;
- 块大小;
- 相邻块重叠;
- 文档层级;
- 表格和图片的语义完整性;
- 查询通常引用的范围。
重叠可以降低边界截断风险,但会导致重复 Token。设每块长度为 ,重叠为 ,步长为 ,文档长度为 ,块数近似为:
当 增大时,块数和索引存储都会增加,而重复内容也会挤占生成上下文。
4.3 一个可运行的最小检索示例
下面的代码只使用 Python 标准库,演示“分词、向量化、余弦相似度、Top-K 召回”。它不是生产级语义检索,因为没有使用语言模型嵌入;它的价值在于明确检索的中间状态。
import math
import re
from collections import Counter
documents = [
{
"id": "doc-a",
"acl": {"alice", "bob"},
"text": "RoPE 通过旋转 Query 和 Key,使注意力分数包含相对位置信息。"
},
{
"id": "doc-b",
"acl": {"bob"},
"text": "KV Cache 保存历史 Token 的 Key 和 Value,可以减少逐 Token 解码的重复计算。"
},
{
"id": "doc-c",
"acl": {"alice"},
"text": "检索系统应在召回阶段考虑租户和访问控制,不能把无权限文档交给模型。"
},
]
def tokenize(text):
# 中文按单字切分只是演示;生产系统应使用适合领域的分词或嵌入模型。
return re.findall(r"[\u4e00-\u9fff]|[A-Za-z0-9_]+", text.lower())
def vectorize(tokens):
return Counter(tokens)
def cosine(a, b):
keys = set(a) | set(b)
dot = sum(a[k] * b[k] for k in keys)
na = math.sqrt(sum(v * v for v in a.values()))
nb = math.sqrt(sum(v * v for v in b.values()))
return dot / (na * nb) if na and nb else 0.0
def retrieve(query, user, top_k=2):
q_vec = vectorize(tokenize(query))
candidates = []
# 权限过滤必须发生在结果进入提示词之前。
for doc in documents:
if user not in doc["acl"]:
continue
score = cosine(q_vec, vectorize(tokenize(doc["text"])))
candidates.append((score, doc))
candidates.sort(key=lambda x: x[0], reverse=True)
return candidates[:top_k]
results = retrieve("如何处理位置和注意力成本?", user="alice", top_k=2)
for score, doc in results:
print(f"{doc['id']}: {score:.3f} | {doc['text']}")
对于 alice,doc-b 不应出现在结果中,即使它与“注意力成本”相关。这个顺序非常重要:若先把所有候选交给重排序模型、摘要模型或 LLM,再在最后一步删除无权限内容,敏感信息可能已经通过模型输入、日志、缓存或错误消息泄露。
该示例中,查询“位置和注意力成本”可能只与 doc-a 的“位置”部分有词面重叠,而对“成本”没有正确语义理解。生产系统通常需要:
- 关键词检索捕获精确术语;
- 向量检索捕获语义相似;
- 重排序模型比较查询与候选块的细粒度相关性;
- 元数据过滤保证租户、时间和权限边界;
- 结果去重避免多个相邻块浪费预算。
4.4 召回率不等于最终回答质量
设问题的正确证据集合为 ,召回结果为 。检索召回率可以定义为:
如果正确证据没有被召回,生成模型无法凭空恢复该事实,回答通常只能猜测或拒答。
但召回到证据也不等于回答正确。证据可能:
- 出现在上下文的中间位置;
- 被相似但冲突的文档淹没;
- 缺少前置定义;
- 超过 Token 预算而被截断;
- 因排序错误放在模型不容易使用的位置;
- 与用户问题的实体、时间或版本不匹配。
因此要分别评测:
- 检索层:Recall@K、MRR、nDCG;
- 上下文层:证据是否完整、是否冲突、是否被截断;
- 生成层:答案准确率、引用正确率、拒答率、幻觉率;
- 系统层:延迟、Token 成本、显存、错误率和权限违规率。
五、什么是“有效上下文”
可以把一段上下文分成三个集合:
- :实际发送给模型的全部 Token;
- :与当前任务真正相关的 Token;
- :模型在生成答案时实际利用的相关 Token。
通常有:
名义上下文长度只测量 ,而有效上下文更接近 。
5.1 “Lost in the Middle”现象
长上下文模型常见的位置偏差是:模型更容易使用开头和结尾的信息,而对中间信息的访问能力较弱。这不是所有模型、任务和提示词都必然发生,但它是必须测试的风险。
一个最小诊断实验是:
- 准备若干无关段落;
- 插入一条唯一的事实,例如
密钥编号是 K-7391; - 分别把事实放在开头、四分之一处、中间、四分之三处和结尾;
- 保持 Token 总数与问题完全相同;
- 比较模型回答准确率。
如果只有中间位置显著失败,就说明名义窗口大于有效定位能力。
可以进一步定义位置准确率:
对一组位置取最小值或分位数,比只报告整体平均准确率更能暴露长上下文问题。
5.2 上下文压缩的因果边界
压缩可以包括:
- 删除与问题无关的块;
- 抽取包含实体、数值和条件的句子;
- 对相邻块去重;
- 先按章节摘要,再对相关章节保留原文;
- 让模型生成查询相关摘要。
压缩的目标不是让文本更短,而是让:
提高,同时不删除回答所需的必要条件。
压缩存在不可逆风险。如果把“只有在租户类型为 enterprise 且版本不低于 3.2 时成立”压缩成“功能在 3.2 可用”,模型可能生成看似合理但条件错误的答案。因此,涉及权限、金额、时间、版本、否定和异常条件的句子,通常应保留原文或保留可追溯引用。
六、提示词布局与 Token 预算
假设模型上下文窗口为 ,需要预留输出 ,系统提示词占用 ,用户问题占用 ,检索内容占用 ,则必须满足:
可用检索预算为:
如果系统设置了较大的 max_new_tokens,检索内容预算就会减少。反过来,盲目增加上下文而不预留输出,会导致生成被截断或接口报错。
一个可解释的布局可以是:
[系统规则与输出格式]
[任务相关的全局定义]
[高置信度、最相关的证据]
[可能冲突的证据及来源]
[用户问题]
但没有一种布局适用于所有模型。应通过位置敏感评测验证,而不是把“相关内容放在开头”当作规范保证。
对于存在多个证据块的任务,还需要处理冲突。每个块至少附带:
- 来源;
- 时间;
- 版本;
- 权限范围;
- 置信度或检索分数。
模型提示词应明确:冲突时依据什么字段判断,无法判断时如何拒答。否则,模型可能把不同版本的文档拼成一个不存在的结论。
七、长上下文推理中的成本与并发
7.1 成本不只由输出 Token 决定
一次请求的资源成本至少包括:
当输入从 8k 增加到 64k 时,即使输出长度不变,Prefill 计算、首 Token 延迟和 KV Cache 都可能显著增加。
**Time To First Token(TTFT)**主要受 Prefill 影响;每 Token 延迟主要受 Decode 阶段读取 KV Cache、模型权重和调度影响。长输入可能表现为“首字很慢”,而长输出可能表现为“首字后持续很慢”。
7.2 并发会放大 KV Cache 问题
若同时处理 个请求,且每个请求的缓存长度分别为 ,则 KV Cache 总量近似为:
因此,平均上下文长度并不能完整描述显存风险。少数超长请求可能导致:
- KV Cache 分配失败;
- 其他请求被驱逐;
- 批处理效率下降;
- 尾延迟(P95/P99)恶化;
- 请求超时后缓存回收不及时。
生产服务通常需要限制单请求输入长度、单租户并发、最大生成长度和总 Token 预算,并在接近显存水位时拒绝或降级,而不是等待系统触发全局 OOM。
7.3 复用前缀与权限边界
如果多个请求共享相同的系统提示词或长文档前缀,可以复用部分 Prefill 或 KV Cache,降低重复计算。但缓存键必须包含足够的隔离信息,例如:
- 模型版本;
- tokenizer 版本;
- 完整前缀内容哈希;
- 租户;
- 权限策略;
- 工具和系统提示版本。
不能因为文本前缀相同,就在不同租户之间共享包含私有信息的缓存。缓存命中优化必须服从数据隔离。
八、训练、微调与数据分布
长上下文能力不是只靠推理时修改参数获得的。若模型需要在新长度上可靠工作,训练数据至少要覆盖以下因素:
- 更长的序列长度;
- 跨段依赖;
- 远距离引用;
- 多文档冲突;
- 证据位于不同位置;
- 长输入下的指令遵循;
- 长输出截断和停止条件。
继续预训练时,长序列会增加单步计算和显存需求;监督微调时,如果样本只是把短问答随机拼成超长文本,模型可能学到“长输入中的局部模式”,却没有学会跨段推理。
训练数据还会影响检索系统的有效性。若文档存在重复版本、错误权限标签、过时内容或解析乱码,模型上下文再长也无法弥补数据问题。生产系统应把模型、索引、文档版本、权限元数据和评测集作为同一个发布单元管理。
九、Hugging Face Transformers 中的长度检查
Hugging Face Transformers 提供模型、Tokenizer 和生成流程的统一接口,但具体长度限制由模型架构、配置和实现共同决定。下面是一个通用的检查示例:
from transformers import AutoTokenizer, AutoModelForCausalLM
model_name = "your-model-name"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name)
print("tokenizer.model_max_length =", tokenizer.model_max_length)
print("config.max_position_embeddings =",
getattr(model.config, "max_position_embeddings", None))
print("config.max_sequence_length =",
getattr(model.config, "max_sequence_length", None))
text = "请总结下面的材料:\n" + ("示例文本。" * 1000)
inputs = tokenizer(
text,
return_tensors="pt",
truncation=False,
)
input_length = inputs["input_ids"].shape[-1]
print("input_length =", input_length)
max_new_tokens = 256
print("total_requested_tokens =", input_length + max_new_tokens)
outputs = model.generate(
**inputs,
max_new_tokens=max_new_tokens,
do_sample=False,
)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
运行前需要安装与模型兼容的 PyTorch 和 Transformers 版本,并准备足够的模型权重和显存。
这里有几个容易混淆的点:
tokenizer.model_max_length是 Tokenizer 侧的限制或提示,不一定等于模型真实可用窗口;max_position_embeddings是某些架构的重要配置,但不保证所有模型都只通过它决定长度;max_new_tokens只限制新生成 Token,不等于总长度;truncation=False可以帮助发现超长输入,但具体 Tokenizer 可能仍有模型相关行为;- 代码成功执行不代表超出训练长度后的质量得到保证。
在设置更长长度前,应同时验证:
- Tokenizer 是否产生预期 Token 数;
- 模型配置和实现是否支持该长度;
- 注意力掩码与位置编码是否正确;
- 显存峰值是否可接受;
- 输出是否在长序列评测中保持质量。
不要把修改配置文件、关闭截断或提高 max_new_tokens 当作位置外推方法。它们最多改变接口行为,不能替代长上下文训练或经过验证的位置缩放方案。
十、常见误解与对应诊断
误解一:窗口越长,模型记忆越强
长窗口只表示模型可以接收更多输入。诊断时应使用“隐藏事实”测试,并改变事实位置、干扰数量和证据重复次数,分别报告准确率,而不是只测试一份长文档摘要。
误解二:使用 RoPE 就能自然外推
RoPE 的相对位置信息有助于泛化,但高频旋转的相位范围仍可能超出训练分布。诊断方法是比较训练长度内、轻度超出和大幅超出的定位准确率,并检查不同距离的性能曲线。
误解三:把所有相关文档放入上下文比检索更可靠
加入更多文档会增加冲突、噪声、Token 成本和中间位置风险。若 Top-K 增加后 Recall@K 上升但最终答案准确率下降,说明瓶颈从“找不到证据”转为“模型无法筛选证据”。
误解四:向量相似度高就代表答案正确
向量检索可能召回主题相似但版本错误、租户错误或条件不同的文档。应把实体、时间、版本和权限作为独立过滤条件,并对召回结果做人工可解释检查。
误解五:只测平均延迟就足够
长上下文系统的风险通常出现在 P95/P99 延迟、显存峰值、超时回收和并发突增。压测时应覆盖不同输入长度、输出长度、并发量和缓存命中率,并记录 TTFT 与 Decode 速度。
十一、一个完整的生产级排查路径
当长上下文回答失败时,可以按数据流逐层定位:
- Token 层:实际输入是多少 Token,是否发生截断,特殊 Token 是否重复;
- 位置层:输入是否超过训练长度,位置缩放配置是否与模型实现匹配;
- 检索层:正确证据是否进入 Top-K,权限过滤是否正确;
- 上下文层:证据是否因去重、压缩或预算限制被删除;
- 布局层:证据位于什么位置,是否存在中间区域性能下降;
- 生成层:模型是否引用证据,是否出现版本冲突或无依据推断;
- 系统层:Prefill 延迟、KV Cache、并发和超时是否改变了请求路径。
例如,答案错误并不一定是“模型推理能力不足”。如果 Recall@20 为零,应该先修复分块或检索;如果 Recall@20 很高但引用错误,应检查重排序、冲突处理和提示布局;如果单请求正确而并发时错误,则需要检查超时、截断、缓存污染或降级逻辑。
十二、工程取舍:扩大窗口、优化检索还是压缩上下文
可以把三种方案放在同一个约束下比较:
| 方案 | 主要收益 | 主要代价 | 适用边界 |
|---|---|---|---|
| 扩大模型上下文 | 减少显式切分,保留更多原文 | 计算、显存和训练成本高 | 需要跨长距离依赖,且有长上下文训练 |
| 改进检索 | 降低无关 Token,提升证据密度 | 需要索引、重排序和权限工程 | 知识库大、问题通常只依赖局部证据 |
| 上下文压缩 | 降低成本,减少噪声 | 可能丢失条件、否定和细节 | 证据较冗余,且可追溯性设计完善 |
| KV Cache 优化 | 降低解码显存和延迟 | 依赖架构和运行时实现 | 多轮对话或长输出 |
| 位置扩展训练 | 提高超长位置可靠性 | 需要训练数据、算力和专项评测 | 业务长期需要超出原始长度 |
没有一种方案可以单独解决全部问题。最可靠的系统通常先保证权限正确和证据可召回,再用上下文预算控制噪声,最后根据评测结果决定是否进行位置扩展或更换模型。
长上下文工程的核心目标不是把更多 Token 发送给模型,而是在明确的位置表示、可控的二次注意力成本、正确的检索边界和可验证的有效上下文之间取得平衡。名义窗口是接口属性;有效上下文则是经过模型、数据、检索、权限、成本和评测共同证明的系统属性。
系列导航与关联阅读
- 系列入口:AI 工程完整学习路线:从机器学习与 Transformer 到 RAG、Agent 和生产治理
- 上一篇:推测解码:草稿模型、接受率、正确性与加速边界
- 下一篇:Prompt Cache 工程:前缀复用、缓存键、隔离、失效和成本
官方资料
本文依据研究论文、标准组织与主流框架官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。

评论
0 条讨论