什么是 RAG 查询改写(Query Rewriting)?它怎么提升检索效果的?
结论:查询改写(Query Rewriting)是在检索之前把用户原话重写成更适合检索的表达,专门治四类病: 指代不清、口语与术语不匹配、一条 query 藏多个意图、以及"问题太具体而文档太抽象"。 它不改知识,只提高"query 与文档字面/语义对齐"的概率,所以对召回率的影响是直接的。
1. 四类该改写的查询
| 类型 | 原始查询 | 改写后 | 手法 |
|---|---|---|---|
| 指代省略 | "它的保修期多久"(上一轮在说 iPad) | "iPad 的保修期是多久" | 多轮历史压缩 |
| 口语 vs 术语 | "东西坏了能换吗" | "质量问题 退换货政策" | 术语归一、同义词扩展 |
| 多意图 | "报销和请假流程" | ["报销流程", "请假流程"] | 子查询分解 |
| 抽象度错位 | "0x0000007B 是什么" | "Windows 蓝屏 0x0000007B 原因 处理" | 关键词补全 |
2. 主流改写手法一览
| 手法 | 做什么 | 额外成本 | 适用 |
|---|---|---|---|
| 指代消解 | 用对话历史把"它/这个"替换成实体 | 1 次小模型调用 | 多轮问答 |
| 多查询扩展(Multi-Query) | 生成 3~5 个同义问法并行检索 | 1 次 LLM + N 次检索 | 召回不足 |
| 查询分解(Decomposition) | 拆成多个子问题分别检索 | 1 次 LLM + N 次检索 | 对比、多跳 |
| 回溯提问(Step-back) | 先问更抽象的上一层问题 | 1 次 LLM + 1 次检索 | 具体问题需背景原理 |
| 术语归一 | 词典把黑话映射到标准词 | 0 | 内部缩写、别名多 |
| HyDE | 先写假设答案,用答案去检索 | 1 次 LLM | 短 query、语义鸿沟大 |
3. 链路位置
用户 query + 最近 N 轮历史
│
▼
┌─ 改写器(规则/小模型)─┐
▼ ▼ ▼
原 query 同义 query 子问题
│ │ │
└──── 各检索一次,合并去重 ────┐
▼
重排 → 生成
关键设计:原始 query 永远保留一路。改写器一旦跑偏(把"退货"改成"退款"),还有原查询兜底。
4. 代码:一次调用产出多路查询
import json
REWRITE = """把用户问题改写成便于检索的查询。输出 JSON:
{"queries": ["改写1", "改写2"], "keywords": ["关键实体"]}
规则:补全指代;口语换成文档常用术语;只改表达不改意图;无需改写时原样返回。"""
def rewrite(q: str, history: list[str]) -> list[str]:
ctx = "\n".join(history[-4:]) # 只带最近 4 轮,避免上下文过长
resp = llm.chat.completions.create(
model="gpt-4o-mini", temperature=0,
response_format={"type": "json_object"},
messages=[{"role": "system", "content": REWRITE},
{"role": "user", "content": f"对话历史:\n{ctx}\n\n当前问题:{q}"}],
)
parsed = json.loads(resp.choices[0].message.content)
return list(dict.fromkeys([q, *parsed.get("queries", [])]))[:4] # 原 query 必留,最多 4 路
def retrieve_multi(q: str, history: list[str], k: int = 20) -> list[dict]:
rankings = []
for sub_q in rewrite(q, history):
hits = store.query(query_embeddings=embed([sub_q]), n_results=k)
rankings.append(hits["ids"][0])
merged = rrf(rankings, k=60) # 多路结果用 RRF 合并,无需比较分数
return [doc_index[i] for i in merged[:20]]
5. 收益与风险的账
| 项目 | 说明 |
|---|---|
| 延迟 | +1 次 LLM(0.1~0.5 s),可用小模型或与检索并行预热压缩 |
| 成本 | 每请求多几百 token,可缓存同 query 的改写结果 |
| 主要风险 | 语义漂移:改写把范围放大或缩小("3 天"变"一周") |
| 兜底 | 保留原 query 一路;改写结果打日志,定期抽检 |
| 评估 | 单变量对比 recall@5,别和换模型一起做 |
6. 常见追问
Q:简单 query 也会被改坏吗? 会。所以提示词里必须写"无需改写时原样返回",并且原 query 一路不能删。
Q:用小模型还是大模型? 指代消解和术语归一,小模型加 few-shot 就够;子问题分解建议用大一点的模型,拆错了整条链路都白费。
Q:多轮历史怎么带? 滑窗 3~5 轮 + 摘要。全量历史会撑爆上下文,还会把早期话题污染到当前问题。
Q:怎么评估改写有没有用? 固定生成模型与重排,只对比"改写前/后"的召回率与最终答案正确率;两个指标都不涨就下线。
小结
查询改写是"用一次便宜的模型调用,换召回率提升"的典型交易。做它是为了让检索看见用户真正想问的东西——不是让 query 变得漂亮。