RAG 工程与检索系统设计

第15章 Query Rewrite:让用户问题变成可检索问题

作者 杨艺韬 · 13,932 字 · 发布于 · 更新于

用户敲进去的是 query,用户真正想要的是 intent。这一章讲怎么在两者之间架桥。

本章要点

  • 真实用户查询经常不适合直接检索——口语化、多意图、含指代、带业务黑话
  • Query Rewrite 五大模式:扩展、拆解、HyDE、routing、上下文消歧——各修一类弱点
  • HyDE(让 LLM 先猜一个假答案,用答案 embedding 去检索)是最有代表性的改写策略之一
  • Rewrite 增加延迟和成本——值不值得看召回质量瓶颈在不在 query 侧
  • 生产 Rewrite 的可观测指标:rewrite 前后 recall 对比、routing 误判率

15.1 为什么原始 query 不够好

生产里的真实用户 query 长这样:

  • "这玩意儿怎么退"(指代 "这玩意儿"——上下文里的某订单?)
  • "上次说的那个"(指代前文)
  • "SSO 怎么弄,要钱吗,啥版本"(复合问题,三个子问题)
  • "新版贵不贵"(模糊、比较词、没说和什么比)
  • "pro 和 ent 的 sso 区别"(缩写、小写,库里可能是"专业版""企业版")

这些 query 直接丢给 Embedding 检索会触发第 3 章的"找不到"或"找错"家族。问题不是检索系统弱,是输入给检索系统的 query 本身缺信息。Query Rewrite 的工作就是在检索之前把 query"整理"成更容易命中相关文档的形式。

Rewrite 的三种产出

根据策略不同,Rewrite 输出三种形式之一:

  • 改写后的单 query:替换术语、补全上下文、扩展同义词,仍然是一条 query
  • 多个子 query:拆解成几条独立 query 并行检索,结果融合
  • 增强文档:不改 query,生成一段"假的答案"作为额外的检索 anchor(HyDE)
flowchart TB
    classDef orig fill:#fed,stroke:#c60
    classDef rw fill:#def,stroke:#06c
    Q[原始 query]:::orig --> MODE{Rewrite 策略}
    MODE --> M1[同一 query 改写]:::rw
    MODE --> M2[多子 query 拆解]:::rw
    MODE --> M3[HyDE 生成假答案]:::rw
    MODE --> M4[Routing 选不同索引]:::rw
    MODE --> M5[上下文消歧补指代]:::rw
    M1 & M2 & M3 & M4 & M5 --> RETRIEVE[检索]

15.2 扩展:同义词、改写

最朴素的 Rewrite:给 query 加上同义词、变体、领域黑话到规范词的映射。

规则式扩展

维护一张同义词表:

SSO, 单点登录, SAML, 身份联合
企业版, ent, enterprise
私有化, on-prem, 本地部署
pro, 专业版, professional

查询 "企业版 SSO 怎么开" 扩展成:

(企业版 OR ent OR enterprise) (SSO OR 单点登录 OR SAML) 怎么开

BM25 直接用这种 OR 语法。Embedding 侧可以给扩展后的 query 生成多个向量并发召回再融合。

规则表从哪来

  • 业务初始:领域专家列 100-200 条核心术语映射
  • 日志挖掘:高频 miss 词 + 人工标注语义对应
  • LLM 辅助:用 LLM 对已有术语生成变体,人工复查

同义词表是持续工作——产品迭代一次,术语可能就加一批。

LLM 式扩展

直接让 LLM 改写:

Prompt: 给定用户查询,生成 3 个不同表达但意思相同的版本。

Query: pro 和 ent 的 sso 区别
Expansions:
1. 专业版和企业版在 SSO 功能上的区别
2. pro 版本和 enterprise 版本的单点登录差异
3. Professional 和 Enterprise 产品 SAML 集成对比

优点:灵活、覆盖面广;缺点:慢(一次 LLM 调用)、成本。

混合策略

生产常见:规则式做高频确定映射(缩写、产品别名),LLM 做长尾兜底。

15.3 拆解:把复合问题切成子问题

复合问题用户经常一股脑问:"SSO 怎么开,要钱吗,哪些版本支持"。单 query 检索得到的 chunk 可能只回答一个子问题。拆解成:

子 q1: SSO 怎么开
子 q2: SSO 要钱吗
子 q3: SSO 支持哪些版本

各自检索,结果合并送 LLM 生成。

LLM 拆解 prompt

把用户的复合问题拆成多个独立子问题。每个子问题自包含、可独立检索。

问题:SSO 怎么开,要钱吗,哪些版本支持
子问题(JSON):
[
  "SSO 如何开启配置",
  "SSO 功能的价格",
  "哪些产品版本支持 SSO"
]

多子 query 的融合

各子 query 召回的 chunk 汇总去重,RRF 融合或直接 union。注意每个子 query 最多带 top-10 candidates,union 不超过 top-30——否则上下文打包塞不下。

什么问题适合拆解

  • 用"并且"、"和"、"以及"连接多个实体的问题
  • 包含多个疑问词的问题("是什么,怎么用,多少钱")
  • 跨主题的问题("A 产品的 X 功能对比 B 产品的 Y 功能")

不适合拆的:简单事实问题、单一意图的模糊问题。拆错会增加噪声。

15.4 HyDE:让 LLM 先编一个假答案

HyDE(Hypothetical Document Embeddings,arXiv:2212.10496,Gao et al.,ACL 2023)是 RAG 领域最有影响力的 query 改写技巧之一。

核心思路

用户 query 和 chunk 的语义空间可能不完全对齐——query 短而问式,chunk 长而陈述式。直接 cosine 距离有"问题 vs 答案"的语义鸿沟。

HyDE 的解法:先让 LLM 根据 query 生成一段假设答案,用这段"假答案"的 embedding 去检索。

Query: 企业版 SSO 怎么开通
↓ (LLM 生成)
假答案: 企业版用户可以通过管理后台的"身份认证"选项配置 SSO,支持 SAML 2.0 和 OIDC。配置流程包括上传 IdP 元数据...
↓ (embedding)
用假答案向量去向量库检索

假答案不需要是"真的对"——只要在向量空间里落在真答案附近就行。原论文在多个数据集上给出了对照结果;你自己的收益要在自己的 gold set 上量,而且方向是可预期的:query 和答案的表达形态差得越远(问句 vs 陈述句、口语 vs 术语),HyDE 的收益越大。这也解释了它为什么对长尾和短 query 特别有效。

工程实现

  • 小模型够用:Claude Haiku / GPT-4o-mini / Qwen 7B 生成假答案足够
  • Prompt 极简:"Answer this question in 2-3 sentences: {query}"
  • 延迟:多一次 LLM 生成,且必须等它出完才能开始检索——这是串行加在链路最前面的,不像 rerank 还能和别的事并行。延迟量级取决于你选的小模型和生成长度,测出来后计入第 21 章 §21.2 的查询理解预算

HyDE 适合的场景

  • Query 太短(< 5 词)embedding 捕捉不够
  • 查询和答案的表达模式差距大(问句 vs 陈述)
  • 冷领域(embedding 训练时没见过的词汇)

不适合:

  • Query 已经很详细(本身就像答案)
  • 有明确关键词("订单 OD-12345"——走 BM25 更准)
  • 延迟敏感场景(对话初次请求 TTFT 敏感)

HyDE 的调优

