news 2026/11/4 17:23:01

长上下文问答中的相关性裁剪:提升大模型准确率的上下文工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
长上下文问答中的相关性裁剪:提升大模型准确率的上下文工程实践

最近做长文档问答的时候,我发现一个很反直觉的现象:上下文塞得越多,模型回答的质量反而越差。一次测试里,我喂了一整份合同加几十封往来邮件,上下文总 token 量已经上万,模型居然连一个明摆着的金额都提取错。后来我只保留了和问题最相关的几个段落,同样的模型、同样的 prompt,答案一下就对了。这个对比让我认真做了一轮“相关性裁剪”的实测,本文就是把这次的思路、步骤、数据和翻车经验完整记录下来。

这里先说明:我说“相关性裁剪”,不是让模型自己对上下文做摘要,也不是用搜索召回去拼接一堆碎片,而是在把文本交给模型之前,先用低成本的方式对上下文做一次“相关性过滤”——只保留与当前任务有关的原文片段,再按原文顺序重新拼接。整个过程保留了原文的表达,没有新增信息。对于那些正在被“长上下文又贵又笨”困扰的人来说,这套方法值得试一下。

1. 先搞清楚:为什么上下文越长,模型反而越笨

1.1 注意力机制不是无限带宽

Transformer 的每个 token 都要和上下文里所有 token 计算注意力权重,上下文越长,注意力分布就越接近“一锅粥”。打个比方,你坐在一个乱糟糟的会议室里,人越多,你越难从嘈杂的背景音里听清真正需要的那句话。从计算原理上看,注意力分数经过 softmax 之后,所有位置的权重总和是固定的,所以无关内容每多一分,关键信息能分到的权重就被稀释一分。这不是模型“不聪明”,而是结构性的、可预见的退化。

现在不少模型把上下文窗口做到几十万甚至百万 token,容易给人一种“可以随便塞”的错觉。但窗口变大解决的是容量上限,不是利用率。你在一个百万 token 窗口里塞进去 90 万无关文本,模型照样会丢关键信息。这也是为什么我只把“窗口大”当作兜底能力,而不是默认使用方式。上下文窗口更像是内存上限,而相关性裁剪是在数据进入内存之前先做一次筛选。

1.2 “Lost in the Middle”现象与长上下文幻觉

业界有个著名现象叫 Lost in the Middle:当关键信息位于上下文开头或结尾时,模型的准确率明显更高;一旦关键信息被埋在中间,模型就很容易看漏。这意味着一个几万 token 的上下文,真正能被模型高效利用的,往往只有开头和结尾附近的部分。这跟人的阅读习惯有些类似,开头和结尾的印象总比中间深刻。

长上下文还有另一个隐患:当模型找不到准确信息时,它不会乖乖说“我不知道”,而是倾向于生成一个看起来合理的幻觉内容。上下文越长,“找不到但必须继续写”的状态越容易被触发,因为模型内部需要维持连贯性,而大量无关信息又提供了足够多的迷惑前缀。我实测里全量塞 12000 token 时准确率只有 40%,很大程度就是这个问题造成的。

1.3 无关片段会污染指令遵循

更隐蔽的是,无关文本不只是“占地方”,它还会主动帮倒忙。我在一次测试里往上下文里混入了一张无关的数据表格,结果模型连回答问题的格式都开始模仿表格,数字对齐、表头全部跑偏。原因不复杂:模型的指令遵循能力本质上是“在所有输入 token 里寻找最合理的输出模式”,无关内容越多,系统提示词的指导地位就越被削弱。

可以把上下文中的无关内容理解为“竞争对手”,它们在跟你的 system prompt 抢注意力。无论是格式、语气还是语义方向,都可能被这些噪声带跑。所以清理无关上下文不只是为了省钱,更是为了守住指令的主导权。这也是“上下文工程”和普通“提示词工程”最明显的分界:提示词管的是怎么写,上下文工程管的是喂什么。

2. 相关性裁剪:思路、边界和选型

