Skip to content

检索后操作相关技术名词part2 ​

1. Intro ​

接下来我们继续介绍和检索后操作相关的技术名词。

在上一篇文章里,我们介绍了 post-retrieval 前半段的几个环节:

  1. Result Filtering:决定哪些 candidates 可以继续往后传
  2. Result Ranking:决定留下来的 candidates 谁更应该排在前面
  3. 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 本身:

  1. 哪些应该留下?
  2. 谁应该排在前面?
  3. 结果是否足够多样?

而 Context Assembly 更关注 prompt 里的上下文:

  1. 最终放多少内容?
  2. 每段内容放多长?
  3. 重要内容应该放在哪里?
  4. 证据之间要不要合并、分组、标注来源?
  5. 来源信息是否需要一起进入上下文?

这一部分通常可以分成两个方向:

  1. Context Window Management
  2. Evidence Organization

Context Window Management 关注的是上下文窗口怎么用。

例如:

  1. 选择多少条 candidates
  2. 给检索资料分配多少 token
  3. 是否需要压缩或截断 chunk
  4. 长上下文里如何排列重要内容

Evidence Organization 关注的是证据材料怎么组织。

例如:

  1. 是否按主题分组
  2. 是否合并来自同一来源的内容
  3. 是否保留 source id
  4. 是否保留来源标题、章节、页码或 URL
  5. 是否方便后续生成阶段做引用和追溯

所以 part2 主要介绍这两个方向。

如果说 part1 解决的是:哪些资料值得进入上下文?

那么 part2 解决的是:这些资料应该怎么变成最终上下文?

这就是 Context Assembly 要处理的核心问题。

2. Context Window Management ​

Context Window Management,中文可以理解为“上下文窗口管理”。

前面已经说过,Context Assembly 关注的是候选资料最终怎么变成 prompt 里的上下文。

而 Context Window Management 是其中偏“空间管理”的部分,它更关心:在有限的 token 预算里,资料最多能放多少?应该选哪些?太长了怎么办?顺序怎么排?

这一部分可以按工程实现顺序拆成几类:

  1. Token Budget Allocation:先决定 token 预算怎么分
  2. Top-K Selection:再决定选多少条 candidates
  3. Chunk Truncation / Context Compression:然后处理太长的内容
  4. Long-context Reordering:最后决定内容在上下文里的排列顺序

2-1. Token Budget Allocation ​

Token Budget Allocation 可以理解为“token 预算分配”。

它解决的问题不是简单地问:上下文还剩多少空间可以塞资料?

更准确地说,它解决的是:一次 prompt 里的 token 空间,应该如何分配给 system prompt、用户问题、历史对话、输出约束、检索资料和预留输出空间。

很多同学第一次接触这个概念时,会把它理解成“防止超过模型最大上下文窗口”。

这个理解没有错,但还不够。

因为即使模型支持很长的上下文,比如 128K、200K,甚至更大的窗口,也不代表我们就应该把所有检索结果都放进去。

长上下文解决的是“能不能放下”的问题,但 RAG 更关心的是“放进去以后有没有用”。

如果上下文里放了大量相关性一般、内容重复或者噪声很高的资料,模型不一定会更好回答问题,反而可能出现几个问题:

  1. 关键证据被低价值内容淹没
  2. prompt 成本明显上升
  3. 响应延迟变长
  4. 输出稳定性下降

所以在 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,就直接简单截掉。更常见的处理方式是:先保留用户当前问题,再从其它部分让出空间。

例如:

  1. 历史对话太长,就先压缩 history
  2. 检索资料预算不够,就减少进入上下文的 candidates
  3. 某些 chunk 太长,就做截断或压缩
  4. 如果用户输入本身像一大段文档,就先做 query compression 或 query rewriting
  5. 如果整体仍然超过模型上限,再考虑拒绝、截断或让用户缩短输入

所以静态预算的特点是简单、清晰,适合大多数基础 RAG 系统。系统先算出检索资料最多能占多少 token,然后再进入后面的 Top-K Selection、Context Compression 等步骤。

不过真实项目里,预算通常不会永远固定。

比如用户只是问:

txt
公司年假一年有几天?

这个问题可能只需要一两段制度原文就能回答,不需要塞很多资料。

但如果用户问的是:

txt
试用期员工、正式员工和外包人员的请假规则有什么区别?

这个问题涉及多个对象和多条规则,就可能需要更多证据。此时 retrieval budget 可以适当放大。

所以更完整的做法是动态预算。

动态预算会根据当前请求调整不同部分的 token 分配。例如:

  1. 用户问题很简单,就减少检索资料数量
  2. 用户问题比较复杂,就给检索资料更多空间
  3. 历史对话太长,就先压缩 history
  4. rerank 分数整体不高,就不要为了填满预算硬塞资料
  5. 多个 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,可能会出现两种问题:

  1. 选出来的内容太长,超过 token budget
  2. 选出来的内容太短,浪费上下文空间

所以更常见的做法是:在 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 有用,但太长,不能原样放进上下文。

两者区别在于:

  1. Chunk Truncation:直接截断
  2. 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 高。