HyDE 不是直接上就最优——下面几个细节对效果的影响不小,都值得单独做开关对照:

  • 多假答案:生成 1 个假答案 vs 3 个再取平均向量。多假答案平均能稳定召回(减少单次生成的方差),代价是生成成本随假答案个数线性增长,3 个就是约 3 倍。原论文的做法本身就是采样 N 个假答案取平均
  • 假答案长度:太短像 query 的改写,不够"答案";太长会引入 LLM 自己的先验,偏差大。合适的长度没有通用值,在自己的 gold set 上扫一遍再定
  • 假答案和原 query 的融合:纯用假答案向量 vs 混入原 query 向量(例如 0.7 * 假答案向量 + 0.3 * 原 query 向量,权重自己标定)——后者稳健,避免假答案跑偏。原论文的式 (8) 就是把原 query 向量当作第 N+1 个"假设",与 N 个假答案向量等权平均
  • 领域特化:通用 prompt 在专业领域效果一般。在 prompt 里加"用 {domain} 的语言回答"("用法律条文的语言"、"用医学术语")往往有帮助——原论文对科学、金融等数据集就换用了领域化的指令(如 "Please write a scientific paper passage to answer the question"),具体收益在自己的 gold set 上量

HyDE 的失败模式:当 LLM 对 query 本身没有先验知识(极新的产品、极冷门的概念),生成的假答案可能完全跑偏,反而把检索带歪。所以 HyDE 要和 BM25 并行(第 13 章 Hybrid)——BM25 命中关键词兜底,HyDE 扩展语义覆盖。单用 HyDE 风险大。

15.5 Routing:不同 query 走不同索引

有些项目的"正确"检索策略因 query 类型而异:

  • 结构化 ID 查询(订单号、合同号)→ 走精确匹配索引
  • 概念性问题 → 走 dense embedding
  • 多步推理问题 → 走 Agent 多轮检索(§1.9 的 Agentic RAG)
  • 历史数据查询 → 走带时间 filter 的冷索引

Query Router 的工作:识别 query 类型,路由到对应索引或 pipeline。

实现方式

  • 规则式:正则匹配关键 pattern(订单号格式、日期、产品代号)
  • 小模型分类:训一个 query classifier,输出 intent label
  • LLM 判断:直接问 LLM "这个 query 是结构化查询 / 概念问题 / 多步推理?"

生产常见:规则优先(零成本)+ LLM 兜底(处理规则没覆盖的)。

flowchart TB
    Q[query] --> ROUTER{Router}
    ROUTER -->|结构化 ID| EXACT[精确匹配]
    ROUTER -->|概念问题| DENSE[Dense retrieval]
    ROUTER -->|时效问题| RECENT[最近 30 天索引]
    ROUTER -->|多步推理| AGENT[Agent 多轮]
    ROUTER -->|未知| DEFAULT[默认 hybrid]
    EXACT & DENSE & RECENT & AGENT & DEFAULT --> OUT[top-k]

Routing 的错判成本

错判是 Routing 的主要风险——把"概念问题"误判成"结构化 ID",走错索引就查不到。缓解:

  • 多路 fallback:首选路径 miss 时自动降级到 default hybrid
  • 监控误判率:对 gold set 标注正确路由,定期回归
  • 人工 review:badcase 里溯源是否 routing 错

Query classifier 的训练

如果选择用小模型做 classifier,数据怎么来:

  • 规则标注起步:先用正则标 500-1000 条——订单号、日期、产品 SKU 都能规则匹配
  • 人工校验:随机抽 100 条看规则对不对,纠正错标
  • LLM 扩展:用 LLM 对 query log 采样打标,人工审
  • 训练小模型:用 distilbert / MiniLM 二分类或多分类,训 3-5 epoch 够用

classifier 的验收标准该由代价不对称性决定,而不是一个通用准确率数字:路由错了的代价是这条 query 走了次优路径(可恢复),所以宁可让 classifier 在没把握时弃权,把 query 交给 default hybrid 兜底,也不要它勉强给一个低置信度的分类。设一个置信度阈值,低于阈值一律走保底。覆盖率和准确率的具体取值,按你自己 badcase 的分布定。

Routing 的演化

生产系统的 routing 通常会演化:

  • V1:全部走 hybrid(不做 routing)
  • V2:加一两条明显规则(ID 走精确、其他走 hybrid)
  • V3:规则扩展到 10+ 类,覆盖大部分高频 query 形态
  • V4:训 classifier,把规则覆盖不到的那部分接上
  • V5:classifier + LLM 兜底,处理极端长尾

每个 V 都看业务 badcase 驱动——没看到对应问题时不加规则。过早复杂化会让维护成本压过收益。

15.6 上下文消歧:多轮对话的指代

对话场景的 query 常带指代——"那个怎么退"、"上面说的配置怎么做"。单独看这条 query 没办法检索——"那个" 是什么?

基本做法

把对话历史 + 当前 query 送 LLM,让它输出"独立版 query":

历史:
- User: SSO 多少钱
- Assistant: 企业增强包包含 SSO,每年 20000 元
- User: 那基础版呢

Rewrite prompt:
根据对话历史改写用户最后的问题,让它独立可检索。

Rewritten: 基础版产品是否包含 SSO 功能?价格是多少?

改写后的 query 就是独立的、可直接用于检索。

延迟优化

每轮都走 LLM 改写太慢。优化:

  • 首轮跳过:首轮 query 没历史,不需要改写
  • 指代检测:先用小模型或规则判断是否有指代词(它、那个、上面、前面),没有就不改写
  • 缓存:同一 session 相同上文的改写结果可以缓存

改写 vs 上下文拼接

另一种思路:不改 query、直接把历史拼到 embedding 输入前面。优点:一次 embedding 调用;缺点:历史越长信号越乱,embedding 模型对长输入处理差。

生产推荐:多轮对话用 LLM 改写,单轮问答不需要。

15.7 Rewrite 的延迟和成本

每种 Rewrite 都增加开销。开销分两档,中间隔着一个数量级——这个分档是结构性的,值得记住:

方法 LLM 调用次数 输出长度 相对开销
规则扩展 0 — 可忽略
Routing(规则) 0 — 可忽略
Routing(LLM) 1 极短(一个标签) 低
LLM 扩展 1 短 中
多轮消歧 1 短(一句改写后的 query) 中
HyDE 1(或 k) 中(100 token 量级的假答案) 中
LLM 拆解 1 中(多个子 query) 中

延迟主要由输出 token 数决定(生成是逐 token 的),成本 = (输入 token + 输出 token) × 当期单价。所以同样是"一次 Haiku 调用",只输出一个路由标签和输出一段假答案,延迟能差好几倍。把你选的模型、你的实际 prompt 长度代进去算,别用通用数字。

成本通常可以接受,延迟才是 RAG 在线链路的硬约束。策略:

  • 首选规则:能用规则的不用 LLM
  • 并行化:LLM 改写和某些不依赖它的阶段并行(比如预热 rerank 服务)
  • 缓存:相同 query 的改写结果缓存 10-60 分钟

15.8 什么时候做 Rewrite,什么时候不做

不做 Rewrite 的情况:

  • 召回质量瓶颈不在 query 侧(比如索引覆盖不够、embedding 弱)
  • 延迟极敏感(对话首屏 TTFT < 500ms)
  • 查询已经结构化(API 调用而非自然语言)

做 Rewrite 的情况:

  • 召回 gold set 上发现 "问题表达和文档表达差距大"
  • 用户 query 短(< 5 词)、指代多、黑话多
  • 多轮对话场景

判断标准:用归因分析(第 3 章)看"找不到"家族里是否有大比例是 query 表达问题——有则做,没有就别加这层复杂度。

15.9 Rewrite 的评估

评估 Rewrite 效果的两个指标:

  • rewrite 前后 recall@k 对比:同一批 query 分别跑"原始 query"和"改写 query",对比召回率。部署门槛该由延迟预算倒推——rewrite 吃掉的那几百毫秒本来可以用在更大的候选集或更强的 rerank 上,所以它的召回增益必须超过「把同样的延迟花在别处」能拿到的增益
  • 语义保真度:改写后的 query 是否扭曲了原意?随机抽 50 条让人工评审