2.1 相关性裁剪到底在做什么

相关性裁剪的做法一句话就能说清:在调用大模型之前,先把上下文切成块,用 embedding 对每个块和当前任务算相关性,然后丢弃低相关块,只把剩余的高相关块按原文顺序拼接回 prompt。它和关键词匹配完全不同,因为查询和文档之间往往没有字面重合,需要语义层面对齐。

有三个关键点。第一,裁剪粒度是“块”,不是“词”,块太小会切断指代和逻辑,块太大会让噪声混进来。第二,相关性的参考对象是“当前任务”,而不是简单的高频词统计。第三,输出时必须按原文顺序重排,不按相似度排序,否则模型会失去因果感。第三点是我踩过最深的坑,后面会细说。

2.2 为什么裁剪比摘要和 RAG 更适合单文档

如果你需要分析一份几十页的合同,大概有三个候选方案:第一,让模型先总结全文再回答;第二,用 RAG 检索出若干片段再回答;第三,用相关性裁剪。我三套都试过,结论非常明确。

方案适合场景主要问题
全文摘要概览、提炼主线摘要会丢失细节,且摘要本身可能幻觉
RAG 检索跨文档搜索、知识库问答取回的是碎片,顺序和逻辑关系容易断裂
相关性裁剪单文档内精确问答、合同审查依赖分块质量和阈值选择,需要多调一次

RAG 其实是为“检索”设计的,不是为“阅读”设计的。它返回一堆相关片段,但这些片段拼在一起往往缺少文档内部的因果链。相关性裁剪可以理解成“先检索、后重组”,但重组时保留了完整的原文顺序,模型读起来更像在读一份连续资料,而不是读一堆便签。对于单文档,裁剪通常比 RAG 更稳。

2.3 与滑动窗口、上下文压缩的边界

做长上下文方案时,很多人会把滑动窗口、上下文压缩和相关性裁剪混为一谈。滑动窗口解决的是流式内容问题,常见于多轮对话,只保留最近 N 轮,老消息直接丢掉。但它对单文档场景不好使,因为关键信息可能出现在文档开头,窗口早就滑过去了。

上下文压缩的思路是把无关 token 用特殊符号替换,保留语义骨架,从而压缩 token 数。这种方法很有效,但属于“损坏性压缩”,模型看到的内容不再是原文字面,对需要逐字比对数字、日期、条款的场景不适用。相关性裁剪对被保留的块来说是“无损”的,它只是不做压缩,只做选择。

所以三者不是竞争关系,而是不同环节的工兵:滑窗管多轮对话的滚动,压缩管 token 预算,裁剪管内容质量。实际项目里可以先裁剪、再压缩,最后用滑窗兜底。我这次的核心实测,就是先把裁剪这一层的效果单拎出来看。

3. 完整实操:从切片、打分到重组上下文

3.1 实验设计与评测指标

我这次用了一个自己拼接的测试集:一份模拟采购合同(约 40 页),加上几十封关于合同细节的往来邮件,总 token 量大约 12000。我准备了 10 个问题,覆盖金额提取、日期判断、责任边界、违约条款等类型,包括:合同总金额、首付款比例、违约金触发条件、验收日定义、争议解决方式、知识产权归属、保密期限、合同终止条件、逾期利息计算、通知送达方式。

评测指标有三个:答案准确率,由人工对照真实文档判断,关键数字、日期、主体不能错;证据引用率,看回答是否提到对应的原文条款编号或段落;生成耗时,从请求发出到首个 token 返回的近似时间。对照组设了四个:全量基线 12000 token,以及裁剪到约 6000、3000、1200 token 三个档位。

3.2 切片与向量化的具体参数

分块是第一步,也是决定整个效果上限的一步。我用的规则是:优先按段落切;段落太长就按 600 字左右切;段间重叠 50 字左右。太小的块虽然过滤精度高,但会把上下文的指代关系切断;太大的块又会让低相关内容和答案绑在一起,过滤等于没过滤。600 字在中文场景里是一个不错的平衡点。

