长上下文:能力与挑战并存
当模型支持百万token的上下文时,新的问题随之而来:模型真的能有效利用这么长的上下文吗?研究表明,上下文长度和上下文利用效率是两个截然不同的问题。
长上下文的技术难点
注意力衰减
模型对上下文中不同位置信息的关注程度不均匀:
def measure_attention_decay(model, context_length, key_position):
"""测量模型对不同位置信息的关注度"""
# 在上下文的不同位置放置关键信息
results = []
for pos in [0, 0.1, 0.25, 0.5, 0.75, 0.9, 1.0]:
# pos=0表示开头,pos=1表示结尾
context = build_context_with_key_at(pos, context_length)
question = "根据上下文中的关键信息回答..."
accuracy = test_answer_accuracy(model, context, question)
results.append({"position": pos, "accuracy": accuracy})
return results
# 典型结果:
# position=0.0: accuracy=85%
# position=0.25: accuracy=52% ← 中间区域下降
# position=0.5: accuracy=48% ← 最低点
# position=0.75: accuracy=55%
# position=1.0: accuracy=82%
Needle-in-a-Haystack测试
这是评估长上下文能力的标准测试:在大量无关文本中隐藏一句关键信息,测试模型能否找到:
def needle_in_haystack(model, context_length, needle_position):
"""大海捞针测试"""
# 生成填充文本
filler = generate_filler_text(context_length - 200)
# 关键信息(针)
needle = "密码是:Sk7-9mPq"
# 在指定位置插入针
insert_pos = int(len(filler) * needle_position)
context = filler[:insert_pos] + needle + filler[insert_pos:]
# 测试模型能否找到
question = "文档中提到的密码是什么?"
response = model.generate(context + "\n\n" + question)
return {
"found": "Sk7-9mPq" in response,
"context_length": context_length,
"needle_position": needle_position
}
def full_evaluation(model):
"""全面评估"""
results = []
for length in [1000, 5000, 10000, 50000, 100000, 500000]:
for pos in [0, 0.1, 0.3, 0.5, 0.7, 0.9, 1.0]:
result = needle_in_haystack(model, length, pos)
results.append(result)
# 生成热力图
return plot_heatmap(results, x="position", y="length", color="found")
多针测试
更复杂的测试在上下文中放置多个关键信息:
def multi_needle_test(model, context_length, n_needles=5):
"""多针测试"""
filler = generate_filler_text(context_length)
needles = []
for i in range(n_needles):
needle = f"事实{i+1}:数值是{random.randint(100,999)}"
pos = random.uniform(0.1, 0.9)
insert_pos = int(len(filler) * pos)
filler = filler[:insert_pos] + needle + filler[insert_pos:]
needles.append({"content": needle, "position": pos})
# 测试模型能否回忆所有事实
questions = [f"事实{i+1}的数值是多少?" for i in range(n_needles)]
recall_rate = 0
for q, needle in zip(questions, needles):
response = model.generate(filler + "\n\n" + q)
if needle["content"].split(":")[1] in response:
recall_rate += 1
return recall_rate / n_needles
长上下文的训练挑战
位置编码外推
模型训练时使用固定长度的位置编码,推理时使用更长上下文时需要外推:
RoPE(旋转位置编码):
class RoPE:
def __init__(self, dim, max_position=8192, base=10000):
self.dim = dim
self.max_position = max_position
self.base = base
def encode(self, position):
"""旋转位置编码"""
freqs = 1.0 / (self.base ** (
torch.arange(0, self.dim, 2).float() / self.dim
))
angles = position * freqs
# 复数旋转
cos = torch.cos(angles)
sin = torch.sin(angles)
return cos, sin
# RoPE的外推问题:训练时max_position=4096,
# 推理时需要处理100K+的位置
YaRN(Yet another RoPE extensioN):
class YaRN:
"""YaRN通过温度缩放和插值实现位置外推"""
def __init__(self, dim, original_max=8192, extended_max=131072):
self.original_max = original_max
self.extended_max = extended_max
self.scale = extended_max / original_max
# 温度参数
self.temperature = 0.1
def adjust_position(self, position):
"""调整位置编码以支持更长上下文"""
# 分段处理
if position < self.original_max * 0.3:
# 低频区域:原始位置
return position
elif position < self.original_max * 0.7:
# 中频区域:插值
return position * (1 - self.temperature) + position / self.scale * self.temperature
else:
# 高频区域:缩放
return position / self.scale
训练数据构造
长上下文能力需要长序列训练数据:
class LongContextDataBuilder:
def build_training_data(self):
data = []
# 1. 长文档
long_docs = collect_long_documents(min_length=10000)
data.extend(long_docs)
# 2. 多文档拼接
multi_docs = self._concat_related_docs(target_length=50000)
data.extend(multi_docs)
# 3. 长对话
long_conversations = collect_long_conversations(min_turns=50)
data.extend(long_conversations)
# 4. 代码仓库级
repo_level = self._build_repo_context(target_length=100000)
data.extend(repo_level)
return data
def _concat_related_docs(self, target_length):
"""拼接相关文档构造长上下文"""
docs = []
current_length = 0
while current_length < target_length:
# 检索与已有内容相关的文档
doc = retrieve_related_doc(docs)
docs.append(doc)
current_length += len(doc)
return "\n\n".join(docs)
长上下文的工程优化
分块注意力
class ChunkedAttention:
"""分块注意力:减少长序列的注意力计算复杂度"""
def __init__(self, chunk_size=4096):
self.chunk_size = chunk_size
def attention(self, query, key, value):
B, T, D = query.shape
# 分块计算
chunks = T // self.chunk_size
outputs = []
for i in range(chunks):
start = i * self.chunk_size
end = start + self.chunk_size
# 块内注意力
q_chunk = query[:, start:end]
k_chunk = key[:, start:end]
v_chunk = value[:, start:end]
# 块内attention + 块间attention
local_attn = self._local_attention(q_chunk, k_chunk, v_chunk)
global_attn = self._global_attention(q_chunk, key, value)
outputs.append(local_attn + global_attn)
return torch.cat(outputs, dim=1)
KV Cache压缩
class KVCacheCompressor:
"""长上下文下KV Cache的压缩"""
def __init__(self, budget=8192):
self.budget = budget # 保留的token数
def compress(self, kv_cache, attention_scores):
# 1. 重要性评分
importance = attention_scores.sum(dim=-1) # [T]
# 2. 保留重要token
if len(importance) > self.budget:
# 保留Top-K + 最近N个
topk_idx = importance.topk(self.budget // 2).indices
recent_idx = torch.arange(
len(importance) - self.budget // 2, len(importance)
)
keep_idx = torch.cat([topk_idx, recent_idx]).unique()
kv_cache = kv_cache[keep_idx]
return kv_cache
检索增强长上下文
class RetrievalAugmentedLongContext:
"""结合检索的长上下文处理"""
def __init__(self, embedder, vector_store, llm):
self.embedder = embedder
self.store = vector_store
self.llm = llm
def process_long_document(self, doc, query):
"""处理超长文档"""
# 1. 分块
chunks = split_into_chunks(doc, size=1000, overlap=200)
# 2. 向量化存储
embeddings = [self.embedder.encode(c) for c in chunks]
self.store.add(chunks, embeddings)
# 3. 检索相关块
query_emb = self.embedder.encode(query)
relevant = self.store.search(query_emb, top_k=10)
# 4. 按原始顺序排列
relevant.sort(key=lambda x: x["original_index"])
# 5. 构建压缩上下文
context = "\n...".join([r["text"] for r in relevant])
return self.llm.generate(query, context=context)
长上下文 vs RAG
| 维度 | 长上下文 | RAG |
|---|---|---|
| 信息完整性 | 高(看全文) | 中(看片段) |
| 准确性 | 依赖模型利用率 | 依赖检索质量 |
| 延迟 | 高(长序列推理慢) | 低(短序列推理快) |
| 成本 | 高(token多) | 低(token少) |
| 可扩展性 | 受窗口限制 | 可扩展到无限文档 |
| 适合场景 | 单个长文档 | 多文档检索 |
最佳实践:长上下文处理核心文档,RAG扩展到更多文档。
结语
长上下文能力的提升不是线性的——从8K到32K是量变,从128K到1M是质变。质变不仅是技术挑战,更是应用范式的转变。当模型能一次"读完"一整本书、一整个代码仓库时,新的应用场景将自然涌现。但前提是我们解决了长上下文下的信息利用效率问题——看得到不等于看得到关键。