所以实际项目里通常会按顺序处理:

  1. 能原样放入,就不压缩
  2. 太长但结构简单,可以截断
  3. 太长且重要,才做 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 更适合这些场景:

  1. 放入上下文的证据很多
  2. 单次 prompt 很长
  3. 重要信息容易被埋在中间
  4. 模型在长上下文下有明显的位置敏感问题

总结一下,Context Window Management 关注的是上下文窗口怎么用。它不是判断资料有没有价值,而是在已有候选资料的基础上,进一步决定:

  1. 检索资料最多能占多少 token
  2. 最终选哪些 candidates
  3. 太长的内容怎么处理
  4. 内容在 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 不是一堆散乱文本,而是应该按主题、来源或作用组织起来。

常见的分组方式有:

  1. 按主题分组
  2. 按来源分组
  3. 按时间分组
  4. 按答案结构分组
  5. 按证据作用分组,例如规则、例外、流程、限制

举个例子,用户问:

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。它也可能来自:

  1. 文档标题或章节结构
  2. indexing 阶段生成的标签
  3. query analysis 提取出的子问题
  4. 模型对 evidence 的归类结果

所以 Evidence Grouping 本质上是在解决:把散乱的证据组织成更适合回答的结构。

3-2. Merging ​

Merging 是证据合并。它解决的问题是:多个 candidates 可能来自同一段、同一节或同一来源,需要合并成更完整的上下文。

举个例子,检索结果里有两条相邻 chunk:

txt
A. 病假超过 3 天,需要提供医院诊断证明。
B. 如遇紧急情况,员工可在事后 24 小时内补交申请。

这两条单独看都有用,但合在一起更完整。

合并后可以变成:

txt
病假超过 3 天,需要提供医院诊断证明。如遇紧急情况,员工可在事后 24 小时内补交申请。

Merging 常见的依据有:

  1. 相同 sourceId
  2. 相同 parentId
  3. 相邻 chunk
  4. 相同 section
  5. 内容上属于同一规则或同一流程

示意代码如下:

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 不是越多越好。

如果把太多内容合在一起,反而会让上下文变长,也会让模型更难定位关键信息。

所以合并时通常要注意:

  1. 只合并相邻或同源内容
  2. 保留必要边界,例如标题、来源、段落
  3. 合并后仍然要受 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 常见做法有:

  1. 给每段 evidence 分配 source id
  2. 在 context 中保留来源标题、章节、页码或 URL
  3. 保留 evidence id 和 source id 的映射关系
  4. 在 debug / trace 里记录哪些证据进入了最终上下文

比如:

ts
type ContextEvidence = {
  id: string;
  content: string;
  source: {
    id: string;
    title: string;
    url?: string;
    sectionTitle?: string;
  };
};

这里需要注意,Source Attribution 只负责“来源信息怎么保留”。

至于答案里如何引用这些来源,以及最终答案是否真的被这些来源支持,那已经属于生成阶段和答案校验阶段要处理的问题。

总结一下,Evidence Organization 关注的是证据材料怎么组织。

它不只是把 candidates 拼接起来,而是进一步决定:

  1. 证据怎么分组
  2. 哪些证据需要合并
  3. 来源信息怎么保留

4. Final ​

到这里,关于检索后操作相关的技术名词,我们就基本介绍完了。

如果把两篇文章合在一起看,post-retrieval 的完整链路大概是:

ts
query -> retrieve candidates -> filtering -> ranking -> diversification -> context assembly -> generate

其中 part1 主要介绍的是 candidates 进入上下文之前的处理:

  1. Result Filtering:先判断哪些 candidates 可以继续使用
  2. Result Ranking:再判断留下来的 candidates 谁更应该排在前面
  3. Result Diversification:最后判断最终选择的 candidates 是否足够丰富

part2 主要介绍的是 context assembly,也就是这些资料最终怎么变成 prompt 里的上下文:

  1. Context Window Management:解决上下文窗口怎么用
  2. 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

所以这些技术名词更像是一套工具箱。

真正落地时,不是把所有工具都用上,而是先看当前系统最容易出什么问题:

  1. 如果经常答非所问,就重点看 relevance filtering 和 reranker
  2. 如果经常重复引用同一段资料,就重点看 redundancy filtering 和 near-duplicate removal
  3. 如果经常使用旧资料,就重点看 recency bias
  4. 如果经常生成答案时缺少来源信息,就重点看 source attribution 是否在 context assembly 阶段保留下来

理解了这一点,post-retrieval 就不会是一堆零散术语,而是一条很清晰的工程链路:

  1. 先让资料更干净
  2. 再让资料更有顺序
  3. 再让资料覆盖更合理
  4. 最后把资料组织成可用上下文

这一整套处理,最终都是为了一个目标:让大模型不是“凭感觉回答”,而是基于更准确、更完整、更可追溯的上下文回答。

至于模型拿到这些上下文之后,应该如何基于证据生成答案、如何处理引用、如何做答案校验,那就是生成阶段要继续解决的问题了。

到这里,检索后操作相关技术名词就介绍完了。

我们下一篇文章再见👋


-EOF-