反模式:改写过度

过度改写的典型表现:

  • Query "SSO 价格" 改成 "企业级产品的单点登录功能的定价策略和商业模式"——加了噪声词,召回漂移
  • 每次都改写——把简单问题搞复杂

Prompt 要明确"只在必要时改写、否则原样返回"。生产 Rewrite 服务应该监控"改写率"——如果 > 80% 的 query 都被改写,说明 prompt 有偏差。

15.10 多策略组合

生产 RAG 往往组合多种 rewrite 策略:

flowchart TB
    Q[原始 query] --> CTX{有历史上下文?}
    CTX -->|是| DISAMB[消歧改写]
    CTX -->|否| RAW[保持原样]
    DISAMB & RAW --> ROUTE{Router 分类}
    ROUTE -->|复合问题| SPLIT[拆解]
    ROUTE -->|短 query| HYDE[HyDE 假答案]
    ROUTE -->|直接| DIRECT[不加处理]
    SPLIT & HYDE & DIRECT --> EXPAND[同义词扩展]
    EXPAND --> RETRIEVE[并行召回]

这条 pipeline 的每一步都有条件跳过——不是所有 query 都走全程。工程要点:默认轻量 + 按需加重。

15.11 跨语言 Query Rewrite

多语言 RAG 的一个常见需求:用户用中文问,知识库是英文的(或反之)。两条路:

策略 A:Query 翻译

把 query 翻译成知识库语言,再检索。

  • 简单直接、一次 LLM 翻译 + 一次检索
  • 翻译质量决定检索上限
  • 失去原 query 的语言信号(对 BM25 是损失)

策略 B:多语言 Embedding

用多语言 embedding 模型(bge-m3、multilingual-e5、jina-v3)——把中英文都映射到同一向量空间,不需要翻译。

  • 无需 LLM 翻译、延迟低
  • 模型能力决定精度
  • 专业术语可能跨语言对不齐(中文"企业版" vs 英文 "Enterprise")

策略 C:双路召回

  • 路 1:中文 query → 中文 BM25 召回 → 如果有中文 chunk
  • 路 2:翻译成英文 query → 英文 BM25 / 英文 embedding 召回
  • 融合两路

覆盖最全、成本也最高。

生产选型

  • 知识库以某一语言为主(> 80%):策略 A
  • 中英各占一半:策略 B(多语言 embedding)
  • 业务强依赖多语言精度:策略 C

跨语言的其他细节

  • Chunk 语言 metadata:每 chunk 标 language=zh/en/ja,filter 下推
  • 答案语言跟随 query:用户中文问、答案中文;英文问、答案英文。prompt 加 "reply in the same language as the user's query"
  • dual-citation:引用的 chunk 和答案不同语言时,同时提供原语言引用("见《产品规格》英文原文第 3 节")

15.12 Rewrite 的失败模式与防线

Rewrite 不是"锦上添花"的中间件——它处在 query 进入检索的必经之路上,一旦出错,下游全错。这种级联失败是生产 Rewrite 的头号风险。必须在架构上先建防线,再谈效果。

级联失败的三种典型

  • 语义漂移:Rewrite 改成了另一个意思的 query,下游召回整个跑偏。例:原问 "退款要多久",改写成 "退款流程是什么"——召回的是流程文档而非时效回答
  • 关键词丢失:原 query 里的产品代号、订单号被 LLM "润色" 掉。例:"OD-12345 怎么取消"改成 "如何取消订单"——丢失了具体订单号,BM25 路完全失效
  • 过度具体化:LLM 基于自己的先验把模糊 query 改成某个具体假设。例:"新功能好用吗" 改成 "最新发布的 AI 助手功能是否易用"——但用户问的其实是另一个新功能

级联失败的隐蔽性在于:下游检索和生成看起来都正常,只是答非所问。监控 retrieval latency / answer length 都看不出来,只有对照 gold 或用户反馈才暴露。

flowchart LR
    classDef bad fill:#fdd,stroke:#c00
    classDef safe fill:#dfd,stroke:#080
    Q[原 query] --> RW[Rewrite]
    RW -->|漂移| BAD1[跑偏 query]:::bad
    BAD1 --> R1[错的召回] --> A1[错的答案]:::bad
    Q --> RAW[原 query 并行检索]:::safe
    RW --> RW2[rewrite query]
    RAW & RW2 --> MERGE[RRF 融合]
    MERGE --> SAFE[兜底正确]:::safe

防线 1:原 query 并行保留

最关键的一条防线:无论做了多少 rewrite,原 query 也一定走一路检索,和 rewrite 后的结果 RRF 融合。rewrite 跑偏时原 query 路兜底,rewrite 正确时两路互相加强。

这条防线的代价只是多一路检索——但换来的是"rewrite 再怎么错都不至于全链路崩溃"的稳健。多数团队只上 rewrite 不留原 query,出事一次就知道为什么要有这条。

防线 2:Rewrite 后的相似度校验

改写结果和原 query 做一次 cosine 相似度检查。相似度低于阈值(阈值依 embedding 模型而定,需要标定)说明改写偏离太远,丢弃改写、退回原 query。这是轻量的"sanity check",避免 LLM 一次极端跑偏污染整次请求。

不是所有场景都适用——HyDE 生成的假答案本来就不要求和原 query 高相似度。相似度阈值要按 rewrite 模式分别标定。

防线 3:Rewrite 的灰度与 shadow

和 ch13 融合调参同理,rewrite prompt 的每次修改也要走 shadow → A/B → 全量:

  • shadow:rewrite v2 对线上流量镜像跑,对比 v1 的召回集差异。差异率本身是最有用的信号——差异接近 0 说明改动没生效(先查是不是 prompt 没发布),差异过大说明改写行为变了性质,无论哪种都要人工抽查一批再往下走
  • A/B:小流量(1-3%)真实下发,看业务指标(答案满意度、对话延展轮数)
  • 回滚路径:rewrite 服务版本化,rollback 到上一版本是一次配置变更,不是代码回滚

Rewrite prompt 改动最容易被低估——它在链路最上游,一句话的改动就可能让下游整条召回换一批结果。prompt 要像代码一样走版本管理和灰度(ch16 §16.20 讲了 prompt template 的版本管理)。

防线 4:用户侧的可见性(可选)

一些 UI 产品显式给用户看 "我理解的问题是:XX"(改写后的 query),让用户可以 "No, I meant..." 纠正。这是最强的 safety net——但只适合能打断用户的产品形态(Chat UI 类),不适合企业 API 场景。

三种常见 badcase 的排查法

症状 可能原因 排查动作
答案完全答非所问 Rewrite 语义漂移 查 trace 看 rewrite 前后 query 对比
精确 ID 查不到但其他正常 Rewrite 去掉了关键词 对比原 query 和 rewrite 里实体列表
新 rewrite prompt 上线后整体 recall 掉 Prompt 改动引入偏差 回滚 rewrite 到上一版、观察是否恢复
某类 query 上 recall 掉、其他正常 Routing 误判 检查 classifier 对这类 query 的分类分布

把这张表写进 runbook——rewrite 出事时按表查比现场推理快得多。

15.13 Step-Back 与 Multi-Query:HyDE 之外的两条路径

§15.4 的 HyDE 把 query 转成一个假答案——用"答案视角"做检索。还有两条和 HyDE 互补的常用路径:Step-Back prompting 和 Multi-Query / RAG-Fusion。它们解决的问题和 HyDE 不同,适用场景也不重合,生产里经常同时存在。

Step-Back:向上抽象、而非向前生成

Step-Back(Zheng et al., Google DeepMind, ICLR 2024,arXiv:2310.06117)的核心观察:复杂推理 query 的关键信息常不在表面,而在其上位概念里。

例子:

