什么是查询重写?它有什么作用?如何基于 Spring AI 实现查询重写?
- 标签:后端、编程导航
🏛 业务架构师
查询重写(Query Rewrite):在检索前,用模型把用户的原始输入改写为更适合检索的形式。两个典型子场景:
- 口语化→规范化:"上个月那些单子要是超时了咋办"→"订单超时未支付的处理规则"——用户说人话,文档说术语,重写是两者之间的翻译官;
- 指代补全:多轮对话里"那第二种呢?"→"年假申请的第二种流程是什么"——把省略的主语从对话历史里补回来,否则检索引擎拿到的是无主语碎片,必然召回垃圾。
业务价值直白讲:RAG 的失败案例里,一大半不是知识库没有答案,而是拿问题去搜没搜到——重写解决的就是这个"检索失配"。它对多轮对话产品是必需品而非优化项:没有指代补全的多轮 RAG,第二轮开始就是灾难。
基于 Spring AI 的实现:Advisor 的 before 阶段,带最近几轮对话历史调轻量模型做重写(prompt:"结合对话历史,将用户最新问题改写为独立、完整的检索查询"),重写结果进入检索管线——对业务代码透明,且可配置开关与 A/B。
工程平衡:重写多一次模型调用(延迟 +100~300ms、成本增加)——我的策略:首轮问题短而完整时跳过重写(规则预判),疑似指代/口语长句才触发;用便宜的小模型做重写(这是模板化任务,不需要旗舰模型)。
一句话:查询重写是把"人的语言"翻译成"检索的语言"的必经工序——尤其多轮对话场景,它是及格线配置。
🏗 系统架构师
查询重写(Query Rewriting)= 在检索前用 LLM 把用户原始输入改写为更适合检索的查询,作用分三个场景:
- 口语/口语补全改写:用户说"那它多少钱"——指代缺失,结合对话历史改写成"iPhone 15 Pro 价格"(上下文补全,多轮对话 RAG 的刚需,没有这步检索基本失效);
- 术语归一:用户俚语/错别字/中英混杂 → 领域标准术语("花呗逾期了咋办" → "消费信贷逾期处理流程"),对齐文档语料表述;
- 复杂问题分解:长问题拆成可检索的子问题(HyDE 反向:生成假设答案来检索也算重写家族)。
Spring AI 实现(Advisor 内做改写):
java
String rewritten = chatClient.prompt()
.system("结合对话历史,将用户问题改写为独立、明确的检索查询,只输出改写结果")
.user(question).call().content();
// 用 rewritten 去向量库检索,后续链不变要点:必须携带对话历史(记忆 Advisor 的消息作为上下文传入);结构化输出可选(直接文本即可);用轻量模型(任务简单)。
工程权衡:每次检索多一次 LLM 调用(+300~800ms 延迟、少量成本)——收益是检索命中率的质变,多轮对话场景这是必选项而非优化项;简单单轮问答可跳过(判断是否需要改写的路由)。我的分层策略:多轮指代必重写、领域术语必重写、简单问题直接检。
💻 全栈开发专家
查询重写(Query Rewrite)= 在检索前,把用户的原始输入改写成"更适合检索的独立完整查询"。两个典型场景:
- 口语化 → 规范化:"咋报销啊那个出差的" → "差旅费报销流程与标准";
- 多轮指代消解(更重要):用户先问"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。