在检索增强生成(RAG)系统的落地实践中,离线微调与静态评测往往只能覆盖理想场景。当系统推向生产环境,面对长尾多轮对话、网络抖动、高并发以及数据安全红线时,单纯依赖“检索 + 拼接 + 提示词直通”的 Naive RAG 会频繁暴露问题。
本文梳理了工业级 RAG 在多轮对话改写(Query Rewriter)中的四级分层防御架构与三维评测闭环体系,旨在为高可用、低延迟的生产系统治理提供切实可行的工程参考。
一、为什么多轮对话需要 Query 改写?
在真实多轮会话中,用户的提问具有极强的指代性(如“它”、“这家机构”)与省略性(如“那外包员工呢?”、“违约金怎么算?”)。
1. 核心痛点:检索器的“无状态”特性
大语言模型(LLM)具备记忆并能联系上下文,但底层的检索器(无论是稠密向量检索还是 BM25 倒排索引)本质上是无状态(Stateless)的。检索器只根据当前轮传入的短文本计算相关度分数:
- 用户问:“那如果是外包员工呢?”
- 检索器仅能捕捉到“外包”、“员工”两个宽泛词汇,完全缺失了前文的主题“年假标准”;
- 召回池充斥着《外包准入管理制度》等无关切片,导致最终生成答非所问,产生检索漂移(Retrieval Drift)。
2. 常见认知误区
- 误区一:把漂移归咎于缓存脏数据。检索漂移的核心不在于是否命中缓存,而是由于残缺 Query 导致检索目标本身发生了偏离。
- 误区二:无脑把多轮对话一股脑丢给 LLM 做重写。大语言模型具有天然的过度创作倾向,面对完全独立的单轮问题(如“公积金如何提取”),LLM 往往会强行扩写同义词或套话,造成意图发散、增加首字延迟(TTFT),甚至破坏原有的关键词命中。
二、高可用落地:四级漏斗重写架构(Rewrite Funnel)
为在消除多轮指代歧义的同时,确保单轮独立问题实现 的原样透传率(Bypass Accuracy),工业界通常采用“规则前置、模型克制、后验兜底”的四级分层防御设计。
[ 用户当前输入 Query + 对话历史 ]
│
▼
┌───────────────────────────────────────────┐
│ 【第一级:代码层硬规则短路】(耗时: 0ms) │
│ · 对话历史为空 (History == ∅) │
│ · 问候、感谢、确认等通用闲聊 (正则匹配) │
└─────────────────────┬─────────────────────┘
│ 未命中(存在历史上下文)
▼
┌───────────────────────────────────────────┐
│ 【第二级:轻量二分类意图门控】(耗时: <15ms)│
│ · 判定是否依赖历史(代词、短省略问句) │
│ · 依赖概率 < 0.85 ──► 直接透传 (Bypass) │
└─────────────────────┬─────────────────────┘
│ 判定高度依赖历史
▼
┌───────────────────────────────────────────┐
│ 【第三级:微调专用小模型改写】(80~120ms) │
│ · 1.5B ~ 3B SLM / 注入大量负样本 SFT │
│ · 若无需补全,模型主动输出 <UNCHANGED> │
└─────────────────────┬─────────────────────┘
│ 产出改写候选句
▼
┌───────────────────────────────────────────┐
│ 【第四级:代码层后置校验网关】(耗时: <5ms) │
│ · 长度异常激增 (>3倍) ──► 强制回滚原句 │
│ · 实体/数字幻觉拦截 ──► 强制回滚原句 │
└─────────────────────┬─────────────────────┘
│
▼
[ 输出最终 Query 送入检索链路 ]
1. 第一级:代码层硬规则短路(Zero-Latency Bypass)
在进入任何模型推理前,以 0ms 开销剔除不需要改写的场景:
- 历史记录为空:冷启动首轮会话 100% 原始透传;
- 低信息量口语拦截:通过正则匹配“你好”、“好的”、“谢谢”、“再见”等模式,直接转入闲聊或直通,避免占用下游算力。
2. 第二级:轻量二分类意图门控(Dependency Gate)
避免直接调用生成式模型,前置一个极轻量分类头(如 DeBERTa-v3-small 或轻量分类模型):
- 职责:判定当前 Query 是否具备上下文依赖性(Context-Dependent);
- 高阈值保护:设置偏向透传的高置信度阈值(如 )。若无法确定是否依赖,宁可漏改,绝不误改,默认走原句透传。
3. 第三级:微调小模型定向改写(SFT Execution)
- 模型选型:采用 1.5B 至 3B 规模的轻量模型(SLM),兼顾推理速度与语言理解力;
- 负样本注入与克制训练:在监督微调(SFT)阶段,至少混入 40% 的自包含独立单轮问题。要求模型面对独立问题时必须输出特殊占位符
<UNCHANGED>,杜绝自作主张的修饰性扩写。
4. 第四级:代码层后验校验网关(Post-Gate Guard)
在最终送入检索前,设立确定性防御策略:
- 长度膨胀熔断:改写后的文本长度若超过原 Query 的 3 倍,判定为模型发散,强制回滚;
- 数字与专有实体守恒:改写句中出现的新数字或英文缩写代码,必须完全来自原 Query 或历史上下文。若检测到凭空捏造的数字或被篡改的专业术语,触发回滚。
三、如何评估 Query 改写器?三维评测指标矩阵
改写模块不能仅凭人工肉眼“看句子是否通顺”来定性,必须建立包含文本质量、检索下游表现与系统工程约束的量化评估矩阵。
| 评估维度 | 核心指标 | 工程定义与及格标准 | 监控目的 |
|---|---|---|---|
| 维度一:文本固有质量 (Intrinsic) | 指代消解率 (Resolution Rate) | 核心实体补全率 | 检验是否成功消解代词与省略。 |
| 意图忠实度 (Intent Preservation) | NLI 双向蕴含判定必须为 True | 确保问题核心诉求没有发生偏离。 | |
| 过度改写率 (Over-rewriting Rate) | 严格为 0% | 杜绝引入前文未提及的伪事实/幻觉。 | |
| 维度二:检索下游增益 (Extrinsic) | 检索增益差值 () | 评估改写对召回排序的实际贡献。 | |
| 检索退化率 (Degradation Rate) | 严格控制在 | 监控原句能搜准、改写后反而改丢的负向样本。 | |
| 维度三:系统工程约束 (System SLAs) | 端到端延迟 (P95 / P99 Latency) | 避免改写模块拖垮系统首字响应耗时。 | |
| 单轮幂等透传率 (Bypass Accuracy) | 独立自包含问题严禁画蛇添足。 | ||
| 格式崩溃率 (Format Failure Rate) | 0% | 拦截输出前缀废话、格式解析失败等异常。 |
四、Python 工程级实现参考
以下给出了符合生产级四级漏斗逻辑的模块化代码实现示例:
import re
from typing import List, Dict, Optional, Tuple
class QueryRewriterService:
def __init__(self, classifier_client=None, llm_client=None):
self.classifier_client = classifier_client
self.llm_client = llm_client
# 常见低信息量口语正则规则
chitchat_patterns = [
r"^(你好|您好|哈喽|hello|hi)[!!。??~]*$",
r"^(谢谢|感谢|多谢|thx|thanks)[!!。??~]*$",
r"^(好的|收到|明白|ok|okay)[!!。??~]*$",
r"^(再见|拜拜|bye)[!!。??~]*$"
]
self.chitchat_regex = re.compile("|".join(chitchat_patterns), re.IGNORECASE)
def rewrite(self, current_query: str, history: Optional[List[Dict[str, str]]] = None) -> Tuple[str, str]:
raw_query = current_query.strip()
# -------------------------------------------------------------
# 第 1 级:代码层硬规则短路 (Zero-Latency)
# -------------------------------------------------------------
if not history or len(history) == 0:
return raw_query, "BYPASS_EMPTY_HISTORY"
if self.chitchat_regex.match(raw_query):
return raw_query, "BYPASS_CHITCHAT"
# -------------------------------------------------------------
# 第 2 级:轻量二分类意图门控
# -------------------------------------------------------------
if not self._check_context_dependency(raw_query, history):
return raw_query, "BYPASS_CLASSIFIER_INDEPENDENT"
# -------------------------------------------------------------
# 第 3 级:微调小模型消解改写
# -------------------------------------------------------------
candidate = self._execute_rewrite_model(raw_query, history)
if candidate.strip() in ["<UNCHANGED>", raw_query]:
return raw_query, "BYPASS_MODEL_UNCHANGED"
# -------------------------------------------------------------
# 第 4 级:代码层后置校验与防漂移网关
# -------------------------------------------------------------
# 长度异常激增校验
if len(candidate) > len(raw_query) * 3 and len(raw_query) > 5:
return raw_query, "ROLLBACK_LENGTH_EXPLODED"
# 数字与专有符号守恒校验(防杜撰)
if not self._verify_entities_and_numbers(raw_query, candidate, history):
return raw_query, "ROLLBACK_ENTITY_HALLUCINATION"
return candidate, "SUCCESS_REWRITTEN"
def _check_context_dependency(self, query: str, history: List[Dict[str, str]]) -> bool:
pronouns = ["它", "其", "这个", "这种", "那个", "那些", "他", "她", "后者", "前者"]
has_pronoun = any(p in query for p in pronouns)
is_short_ellipsis = len(query) <= 6 and any(k in query for k in ["呢", "吗", "怎么"])
if has_pronoun or is_short_ellipsis:
return True
# 若接入专用分类模型,设置高置信度阈值判定
if self.classifier_client:
return self.classifier_client.predict_dependency(history[-2:], query) > 0.85
return False
def _execute_rewrite_model(self, query: str, history: List[Dict[str, str]]) -> str:
prompt = (
"你是一个Query改写引擎。请分析当前Query是否省略了上下文或包含代词:\n"
"1. 若不需要补全,直接输出:<UNCHANGED>\n"
"2. 若存在指代或省略,仅补全缺失实体,禁止添加额外无关修饰词。\n"
)
if self.llm_client:
return self.llm_client.generate(prompt=prompt, history=history, query=query)
return query
def _verify_entities_and_numbers(self, raw_query: str, rewritten: str, history: List[Dict[str, str]]) -> bool:
# 校验数字守恒:改写句中的数字必须来自原句或历史
raw_numbers = set(re.findall(r"\d+", raw_query))
rewritten_numbers = set(re.findall(r"\d+", rewritten))
history_text = " ".join([h.get("content", "") for h in history])
history_numbers = set(re.findall(r"\d+", history_text))
for num in rewritten_numbers:
if num not in raw_numbers and num not in history_numbers:
return False # 检测到杜撰数字
# 校验专有名词/英文代码不丢失
raw_codes = set(re.findall(r"[A-Za-z0-9_-]{2,}", raw_query))
for code in raw_codes:
if code not in rewritten:
return False # 关键代号被篡改或删减
return True
五、结语
在构建企业级 RAG 架构时,优秀的系统治理不依赖于不可控的提示词魔法,而取决于清晰的责任划分与多层防御的工程兜底:
- 改写模块的使命是“还原”而非“生成”:通过结构化规则、轻量分类与微调模型协同,把决定权从 LLM 单点收回;
- 评测体系不仅要看召回增益,更要严防检索退化:以 的幂等透传率作为底线,确保复杂多轮与简单单轮系统的长期稳定。