原 query: 在 2018 年到 2020 年期间,埃隆·马斯克担任 SpaceX CEO 吗?

直接检索:命中的 chunk 是 Musk 传记的细节段落、噪声大

Step-Back 出的抽象问题:
"埃隆·马斯克在 SpaceX 担任过什么职位?"

用抽象问题检索:命中高层 fact 文档、再让 LLM 推理时间范围

Step-Back 让 LLM 先生成一个更一般化的问题,用它去检索背景知识,再用背景知识回答原问题。

flowchart LR
    classDef orig fill:#fed,stroke:#c60
    classDef abs fill:#def,stroke:#06c
    classDef ans fill:#efe,stroke:#080
    Q[原 query<br/>具体/复杂]:::orig --> LLM1[LLM 抽象化]
    LLM1 --> QA[抽象 query<br/>上位概念]:::abs
    QA --> RET[检索背景 facts]:::ans
    Q & RET --> LLM2[LLM 推理回答]

和 HyDE 的差异:

维度 HyDE Step-Back
方向 向下生成"假答案" 向上生成"抽象问题"
适合 事实型 query 推理型 / 时间范围 / 比较型
失败模式 假答案跑偏 抽象过度、丢失关键约束
成本 1 次 LLM 调用 1-2 次 LLM 调用

原论文验证的是 Step-Back 提示对复杂推理任务(STEM、知识问答、多跳推理)准确率的提升,不是直接给「RAG 召回提升几个点」——想知道它在你的检索链路里值多少,要自己按 query 类型分层量。可以预期的是收益高度集中在推理型和时间范围型 query 上,事实型 query 上基本无效甚至有害(抽象化会丢掉关键约束)。

Multi-Query / RAG-Fusion:广覆盖式改写

Multi-Query(LangChain 社区模式;在它基础上加 RRF 的变体叫 RAG-Fusion,见 Rackauckas 2024,arXiv:2402.03367)的思路:让 LLM 对同一 query 生成 k 个语义等价但表达不同的变体,每个变体独立检索,最后融合。

原 query: 企业版 SSO 怎么配置

生成 4 个变体:
1. 企业增强包的 SSO 配置步骤
2. Enterprise 版本 SAML 集成方法
3. 企业版身份联合认证的开启流程
4. 企业版单点登录怎么开通

每个变体跑一次 hybrid 检索、top-20 候选、RRF 融合、取最终 top-10

这招的价值来自 query 表达方差——单一 query 的召回可能被某个措辞带偏,多个变体汇总能降低"表达敏感"的 badcase。

但"最后怎么融合"在现成实现里并不统一,照抄默认值会和你以为的不是一回事,这三个默认值值得先去源码确认:

  • LangChain 的 MultiQueryRetriever 默认 prompt 要 3 个变体,融合方式是 unique union(按 Document 整体相等去重的并集——内容相同但 metadata 不同的两条会都留下),不是 RRF;而且 include_original 默认是 False——原 query 根本不参与检索,只有 LLM 改出来的三条在跑(langchain-core-1.2.26/libs/langchain/langchain_classic/retrievers/multi_query.py:33,60,114)。改写跑偏时没有原 query 兜底,这一条是生产上最该改掉的默认
  • LlamaIndex 的 QueryFusionRetriever 默认 num_queries=4(原 query + LLM 生成 3 条),但默认 mode 是 FUSION_MODES.SIMPLE——同一个节点在多路里出现时取最高分去重,同样不是 RRF;要 RRF 得显式传 mode="reciprocal_rerank"(llamaindex-0.14.24/llama-index-core/llama_index/core/retrievers/fusion_retriever.py:39,41,213,286)

所以"Multi-Query 就是多路 + RRF"这个说法只在你自己接了 RRF 时才成立。用现成组件时先把 include_original / mode 这两个开关摆对,再谈调参。

它的收益来源清楚,因此也能预判什么时候收益大:query 表达方差越大的场景收益越大——用户措辞五花八门的客服场景明显,措辞高度规范的内部工单场景就不明显。成本是确定的:k 个变体就是 k 次检索(可以并行)加 1 次 LLM 生成(串行在前)。

三种技术的选型

不是用一个就够——三者解决不同问题:

场景 推荐
短 / 模糊 / 语言表达差距大 HyDE
多跳 / 时间约束 / 需要背景知识 Step-Back
口语化 / 表达方差大 / 同义多种说法 Multi-Query
以上多种共存 组合用(但延迟翻倍)

生产常见组合:Query 分类器识别 query 类型 → 按类选技术:

def pick_rewrite(query_type):
    if query_type == "short_ambiguous":
        return "hyde"
    if query_type == "reasoning":
        return "step_back"
    if query_type == "colloquial":
        return "multi_query"
    return "none"

不要"全部都上"——延迟和成本会崩。

组合的工程代价

如果真的需要组合多种技术,延迟叠加要算清楚:

  • HyDE:1 次生成(中等长度输出)
  • Step-Back:最多 2 次 LLM(先抽象、再检索,第二次可能还要生成)
  • Multi-Query:1 次生成 + k 次并行检索

关键在于这些 LLM 调用都是串行加在链路最前面的——检索必须等它们全部结束才能开始,没有并行余地。三个全上就是三段串行生成叠加,对 TTFT 有要求的场景直接崩溃。按 query 类型路由比"全 pipeline 全开"值钱得多:让每条 query 最多只吃一次生成的开销。

这些技术的发展趋势

近几年的研究方向:

  • 自适应 rewrite:LLM 自己判断当前 query 是否需要 rewrite,需要哪种——不靠外部 classifier。延迟代价 1 次 LLM 决策
  • LLM 作检索主控(Self-RAG、CRAG):LLM 不只做 rewrite,还控制"要不要检索"、"检索结果是否够"——Agent 化的 RAG
  • 多轮 rewrite 协同:把 HyDE + Step-Back + Multi-Query 在同一个 prompt 里让 LLM 一次产出所有变体

2026 年这些路线仍在快速演化——生产项目不追最新,等 6-12 月社区验证再采用。但把 query 视为可修复的输入,而非固定输入是确定的方向。

15.14 领域专门的 Rewrite 模板

前 13 节的 rewrite 技术都是通用的——适合任何领域,但都不是任何领域的最优。真实企业 RAG 通常服务特定垂直(代码 / 法律 / 医疗 / 金融),这些领域的 query 有独特模式,通用 rewrite 帮助有限,领域专门模板的收益明显——具体多少个点要在自己的领域 gold set 上量,别照抄任何现成数字,因为它完全取决于你这个领域离通用语料有多远。

为什么通用 rewrite 不够

通用 LLM 对领域术语的理解往往浅:

  • 代码搜索:UserService.authenticate() 需要保留精确符号,不能"改写成自然语言"
  • 法律:条款编号 "第 35 条第 2 款" 必须精确匹配,同义改写有害
  • 医疗:疾病 ICD 编码、药品规范名 vs 商品名的映射
  • 金融:产品代号、监管条款、合规术语的精确对应
flowchart TB
    classDef bad fill:#fdd,stroke:#c00
    classDef good fill:#dfd,stroke:#080
    Q1[代码 query: UserService.login]:::good
    G1[通用 rewrite:<br/>用户服务登录方法]:::bad
    S1[领域 rewrite:<br/>保留符号 + 加包路径]:::good
    Q1 --> G1
    Q1 --> S1

通用 rewrite 可能把精确信号"改写"成模糊语义——对代码 / 法律场景是灾难。

代码 RAG 的 Rewrite 模板

代码 query 的典型模式和对应策略:

Query 模式 Rewrite 策略
函数名 getUserById 保留原名 + 加 camelCase 切分 getUserById OR "get user by id"
包路径 com.acme.User 精确匹配、不改写
错误消息 NullPointerException at line 42 保留 exception 类名 + 剥离行号
自然语言描述 "怎么验证密码" 扩展代码相关关键词 密码 OR password OR pwd OR auth
跨语言查找 "类似 Rust 的 ? 操作符在 Go 里" 拆解为 "Go 错误处理" + "Rust ? 操作符等价" 两子 query

