长文本基准分数背后至少混入了两类干扰:关键证据落在文档的什么位置,以及这些证据以怎样的结构出现。模型在证据位于开头或结尾时表现尚可,一旦证据被压到长文中间,准确率就会快速下降;同样一段事实,放在普通段落里能提取出来,塞进列表或表格后可能就失效。这种位置依赖和结构依赖如果不单独度量,平均分再高也可能掩盖局部失败。

要解决这一问题,不能只靠更长的上下文窗口或更大的模型。评估基准本身需要重新设计,模型训练也需要引入结构感知和更稳健的位置编码,最终评估报告则应把位置与结构分开呈现。
一、先量化问题:位置与结构如何扭曲评估结果
位置依赖最典型的表现是U型曲线。把同一个事实性问题插入一篇8000字的文档,改变插入位置,模型准确率往往在前5%和末尾5%较高,在中间40%到60%处明显塌陷。这个现象并不局限于某一种模型结构,从因果解码器到编码器-解码器架构都出现过。原因通常不是模型读不到中间内容,而是训练阶段对长序列中段的有效监督不足,加上相对位置编码在长距离上的注意力分数被稀释,使中间片段无法获得足够强的查询-键匹配。
为了看清影响幅度,可以比较同一批问题在三种结构下的得分。以下是一次受控评测的示意结果:
| 关键证据位置 | 段落结构得分 | 列表结构得分 | 表格结构得分 |
|---|---|---|---|
| 前5% | 82.4 | 78.1 | 71.6 |
| 中间50% | 64.8 | 59.3 | 48.2 |
| 末尾5% | 85.2 | 80.6 | 73.5 |
表格中段的48.2分与末尾段落结构的85.2分相差超过37个点,这说明位置和结构不是两个孤立变量。同一个位置下,表格和列表往往比自然段落更困难,因为模型需要额外理解单元格边界、列名关系以及换行符的语义。预训练语料中连续叙述文本占主导,结构化标记的分布频率低,导致模型对表格和列表的内部表示不够稳定。
结构依赖还体现在跨结构推理上。一个问题如果要求同时使用段落中的背景信息和表格中的数值,模型经常只回答其中一个来源,或者把数值错配到错误的列。很多基准只统计单点证据抽取,跨结构推理题占比很小,这让结构依赖问题更加隐蔽。
二、基准设计:用均匀采样和结构覆盖降低偏差
要评估一个模型是否真正理解长文本,基准中的证据位置不能只放在开头或结尾。合理的做法是把文档按相对位置分成多个区间,在每个区间内随机插入关键证据,保证每个区间的测试样本数量大致相同。这样训练集或评测集中的位置先验会被削弱,模型无法通过记住开头或结尾来刷分。相对位置的计算应当基于文档总长度,而不是字符数或词数,因为不同切分方式会引入额外偏差。
结构覆盖同样不能只做自然段落。文档中的标题、列表、表格、代码块、引用块、对话轮次都需要有对应测试样本。更好的方式是让同一个事实在不同结构中反复出现,但每次只保留一种结构作为可回答问题所需的证据源,其他结构里放置干扰信息。这样能检测模型是否学到了结构无关的语义提取能力。
下面是一个生成多位置探针数据的简单脚本,它按相对位置均匀插入关键句,避免只测两端:
import json
def build_position_probes(total_len=8000, num_positions=10):
probes = []
unit = "这是一段与问题无关的背景文本,用来填充文档长度。\n"
key_sentence = "目标项目的季度增长率为 23.6%。"
for i in range(num_positions):
rel_pos = i / (num_positions - 1)
insert_at = int(total_len * rel_pos)
prefix = unit * insert_at
suffix = unit * (total_len - insert_at - 1)
document = prefix + key_sentence + "\n" + suffix
probes.append({
"relative_position": round(rel_pos, 2),
"answer": "23.6%",
"document": document
})
return probes
if __name__ == "__main__":
samples = build_position_probes()
with open("position_probes.json", "w", encoding="utf-8") as f:
json.dump(samples, f, ensure_ascii=False, indent=2)
上面的脚本生成10个相对位置均匀分布的样本,位置从0到1线性变化。实际基准中还应加入答案类型多样性、干扰项和负样本,避免模型只学会抽取固定句式。结构覆盖可以遵循同样思路:把关键句子分别嵌入段落、列表、表格单元格或代码注释中,生成等价问题。评估时按结构和位置分别统计,才能看到模型短板所在。
三、模型侧:从位置编码与结构感知入手
位置编码是影响长文本中段性能的核心因素之一。RoPE在长距离上通过频率缩放可以缓解外推问题,NTK-aware缩放和YaRN等方法将高频部分保持不动、低频部分按比例压缩,让模型在超出训练长度后仍能维持合理的注意力分布。ALiBi则直接在注意力分数上施加与距离成正比的惩罚,使中段token不会被过度忽略。这些改进的共同目标是降低绝对位置对语义匹配的干扰,让模型不是依赖出现在第几个token附近,而是依赖内容本身的相关性。
结构感知方面,可以在分块后给每个片段添加结构类型标记。模型看到<list>、<table>、<code>等标记后,会调整对外部结构边界和内部字段关系的建模方式。更精细的做法是层次化位置编码,把段级位置、句级位置和词级位置分开表示,避免长文档中单个整数位置编码把不同层级的信息混在一起。表格类数据还可以在注意力机制中加入行、列偏置,使同一行的单元格更容易互相注意到,不同列之间保持较弱关联。
代码实现上,最常见的第一步是对RoPE做NTK缩放,下面是一个简化示例:
import math
class NTKScaledRoPE:
def __init__(self, dim, max_len, base=10000.0, scale=8.0):
self.dim = dim
self.max_len = max_len
self.base = base * scale
self.inv_freq = []
for i in range(0, dim, 2):
self.inv_freq.append(1.0 / (self.base ** (i / dim)))
def encode(self, seq_len):
pos = [float(p) for p in range(seq_len)]
sin_list = []
cos_list = []
for p in pos:
for freq in self.inv_freq:
angle = p * freq
sin_list.append(math.sin(angle))
cos_list.append(math.cos(angle))
return sin_list, cos_list
实际上大多框架已内置成熟缩放参数,不需要从零实现。关键是理解缩放系数和基础频率之间的取舍:缩放过大会让短距离位置分辨能力下降,缩放不足则中段注意力仍然分散。结构标记注入通常更简单,只需在分词后插入特殊token,或把标记映射为额外的可学习嵌入,与token嵌入相加即可。
四、评估解耦:用位置敏感曲线和结构鲁棒性分数取代单一总分
即使基准和模型都做了改进,评估报告如果只给一个总分,仍然无法判断位置依赖和结构依赖是否被解决。更合理的做法是按相对位置分桶,分别报告每个桶的准确率。把位置从0到1分成10个区间,绘制位置敏感曲线,观察曲线是否仍然呈现明显U型。如果中间区间的得分与两端差距缩窄到5个百分点以内,可以认为位置鲁棒性显著改善。
结构鲁棒性可以单独报告段落、列表、表格、代码块四个结构维度的得分,并计算结构间方差。方差越小,说明模型对文本形态的依赖越低。还可以设计跨结构组合题,要求模型从段落中取一个条件,再从表格中取一个数值,最后给出判断。这类题目的通过率比单点抽取题更能反映真实理解能力。
工程上可以维护一份评分配置文件,让评测脚本同时输出位置分桶、结构分桶和组合题得分。模型选型时,不应只看综合分,还要看中段位置分和表格结构分。若某个模型总分最高,但中段分仅为42%,说明它并不能稳定处理长文本,只是靠开头结尾样本拉高了均值。只有位置与结构维度同时改善,长文本基准的分数才具有可迁移性。