查询改写有哪些方法?
一句话回答
用户的原始问题不一定适合直接检索:多轮对话里有"它""这个"这类指代,口语和文档的用词不一致,复杂问题一次检索找不全。查询改写就是在检索前用规则或大模型把问题改成更好检索的形式,常见方法有指代补全、多查询扩展、HyDE、子问题拆分和退一步提问。每种方法都会多一次模型调用、增加延迟,要按问题类型选用,并用评测集确认真的有收益。
详细解析
常见方法
| 方法 | 做法 | 适合 | 额外开销 |
|---|---|---|---|
| 指代补全 | 结合对话历史,把"那它多少钱?"改写成"X 产品的价格是多少?" | 多轮对话,基本是必需的 | 一次模型调用,可以和其他改写合并 |
| 多查询扩展 | 生成几个不同说法的查询,分别检索后合并结果 | 用户的用词和文档的用词差别大 | 一次生成,加上多次检索(可以并行) |
| HyDE | 先让模型写一段假设的答案,用它去检索 | 问题很短,和文档的表达方式差别大 | 一次生成,要等生成完才能检索,延迟比较明显 |
| 子问题拆分 | 把"A 和 B 的退款政策有什么区别"拆成"A 的退款政策"和"B 的退款政策" | 比较类、多实体、多跳问题 | 一次生成,加上多次检索;多跳问题要串行 |
| 退一步提问 | 先生成一个更抽象的问题,比如把"为什么升级后内存变高了"退一步问成"新版本的内存管理机制是什么",两个问题都去检索 | 需要背景原理才能回答的具体问题 | 一次生成,加上一次检索 |
HyDE 为什么有效
问题和答案的文本形式不一样:问题短,是问句;文档是大段的陈述。用问题的 Embedding 去匹配文档,等于拿问句去找陈述句。HyDE(Hypothetical Document Embeddings)让模型先写一段"看起来像答案"的文字,即使其中有事实错误,它的用词和表达方式也更接近真正的文档,所以更容易匹配上。
风险是模型不了解这个领域(比如公司内部的系统)时,假设答案的方向就错了,会把检索带偏。所以不要用它完全替代原问题:原作者的实现是生成几个假设答案,把它们的向量和问题本身的向量取平均后再检索;工程上也常把它的检索结果和原问题的检索结果合并。
工程上的取舍
- 延迟:改写在检索之前,直接增加首 token 延迟。改写用小而快的模型,限制输出长度,把指代补全和多查询合在一次调用里完成
- 按需触发:没有对话历史、问题已经很完整时可以跳过;也可以先用原问题检索,最高的相关分低于阈值时再改写重试
- 不能改丢约束:型号、日期、数字、否定词要原样保留,Prompt 里要明确要求
- 可观测:日志里记录原问题、改写结果和各自的检索结果,排查时才知道是不是改写出了问题
代码示例
一次调用同时完成指代补全和多查询扩展,再并行检索、用 RRF 合并(rrf 的实现见 混合检索):
const REWRITE_PROMPT = `你负责把用户的问题改写成适合检索知识库的查询。
1. 结合对话历史补全指代(它、这个、上面说的),改写成脱离上下文也能看懂的完整问题
2. 再给出 2 个不同说法的查询,尽量使用文档里可能出现的正式术语
3. 保留原问题中的产品名、型号、日期、数字和否定词,不要添加原问题没有的条件
只输出 JSON:{"standalone": "完整问题", "variants": ["说法一", "说法二"]}`
async function rewriteAndRetrieve(history: Message[], question: string) {
let standalone = question
let variants: string[] = []
try {
const res = await llm.chat({
model: REWRITE_MODEL, // 用小而快的模型
messages: [
{ role: 'system', content: REWRITE_PROMPT },
{ role: 'user', content: `<history>\n${formatHistory(history.slice(-6))}\n</history>\n<question>${question}</question>` },
],
})
const data = JSON.parse(res.content)
standalone = String(data.standalone || question)
variants = Array.isArray(data.variants) ? data.variants.slice(0, 2).map(String) : []
} catch {
// 改写失败不影响主流程,退回用原问题检索
}
// 原问题也参与检索,防止改写跑偏;多个查询并行检索,结果用 RRF 合并
const queries = [...new Set([standalone, ...variants, question])]
const results = await Promise.all(queries.map((q) => hybridSearch(q, 20)))
return rrf(results)
}
面试官可能追问
改写会不会改错用户的意思?怎么防?
会。常见的是丢掉否定和限定条件,比如把"哪些情况不能退款"改成"退款条件",或者补全指代时选错了对象。防范办法:Prompt 里要求保留关键约束;原问题也一起检索;在日志里抽查改写结果;评测集里专门放多轮对话和带否定的问题。
改写增加的延迟怎么控制?
用小模型并限制输出长度;多种改写合并到一次调用;多个查询并行检索;只在需要时触发,比如先用原问题检索,结果不理想再改写;热门问题的改写结果可以缓存。HyDE 这类必须等生成完才能检索的方法,只用在确实有收益的问题类型上。
用户用中文提问,知识库是英文文档,怎么办?
两种做法:改写时顺便把查询翻译成文档的语言再检索;或者使用支持多语言的 Embedding 模型,让不同语言的同义内容落在相近的位置。关键词检索这一路必须用和文档相同的语言。两种做法可以在评测集上对比。
易错点
- 多轮对话里直接拿最后一句话检索,"它""这个"会让检索完全跑偏
- 改写结果主要服务于检索,生成回答时仍要把原问题和对话历史交给模型
- HyDE 生成的假设答案只用来检索,不能当作资料放进上下文
- 改写方法不是越多越好,每加一种都要用评测集证明有收益
AI 模拟面试官
用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮
这道题你掌握了吗?
选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。
学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。