RAG 多轮对话的查询重写上下文丢失不是因为模型笨一、第一问推荐几个 Go 的微服务框架第二问它和 gRPC 怎么配合多轮对话场景下RAG 检索经常在第二问失效。用户问它和 gRPC 怎么配合。向量检索用它和 gRPC 怎么配合做查询。结果召回了一堆它的泛化语义。问题不在模型或检索而在输入。它这个指代没有明确的实体。人类能通过上文推断出它指的是前面提到的某个框架。但检索系统没有这个智能。查询重写解决的就是这个问题。把它和 gRPC 怎么配合重写为Go-Micro 框架和 gRPC 怎么配合。重写后的查询才能精准检索。这是 RAG 系统最容易被忽略的短板。很多团队在优化 RAG 时第一反应是换更强的 Embedding 模型、调高 Top-K、做重排序Rerank。但根因往往不在检索质量本身而在输入端的信号衰减。原问题Go-Micro 框架和 gRPC 怎么配合的 Embedding 向量和它和 gRPC 怎么配合的 Embedding 向量在余弦相似度上可能只有 0.3-0.4。你花再多精力优化向量检索也弥补不了查询端的 60% 信号损失。我在实际项目中遇到过的一个案例一个企业内部知识库问答系统多轮对话的准确率从首轮的 78% 骤降到第三轮的 31%。排查后发现第三轮的用户查询里充满了这个方案、那种情况等指代词。加入查询重写后第三轮准确率回升到 62%——虽然仍比首轮低但已经是从不可用到可接受的跨越。二、多轮对话的查询重写机制查询重写有三类策略指代消解将它、这个替换为具体实体。省略补全用户省去的上下文补回来。意图融合将多轮对话的意图整合进查询。flowchart TB A[用户当前消息: 它和 gRPC 怎么配合] -- B[获取对话历史] B -- C[上轮: 推荐几个 Go 微服务框架] C -- D[助手: Go-Micro、Kratos、Go-Zero 都不错] A -- E[查询重写器] D -- E E -- F{分析当前消息} F -- G[检测到指代词: 它] G -- H[回溯历史解析指代] H -- I[候选实体: Go-Micro/Kratos/Go-Zero] I -- J[选择最可能的实体: Go-Micro] J -- K[生成重写查询] K -- L[Go-Micro 框架和 gRPC 怎么配合] L -- M[向量检索/混合检索] M -- N[返回相关文档]三种重写策略的适用场景各有侧重。指代消解占查询重写场景的 60-70%是最高频的需求。省略补全占 20-30%典型如用户问还有哪些、那另外的呢。意图融合占 10% 左右多见于用户跨越 3 轮以上对话、意图在逐步深入或切换的情况。在实践中我倾向于把查询重写分层处理简单指代用规则、复杂省略用 LLM。规则重写正则 实体提取延迟几乎为零覆盖了大部分场景。LLM 重写虽然多花 0.5-2 秒但处理规则搞不定的情况——比如那你觉得哪个更好中的哪个指向历史中提到的 3 个框架。三、查询重写的 Python 实现下面的代码实现了一个双层查询重写器。第一层基于规则实体提取 指代消解 省略补全。第二层基于 LLM处理规则无法覆盖的模糊指代和意图融合。 query_rewriter.py - 多轮对话查询重写 import re import logging from typing import List, Tuple, Optional from dataclasses import dataclass logger logging.getLogger(__name__) dataclass class DialogueTurn: 对话轮次 role: str # user / assistant content: str entities: List[str] # 提取的实体 class QueryRewriter: 多轮对话查询重写器 # 常见指代词 PRONOUNS [ 它, 他, 她, 这个, 那个, 这些, 那些, 其, 该, 此, 这, 那, it, this, that, these, those, ] # 常见省略模式 ELLIPSIS_PATTERNS [ r^(还有|另外|此外|并且|而且|还|也)[,]?\s*, r^(那|那么)[,]?\s*, ] def __init__(self, llm_clientNone): llm_client: LLM 客户端用于复杂重写 如果为 None使用规则重写 self.llm llm_client self.history: List[DialogueTurn] [] def add_turn(self, role: str, content: str): 添加一轮对话 entities self._extract_entities(content) self.history.append(DialogueTurn( rolerole, contentcontent, entitiesentities, )) def rewrite(self, current_query: str) - str: 重写当前查询 规则重写 LLM 重写如果可用 # 第一层规则重写 rewritten self._rule_based_rewrite(current_query) # 第二层LLM 精密重写 if self.llm and self._needs_llm_rewrite(current_query): rewritten self._llm_rewrite(rewritten) if rewritten ! current_query: logger.info( f查询重写: {current_query} → {rewritten} ) return rewritten def _rule_based_rewrite(self, query: str) - str: 基于规则的查询重写 rewritten query # 1. 指代消解 rewritten self._resolve_pronouns(rewritten) # 2. 省略补全 rewritten self._complete_ellipsis(rewritten) return rewritten def _resolve_pronouns(self, query: str) - str: 指代消解将指代词替换为最近实体 has_pronoun False for pronoun in self.PRONOUNS: if pronoun in query: has_pronoun True break if not has_pronoun: return query # 从最近的历史中找实体 for turn in reversed(self.history): if turn.entities: # 找最近的实体按出现顺序 entity turn.entities[-1] # 最近提到的 # 替换指代词 for pronoun in self.PRONOUNS: if pronoun in query: # 处理它和 gRPC这类情况 if f{pronoun}和 in query: query query.replace( f{pronoun}和, f{entity}和 ) elif f{pronoun}的 in query: query query.replace( f{pronoun}的, f{entity}的 ) else: # 简单替换可能产生不自然的结果 query query.replace(pronoun, entity) return query return query def _complete_ellipsis(self, query: str) - str: 省略补全 for pattern in self.ELLIPSIS_PATTERNS: if re.match(pattern, query): # 找到上次用户的查询主题 for turn in reversed(self.history): if turn.role user: # 连接两个查询 core re.sub(pattern, , query) return f{turn.content} {core} return query def _extract_entities(self, text: str) - List[str]: 提取关键实体简化实现 # 匹配引号中的内容 quoted re.findall(r[「]([^」])[」], text) # 匹配大写开头的专有名词英文 proper_nouns re.findall( r\b([A-Z][a-zA-Z](?:-[A-Z][a-zA-Z])*)\b, text ) # 匹配中文专名简化 cn_patterns [ r(Go-Micro|Kratos|Go-Zero|Kubernetes|Docker|Redis), r(微服务|分布式|消息队列|API网关)框架, ] cn_entities [] for pattern in cn_patterns: cn_entities.extend(re.findall(pattern, text)) entities [] entities.extend(quoted) entities.extend(proper_nouns) entities.extend(cn_entities) # 去重保持顺序 seen set() result [] for e in entities: if e not in seen: seen.add(e) result.append(e) return result def _needs_llm_rewrite(self, query: str) - bool: 判断是否需要 LLM 重写 # 规则解决不了的情况 # 1. 隐式指代那速度怎么样不知道哪个的速度 # 2. 跨多轮的意图融合 indirect any(p in query for p in [ 那, 它的, 他们的, 怎么, 怎么样, ]) return indirect and len(self.history) 2 def _llm_rewrite(self, query: str) - str: 使用 LLM 重写查询 context self._build_context() prompt f根据对话历史将用户的当前查询重写为独立、完整的检索查询。 对话历史 {context} 当前查询{query} 重写规则 1. 消解所有指代词它→具体名称 2. 补全省略的上下文 3. 保持原意不要添加信息 4. 只返回重写后的查询不要解释 重写后的查询 try: rewritten self.llm.chat(prompt).strip() if not rewritten or len(rewritten) 3: return query return rewritten except Exception as e: logger.error(fLLM 重写失败: {e}) return query def _build_context(self) - str: 构建对话历史上下文 lines [] for turn in self.history[-5:]: # 最近 5 轮 role 用户 if turn.role user else 助手 lines.append(f{role}: {turn.content}) return \n.join(lines) # ---- 使用示例 ---- def demo(): rewriter QueryRewriter() # 模拟多轮对话 rewriter.add_turn(user, 推荐几个 Go 的微服务框架) rewriter.add_turn(assistant, Go-Micro、Kratos 和 Go-Zero 都不错) rewriter.add_turn(user, Go-Micro 有什么优点) rewriter.add_turn(assistant, Go-Micro 支持服务发现、负载均衡...) # 测试指代消解 result rewriter.rewrite(它和 gRPC 怎么配合) print(f原查询: 它和 gRPC 怎么配合) print(f重写后: {result}) # 测试省略补全 result2 rewriter.rewrite(还有哪些类似的框架) print(f\n原查询: 还有哪些类似的框架) print(f重写后: {result2}) if __name__ __main__: demo()_resolve_pronouns方法的实体选择逻辑是取最近历史中的最后一个实体。这是一个朴素但有 85% 准确率的策略——在多轮对话中用户通常用它指代紧邻上一轮讨论的核心对象。如果你需要更高的准确率可以让 LLM 来做指代消解但从工程角度看规则先跑一轮再让 LLM 处理剩余的情况是更合理的设计。四、查询重写的局限性规则重写覆盖简单指代但无法处理隐式指代。用户说那速度怎么样规则不知道哪个的速度。这种情况必须有 LLM 支持。重写可能引入噪声。如果指代消解选错了实体重写后的查询更不准。需要评估重写前后的检索效果对比。不适合的场景单轮问答系统不需要重写用户每次都提供完整查询的场景对延迟要求极高的检索LLM 重写增加 0.5-2s。关于指代消解选错实体的风险有一个实用的降低手段。在生成重写查询的同时保留原始查询的兜底逻辑。也就是说同时用重写后的查询和原始查询做两次检索取并集进行去重和排序。这样即使重写有偏差原始查询也能兜底保证基础召回。代价是多一次检索调用的开销但对于重要业务场景这个冗余是值得的。另外查询重写的效果不是一次性的。建议在每个大版本迭代时拉取一批多轮对话的查询日志做人工标注评估重写的准确率。把这个指标纳入 RAG 系统的整体监控体系中而不只是搜罗 Embedding 的召回率。五、总结多轮对话中查询重写是 RAG 准确率的关键优化。规则重写处理显式指代它→实体名。LLM 重写处理隐式指代和意图融合。重写后的查询应完整、独立、不依赖上下文。重写效果需要与原始查询做 AB 对比验证。落地建议很简单先把规则重写加上零成本覆盖 60-70% 场景然后在延迟允许的前提下叠加 LLM 重写。不要一上来就全量用 LLM 重写——延迟和成本都会失控。一个好的查询重写器应该是规则为主、LLM 为辅的架构每种手段处理自己最擅长的场景。