Appearance
检索前操作相关技术名词
接下来我们来看一下和检索前操作相关的技术名词,以及具体的落地方针、还有相关的面试题。
检索前处理常见的技术名词有:
- Query Transformation
- Query Rewrite
- Query Expand
- Multi-query
- Query Decomposition
- HyDE
首先需要理解的是 Query Transformation,这个中文叫做“查询转换”。
为什么需要先理解这个呢?
因为它通常指“在正式检索前,对用户 query 做各种变换处理”的 总称,可以算是一个总类。
它下面的具体策略有:
- Query Rewrite
- Query Expand
- Multi-query
- Query Decomposition
- HyDE
如果要对这些具体策略做分类,也可以分为 3 类:
- Query 改写类:让 query 更“标准、更清晰”
- Query 扩展类:提升召回率
- Query 结构化类:把复杂问题变成“可检索的问题集合”
1. Query 改写类
Query Rewrite 就属于一种改写策略,翻译成中文意思是“查询改写”。
意思是:在不改变用户核心意图的前提下,把原始 query 改写得更标准、更清晰、更适合检索。
它更像“润色原问题”。
举个例子:用户原话
苹果 15 续航咋样改写后:
iPhone 15 的电池续航表现如何?这里没有改变问题本身,只是把口语化、模糊、残缺的表达,变成了更完整、更规范的检索语句。
2. Query 扩展类
Query Expand 属于一种 Query 扩展策略。核心是:给原问题加料,但通常还是围绕一个检索请求展开。
例如,用户问:
笔记本电脑总死机怎么办Expand 后可能变成:
笔记本电脑死机 原因 解决办法 蓝屏 卡顿 散热 驱动问题这里重点是补充了相关词:
死机、蓝屏、驱动、散热目的是让召回更全。
Multi-query 也是一种 Query 扩展策略。核心是:从不同角度生成多条 query,分别检索,再合并结果。
例如,用户问:
如何提高团队研发效率系统生成多条 query:
- 提高研发团队效率的方法
- 软件团队协作效率提升实践
- 研发流程优化 OKR sprint code review
- 影响研发效率的常见瓶颈
这里不是简单“加词”,而是生成了多种问法/多种角度。
3. Query 结构化类
Query Decomposition 可以看作是一种 Query 结构化的工作。可以通俗理解成:把一个太大、太复杂、没法直接查的问题,拆成几个更小、更具体、更容易检索的问题。
它属于 Query Transformation 里面的一种,因为它不是直接拿用户原问题去查,而是先“拆问题”再检索。
比如用户问:A 公司和 B 公司哪家更值得去?
这个问题其实很难直接检索,因为“更值得去”太笼统了。
系统会先把它拆成几个子问题:
- A 公司薪资水平怎么样
- B 公司薪资水平怎么样
- A 公司加班情况怎么样
- B 公司加班情况怎么样
- A 公司发展前景怎么样
- B 公司发展前景怎么样
这样每个子问题都更具体,也更容易从知识库里找到对应信息,最后再把结果汇总,给出整体回答。
当然说到这里,你肯定有一个疑问:这不就和前面所介绍的 Multi-query 一样的么?
看起来很像,但 不完全一样。虽然它们最后都可能变成“多条 query”。区别主要在于:为什么要变成多条 query。
Multi-query 核心目标是:同一个问题,换几个不同说法/不同检索角度,尽量多召回一些内容。它本质上还是在查同一件事。
例如:
什么是 RAG生成 multi-query:
- RAG 是什么
- 检索增强生成是什么
- RAG 的定义
- RAG 的基本原理
这几条 query 本质上都在问同一个问题,只是换了表达方式和检索角度。
Query Decomposition 核心目标是:一个复杂问题里本来就包含多个子问题,所以先拆开,再分别查。
它本质上是在查不同子任务。
例如:
RAG 和微调有什么区别,哪个更适合企业知识库?拆解后:
- RAG 的特点是什么
- 微调的特点是什么
- RAG 的成本如何
- 微调的成本如何
- 企业知识库的核心需求是什么
这些已经不是同一个问题的不同说法了,而是不同维度的小问题。
总结一下:
- Multi-query 是“同一问题的多种问法”
- Query Decomposition 是“复杂问题拆成多个子问题”
4. HyDE
HyDE 的英语全称为 Hypothetical Document Embeddings,它的核心不是单纯改写,也不只是扩展,而是:
先让模型生成一段“假想答案/假想文档”,再用这段内容去做向量检索。
为什么要这么做?
因为用户原始 query 往往很短、很口语、信息量太少,不一定容易直接搜到最相关内容。
所以系统会先补一步:
- 根据用户问题,让大模型先写一段“可能会出现在正确答案里的内容”
- 再用这段内容做向量检索
- 找到真正相关的文档
通俗例子
用户问:
为什么 RAG 比微调更适合经常更新的知识库?如果直接拿这句话去检索,可能效果一般。因为它只是一个问题句,信息密度不高。
HyDE 会先让模型生成一段“假想答案”,例如:
RAG 更适合频繁更新的知识库,因为它不需要重新训练模型,只需要更新外部知识库内容。
相比之下,微调通常需要重新训练或重新部署,成本更高,更新周期更长。然后系统不是直接相信这段话,而是拿这段“像答案的文字”去做检索,去知识库里找真正相关的内容。
你可以把它想成:
- 普通检索:拿“问题”去搜
- HyDE:拿“一个像答案的草稿”去搜
因为“答案草稿”通常比“问题句”更完整,语义更丰富,所以更容易和真正相关文档对上。
在工程实践里,HyDE 常常属于:
- 一个可选增强策略
- 一个实验项
- 一个针对特定数据集的优化手段
而不是“RAG 标准组件”。因为做普通 RAG 的时候,先把基础做好,效果往往就够了。真实项目里,大家更常优先做的是这些:
- chunk 切分优化
- embedding 模型选择
- 向量检索 + BM25 混合检索
- rerank 重排
- query rewrite / multi-query
- metadata filter
这些手段通常更直接、收益更稳定、可控性更强。所以很多团队还没走到要上 HyDE 那一步。
HyDE 会多一层生成,链路更长,用了 HyDE,流程会变成:
- 先让模型生成“假想答案”
- 再拿它去检索
- 再生成最终回答
这样会带来:
- 延迟更高
- 成本更高
- 系统更复杂
如果原本检索效果已经不错,团队往往不会主动加这一步。不过大家还是有必要了解这个专业名词。
5. 如何落地
接下来我们来讨论一下这些技术如何落地。先回忆一下我们当前 RAG SDK 的设计:
- runtime 主要负责定义抽象接口,以及做流程的编排,适合放进 SDK 核心的有:
- 查询期主流程编排
- 标准扩展接口
- 统一输入输出契约
- 调试信息与可观测字段
- 通用的结果合并/后处理基础能力
- adapter 这部分要解决的是“怎么把外部生态接进来”。适合放进官方 adapter 的有:
- 把 LangChain / LlamaIndex / OpenAI / 其他框架能力适配成 SDK 内置接口
- 把第三方 retriever / generator / embeddings / vector store 接进来
- 把外部现成的 query transformation 组件接进来
至于我们上面所讨论的检索前操作,例如:
- Query Rewrite
- Multi-query
- Decomposition
- HyDE
这些往往和业务场景强相关,很难做成一个真正通用的官方默认件。换句话说,这部分通常由开发者自己实现。
举几个例子:
用户要实现 Query rewrite:
ts
import type {
QueryPreprocessor,
RetrievalRequest,
RuntimeContext,
RuntimeQueryInput,
} from "@rag-sdk/runtime";
export class SimpleRewritePreprocessor implements QueryPreprocessor {
async preprocess(
input: RuntimeQueryInput,
_context: RuntimeContext,
): Promise<RetrievalRequest> {
const original = input.query;
// 这里只是演示,真实场景可换成 LLM 改写
const rewritten = rewriteQuery(original);
return {
originalQuery: { query: original },
effectiveQuery: { query: rewritten },
rewriteReason: "normalize-user-query",
strategy: "single-query-rewrite",
metadata: input.metadata,
};
}
}
function rewriteQuery(query: string): string {
if (query.includes("苹果 15 续航咋样")) {
return "iPhone 15 的电池续航表现如何?";
}
return query.trim();
}再比如 Multi-query,示意代码如下:
ts
import type {
RetrievalCandidate,
RetrievalRequest,
RuntimeContext,
RuntimeRetriever,
RuntimeRetrievalResult,
} from "@rag-sdk/runtime";
type SearchBackend = {
search(query: string): Promise<RetrievalCandidate[]>;
};
export class MultiQueryRetriever implements RuntimeRetriever {
constructor(private readonly backend: SearchBackend) {}
async retrieve(
request: RetrievalRequest,
_context: RuntimeContext,
): Promise<RuntimeRetrievalResult> {
const baseQuery = request.effectiveQuery.query;
const queries = buildMultiQueries(baseQuery);
const candidateGroups = await Promise.all(
queries.map((q) => this.backend.search(q)),
);
const merged = dedupeCandidates(candidateGroups.flat());
return {
candidates: merged,
retrievalMetadata: {
strategy: "multi-query",
generatedQueries: queries,
},
};
}
}
function buildMultiQueries(query: string): string[] {
return [
query,
`${query} 定义`,
`${query} 原理`,
`${query} 示例`,
];
}
function dedupeCandidates(
candidates: RetrievalCandidate[],
): RetrievalCandidate[] {
const seen = new Set<string>();
const result: RetrievalCandidate[] = [];
for (const candidate of candidates) {
if (seen.has(candidate.chunk.id)) continue;
seen.add(candidate.chunk.id);
result.push(candidate);
}
return result;
}之后在创建 runtime 的时候,就可以像这样:
ts
const runtime = createRuntime({
preprocessor: new SimpleRewritePreprocessor(),
retriever: ...
postprocessor: ...
generator: ...
});🤔思考:检索前操作能不能配置多个?
可以配多个,而且真实项目里经常就是多个串起来用。
比如很常见的链路就是:
- Query Rewrite
- 再做 Multi-query
- 或者先 Decomposition
- 然后对子问题再 Rewrite
- 最后再去检索
所以从 RAG 方法论上说,检索前操作不是“只能选一个”,而是可以组合的。
如果要设计成接收多个检索前操作的话,preprocessor 的值就应该是接收一个数组:如下:
ts
const runtime = createRuntime({
preprocessor: [new SimpleRewritePreprocessor(), new MultiQueryRetriever()],
retriever: ...
postprocessor: ...
generator: ...
});当然,这同样也就意味着 runtime 包内部也要做一定的修改,目前 rumtime 包内部在编排流程的时候,是这么写的:
ts
const request = await measureStage(timings, "pre-retrieval", async () => {
try {
return await runtime.preprocessor.preprocess(input, context);
} catch (error) {
throw toRuntimeError(error, "pre-retrieval", originalQuery);
}
});其中 runtime.preprocessor.preprocess(input, context) 这一块儿就需要修改,因为 ``runtime.preprocessor` 拿到的是一个数组。
🤔思考:思考另外一个问题,能否提供默认的实现?
在 langchain 中,实际上是提供了部分检索前操作的 API 的,例如 HydeRetriever
这个检索器就是支持 HyDE 特性的一种检索器。我们可以在 adapter 包中添加该检索的封装:
ts
import type { DocumentInterface } from "@langchain/core/documents";
import { HydeRetriever } from "@langchain/classic/retrievers/hyde";
import {
createLangChainBaseRetrieverRuntimeAdapter,
type CreateLangChainBaseRetrieverRuntimeAdapterOptions,
} from "./langchain-base-retriever-runtime-adapter.js";
type HydeRetrieverOptions = ConstructorParameters<typeof HydeRetriever>[0];
export type CreateLangChainHydeRuntimeRetrieverAdapterOptions<
Metadata extends Record<string, any> = Record<string, any>,
> = Omit<
CreateLangChainBaseRetrieverRuntimeAdapterOptions<Metadata>,
"retriever"
> & {
hyde: HydeRetrieverOptions;
};
export function createLangChainHydeRuntimeRetrieverAdapter<
Metadata extends Record<string, any> = Record<string, any>,
>(
options: CreateLangChainHydeRuntimeRetrieverAdapterOptions<Metadata>,
) {
const hydeRetriever = new HydeRetriever(options.hyde);
return createLangChainBaseRetrieverRuntimeAdapter<Metadata>({
...options,
retriever: hydeRetriever as unknown as {
invoke(
query: string,
config?: unknown,
): Promise<DocumentInterface<Metadata>[]>;
},
buildRetrievalMetadata:
options.buildRetrievalMetadata ??
(({ filteredCandidates, request }) => ({
provider: "langchain",
retrieverType: "hyde",
strategy: request.strategy ?? "hyde",
effectiveQuery: request.effectiveQuery.query,
returnedCandidates: filteredCandidates.length,
})),
});
}然后开发者就可以直接使用,不过这里不是作为一种 preprocessor,而是一种 retriever:
ts
import { Document } from "@langchain/core/documents";
import { OpenAI, OpenAIEmbeddings } from "@langchain/openai";
import { MemoryVectorStore } from "@langchain/classic/vectorstores/memory";
import { createDefaultRuntime, NoopQueryPreprocessor } from "@rag-sdk/runtime";
import { createLangChainHydeRuntimeRetrieverAdapter } from "@rag-sdk/adapters";
async function main() {
const embeddings = new OpenAIEmbeddings({
model: "text-embedding-3-small",
});
const vectorStore = new MemoryVectorStore(embeddings);
await vectorStore.addDocuments([
new Document({
id: "doc-1",
pageContent: "RAG 更适合频繁更新的知识库,因为它通常不需要重新微调模型。",
metadata: {
sourceId: "kb/rag",
category: "rag",
},
}),
new Document({
id: "doc-2",
pageContent: "微调适合把稳定领域知识和输出风格固化进模型参数里。",
metadata: {
sourceId: "kb/finetune",
category: "finetune",
},
}),
]);
const llm = new OpenAI({
model: "gpt-4o-mini",
temperature: 0,
});
const runtime = createDefaultRuntime({
preprocessor: new NoopQueryPreprocessor({
strategy: "hyde",
topK: 4,
}),
retriever: createLangChainHydeRuntimeRetrieverAdapter({
hyde: {
vectorStore,
llm,
k: 4,
},
filterByRequest: true,
buildRetrievalMetadata({ request, filteredCandidates }) {
return {
provider: "langchain",
retrieverType: "hyde",
strategy: request.strategy ?? "hyde",
candidateCount: filteredCandidates.length,
};
},
}),
generator: {
async generate({ request, chunks }) {
return {
answer: [
`query: ${request.originalQuery.query}`,
`strategy: ${request.strategy}`,
`chunks: ${chunks.map((chunk) => chunk.content).join("\n")}`,
].join("\n\n"),
};
},
},
});
const result = await runtime.run(
{
query: "为什么 RAG 比微调更适合经常更新的企业知识库?",
},
{
includeDebug: true,
},
);
console.log(result);
}
void main();6. 相关面试题
1. 你怎么区分 Query Rewrite、Multi-query、Query Decomposition、HyDE?它们分别适合什么场景?
题目分析
这题很常见,因为面试官通常不是要你背名词,而是看你是否真的理解它们的边界。 考察点:
- Rewrite 是把问题改得更清晰
- Multi-query 是同一问题生成多种问法,提升召回
- Decomposition 是把复杂问题拆成多个子问题
- HyDE 是先生成假想文档,再拿它去检索
延伸追问通常会是:如果只能先落地一种,你会先选哪种,为什么?
参考答案
我的理解是,这四种都属于检索前处理,但解决的问题不一样。
Query Rewrite 主要解决的是用户问题表达不规范、不完整、太口语化的问题,本质上是把 query 改得更清晰、更标准,但不改变核心意图。 Multi-query 是针对同一个问题,生成多种问法或多个检索角度,目的是提升召回率,适合 query 很短、表述多样、容易漏召回的场景。 Query Decomposition 是把一个复杂问题拆成多个子问题,适合比较型、组合型、多约束型问题,比如“A 和 B 有什么区别,哪个更适合某场景”。 HyDE 则比较特殊,它不是简单改写 query,而是先生成一段假想答案或假想文档,再拿这段文本去做检索,适合 query 和知识库文档表述差异比较大、纯向量检索效果不稳定的场景。
如果是工程优先级,我通常会先考虑 Rewrite 和 Multi-query,因为它们收益比较稳,HyDE 会放在后面按实验效果决定。
2. 在真实 RAG 项目里,什么时候你会考虑上 Multi-query 或 HyDE?什么时候反而不建议用?
题目分析
面试官想听的通常是:
- Multi-query 适合 query 很短、表述多样、召回不足的场景
- HyDE 适合 query 和文档表达差异大、纯向量检索不稳定的场景
- 不建议乱上,因为会增加延迟、成本、链路复杂度,还可能引入噪声
这题答得好不好,关键在于你能不能讲出收益和代价。
参考答案
我的判断标准主要看当前系统的主要瓶颈是不是“召回不足”。
如果用户 query 很短、歧义比较大、同一个意思有很多不同说法,或者知识库里的表达方式比较分散,这时候我会优先考虑 Multi-query,因为它本质上是在扩大召回覆盖面。 如果我发现用户问题和知识库文档的语言风格差异很大,比如用户问得很口语,但文档写得很正式,或者纯向量检索召回不稳定,我会考虑 HyDE,因为它通过生成一段更像文档语言的假想文本来帮助匹配。
但这两种我都不会默认直接上。因为它们都会增加链路复杂度、延迟和成本,而且也可能引入噪声。尤其 HyDE,如果生成的假想内容偏了,检索也可能被带偏。所以我一般会先把 chunk、embedding、hybrid search、rerank 这些基础能力做好,再判断是否值得加 Multi-query 或 HyDE。
3. 一个用户问题既可以做 Query Rewrite,也可以做 Query Decomposition。你会怎么判断该走哪条路径?
题目分析
这题很像中高级面试会问的“策略选择题”。
面试官想看的是你有没有清晰判断标准,例如:
- 如果只是口语化、不完整、指代不清,优先 Rewrite
- 如果一个问题里包含多个维度、比较关系、复合约束,优先 Decomposition
- 有时候也可以先 Rewrite,再决定是否 Decomposition
这题本质是在考:你是否能从 query 特征出发做策略分流。
参考答案
我会先判断这个问题到底是“表达问题”还是“结构问题”。
如果问题的核心意思其实很单一,只是用户说得比较口语、不完整、带指代、关键词不标准,那我会优先做 Query Rewrite。因为这类问题本质上还是一个问题,只是需要改写成更适合检索的形式。 但如果这个问题本身就包含多个维度,比如比较两个方案、同时问优缺点和适用场景,或者一个问题里其实藏了多个子任务,那我会优先做 Query Decomposition。因为这种情况下,不拆开检索的话,往往很难把信息查全。
实际工程里我觉得这两者也不是完全互斥的。比较常见的做法是先做轻量 rewrite,把问题表达整理清楚,再判断它是不是复杂问题,如果是,再进入 decomposition。也就是说,Rewrite 更像基础清洗,Decomposition 更像复杂问题编排。
4. 如果你设计一个 RAG SDK,你会把这些检索前操作放在哪一层?哪些应该做成框架能力,哪些应该交给业务方?
题目分析
理想回答通常会涉及:
- SDK 核心应该提供 QueryPreprocessor 之类的扩展点
- adapter 层负责接 LangChain / OpenAI 等外部能力
- 具体采用 Rewrite / Multi-query / Decomposition / HyDE,通常由业务方按场景决定
- Rewrite 比较容易标准化
- Decomposition 更依赖业务编排,不适合一开始就做死
这题很能拉开差距,因为它考察的是抽象能力和落地能力。
参考答案
如果我来设计,我会把它拆成三层。
第一层是 SDK 核心能力,这一层不应该强绑定某一种具体策略,而是提供统一的扩展点和运行时编排能力,比如 QueryPreprocessor、Retriever、Postprocessor 这类接口,让外部可以接入不同的检索前处理策略。 第二层是官方 adapter,这一层主要负责把 LangChain、OpenAI 之类外部生态能力适配成 SDK 的统一契约。比如把 LangChain 的 HydeRetriever 或 SelfQueryRetriever 适配成 SDK 里的 retriever。 第三层是业务方自定义策略,这一层决定具体什么时候用 Rewrite、什么时候用 Multi-query、是否启用 Decomposition、HyDE 的 prompt 怎么写,这些我觉得都应该交给业务方,因为它们强依赖具体数据和业务目标。
所以我的原则是:核心层负责抽象和编排,adapter 层负责生态接入,策略选择和效果优化交给业务方。这样 SDK 才既通用,又不会把业务策略写死。
-EOF-