关键工具:tree-sitter AST 识别(第 5 章)——从 query 里识别代码片段、符号、语言,单独处理。

法律 RAG 的 Rewrite 模板

法律 query 对精确性要求极高:

Query 模式 Rewrite 策略
"XXX 法第 N 条" 精确保留条款号、不改写
"侵权责任" 扩展同义 + 相关条款路径
"合同违约怎么处理" 识别法律实体 → 路由到合同法 + 相关判例
跨法域查询 "欧盟 vs 中国的数据保护" 拆解为两子 query、分法域检索
非专业用户口语 "我被骗了能告吗" LLM 判断法律领域 → 生成正式法律 query

重要约束:绝不能让 rewrite 改变条款号或引用——出现 "第 35 条" 必须原样保留。一个字错,答案全错。

医疗 RAG 的 Rewrite 模板

医疗有术语规范(ICD-10、SNOMED-CT、药品规范名):

Query 模式 Rewrite 策略
规范名 "高血压" 扩展 ICD 编码 I10 + 相关并发症
商品名 "施慧达" 映射到规范名 "苯磺酸左旋氨氯地平"
口语 "头疼怎么办" 识别 "头疼" → 规范化为 "头痛" + 列可能病因类别
复杂病情 "70岁老人高血压糖尿病" 拆解为年龄 filter + 两个疾病子 query
症状 query "胸闷气短" 关联 ICD 编码 + 紧急程度判断(可能是心脏相关 → 高优先)

关键:领域词典——ICD / SNOMED / 药品名映射表,rewrite 时显式替换,不靠 LLM 自由发挥。LLM 在医学术语上会幻觉,词典映射是确定性的。

金融 RAG 的 Rewrite 模板

金融领域有严格的产品代号和监管术语:

Query 模式 Rewrite 策略
产品代号 "沪深 300 ETF" 保留 + 扩展代号 "510300"
监管术语 "资管新规" 精确保留、扩展到具体文件号 "《关于规范金融机构资产管理业务的指导意见》"
流程问题 "开户要什么材料" 识别客户类型(个人/机构) → 路由对应 SOP
风险查询 "高风险产品" 按风险等级 filter、返回 R4-R5 产品
时效敏感 "当前利率" 加 freshness 权重、优先召回最新公告

金融和法律一样:编号、文件号、产品代号不能改写。同义词扩展只用在模糊表达上。

垂直领域 rewrite 的通用方法论

不管什么领域,好的领域 rewrite 遵循三条原则:

flowchart LR
    classDef step fill:#def,stroke:#06c
    P1[识别精确信号]:::step
    P2[保留并增强]:::step
    P3[对模糊部分 LLM 扩展]:::step
    P1 --> P2 --> P3
  1. 识别精确信号:符号、编号、规范名、产品代号——这些是精确匹配的信号
  2. 保留并增强:精确信号原样保留 + 加别名 / 编码 / 路径等关联精确信号
  3. 对模糊部分 LLM 扩展:只在自然语言描述部分用 LLM,精确信号区绝不动

这个顺序反过来会出问题——先 LLM 后识别,精确信号已经被改写丢失。

实现:hybrid pipeline

领域 rewrite 的典型实现是 规则 + 词典 + LLM 三层 pipeline:

def domain_rewrite(query, domain):
    # 1. 规则识别精确信号
    symbols = extract_symbols(query, domain)  # 符号/编号/代号
    natural_part = remove_symbols(query, symbols)  # 剩余自然语言

    # 2. 词典扩展精确信号
    expanded_symbols = []
    for sym in symbols:
        expanded_symbols += lookup_synonyms(sym, domain_dict[domain])

    # 3. LLM 对自然语言部分扩展
    if natural_part:
        natural_expanded = llm_expand(natural_part, domain_context=domain)
    else:
        natural_expanded = ""

    return {
        "symbols": symbols + expanded_symbols,  # 精确 OR 查询
        "natural": natural_expanded,             # 语义扩展
    }

检索端两部分分别走 BM25 精确匹配和 dense 检索,最后 RRF 融合。

词典维护的工程化

领域词典是领域 rewrite 的核心资产,不是一次性建完就完:

  • 初始:从开源词表(ICD / SNOMED / 法规库)拿底
  • 挖掘:从业务 query log 挖高频 OOV 词,人工审核加入
  • 时效:法规更新 / 新产品上线 / 药品新增——词典要同步
  • 版本化:词典变更要版本化,线上 rewrite 带 dict_version,便于回滚

词典的维护工作量不小——专人每月 2-4 小时,或 scheduled pipeline 自动更新。这是垂直 RAG "能用 → 好用" 的分水岭。

什么时候上领域 rewrite

不是所有项目都需要领域 rewrite:

  • 通用企业知识库:通用 rewrite 够用,领域细分不值
  • 单一垂直产品:值得、ROI 高
  • 多垂直并存:先上 query classifier 分流,再按领域走对应 rewrite

判断标准:通用 rewrite 在本领域的收益明显小于它在通用语料上的收益、且领域术语密度高 → 值得上。要比的是同一套 gold set 上「不改写」「通用 rewrite」「领域 rewrite」三者的差距,不是去对某个绝对分数线。

15.15 查询纠错与输入规范化

前面几节的 rewrite 默认 query 是规范的——只是需要语义改写、扩展、拆解。真实用户 query 经常是不规范的:错别字、漏字、大小写混乱、全半角错误、标点缺失、口语化缩写。不先做一轮纠错和规范化,所有下游 rewrite 都是在噪声上做优化。这节把这个经常被忽视的前置环节讲清楚。

真实用户 query 的噪声

flowchart TB
    classDef noise fill:#fdd,stroke:#c00
    classDef clean fill:#dfd,stroke:#080
    U[用户输入]
    U --> N1[错别字<br/>企业板 SSO]:::noise
    U --> N2[漏字 / 多字<br/>企业版的 SSO 的吗]:::noise
    U --> N3[大小写 / 全半角<br/>SSO怎么开]:::noise
    U --> N4[口语化<br/>这玩意儿咋整]:::noise
    U --> N5[拼音 / 字母混用<br/>qiyeban sso]:::noise
    N1 & N2 & N3 & N4 & N5 --> NORM[规范化]:::clean
    NORM --> CLEAN[企业版 SSO 怎么开通]:::clean

每种噪声都会让下游 rewrite / 检索偏差,而且是叠加的:一条 query 上同时出现错别字和全角字符,两个问题各自都能让 BM25 完全失配。要知道噪声在你这里造成多大损失,最直接的办法是拿一批真实 query 跑两遍——原样一遍、人工清洗后一遍,两者的 recall 差就是规范化的收益上限。

三层纠错 pipeline

flowchart LR
    classDef l1 fill:#dfd,stroke:#080
    classDef l2 fill:#fed,stroke:#c60
    classDef l3 fill:#fef,stroke:#808
    Q[raw query] --> L1[规则规范化]:::l1
    L1 --> L2[拼写检查]:::l2
    L2 --> L3[LLM 润色]:::l3
    L3 --> OUT[规范 query]

L1 规则规范化(必做、零延迟):

  • Unicode 标准化(NFKC)
  • 全角转半角
  • 繁简转换(按业务选方向)
  • 大小写统一(英文小写)
  • 去除多余空白和控制字符
  • 标点统一(中英文标点混用规范化)
import re
import unicodedata

def normalize_query(q):
    q = unicodedata.normalize('NFKC', q)  # NFKC 已把全角字母、数字、ASCII 标点转成半角
    q = q.strip().lower()
    q = re.sub(r'\s+', ' ', q)
    return q

