长上下文问答是大模型应用中最常见的场景之一。无论是企业知识库检索、合同条款审查,还是技术文档答疑,用户的问题往往需要模型从几万字甚至几十万字的材料里找到那一句关键依据。实践中有两个问题特别突出:一是模型对超长文本的理解能力有限,中间部分的信息容易被忽略,也就是所谓“迷失在中间”现象;二是定位不准,检索回来的片段与问题相关性弱,导致答案张冠李戴。要解决这些问题,需要从检索策略和阅读策略两方面同时入手。

长上下文问答为什么难:理解与定位的双重瓶颈
第一个瓶颈是理解层面。即便大模型宣称支持128K甚至更长的上下文窗口,模型对窗口内信息的利用率并不均匀。研究表明,位于文本开头和结尾的信息被正确利用的概率明显高于中间部分。当文档长度增加时,模型的有效推理能力会衰减,出现幻觉和漏答的概率随之上升。也就是说,把整篇长文一次性塞进模型,并不等于模型真正“读懂”了全文。
第二个瓶颈是定位层面。长文本中的关键信息分布极不均匀,一份上百页的技术规范里,与用户问题直接相关的内容可能只有几百字。如果定位策略粗糙,比如只按固定长度切块,很容易把答案切断在两个分块之间,或者召回大量无关片段,既浪费了上下文空间,又稀释了模型的注意力。
此外还有工程上的成本问题。长上下文意味着更高的token消耗和更慢的响应速度。在延迟敏感的场景中,把十万字文档全部送入模型推理,单次请求的耗时可能达到几十秒,这显然无法接受。因此好的方案必须在准确率和效率之间找到平衡点。
主流优化方案:分段检索、滑动窗口与重排序
分段检索是最经典的思路,也就是RAG(检索增强生成)的基本形态。将长文档切成若干片段,用向量相似度检索出与问题最相关的若干片段,再送入模型作答。它的关键在于分块策略:块太大会引入噪声,块太小会破坏语义完整性。实践中常用的做法是按标题层级或语义边界切块,并给每个块保留一定的重叠区域,避免答案被切断在边界上。
滑动窗口是另一种思路,适合无法依赖向量检索的场景。让模型按固定窗口顺序阅读文本,每个窗口输出一次中间摘要或候选答案,最后汇总这些中间结果得出最终答案。这种方式计算量较大,但不需要预先建立索引,适合一次性处理陌生长文档的任务。
重排序是提升定位精度的有效补充。先用量化后的粗检索模型(比如BM25或量化向量索引)快速召回前50个候选片段,再用一个精度更高但更慢的交叉编码器对候选逐一打分,选出真正相关的少数片段。这种两阶段漏斗结构兼顾了速度和精度,是目前长文本问答系统的标准做法。
基于RAG的工程实践:分块、检索与重排序代码示例
下面给出一个完整的Python实现流程,包含语义分块、向量检索和交叉编码器重排序三个步骤。这里使用sentence-transformers模型做向量化,实际项目中可替换为任意向量数据库。
from sentence_transformers import SentenceTransformer, CrossEncoder
import numpy as np
# 语义分块:按固定长度切块并保留重叠,避免答案被切断
def chunk_text(text, chunk_size=500, overlap=100):
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunk = text[start:end]
# 尝试在句子边界处截断,保持语义完整
last_period = chunk.rfind("。")
if last_period > chunk_size // 2:
end = start + last_period + 1
chunks.append(text[start:end])
start = end - overlap
return chunks
# 第一步:向量粗检索,召回前50个候选片段
embed_model = SentenceTransformer("BAAI/bge-small-zh-v1.5")
def retrieve(question, chunks, top_k=50):
q_vec = embed_model.encode(question)
c_vecs = embed_model.encode(chunks)
scores = np.dot(c_vecs, q_vec) / (
np.linalg.norm(c_vecs, axis=1) * np.linalg.norm(q_vec)
)
idx = np.argsort(scores)[::-1][:top_k]
return [chunks[i] for i in idx]
# 第二步:交叉编码器重排序,精选前5个片段
reranker = CrossEncoder("BAAI/bge-reranker-base")
def rerank(question, candidates, top_k=5):
pairs = [(question, c) for c in candidates]
scores = reranker.predict(pairs)
order = np.argsort(scores)[::-1][:top_k]
return [candidates[i] for i in order]
# 完整流程
def answer(question, long_text):
chunks = chunk_text(long_text)
candidates = retrieve(question, chunks)
return rerank(question, candidates)这套流程中,分块函数的重叠参数很关键。重叠区域设为块长的五分之一到四分之一通常效果较好,太小容易漏掉边界信息,太大则浪费检索时的去重成本。句子边界截断的逻辑虽然简单,却能显著减少“答案被切成两半”的情况。
定位策略对比与选型建议
不同策略在召回质量和耗时上差异明显。下表是常见方案的对比,数据基于一份约五万字的技术文档做问答测试的经验值,仅供参考:
| 策略 | 平均召回率 | 平均耗时 | 适用场景 |
|---|---|---|---|
| 全文直接输入 | 较高 | 很慢 | 文档短于两万字且延迟不敏感 |
| 固定分块检索 | 中等 | 快 | 对精度要求一般的高并发场景 |
| 语义分块加粗检索 | 较高 | 快 | 大多数知识库问答场景 |
| 粗检索加重排序 | 很高 | 中等 | 精度优先的严肃场景 |
| 滑动窗口逐段阅读 | 中等 | 很慢 | 无索引条件的离线分析任务 |
从表中可以看出,粗检索加重排序的组合在精度和速度上取得了最好的平衡。如果业务对准确率极度敏感,比如法律或医疗问答,建议保留重排序环节;如果是高频低价值的场景,纯向量检索已经够用,还能省下交叉编码器的计算成本。
最后还有几点实践经验值得注意。第一,提示词中要明确要求模型只依据给定的片段作答,并在答案末尾标注依据片段的编号,这样既减少幻觉,也方便人工核查。第二,评估环节一定要用真实的长文档构建测试集,短文本上的测试结论对长文本场景几乎没有参考价值。第三,当文档更新频繁时,优先做增量索引而不是全量重建,否则维护成本会吞噬掉方案本身带来的收益。把检索、重排序和生成三个环节分别评估,才能准确定位系统瓶颈究竟出在理解还是出在定位上。