重叠 50 字是给边界信息上保险。比如一个关键句子的前半在上一块、后半在下一块,如果不重叠,两边单独向量化后都可能因为语义残缺而拿不到足够高的相关度。切完块后,我用 embedding 模型做向量化。实测中对比了 bge-m3 和 OpenAI 的 text-embedding-3-small,两者整体趋势一致,但 bge-m3 的相似度分数偏高,阈值不能跨模型复用。参考代码如下:

from sentence_transformers import SentenceTransformer import numpy as np model = SentenceTransformer('BAAI/bge-m3') chunks = [...] # 结构为 [(order, text), ...] docs = [text for _, text in chunks] # 归一化之后,点积就是余弦相似度 doc_vecs = model.encode(docs, normalize_embeddings=True) query = "请找出合同中关于违约金比例、赔偿条件和触发方式的条款" query_vec = model.encode([query], normalize_embeddings=True)[0] scores = doc_vecs @ query_vec

这里有个经验:query 一定不能只写一个词。把“违约金”扩写成“请找出合同中关于违约金比例、赔偿条件和触发方式的条款”,相关块的得分会明显拉开差距。因为 embedding 模型对完整句子更敏感,一个孤零零的名词很难捕获任务意图。

3.3 阈值选择与 Token 预算控制

相关性得分出来以后,怎么决定“留哪些块”?我试过两种策略:固定阈值法设定一个相似度阈值,高于就保留,缺点是分数分布受文本和模型影响很大,阈值基本不能复用;动态预算法先定一个目标 token 预算,再按得分从高到低累加块,直到接近预算,这种方式更符合“上下文容量是硬约束”的真实场景。

max_tokens = 3000 def estimate_tokens(text): return len(text) * 1.3 # 中文粗略系数,上线前请用真实 tokenizer scored = sorted(zip(chunks, scores), key=lambda x: x[1], reverse=True) selected = [] total = 0 for (order, text), score in scored: if total + estimate_tokens(text) > max_tokens: continue selected.append((order, text, score)) total += estimate_tokens(text) # 关键:最后按原文顺序排回,而不是按分数排 selected.sort(key=lambda x: x[0])

实际项目中建议用 tiktoken 或模型自带的 tokenizer 精确计算 token,比例系数只适合快速验证。还有一点,动态预算选完后,最好快速人工扫一眼高分块的 top 条,确认没有明显的方向性错漏,防止 embedding 被某些长段落带偏。

3.4 重组上下文与 Prompt 细节

裁剪完不是直接把文本塞给模型就结束,prompt 里最好写清楚“这些资料是筛选过的”。我用的模板大致是:

以下是从原始文档中抽取的相关片段,按原文顺序排列。 片段可能不完整,也可能遗漏原文中的其他信息。 请严格基于给定片段回答问题,如果片段中没有足够信息,请明确说“信息不足”。 [片段1] ... [片段2] ... 问题:{question}

加“片段可能不完整”这句话,能明显减少模型过度自信的脑补。另一个技巧是给每个片段加编号,并要求回答里注明依据来源,例如“违约金比例为 3%,见[片段2]”。这个习惯既能帮你排查幻觉,也能让结果更可追溯。

3.5 实测数据与对比结果

我跑了一轮完整对照,取的是三组问题的平均准确率,结果如下:

方案平均上下文 token准确率证据引用率生成耗时
全量基线约 1200040%30%1.0x(基准)
裁剪到 50%约 600068%55%约 0.7x
裁剪到 25%约 300082%80%约 0.5x
裁剪到 10%约 120062%45%约 0.4x

全量基线最差,并不是因为模型不行,而是因为正确答案只占全文极小比例,其余 95% 的内容全在制造干扰。裁剪到 3000 token 时,噪声被清走,模型能够专注阅读真正与问题相关的段落;再裁到 1200 token,噪声虽然更少,但真正的答案段可能也只留了一半,信息不完整导致准确率又掉下来。所以相关性裁剪的本质,是在“噪声”和“信息量”之间找平衡点。