L2 拼写检查(查表级开销、必做):

  • 英文:基于词典(pyspellchecker / SymSpell)、编辑距离 ≤ 2 的替换候选
  • 中文:错别字识别(如"企业板"→"企业版")、同音字纠错("升级"vs"深级")

常用工具:

  • 英文:hunspell、SymSpell
  • 中文:pycorrector、百度 API
  • 多语言:LanguageTool

L3 LLM 润色(可选,代价是一次 LLM 往返,具体多少毫秒按自己用的模型量):

L1+L2 处理不了的高层规范化:

  • 口语化变书面语("这玩意儿" → "这个功能")
  • 拼音变汉字(qiyeban → 企业版)
  • 补全省略("咋弄" → "如何配置")

只在 L1+L2 后仍明显不规范时触发——不是每个 query 都走。

中文的特殊挑战

中文纠错比英文难:

  • 错别字分类:形近字("企业版"→"企业板")、音近字("升级"→"深级")、字形错误(输入法问题)
  • 字粒度歧义:单字替换可能改变整个词的意思
  • 同音不同字:中文同音字多(升级 / 生机)、只能靠上下文判断
  • 专业术语:技术名词拼写错("单点登陆" 应是 "单点登录")——需要领域词典

解决:规则 + 领域词典 + 小模型(如 Macbert)联合。规则和词典能覆盖高频的固定错法(收益大、成本近零),小模型补长尾——先把规则和词典做扎实再上模型,顺序反了会发现模型在修一些本来一条规则就能修掉的东西。

拼音 / 字母混用

特殊场景:用户用拼音或英文缩写替代中文术语:

  • "qiyeban SSO" → "企业版 SSO"
  • "dakai SSO" → "打开 SSO"

处理:

  • 检测纯拼音段(^[a-z\s]+$ 且不含英文词)
  • 用拼音 → 汉字转换(pypinyin 反查)
  • 配合业务词典(常见产品名的拼音映射)

这是中文 RAG 的独特问题,英文场景不需要考虑。

纠错的安全边界

纠错不能改变 query 意图——这是红线:

  • 用户问 "企业板 SSO"——纠错改成 "企业版 SSO" 合理
  • 用户问 "OD-12345"——绝不能纠错(精确 ID)
  • 用户问 "升级 vs 降级"——不能把"降级"当错别字纠到"升级"

规则:

  • 识别精确信号保留:编号 / 代号 / 专有名词 / 含数字字母的 token 不纠
  • 高风险领域保守纠错:法律 / 医疗 / 金融场景,宁可不纠也不错纠
  • 纠错阈值设高:编辑距离 > 2 的不自动替换,需要用户确认

用户可见性

纠错是否让用户看到?两种策略:

  • 静默纠错:应用自动改,用户不感知——简单,但纠错出错时用户不知道为什么答错
  • 显式纠错:UI 显示 "你是否想问:企业版 SSO?"——用户确认后再 query。Google 风格

生产选择:

  • 消费级产品(聊天助手):静默纠错 + 如果置信度低,在答案开头加 "我理解你问的是 XX"
  • 企业级产品(合规、法律):显式纠错、用户确认
  • 专业用户(开发者、研究员):不纠错、尊重用户输入

纠错效果的评估

建 gold set:

  • 带错别字的 query → 正确 query 映射、100-500 对
  • 评估纠错后 query 和正确 query 的 BLEU / 精确匹配
  • 下游:纠错后的 query 在下游检索的 recall 变化

只看纠错本身正确率不够——纠错的价值要下游端到端看。

常见坑

  • 纠错太激进:把正确但不常见的术语当错别字纠——recall 下降
  • 纠错和查询扩展冲突:纠错改了 term,扩展基于错 term 生成同义词
  • 纠错不更新词典:新产品上线后,用户用新术语被当错别字
  • 多语言 query 纠错单语:中英混文只纠中文,英文部分保留,整体不协调
  • 纠错延迟太高:L3 LLM 每 query 都跑,每次一个 LLM 往返都串行累加到检索链路

纠错的监控

生产指标:

  • correction_rate:被纠错的 query 比例——健康区间取决于你的用户群和输入方式(移动端手输和桌面端差很多),用自己前几周的数据划
  • correction_recall_lift:纠错后 recall 相对原 query 的提升
  • over_correction_rate:纠错把正确 query 改错的比例(用 shadow 对比估算)
  • correction_latency:纠错本身的延迟。L1/L2 应该是可忽略的量级(纯规则和查表),一旦这个指标明显上去,多半是 L3 的 LLM 被触发得太频繁了——回去查触发条件

纠错 vs 其他 rewrite 的顺序

纠错应该是 rewrite pipeline 的第一步:

raw query → 纠错规范化 → 语义扩展 → 拆解 / HyDE → 检索

顺序颠倒的话——语义扩展在错别字上跑,同义词扩展也错。纠错是清洗,其他 rewrite 是加工——先洗后加工。

纠错的投入产出

对大多数 RAG:

  • L1 规则:必做。零成本、零延迟,修的是全半角、大小写、多余空白这类纯机械问题
  • L2 拼写:投入低(有现成工具和词典),修的是高频错别字
  • L3 LLM:投入高(延迟 + 成本),只在 L1+L2 都没救时触发

ROI 顺序是 L1 > L2 > L3,这个顺序不需要实测就能确定——因为三者的成本差了几个数量级,而 L1 修的又恰恰是最容易出现、最容易修的一类噪声。别跳过 L1 直接上 L3:那等于用 LLM 去做一次 strip()。

15.16 Rewrite 的调试:当改写出问题时怎么查

§15.12 讲了 rewrite 的防线——事前预防。但实际项目里 rewrite 仍会出问题——答案错了,用户投诉了,怎么快速定位是哪里出错?这节给一套 rewrite 问题的调试手册——让 on-call 同学从"一头雾水"到"半小时找到根因"。

调试的典型症状

flowchart TB
    classDef s fill:#fed,stroke:#c60
    S[Rewrite 问题的症状]
    S --> S1[答案错]:::s
    S --> S2[召回不对]:::s
    S --> S3[延迟变高]:::s
    S --> S4[某类 query 质量骤降]:::s
    S --> S5[Rewrite 输出不稳定]:::s

每个症状指向不同根因——先识别症状,再对症下药。

Trace 字段的设计

Rewrite 调试第一前提是有完整 trace——每次 rewrite 记录:

{
  "request_id": "req-abc",
  "original_query": "企业版 SSO 怎么配",
  "rewrite_strategy": "hyde",
  "rewrite_input": {
    "strategy_params": {"temperature": 0.3, "prompt_version": "v3"},
    "model": "claude-haiku-4-5"
  },
  "rewrite_output": "配置企业版的 SSO,首先进入管理后台...",
  "rewrite_duration_ms": 320,
  "rewrite_cost_usd": 0.000645,
  "rewrite_token_usage": {"input": 45, "output": 120},
  "post_rewrite_similarity": 0.78,
  "downstream_retrieval_ids": ["doc-1", "doc-3", "doc-5"]
}

post_rewrite_similarity 是原 query 和改写后 query 的相似度;rewrite_cost_usd 按 claude-haiku-4-5 当期单价(输入 $1、输出 $5 每百万 token,官方定价页 2026-09-25 核对)算:45 × 1/10⁶ + 120 × 5/10⁶ = 0.000645,单价变了按同一算式重算。

关键字段:strategy 版本 + 模型 + 输入输出 + 延迟成本 + 下游影响。缺字段事后查抓瞎。

五步调试流程

步骤 1:确认 rewrite 真的出问题了

先排除 "rewrite 是无辜的" —— 关 rewrite 跑同一 query:

