Skip to content

什么是查询重写?它有什么作用?如何基于 Spring AI 实现查询重写? ​

  • 标签:后端、编程导航

🏛 业务架构师 ​

查询重写(Query Rewrite):在检索前,用模型把用户的原始输入改写为更适合检索的形式。两个典型子场景:

  • 口语化→规范化:"上个月那些单子要是超时了咋办"→"订单超时未支付的处理规则"——用户说人话,文档说术语,重写是两者之间的翻译官;
  • 指代补全:多轮对话里"那第二种呢?"→"年假申请的第二种流程是什么"——把省略的主语从对话历史里补回来,否则检索引擎拿到的是无主语碎片,必然召回垃圾。

业务价值直白讲:RAG 的失败案例里,一大半不是知识库没有答案,而是拿问题去搜没搜到——重写解决的就是这个"检索失配"。它对多轮对话产品是必需品而非优化项:没有指代补全的多轮 RAG,第二轮开始就是灾难。

基于 Spring AI 的实现:Advisor 的 before 阶段,带最近几轮对话历史调轻量模型做重写(prompt:"结合对话历史,将用户最新问题改写为独立、完整的检索查询"),重写结果进入检索管线——对业务代码透明,且可配置开关与 A/B。

工程平衡:重写多一次模型调用(延迟 +100~300ms、成本增加)——我的策略:首轮问题短而完整时跳过重写(规则预判),疑似指代/口语长句才触发;用便宜的小模型做重写(这是模板化任务,不需要旗舰模型)。

一句话:查询重写是把"人的语言"翻译成"检索的语言"的必经工序——尤其多轮对话场景,它是及格线配置。

🏗 系统架构师 ​

查询重写(Query Rewriting)= 在检索前用 LLM 把用户原始输入改写为更适合检索的查询,作用分三个场景:

  1. 口语/口语补全改写:用户说"那它多少钱"——指代缺失,结合对话历史改写成"iPhone 15 Pro 价格"(上下文补全,多轮对话 RAG 的刚需,没有这步检索基本失效);
  2. 术语归一:用户俚语/错别字/中英混杂 → 领域标准术语("花呗逾期了咋办" → "消费信贷逾期处理流程"),对齐文档语料表述;
  3. 复杂问题分解:长问题拆成可检索的子问题(HyDE 反向:生成假设答案来检索也算重写家族)。

Spring AI 实现(Advisor 内做改写):

java
String rewritten = chatClient.prompt()
    .system("结合对话历史,将用户问题改写为独立、明确的检索查询,只输出改写结果")
    .user(question).call().content();
// 用 rewritten 去向量库检索,后续链不变

要点:必须携带对话历史(记忆 Advisor 的消息作为上下文传入);结构化输出可选(直接文本即可);用轻量模型(任务简单)。

工程权衡:每次检索多一次 LLM 调用(+300~800ms 延迟、少量成本)——收益是检索命中率的质变,多轮对话场景这是必选项而非优化项;简单单轮问答可跳过(判断是否需要改写的路由)。我的分层策略:多轮指代必重写、领域术语必重写、简单问题直接检。

💻 全栈开发专家 ​

查询重写(Query Rewrite)= 在检索前,把用户的原始输入改写成"更适合检索的独立完整查询"。两个典型场景:

  1. 口语化 → 规范化:"咋报销啊那个出差的" → "差旅费报销流程与标准";
  2. 多轮指代消解(更重要):用户先问"Java 线程池有哪些参数?",接着问"那第二个参数有默认值吗?"——"第二个参数"直接去检索向量库必然检索歪,必须结合历史重写成"Java 线程池 maximumPoolSize 参数的默认值"。

Spring AI 实现(Advisor 的 before 阶段):

java
public class QueryRewriteAdvisor implements CallAdvisor<...> {
    public ChatClient.ChatClientRequest adviseRequest(req, chain) {
        String history = compactHistory(req);        // 近几轮对话
        String question = req.prompt().getUserMessage().getText();
        String rewritten = chatClient.prompt()
            .system("""
                你是查询改写器。根据对话历史,把用户最新问题改写为
                不含代词、独立可理解的检索查询。只输出改写结果。
                对话历史:%s""".formatted(history))
            .user(question)
            .call().content();
        return chain.nextAdvice(replaceUserMessage(req, rewritten));
    }
}

工程要点:重写放在记忆 Advisor 之后、RAG Advisor 之前(顺序错了拿不到历史);重写失败兜底用原查询(这层只增强不能成为故障点);prompt 强调"只输出改写结果"防止模型啰嗦;重写结果记进日志,方便排查"检索歪了"的 case。