4. 实测翻车现场与排查心得

4.1 隐性前提被裁掉的典型事故

我第一次跑裁剪就翻车了。文档前半部分有一段“术语定义”,写明“验收日 = 货物交付后 30 天”,后面所有邮件都在讨论验收日,但从未再重复定义。按相似度裁剪时,术语定义段和当前问题的字面重合度很低,分数不高,被裁掉了。模型看到后面一堆“验收日”,只能自己猜含义,答案自然错得离谱。

这个事故让我加了一条规则:裁剪时永远保留文档的“硬保留区”,包括开头的定义、术语、背景说明,以及结尾的签署页、附件清单。因为合同生效日期、签署方全称等关键信息经常出现在开头和结尾,但它们的 embedding 相似度可能并不高。先保住文档骨架,再根据相关性决定保留哪些肌肉,这样才不容易出现系统性遗漏。

4.2 相似度分数不等于真实任务相关性

embedding 的相似度衡量的是“语义相近程度”,不是“这个块是否包含回答问题所需的证据”。两者绝大多数时候一致,但坑很多。比如问题问“如果供货方延迟交付,采购方可以主张什么责任”,文档里有一段“延迟交付的例外条款”,语义上和“延迟交付责任”很接近,分数很高,但这不是答案;真正的责任条款在另一个段落,分数反而不高。

我的应对办法是:选块时不要只取最高分,要多打印几个高分段看一眼;同时把 query 改成“任务描述 + 证据类型”,让检索目标更明确。比如把 query 扩写成“找出合同中关于延迟交付后果、赔偿和采购方权利的条款”,真正包含答案的块分数就会上升。这一步对最终效果的影响,往往比换一个 embedding 模型还大。

4.3 模型会把裁掉的内容脑补回来

裁剪后,模型面对的是一份缺失的文档,但它的训练习惯是“有信息缺口就生成补全”。如果 prompt 里不提醒它片段不完整,它很容易把这些缺口脑补出来,然后一本正经地给出错误答案。实测中,光是加了“片段可能不完整”这句话,幻觉次数就明显减少。

还有一个更稳的办法:强制模型在回答中写证据编号,比如“根据[片段3],乙方应在三日内赔付”。一旦要求编号,模型就不得不把答案和具体片段绑定,编造的成本和难度都会变高。这个方法在任何长文档问答场景下我都推荐,它相当于给模型的输出加了一层引用约束。

4.4 评测指标会骗人:准确率上升也可能是敷衍

只看“答案是否提到关键词”是不行的。裁剪后的模型可能学会一个偷懒策略:直接从保留片段里抄一个看起来最像答案的句子,哪怕句子回答的根本不是当前问题,只要关键词对上了就算答对。这种表面正确会虚高准确率,实际一复核就露馅。

我的做法是:人工复核时同时要求模型输出“支持这段话的原文片段编号”,如果答案和编号对不上,直接判错。更省力的方式,是让另一个模型当裁判,给它原始文档片段、问题和模型答案,让它判断答案是否忠实于原文。把这段逻辑写进评测脚本,比肉眼抽查可靠得多。

4.5 裁剪本身也有成本,但可以忽略

有人会问:裁剪不也要调用 embedding 吗?确实有成本,但 embedding 接口通常比生成 token 便宜很多,而且同一个文档可以缓存。第一次处理时把分块和向量结果存到本地,后面每次提问只需要算一次 query 的向量,再做一次矩阵乘法,耗时通常不到 50 毫秒。我同一个文档问过 20 个问题,缓存后裁剪成本基本可以忽略。

另一个容易被忽略的问题是第三方 embedding API 的网络延迟。如果对首 token 时间敏感,建议用本地 embedding 模型;如果只能用 API,就把分块和向量化放到异步任务里,提前算好缓存。这样裁剪就成了一个实时前置过滤环节,而不是拖慢主流程的额外负担。

5. 几个能直接上手的建议