def debug_rewrite(query, user_ctx):
    # 对照组:不 rewrite
    baseline = rag.query(query, user_ctx, skip_rewrite=True)
    # 实验组:正常 rewrite
    current = rag.query(query, user_ctx)
    
    print("Baseline recall:", recall(baseline))
    print("Current recall:", recall(current))
    print("Diff:", current.retrieved - baseline.retrieved)

如果两者差不多——rewrite 不是元凶,查下游(rerank / 生成)。差距大——rewrite 有嫌疑,继续查。

步骤 2:对比原 query 和 rewrite 后的 query

拿出 trace 里的 rewrite_output,和 original_query 对比:

  • 语义有没漂移:改写后意思变了吗
  • 关键实体是否保留:订单号 / 产品代号还在吗
  • 长度是否合理:原 20 字改成 200 字是过度扩展

步骤 3:手动跑 rewrite prompt

把 prompt 和 query 在 playground 里手动跑几次:

  • 结果稳定吗?(多次跑同 query,输出应相似)
  • prompt 有没有被 query 劫持?(prompt injection)
  • 温度参数是不是太高?

步骤 4:对比新老版本 rewrite

如果最近改过 rewrite prompt / model,拿新老版本在 gold set 上对比:

for case in gold_set:
    old_rewrite = old_rewriter.rewrite(case.query)
    new_rewrite = new_rewriter.rewrite(case.query)
    old_recall = recall_with_rewrite(case, old_rewrite)
    new_recall = recall_with_rewrite(case, new_rewrite)
    if new_recall < old_recall:
        print(f"Regression: {case.query}")
        print(f"  Old: {old_rewrite}")
        print(f"  New: {new_rewrite}")

步骤 5:归因到具体 regression 原因

根据前四步定位到具体问题:

  • Prompt 模板改了 → 回滚
  • Classifier 误判 query 类型 → 看 classifier trace
  • LLM 升级后行为变 → 固定版本 / 调 prompt
  • 温度参数变了 → 降 temperature
  • 训练数据污染 → 检查 gold set

常见根因分类

按概率排序:

根因 频率 特征症状
Prompt 改动 最高 最近改过 prompt、所有 query 都变
LLM 静默升级 高 Claude / GPT 版本变了、rewrite 输出风格变
Classifier 误判 中 某类 query 走错策略(如事实 query 走了 HyDE)
温度过高 中 同一 query 多次 rewrite 不稳
数据污染 中 Gold set 或训练数据混入坏例子
上下文消歧丢了 低 多轮对话指代错、但单轮没事

绝大多数 rewrite 问题都出在前三项——改 prompt 前先按这个顺序审一遍,别一上来就调 prompt。

A/B 对比工具

调试时一个实用工具——并行跑多个 rewrite 版本看差异:

def ab_rewrites(query, strategies):
    results = {}
    for strategy in strategies:
        r = rewrite(query, strategy=strategy)
        recall = retrieve_and_evaluate(r)
        results[strategy.name] = {
            "rewrite": r,
            "recall": recall,
            "latency": ...
        }
    return results

# 使用
ab_rewrites("新版 SSO 咋开", [
    no_rewrite,
    expansion_rewrite,
    hyde_rewrite,
    decomposition_rewrite,
])

立即看出哪个策略好——比纸面推理快。

Rewrite 的单元测试

调试完后,把 regression case 加进单元测试:

def test_rewrite_preserves_order_id():
    query = "订单 OD-12345 状态"
    rewritten = rewriter.rewrite(query)
    assert "OD-12345" in rewritten  # 订单号不能丢

def test_rewrite_keeps_comparison():
    query = "A 和 B 的区别"
    rewritten = rewriter.rewrite(query)
    assert "A" in rewritten and "B" in rewritten

def test_rewrite_short_query():
    query = "SSO"
    rewritten = rewriter.rewrite(query)
    assert len(rewritten) > len(query)  # 该扩展
    assert len(rewritten) < 500  # 不该过度

这些测试跑在 CI 里,下次同类 regression 自动挡下。

调试的自动化

长期看,自动化 rewrite regression 检测:

# 每小时跑一次、对 100 条 canary query
@scheduled(every="1h")
async def rewrite_canary():
    for query in canary_rewrites:
        current = rewrite(query.text)
        expected = query.expected_pattern
        if not matches(current, expected):
            alert(f"Rewrite regression: {query.text}\nGot: {current}\nExpected pattern: {expected}")

Canary 模式提前发现 regression——不等用户投诉。

Rewrite 问题的响应时间

不同严重程度的响应:

  • Regression 影响 < 1% query:日级修复
  • Regression 影响 5%+ query:立即回滚,之后修
  • Regression 影响特定业务(如某大客户):立即修 + 通知业务方
  • Rewrite 挂 / 超时:触发熔断,降级到无 rewrite

响应时间和业务影响成正比——不能一视同仁。

和评估的联动

§20.18 评估驱动优化、§15.9 rewrite 评估——两者结合:

  • 评估看到 recall 降 → 怀疑 rewrite → 调用本节调试流程
  • 调试定位到具体原因 → 回到评估验证修复效果
  • 修复进单元测试 / canary,防止再发

这是一个完整回路——没这个回路,rewrite 问题反复发生。

Rewrite debug 的工具链

生产推荐的工具栈:

  • Prompt playground:手动试 prompt、LLM API 的 playground 或自建
  • Rewrite trace explorer:Grafana / Datadog 上按 request_id 查完整 trace
  • Diff viewer:对比新老 rewrite 的差异(git diff 风格)
  • Canary dashboard:实时显示 canary rewrites 的健康度

投入几个人周搭起来。它的价值不在于某个倍数,而在于把「rewrite 好像有问题」变成「第 3 步的实体识别把 A 识别成了 B」——能定位到具体环节,修复才谈得上。

不要调试"好像不对"

一个反模式:基于直觉调 rewrite——"感觉改写得不好,改个 prompt 试试"。不行:

  • 要量化问题(用 gold set 或真实流量)
  • 要对比新老(不是只看当前)
  • 要假设 → 实验 → 验证(不是拍脑袋)

没有 disciplined debug,rewrite 会越改越糟——每次改看起来修了一个问题,实际引入两个新问题。

调试成本的长期投资

完整 debug 工具链的投入:

  • Trace 字段:1-2 人周
  • Canary + 自动化:1-2 人周
  • 单元测试:持续维护,每次 regression 加一条
  • Playground 和 dashboard:1 人周

总计 1 人月起步——但每次 regression 事故都能省几天人工——回本 2-3 次 regression 就划算。

15.17 训练专用小 rewriter:从 LLM 蒸馏到产线部署

前面讲的 rewrite 都假设用通用 LLM 做改写——每条请求多一次生成调用。这笔开销随 QPS 线性增长,算式是 日请求数 × (输入 + 输出 token) × 当期单价;延迟则是串行加在链路最前面的(§15.13)。规模一上来,这两笔都会变成显眼的数字——把你自己的 QPS 和当期单价代进去算一次,就知道值不值得做蒸馏。成熟的做法是用通用 LLM 产出的 rewrite 数据蒸馏一个小模型,本质是用 LLM 的泛化能力换小模型的推理效率(和 §14.16 的 rerank 蒸馏是一对)。

flowchart LR
    classDef llm fill:#fed,stroke:#c60
    classDef small fill:#dfd,stroke:#080
    L[通用 LLM rewrite]:::llm
    L --> L1[延迟 高<br/>一次生成调用]
    L --> L2[成本 按 token 计费]
    L --> L3[灵活但贵]
    S[蒸馏小模型]:::small
    S --> S1[延迟 低<br/>小模型一次前向]
    S --> S2[成本 摊薄的机器时间]
    S --> S3[固定但快]

但不是所有 rewrite 都值得蒸馏,先看任务类型:

