Appearance
检索后操作相关技术名词part2
1. Intro
接下来我们继续介绍和检索后操作相关的技术名词。
在上一篇文章里,我们介绍了 post-retrieval 前半段的几个环节:
- Result Filtering:决定哪些 candidates 可以继续往后传
- Result Ranking:决定留下来的 candidates 谁更应该排在前面
- Result Diversification:决定最终选择的 candidates 是否足够丰富
经过这几步之后,系统得到的已经不是原始检索结果,而是一批更干净、更有顺序、信息覆盖也更均衡的候选资料。
但是,这些资料还不能直接全部塞进 prompt。
因为大模型有上下文窗口限制,而且 prompt 里面不只有检索资料,还可能包括 system prompt、用户问题、历史对话、工具调用结果等内容。
所以接下来要进入 post-retrieval 的后半段:Context Assembly。
Context Assembly,中文可以理解为“上下文组装”。
它要解决的问题是:这些已经筛选、排序、多样化之后的 candidates,最终应该怎么组织成大模型可以使用的上下文?
在完整链路里,它的位置大概是这样:
ts
query -> retrieve candidates -> filtering -> ranking -> diversification -> context assembly -> generate前面的 filtering、ranking、diversification 更关注 candidates 本身:
- 哪些应该留下?
- 谁应该排在前面?
- 结果是否足够多样?
而 Context Assembly 更关注 prompt 里的上下文:
- 最终放多少内容?
- 每段内容放多长?
- 重要内容应该放在哪里?
- 证据之间要不要合并、分组、标注来源?
- 来源信息是否需要一起进入上下文?
这一部分通常可以分成两个方向:
- Context Window Management
- Evidence Organization
Context Window Management 关注的是上下文窗口怎么用。
例如:
- 选择多少条 candidates
- 给检索资料分配多少 token
- 是否需要压缩或截断 chunk
- 长上下文里如何排列重要内容
Evidence Organization 关注的是证据材料怎么组织。
例如:
- 是否按主题分组
- 是否合并来自同一来源的内容
- 是否保留 source id
- 是否保留来源标题、章节、页码或 URL
- 是否方便后续生成阶段做引用和追溯
所以 part2 主要介绍这两个方向。
如果说 part1 解决的是:哪些资料值得进入上下文?
那么 part2 解决的是:这些资料应该怎么变成最终上下文?
这就是 Context Assembly 要处理的核心问题。
2. Context Window Management
Context Window Management,中文可以理解为“上下文窗口管理”。
前面已经说过,Context Assembly 关注的是候选资料最终怎么变成 prompt 里的上下文。
而 Context Window Management 是其中偏“空间管理”的部分,它更关心:在有限的 token 预算里,资料最多能放多少?应该选哪些?太长了怎么办?顺序怎么排?
这一部分可以按工程实现顺序拆成几类:
- Token Budget Allocation:先决定 token 预算怎么分
- Top-K Selection:再决定选多少条 candidates
- Chunk Truncation / Context Compression:然后处理太长的内容
- Long-context Reordering:最后决定内容在上下文里的排列顺序
2-1. Token Budget Allocation
Token Budget Allocation 可以理解为“token 预算分配”。
它解决的问题不是简单地问:上下文还剩多少空间可以塞资料?
更准确地说,它解决的是:一次 prompt 里的 token 空间,应该如何分配给 system prompt、用户问题、历史对话、输出约束、检索资料和预留输出空间。
很多同学第一次接触这个概念时,会把它理解成“防止超过模型最大上下文窗口”。
这个理解没有错,但还不够。
因为即使模型支持很长的上下文,比如 128K、200K,甚至更大的窗口,也不代表我们就应该把所有检索结果都放进去。
长上下文解决的是“能不能放下”的问题,但 RAG 更关心的是“放进去以后有没有用”。
如果上下文里放了大量相关性一般、内容重复或者噪声很高的资料,模型不一定会更好回答问题,反而可能出现几个问题:
- 关键证据被低价值内容淹没
- prompt 成本明显上升
- 响应延迟变长
- 输出稳定性下降
所以在 RAG 里,上下文不是越长越好,而是有效信息密度越高越好。
Token Budget Allocation 的价值就在这里:它不是单纯做“空间填充”,而是在做“上下文资源调度”。
例如一次生成请求里,prompt 可能由这些部分组成:
txt
system prompt
用户问题
历史对话
输出格式要求
检索资料
预留输出空间
buffer这些内容不应该随意抢占 token 空间。
一般来说,system prompt、用户问题、输出约束是比较稳定的部分;历史对话和检索资料则更容易变长,需要根据预算做控制。
先看一个最简单的静态预算例子。
假设模型上下文窗口是 8000 tokens。系统可能不会真的把 8000 tokens 用满,而是只使用 7200 tokens,剩下 800 tokens 作为 buffer,避免 token 估算误差导致超限。
在 7200 tokens 里面,还要预留一部分给模型输出。例如:
txt
max context: 8000 tokens
safe context: 7200 tokens
system prompt: 800 tokens
用户问题: 按实际 query 计算,例如 100 tokens
历史对话: 1500 tokens
输出格式要求: 300 tokens
预留输出空间: 800 tokens
检索资料: 剩余 token对应到代码里,大概可以这样写:
ts
type TokenBudget = {
maxContextTokens: number;
bufferTokens: number;
systemPromptTokens: number;
queryTokens: number;
historyTokens: number;
outputFormatTokens: number;
reservedOutputTokens: number;
};
function getRetrievalBudget(budget: TokenBudget) {
const safeContextTokens = budget.maxContextTokens - budget.bufferTokens;
return (
safeContextTokens -
budget.systemPromptTokens -
budget.queryTokens -
budget.historyTokens -
budget.outputFormatTokens -
budget.reservedOutputTokens
);
}这类方式可以叫静态预算。
这里要注意一点:上面写的“用户问题 100 tokens”只是一个示例,不是说用户问题只能占 100 tokens。
真实系统里,用户问题通常会按实际 token 数计算:
ts
const queryTokens = estimateTokens(userQuery);如果用户问题比较短,可能只有几十个 tokens;如果用户问题很长,可能是几百甚至上千 tokens。
用户当前输入一般是高优先级内容,不能因为它超过了示例里的 100 tokens,就直接简单截掉。更常见的处理方式是:先保留用户当前问题,再从其它部分让出空间。
例如:
- 历史对话太长,就先压缩 history
- 检索资料预算不够,就减少进入上下文的 candidates
- 某些 chunk 太长,就做截断或压缩
- 如果用户输入本身像一大段文档,就先做 query compression 或 query rewriting
- 如果整体仍然超过模型上限,再考虑拒绝、截断或让用户缩短输入
所以静态预算的特点是简单、清晰,适合大多数基础 RAG 系统。系统先算出检索资料最多能占多少 token,然后再进入后面的 Top-K Selection、Context Compression 等步骤。
不过真实项目里,预算通常不会永远固定。
比如用户只是问:
txt
公司年假一年有几天?这个问题可能只需要一两段制度原文就能回答,不需要塞很多资料。
但如果用户问的是:
txt
试用期员工、正式员工和外包人员的请假规则有什么区别?这个问题涉及多个对象和多条规则,就可能需要更多证据。此时 retrieval budget 可以适当放大。
所以更完整的做法是动态预算。
动态预算会根据当前请求调整不同部分的 token 分配。例如:
- 用户问题很简单,就减少检索资料数量
- 用户问题比较复杂,就给检索资料更多空间
- 历史对话太长,就先压缩 history
- rerank 分数整体不高,就不要为了填满预算硬塞资料
- 多个 chunk 内容重复,就减少重复内容占用的 token
示意代码如下:
ts
type RuntimeContext = {
query: string;
historyTokens: number;
candidateStats: {
maxRerankScore: number;
duplicated: boolean;
};
};
function allocateBudget(ctx: RuntimeContext) {
const base = {
system: 800,
query: estimateTokens(ctx.query),
outputFormat: 300,
reservedOutput: 800,
buffer: 800,
};
const history = Math.min(ctx.historyTokens, 1200);
let retrieval = 3000;
if (isComplexQuery(ctx.query)) {
retrieval += 1000;
}
if (ctx.candidateStats.maxRerankScore < 0.5) {
retrieval -= 800;
}
if (ctx.candidateStats.duplicated) {
retrieval -= 500;
}
return {
...base,
history,
retrieval,
};
}这段代码不是一个完整实现,只是表达思路:Token Budget Allocation 不一定只是一个减法公式,它也可以根据 query、history 和候选资料质量动态调整。
总结一下,Token Budget Allocation 解决的不是“能不能把资料塞进上下文”,而是“应该给资料多少空间,以及怎样保证这部分空间被高质量内容使用”。
长上下文模型可以缓解空间不足的问题,但不能替代 token 预算。因为 RAG 最终追求的不是把上下文填满,而是让模型看到足够相关、足够清晰、足够可控的资料。
有了 retrieval budget 以后,下一步才是决定具体放哪些 candidates,这就进入了 Top-K Selection。
2-2. Top-K Selection
Top-K Selection 是指从 candidates 里选择前 K 条进入上下文。它解决的问题是:候选资料很多,但最终只能选一部分。
经过前面的 filtering、ranking、diversification 之后,系统可能还有 20 条 candidates。
但根据 token budget,最终可能只能放 5 条。
这时就需要做 Top-K Selection。
最简单的做法是直接取排序靠前的 K 条:
ts
function selectTopK<T>(candidates: T[], k: number) {
return candidates.slice(0, k);
}但真实项目里,Top-K 不一定是固定值。因为不同 chunk 的长度不一样。
比如:
txt
candidate A: 200 tokens
candidate B: 500 tokens
candidate C: 1200 tokens如果只用固定的 topK = 5,可能会出现两种问题:
- 选出来的内容太长,超过 token budget
- 选出来的内容太短,浪费上下文空间
所以更常见的做法是:在 token budget 内尽量选择排序靠前的 candidates。
示意代码如下:
ts
type Candidate = {
id: string;
content: string;
score: number;
tokenCount: number;
};
function selectByTokenBudget(candidates: Candidate[], maxTokens: number) {
const results: Candidate[] = [];
let usedTokens = 0;
for (const candidate of candidates) {
if (usedTokens + candidate.tokenCount > maxTokens) {
continue;
}
results.push(candidate);
usedTokens += candidate.tokenCount;
}
return results;
}也就是说,Top-K Selection 在工程里经常会变成:不是简单选 K 条,而是在 token budget 内选最合适的一组。
2-3. Chunk Truncation / Context Compression
Chunk Truncation 和 Context Compression 都是在处理“内容太长”的问题。它们解决的问题是:某些 candidate 有用,但太长,不能原样放进上下文。
两者区别在于:
- Chunk Truncation:直接截断
- Context Compression:压缩内容,尽量保留关键信息
先看 Chunk Truncation。
假设某个 chunk 有 2000 tokens,但当前只剩 600 tokens 的预算。
最简单的做法就是截断:
ts
function truncateChunk(content: string, maxLength: number) {
if (content.length <= maxLength) {
return content;
}
return content.slice(0, maxLength);
}这种方式实现简单,成本低。但问题也很明显:重要信息可能刚好在后半段,被直接截掉。
所以很多时候会使用 Context Compression。
Context Compression 不是直接裁掉内容,而是把长内容压缩成更短的摘要或关键信息。
比如原文是:
txt
员工病假超过 3 天,需要提供医院诊断证明。若遇紧急情况无法提前申请,应在事后 24 小时内补交申请。病假工资按照公司考勤制度计算。针对问题:
txt
病假超过 3 天需要什么材料?压缩结果可以是:
txt
病假超过 3 天,需要提供医院诊断证明。也就是说,compression 最好是 query-aware 的,它不是随便摘要,而是保留对当前问题有用的信息。
示意流程可以是:
ts
async function compressContext(query: string, content: string) {
return llm.generate({
prompt: `
请根据用户问题,压缩资料内容,只保留回答问题需要的信息。
用户问题:
${query}
资料内容:
${content}
`,
});
}不过 Context Compression 的成本比 Truncation 高。
所以实际项目里通常会按顺序处理:
- 能原样放入,就不压缩
- 太长但结构简单,可以截断
- 太长且重要,才做 compression
2-4. Long-context Reordering
Long-context Reordering 可以理解为“长上下文重排”。它解决的问题是:选出来的内容应该按什么顺序放进上下文。
很多人会以为,只要前面已经做过 ranking,这里就不需要再考虑顺序。
但实际不是这样。
- Ranking 决定的是 candidates 的重要性顺序。
- Long-context Reordering 决定的是这些内容在 prompt 里的摆放位置。
这两个不是完全一样。
原因在于,长上下文模型并不一定对所有位置的信息同等敏感。在一些场景里,放在开头和结尾的信息更容易被模型利用,而夹在中间的信息可能更容易被忽略。
所以有些系统会把最重要的证据放在更靠前或更靠后的位置。
例如,经过 ranking 后结果是:
txt
A score = 0.95
B score = 0.88
C score = 0.81
D score = 0.76直接放入上下文可能是:A -> B -> C -> D
但如果希望重要内容出现在首尾,可以重排成:A -> C -> D -> B
这里只是示意,真实策略会根据模型、prompt 结构和任务类型调整。
示意代码如下:
ts
function reorderForLongContext<T>(items: T[]) {
const results: T[] = [];
let left = 0;
let right = items.length - 1;
while (left <= right) {
results.push(items[left]);
if (left !== right) {
results.push(items[right]);
}
left++;
right--;
}
return results;
}这段代码只是展示一种“首尾交错”的思路,不代表所有项目都要这么做。很多企业项目里,如果上下文不长,直接按 ranking 顺序放入就可以。
Long-context Reordering 更适合这些场景:
- 放入上下文的证据很多
- 单次 prompt 很长
- 重要信息容易被埋在中间
- 模型在长上下文下有明显的位置敏感问题
总结一下,Context Window Management 关注的是上下文窗口怎么用。它不是判断资料有没有价值,而是在已有候选资料的基础上,进一步决定:
- 检索资料最多能占多少 token
- 最终选哪些 candidates
- 太长的内容怎么处理
- 内容在 prompt 里怎么排列
3. Evidence Organization
Evidence Organization,中文可以理解为“证据组织”。
它解决的问题是:候选资料已经选出来了,但这些资料应该怎么组织,才能让模型更容易基于证据生成答案?
前面的 Context Window Management 更关注空间问题:
txt
能放多少?选哪些?太长怎么办?顺序怎么排?而 Evidence Organization 更关注证据结构:
txt
这些资料之间是什么关系?来自哪里?应该如何保留来源信息?举个例子,用户问:
txt
病假超过 3 天需要注意什么?系统最终选出来的 candidates 可能包括:
txt
A. 病假超过 3 天,需要提供医院诊断证明。
B. 如遇紧急情况,应在事后 24 小时内补交病假申请。
C. 病假申请需要直属负责人审批。
D. 来源:员工考勤制度,2026 版。这些内容如果只是简单拼接,也可以交给模型。但更好的做法是把它们组织成更清楚的证据结构。
比如:
txt
材料要求:
- 病假超过 3 天,需要提供医院诊断证明。
补交流程:
- 如遇紧急情况,应在事后 24 小时内补交病假申请。
审批要求:
- 病假申请需要直属负责人审批。
来源:
- 员工考勤制度,2026 版。这样模型更容易知道每条证据分别支持答案里的哪一部分。
常见的 Evidence Organization 可以拆成几类:
| 类型 | 解决的问题 |
|---|---|
| Evidence Grouping | 把散乱证据组织成结构 |
| Merging | 把碎片化证据合并成更完整的上下文 |
| Source Attribution | 保留来源,让后续答案可追溯 |
3-1. Evidence Grouping
Evidence Grouping 是证据分组。它解决的问题是:多个 candidates 不是一堆散乱文本,而是应该按主题、来源或作用组织起来。
常见的分组方式有:
- 按主题分组
- 按来源分组
- 按时间分组
- 按答案结构分组
- 按证据作用分组,例如规则、例外、流程、限制
举个例子,用户问:
txt
病假超过 3 天需要注意什么?候选资料可以分成:
txt
材料要求:
- 病假超过 3 天,需要提供医院诊断证明。
补交流程:
- 紧急情况可在事后 24 小时内补交申请。
审批要求:
- 病假申请需要直属负责人审批。这样做的好处是,模型在生成答案时更容易形成结构化回答,而不是把几条证据混在一起。
示意代码如下:
ts
type Candidate = {
id: string;
content: string;
metadata?: {
topic?: string;
sourceId?: string;
};
};
function groupByTopic(candidates: Candidate[]) {
const groups = new Map<string, Candidate[]>();
for (const candidate of candidates) {
const topic = candidate.metadata?.topic ?? "其他";
const group = groups.get(topic) ?? [];
group.push(candidate);
groups.set(topic, group);
}
return groups;
}真实项目里,topic 不一定来自 metadata。它也可能来自:
- 文档标题或章节结构
- indexing 阶段生成的标签
- query analysis 提取出的子问题
- 模型对 evidence 的归类结果
所以 Evidence Grouping 本质上是在解决:把散乱的证据组织成更适合回答的结构。
3-2. Merging
Merging 是证据合并。它解决的问题是:多个 candidates 可能来自同一段、同一节或同一来源,需要合并成更完整的上下文。
举个例子,检索结果里有两条相邻 chunk:
txt
A. 病假超过 3 天,需要提供医院诊断证明。
B. 如遇紧急情况,员工可在事后 24 小时内补交申请。这两条单独看都有用,但合在一起更完整。
合并后可以变成:
txt
病假超过 3 天,需要提供医院诊断证明。如遇紧急情况,员工可在事后 24 小时内补交申请。Merging 常见的依据有:
- 相同
sourceId - 相同
parentId - 相邻 chunk
- 相同 section
- 内容上属于同一规则或同一流程
示意代码如下:
ts
type Evidence = {
id: string;
content: string;
metadata?: {
parentId?: string;
};
};
function mergeByParentId(evidenceList: Evidence[]) {
const groups = new Map<string, Evidence[]>();
for (const evidence of evidenceList) {
const parentId = evidence.metadata?.parentId ?? evidence.id;
const group = groups.get(parentId) ?? [];
group.push(evidence);
groups.set(parentId, group);
}
return [...groups.entries()].map(([parentId, items]) => ({
parentId,
content: items.map((item) => item.content).join("\n"),
}));
}不过 Merging 不是越多越好。
如果把太多内容合在一起,反而会让上下文变长,也会让模型更难定位关键信息。
所以合并时通常要注意:
- 只合并相邻或同源内容
- 保留必要边界,例如标题、来源、段落
- 合并后仍然要受 token budget 限制
3-3. Source Attribution
Source Attribution,可以理解为“来源归因”。
在 Evidence Organization 里面,它解决的问题是:检索出来的证据材料,应该如何保留来源信息。
在 RAG 场景里,尤其是企业知识库、制度问答、法务合同、产品文档问答里,后续生成答案时通常需要知道:
txt
这段证据来自哪份资料?
来自哪个文档?
是哪一节?
有没有原文链接?所以在 Context Assembly 阶段,系统不应该只把 evidence 的正文交给模型,也应该尽量保留 source 信息。
比如一个 candidate 可以带上这样的 metadata:
ts
type Evidence = {
id: string;
content: string;
metadata: {
sourceTitle: string;
sourceUrl?: string;
sectionTitle?: string;
page?: number;
};
};然后在组装上下文时,把来源信息一起保留下来:
txt
[来源 1]
标题:员工考勤制度
章节:病假管理
内容:病假超过 3 天,需要提供医院诊断证明。这样做的价值是,生成阶段如果需要输出引用,就有稳定的 source 信息可以使用。
Source Attribution 常见做法有:
- 给每段 evidence 分配 source id
- 在 context 中保留来源标题、章节、页码或 URL
- 保留 evidence id 和 source id 的映射关系
- 在 debug / trace 里记录哪些证据进入了最终上下文
比如:
ts
type ContextEvidence = {
id: string;
content: string;
source: {
id: string;
title: string;
url?: string;
sectionTitle?: string;
};
};这里需要注意,Source Attribution 只负责“来源信息怎么保留”。
至于答案里如何引用这些来源,以及最终答案是否真的被这些来源支持,那已经属于生成阶段和答案校验阶段要处理的问题。
总结一下,Evidence Organization 关注的是证据材料怎么组织。
它不只是把 candidates 拼接起来,而是进一步决定:
- 证据怎么分组
- 哪些证据需要合并
- 来源信息怎么保留
4. Final
到这里,关于检索后操作相关的技术名词,我们就基本介绍完了。
如果把两篇文章合在一起看,post-retrieval 的完整链路大概是:
ts
query -> retrieve candidates -> filtering -> ranking -> diversification -> context assembly -> generate其中 part1 主要介绍的是 candidates 进入上下文之前的处理:
- Result Filtering:先判断哪些 candidates 可以继续使用
- Result Ranking:再判断留下来的 candidates 谁更应该排在前面
- Result Diversification:最后判断最终选择的 candidates 是否足够丰富
part2 主要介绍的是 context assembly,也就是这些资料最终怎么变成 prompt 里的上下文:
- Context Window Management:解决上下文窗口怎么用
- Evidence Organization:解决证据材料怎么组织
所以可以简单理解为:
- part1:先把候选资料筛好、排好、选好
- part2:再把这些资料压好、摆好、组织好
这里还需要再强调一点:这些环节不是每个项目都要全部做一遍。真实项目里,通常会根据业务风险、成本和效果来选择组合。一个比较常见的基础链路是:
txt
retrieve topN
-> basic filtering
-> rerank
-> topK selection
-> context assembly
-> generate如果是企业知识库、制度问答、法务合同、医疗金融这类对准确性和可追溯性要求更高的场景,就可能继续加上:
txt
safety filtering
-> conflict filtering
-> recency / authority signals
-> evidence grouping
-> source attribution如果问题比较开放,或者答案需要覆盖多个角度,还可能加上:
txt
diversification
-> topic / source coverage
-> near-duplicate removal所以这些技术名词更像是一套工具箱。
真正落地时,不是把所有工具都用上,而是先看当前系统最容易出什么问题:
- 如果经常答非所问,就重点看 relevance filtering 和 reranker
- 如果经常重复引用同一段资料,就重点看 redundancy filtering 和 near-duplicate removal
- 如果经常使用旧资料,就重点看 recency bias
- 如果经常生成答案时缺少来源信息,就重点看 source attribution 是否在 context assembly 阶段保留下来
理解了这一点,post-retrieval 就不会是一堆零散术语,而是一条很清晰的工程链路:
- 先让资料更干净
- 再让资料更有顺序
- 再让资料覆盖更合理
- 最后把资料组织成可用上下文
这一整套处理,最终都是为了一个目标:让大模型不是“凭感觉回答”,而是基于更准确、更完整、更可追溯的上下文回答。
至于模型拿到这些上下文之后,应该如何基于证据生成答案、如何处理引用、如何做答案校验,那就是生成阶段要继续解决的问题了。
到这里,检索后操作相关技术名词就介绍完了。
我们下一篇文章再见👋
-EOF-