5.1 先给任务定一个“相关性预算”

每次准备上下文前,先问自己三个问题:这个问题需要几个关键事实?这些事实大概率在哪些区域?最坏情况下要读多少才够?根据回答设置 token 预算。我自己常用的参考值如下:

任务类型建议预算
单点信息抽取(金额、日期、名称)1-2k token
单文档问答与简单推理3-5k token
多文档对比或复杂逻辑链5-8k token
必须全文逐字核对10k 以上,但极少见

预算定好后再做裁剪,就不会掉进“裁太狠把答案切了”或“不裁导致模型变笨”两个极端。这个预算本质上是给你自己一个约束,逼迫你思考哪些上下文是真正必要的。

5.2 两段式裁剪:先粗筛再细筛

面对几十页的大文档,一次 TopK 容易漏掉藏在奇怪位置的答案。更稳的是两段式:第一阶段按章节粗筛,把相似度极低的整章扔掉,保留大概 30% 的候选区域;第二阶段对候选区域做细粒度切块,再筛到目标 token。这样既保证了查找广度,又保证最终喂给模型的都是高相关片段。

这里体现了一个思想:裁剪不是越细越好,而是粒度要和文档结构匹配。段落边界是天然的语义单元,先粗筛能快速缩小范围,细筛负责在候选区里找精确证据,两级配合能显著降低漏检率。

5.3 裁剪后补一轮“信息充足性自检”

如果担心裁太狠,可以在正式回答问题前,让模型先回答一个附加问题:“基于目前给到的片段,是否足够回答用户问题?如果不够,还缺少哪部分信息?”把这个逻辑写成代码判断分支:模型说缺信息,就自动扩大保留范围,最多重试两轮。

“自检-补裁”循环在我看来是性价比最高的兜底手段。它把一次性的静态裁剪变成了带反馈的动态过程,能有效降低预算设错导致的信息缺失。代价很小,通常只需要多一次短请求,但换来的是稳定性。

5.4 离线缓存相关块向量

长文档问答中,同一个文档经常被不同问题反复查询。强烈建议第一次处理时就把分块和向量结果缓存起来。后续每次提问,只需要算 query 的向量、做一次余弦距离、挑高分块。本地向量库方案可以用 sqlite 加 numpy,也可以用 faiss、chroma 这类现成工具。

缓存带来的不仅仅是省钱,更重要的是让裁剪环节变得足够快,从而可以把它嵌进交互应用的主流程。如果每次提问都要重新分块、重新 embedding,很多场景根本跑不起来。提前把这一步做掉,整个上下文工程才谈得上“工程”而不是“临时脚本”。

6. 同样思路还能用在哪里

6.1 多轮对话历史:别把寒暄喂给模型

相关性裁剪不只适用于长文档。多轮对话里往往塞满了“好的”“谢谢”“等下”这类无效历史,全量交给模型后,当前回合的判断会被稀释。我试过一种做法:最近两轮保留完整原文,更早的轮次先压缩成一两句摘要,如果用户新问题引用了很早之前的信息,再通过相关性裁剪从历史里把对应轮次捞回来。

这个方案比固定滑窗更聪明。滑窗只看“时间近”,不关心“内容是否相关”,而裁剪是真正按当前问题往回找证据。两种可以配合:先按滑窗圈定最近 N 轮,再在圈定结果里做相关度排序,效果会更好。

6.2 Agent 工具调用返回结果

Agent 场景里,模型每调一次工具就会收到一大段 JSON、网页文本或日志。如果不处理,下一轮推理很容易被日志里的错误信息带偏。我现在的做法是:工具返回后,先用轻量 embedding 对内容做相关性过滤,只把和当前目标相关的片段传回给规划模型。

这个过滤动作相当于给 Agent 加了一层“注意力路由”。它不只是省钱,更重要的是让 Agent 在面对一长串工具输出时,还能牢牢记得自己原本要解决什么问题。实测下来,加了这步后 Agent 的步骤连贯性和任务完成率都有提高。