任务 适合蒸馏 原因
同义词扩展 ✓ 模式固定、小模型能学
Query 拆解 ✓ 结构明确
上下文消歧(指代解析) ✓ 短 input、短 output
HyDE(生成假答案) 勉强 需要领域知识、小模型弱
Step-Back(抽象) 不适合 需要推理能力、小模型差
Routing 分类 ✓ 分类任务、小模型擅长

规律很清楚:结构化任务适合蒸馏,需要推理的不适合。

数据、模型与训练

训练数据靠通用 LLM 批量生成:

def build_training_data():
    queries = load_query_log(sample_size=50000)
    training = []
    for q in queries:
        # 用 Claude / GPT 做 rewrite
        rewritten = llm.rewrite(q, task="expansion")
        training.append({"input": q, "output": rewritten})
    return training

样本量在几万到十几万条这个量级,成本 = 样本数 × 每条的 teacher token 费,一次投入长期用。但 LLM 产出的数据不是百分百可靠,数据质量决定蒸馏的上限,四道过滤不能省:同一 input 去重、丢掉过短或过长的 output、结构化输出做格式校验、再随机抽 100 条人工 review。

学生模型不是越大越好,rewrite 任务上通常 1–3B 参数量级就够用,再往上加是否值得要自己测。中文场景可选 Qwen2.5-1.5B / Qwen2.5-0.5B(中文强,decoder-only)、Gemma-2B(通用快,decoder-only)、mT5-small(encoder-decoder,天生适配"输入一句话、输出一句话"这种 seq2seq 形态),或基于 LLaMA / Qwen 自行微调。选哪一类会直接决定下面代码里用哪个 auto class。

# 1. 准备数据
train_data = load_distillation_data()  # 5-10 万 (input, output) 对
train_data, val_data = split(train_data, 0.9)

# 2. 加载基础模型
# 注意 auto class 要和模型架构对得上:Qwen 是 decoder-only,只登记在 transformers
# models/auto/modeling_auto.py 的 MODEL_FOR_CAUSAL_LM 映射表里,
# 不在 MODEL_FOR_SEQ_TO_SEQ_CAUSAL_LM 那张——
# 用 AutoModelForSeq2SeqLM 加载会直接报 "Unrecognized configuration class"。
# 选 mT5 这类 encoder-decoder 时才用 AutoModelForSeq2SeqLM。
base_model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-1.5B")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-1.5B")

# 3. 微调
trainer = Trainer(
    model=base_model,
    train_dataset=train_data,
    eval_dataset=val_data,
    args=TrainingArguments(
        num_train_epochs=3,
        per_device_train_batch_size=16,
        learning_rate=1e-5,
    ),
)
trainer.train()

# 4. 保存
trainer.save_model("rewriter-qwen2.5-1.5b")

训练本身是 1-2 张卡跑几小时到一天的量级,费用 = 卡时单价 × 时长——相对上一步的数据生成通常是小头。

验收、部署与持续迭代

蒸馏完要和原 LLM 正面比,三类指标同时达标才算值得:输出相似度(BLEU 或语义相似度,阈值自己定,先看 teacher 自己两次生成之间的相似度是多少,学生达到那个水平就够了)、下游 recall 不明显退化(这是真正的验收标准——改写措辞不同但召回一样好,就是成功)、以及延迟确实降下来了(否则蒸馏没有意义)。

def compare_rewriters(test_queries):
    for q in test_queries:
        llm_result = llm.rewrite(q)
        distilled_result = distilled_model.rewrite(q)

        # 用 LLM-as-judge 打分
        judge_score = llm_judge.compare(q, llm_result, distilled_result)
        print(f"Query: {q}, Judge: {judge_score}")

部署上小模型跑 CPU 通常就够,ONNX + CPU 最省;要更低延迟可以上 GPU;高并发场景用 vLLM / TGI 拿 batch 效率。三种部署方式的延迟自己压一遍——rewrite 是串行在链路最前面的,这里省下的每一毫秒都直接落到 TTFT 上。

上线不等于放手不管。日常让蒸馏模型接大部分请求,同时抽 1–5% 的请求两边都跑,比对差异,差异超阈值就告警(多半是模型漂移),蒸馏模型失败时自动回落到 LLM。这套机制让蒸馏的风险始终可控,而不是「换掉 LLM 就不管了」。

它也不是一次性工程:新的 query 类型出现、LLM 升级带来更好的教师输出、prompt 改动,都需要重新生成数据再训一次。典型节奏是每 3–6 个月重蒸馏一次。

什么时候不该蒸馏

这笔账的算式和 §14.16 的 rerank 蒸馏是同一个形状:一次性投入(生成蒸馏数据的 LLM token 费 + 训练卡时 + 1-2 人月工程 + 之后每几个月一人周的维护)对上持续节省(每请求省下的 LLM 调用费 × 日请求数,加上省下的那段串行延迟)。回收期 = 一次性 ÷ 每日节省。QPS 越高回收越快;QPS 很低时这个投入不划算,继续用通用 LLM 就好。

但四种情况不该做:QPS 很低(ROI 不划算,直接用 LLM)、rewrite 任务类型太杂(小模型学不全)、业务变化快到要每月重蒸馏(维护成本吃掉收益)、团队没有 ML 能力(蒸馏是模型工程,不是配置一下 LLM API)。

放到更大的选择空间里,四条路是按规模递进的:

方式 投入 收益 适合
Prompt engineering 低 小 早期
Fine-tune 通用 LLM 中 中 领域特化
蒸馏小模型 中 大(规模大时) 高 QPS
从零训练 极高 超大 独家业务

而把 §14.16 的 rerank 蒸馏、本节的 rewriter 蒸馏、再加上 embedding 的领域微调放在一起,RAG 的「专用小模型」策略就成型了——rerank 用微调或蒸馏过的 bge-reranker,rewriter 用蒸馏的 Qwen2.5-1.5B,embedding 用领域数据训过的 bge-m3,通用大模型只留给最后的 generation。结果是绝大部分计算跑在小模型上,只有最后一步用大 LLM,成本能明显下降(降多少按各环节的请求量和单价代入第 21 章的成本算式自己算);质量损失有多大,是这套方案里唯一必须自己实测的数字(§14.16 讲蒸馏时是同一句话)。规模上来的 RAG 项目基本都会走到这一步。

15.18 跨专栏关联:Rewrite 和 Agent 的边界

Query Rewrite 和 Agent 多轮检索(§1.9 的 Agentic RAG)在边界上有重叠——后者可以看作"在线交互式 rewrite"。

  • Rewrite:一次 LLM 调用 把 query 变好,继续原流水线
  • Agent:多次 LLM + 检索 循环,Agent 自己决定下一步

边界选择:Rewrite 是 RAG 的线性扩展,Agent 是 RAG 的迭代包装。《LangGraph 设计与实现》讨论的 Agent 编排和 RAG 的 rewrite 在统一的"智能体访问外部知识"框架里。Agentic RAG 的形态见 §1.9。

15.19 本章小结

  1. 真实用户 query 经常不适合直接检索——口语化、多意图、指代、黑话
  2. 五大 Rewrite 模式:扩展 / 拆解 / HyDE / Routing / 上下文消歧
  3. HyDE 是 RAG 领域最有代表性的改写技巧之一——让 LLM 先生成假答案,用假答案 embedding 去检索
  4. 成本和延迟有代价——规则优先 + LLM 兜底是生产原则
  5. Rewrite 不是必做 ——只在归因显示 query 侧是瓶颈时做
  6. 评估 Rewrite 看rewrite 前后 recall 对比——涨幅要盖过它多花的延迟和成本才值得
  7. 过度改写是反模式——要监控改写率

下一章讨论 Context Packing——把 rerank 后的 top-k chunk 装进有限的 LLM 上下文窗口,保留最高证据密度。