查询改写有哪些方法?

进阶实践约 7 分钟读完

一句话回答

用户的原始问题不一定适合直接检索:多轮对话里有"它""这个"这类指代,口语和文档的用词不一致,复杂问题一次检索找不全。查询改写就是在检索前用规则或大模型把问题改成更好检索的形式,常见方法有指代补全、多查询扩展、HyDE、子问题拆分和退一步提问。每种方法都会多一次模型调用、增加延迟,要按问题类型选用,并用评测集确认真的有收益。

详细解析

常见方法

方法 做法 适合 额外开销
指代补全 结合对话历史,把"那它多少钱?"改写成"X 产品的价格是多少?" 多轮对话,基本是必需的 一次模型调用,可以和其他改写合并
多查询扩展 生成几个不同说法的查询,分别检索后合并结果 用户的用词和文档的用词差别大 一次生成,加上多次检索(可以并行)
HyDE 先让模型写一段假设的答案,用它去检索 问题很短,和文档的表达方式差别大 一次生成,要等生成完才能检索,延迟比较明显
子问题拆分 把"A 和 B 的退款政策有什么区别"拆成"A 的退款政策"和"B 的退款政策" 比较类、多实体、多跳问题 一次生成,加上多次检索;多跳问题要串行
退一步提问 先生成一个更抽象的问题,比如把"为什么升级后内存变高了"退一步问成"新版本的内存管理机制是什么",两个问题都去检索 需要背景原理才能回答的具体问题 一次生成,加上一次检索

HyDE 为什么有效

问题和答案的文本形式不一样:问题短,是问句;文档是大段的陈述。用问题的 Embedding 去匹配文档,等于拿问句去找陈述句。HyDE(Hypothetical Document Embeddings)让模型先写一段"看起来像答案"的文字,即使其中有事实错误,它的用词和表达方式也更接近真正的文档,所以更容易匹配上。

风险是模型不了解这个领域(比如公司内部的系统)时,假设答案的方向就错了,会把检索带偏。所以不要用它完全替代原问题:原作者的实现是生成几个假设答案,把它们的向量和问题本身的向量取平均后再检索;工程上也常把它的检索结果和原问题的检索结果合并。

工程上的取舍

  • 延迟:改写在检索之前,直接增加首 token 延迟。改写用小而快的模型,限制输出长度,把指代补全和多查询合在一次调用里完成
  • 按需触发:没有对话历史、问题已经很完整时可以跳过;也可以先用原问题检索,最高的相关分低于阈值时再改写重试
  • 不能改丢约束:型号、日期、数字、否定词要原样保留,Prompt 里要明确要求
  • 可观测:日志里记录原问题、改写结果和各自的检索结果,排查时才知道是不是改写出了问题

代码示例

一次调用同时完成指代补全和多查询扩展,再并行检索、用 RRF 合并(rrf 的实现见 混合检索):

TypeScript
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 轮

登录后就可以和 AI 面试官对练,面试记录也会保存下来。登录

这道题你掌握了吗?

选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。

学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。