6.3 批量日志与报告分析

日志分析是另一个很适合相关性裁剪的场景。几万行日志里,真正对应异常窗口的可能只有几十行。以往的做法是直接把整段日志塞给模型,让模型自己找,容易找不准;现在我会先把日志按时间窗口切片,用问题描述做相关性过滤,只保留高相关窗口,再让模型分析。

这个场景里可以结合滑动窗口:先按异常时间附近的固定窗口切日志,再用相关性裁剪把无关行丢掉。和纯滑窗相比,裁剪能保留那些虽然离异常发生点有点远、但确实包含根因信息的日志行,分析结果会更接近真实原因。

我个人在几次长上下文实测里印象最深的一点是:真正让模型变笨的往往不是模型本身,而是我在上游给它喂了太多“看起来有用、实际没用”的噪声。相关性裁剪的底层逻辑其实非常简单——把上下文当成一个需要筛选的输入信号,而不是一个可以无限堆放的仓库。它未必适合所有任务,但如果你在做长文档问答、合同审查、报告分析这类场景,我强烈建议你先花半小时跑一次上面这套流程,用数据说服自己。最后再分享一个小技巧:裁剪比例不必一开始就定死,先试试“只保留 25%”和“只保留 10%”两组,看看答案在哪一组开始崩坏,那个边界就是你这个任务的最优预算。祝大家在上下文工程上少踩坑、多产出。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/4 17:22:17

GPT-4o写Verilog为何翻车?OPL4GPT检索增强提示优化实战

先说一个我自己观察到的现象:身边的同学同事用GPT-4o写C代码已经很顺手了,但一切换到Verilog,画风就立刻变了——生成的模块经常在仿真阶段崩掉,时序逻辑和组合逻辑混在一起,甚至有些代码风格一看就是“用C语言思维写硬…

作者头像 李华
网站建设 2026/11/4 17:20:04

大模型双回答机制:提升推理效率与准确性的新方法

1. 大模型推理新范式解析:双回答机制如何重塑思维链在自然语言处理领域,大模型的推理能力一直是衡量其智能水平的核心指标。最近出现的一种创新方法——"一次思考两次回答"机制,正在改变我们优化模型推理过程的传统思路。这种技术不…

作者头像 李华
网站建设 2026/11/4 17:18:19

智能CRM如何破解企业增长困境与数据孤岛

1. 企业增长困境与CRM智能化转型契机2026年的商业环境正在经历深刻变革。根据第三方调研数据显示,超过78%的B2B企业在客户生命周期管理中存在数据孤岛问题,而传统CRM系统平均只能满足企业42%的智能化需求。我在为多家上市公司提供数字化咨询时发现&#…

作者头像 李华
网站建设 2026/11/4 17:15:31

游戏多开实战:进程隔离与配置分离实现稳定多实例

多开这件事,在动作角色扮演游戏圈子里从来都不是什么新鲜需求。早年网吧时代,一台机器上挂两个账号互相倒装备、带小号刷牛关,是再正常不过的操作。后来游戏出了重制版,画面翻新了,底层登录流程也跟着变了,…

作者头像 李华
网站建设 2026/11/4 17:13:07

Rufus制作U盘启动盘全攻略:从下载安装到GPT/MBR分区方案选择

U盘启动盘这东西,平时用不上,真到系统崩了、新机装系统、或者帮朋友救急的时候,没有它就只能干瞪眼。我这些年经手的机器没有一百也有八十台,试过的写盘工具从官方媒体创建工具到各种第三方小软件,绕了一圈最后留在工具…

作者头像 李华
网站建设 2026/11/4 17:13:07

高效文件管理工具的核心功能与优化实践

1. 为什么我们需要文件快捷管理工具在日常工作中,我经常遇到这样的困扰:项目文件散落在不同的文件夹层级中,每次都要花费大量时间在资源管理器中层层点击;常用的文档和工具分散在各个角落,使用时需要反复打开多个窗口&…

作者头像 李华