主题 2 · Query 改写优化#
定位:RAG 管线第二层——查询理解、改写、分类、路由,决定了检索方向是否正确
一、通用知识#
1.1 核心概念与原理#
为什么需要 Query 改写——语义鸿沟(Semantic Gap)
面试怎么讲:
“用户的提问方式和文档的写法之间存在天然的语义鸿沟。用户说’怎么配 SSL’,文档写的是’TLS 证书配置指南’;用户说’这东西为啥这么慢’,文档里是’性能瓶颈分析与调优方案’。如果直接拿用户的原始 query 做向量检索或 BM25 检索,召回率和准确率都会大打折扣。Query 改写的核心目标就是弥合这个鸿沟——把用户的口语化、省略化、指代化的表述,转换成更接近文档表述的检索友好查询。”
语义鸿沟的三个典型来源:
1. 词汇鸿沟(Lexical Gap)
用户:"SSL" → 文档:"TLS 证书"
同义词/近义词/缩写导致关键词不匹配
2. 表述鸿沟(Expression Gap)
用户:"这东西为啥这么慢" → 文档:"性能瓶颈分析"
口语 vs 书面语,抽象 vs 具体
3. 上下文鸿沟(Context Gap)
用户:"那这个呢?" → 需要结合前几轮对话才知道"这个"指的是什么
指代消解、省略补全plaintextQuery Understanding 的完整流程
一个完备的 Query Understanding 层应覆盖以下环节:
原始 query
│
├─① 意图识别(Intent Classification)
│ factoid / procedural / comparison / opinion / chitchat
│
├─② 实体提取(Entity Extraction)
│ 识别查询中的关键实体、术语、编号
│
├─③ 指代消解(Coreference Resolution)
│ "它/这个/上面/那种" → 补全为具体对象
│
├─④ 改写/扩展(Rewriting / Expansion)
│ 口语→书面语,补全省略成分,同义词扩展
│
├─⑤ 复杂度评估(Complexity Assessment)
│ SIMPLE / MEDIUM / COMPLEX → 决定检索路径深度
│
└─⑥ 路由决策(Routing Decision)
是否需要 RAG → 走哪条检索路径 → 调哪些工具plaintext业界痛点:传统做法是每个环节一个独立组件各调一次 LLM,5 个环节就 5 次 LLM 调用,延迟和成本不可接受。核心优化方向是合并调用——用一个精心设计的 prompt 让 LLM 一次性输出所有信号。
Query Rewriting 的三种范式
| 范式 | 思路 | 典型做法 |
|---|---|---|
| 规则式 | 正则/模板/同义词表 | 停用词去除、同义词替换、拼写纠错 |
| LLM 式 | LLM 改写为检索友好表述 | 一次调用输出改写后查询 |
| Embedding 式 | 不改写 query 文本,改写 query 的 embedding | HyDE(用假设文档的 embedding 替代 query embedding) |
1.2 业界主流方案对比(表格形式)#
| 技术 | 原理 | 优点 | 缺点 | 代表实现 |
|---|---|---|---|---|
| Query Rewriting | LLM 改写为更适合检索的表述 | 通用性强,可同时做指代消解和口语化处理 | 改写可能偏离原意;需额外一次 LLM 调用 | LangChain / LlamaIndex |
| HyDE | 生成假设文档,用其 embedding 检索 | 弥合 query-doc 语义密度鸿沟(短 query vs 长 doc) | 假设偏离则噪声放大;需额外 LLM 调用 | Gao et al. 2022 |
| Step-Back Prompting | 先抽象化问题再检索 | 适合从具体到通用的问题升维 | 过度抽象丢失细节 | Google DeepMind 2023 |
| Multi-Query | 生成多个查询变体并行检索 | 覆盖面广,降低单次改写偏离风险 | 成本翻倍(N 个变体 = N 次检索) | LangChain MultiQueryRetriever |
| RAG-Fusion | Multi-Query + RRF 合并 | 多角度召回 + 排名融合去重 | 成本高,N 次检索 + RRF 融合 | Raudaschl 2023 |
| Query Decomposition | 复杂问题拆解为子问题 | 解决多跳/对比类查询 | 拆解噪声、子问题偏移、答案合并困难 | LlamaIndex SubQuestionQueryEngine |
| Sub-Query + Parallel | 子问题并行检索再合并 | 延迟不累加 | 子问题间依赖关系难处理 | LangGraph fan-out |
| Adaptive Routing | 根据复杂度动态选路 | 简单问题快速响应,复杂问题深度检索 | 路由器本身需要准确 | Adaptive-RAG (Jeong 2024) |
面试怎么讲:
“这些技术不是互斥的,实际系统里经常组合使用。比如 DocMind 的做法是:一次 LLM 调用同时完成改写 + 分类 + 路由(省调用),然后根据 specificity=FUZZY 有条件地启用 HyDE(双路检索),复杂问题交给 agentic 循环在运行时自行拆解(替代静态 Decomposition)。关键设计原则是:能一次做完的不拆两次,能运行时决定的不提前硬编码。“
1.3 关键论文与技术要点#
HyDE — Hypothetical Document Embeddings(Gao et al., 2022)
核心洞察:短 query 的 embedding 和长文档的 embedding 在向量空间中的分布差异很大——query 只有几个词,信息密度低,embedding 不够”靠近”相关文档。HyDE 的做法是让 LLM 先生成一段假设性回答(即使不准确),用这段回答的 embedding 来检索——因为假设文档和真实文档在”文档空间”里更近。
传统检索:
query("怎么配 SSL") → embed → 向量检索 → 可能找不到"TLS 证书配置指南"
HyDE:
query("怎么配 SSL") → LLM 生成假设回答(~200 字) → embed 假设回答 → 向量检索
假设回答虽然可能不准确,但其 embedding 比短 query 更接近真实文档的 embeddingplaintext关键局限:如果 LLM 的假设完全跑偏(比如把 SSL 理解成 Secure Sockets Layer 协议原理而非配置方法),反而引入噪声。因此 HyDE 适合模糊/概念性问题,不适合精确查询。
Step-Back Prompting(Google DeepMind, 2023)
对具体问题先做一步抽象升维:
具体问题:"Python list 和 tuple 的区别"
Step-Back:"Python 数据结构的类型与特性"
具体问题:"ECS 怎么挂载数据盘"
Step-Back:"云服务器存储管理方案"plaintext检索更宽泛的上下文后再回答具体问题。适合”具体→通用”方向的问题,但对已经足够抽象的问题反而过度泛化。
Adaptive-RAG(Jeong et al., 2024, NAACL)
核心贡献:根据查询复杂度自适应选择不同检索策略,而非所有查询走同一条管线。
三级路由:
简单查询(single-hop factoid) → 单次检索即可
中等查询(需要推理/多角度) → 多步检索
复杂查询(multi-hop) → 迭代检索 + 推理链
无需检索(闲聊/历史回顾) → 短路,跳过检索plaintext对 RAG 工程的启示:不要”一刀切”——简单问题走重管线是浪费延迟和成本,复杂问题走轻管线是牺牲质量。路由器的准确性直接决定整条管线的性价比。
Query Routing 的四种模式
| 模式 | 原理 | 延迟 | 准确率 | 适用场景 |
|---|---|---|---|---|
| Semantic Router | embedding 相似度匹配预定义类别 | ~10ms | 中 | 类别固定、训练数据充足 |
| LLM Router | LLM 判断走哪条管线 | ~200ms | 高 | 类别灵活、边界模糊 |
| Rule-based Router | 正则/关键词/规则匹配 | <1ms | 高(覆盖范围内) | 高确定性模式、零成本短路 |
| Hybrid(级联) | 规则快路径 + LLM 兜底 | 0~200ms | 最高 | 生产系统首选 |
面试怎么讲:
“纯 LLM 路由准确但慢且贵,纯规则路由快但覆盖面窄。最佳实践是级联——先用规则抓住高确定性的 pattern(闲聊问候、历史指代、身份声明,<1ms、100% 精确),命中就短路;没命中的交给 LLM,用一次调用同时输出路由 + 分类 + 改写。这样大部分简单 query 零 LLM 成本就能处理,只有真正需要判断的 query 才消耗一次 LLM 调用。“
1.4 常见面试问答#
Q1: HyDE 的核心原理和局限性?
A: HyDE 的核心洞察是”短 query 的 embedding 和长文档的 embedding 分布差异大”。它让 LLM 先生成一段假设性回答(150-300 字),然后用这段假设回答的 embedding 来做向量检索——因为假设文档即使不完全准确,其 embedding 也比只有几个词的 query 更接近真实文档的 embedding 空间。局限有两个:一是假设完全跑偏时反而引入噪声(比如 query 有歧义),所以应该只对模糊/概念性问题启用,精确查询(含错误码、API 名)不应该用 HyDE;二是需要额外一次 LLM 调用,增加延迟。最佳实践是把 HyDE 限制在 specificity=FUZZY 的场景,并且向量路和 BM25 路分别用假设文档和原始 query,双路互补。
Q2: Query 改写和 Query 扩展(expansion)的区别?
A: 改写(Rewriting)是把原始 query 变换为另一种表述——解决指代消解、口语化、省略等问题,输出是一条新 query,替代原始 query。扩展(Expansion)是在原始 query 基础上增加同义词、相关术语——不改变原文,而是追加更多关键词增加召回面。改写适合解决”用户说的和文档写的不一样”,扩展适合解决”用户说的太少,关键词覆盖不够”。两者可以组合:先改写解决语义对齐,再扩展增加召回覆盖。
Q3: Query Decomposition 的三种模式?
A: 三种模式:Tree(树形)——问题拆成独立的子问题,并行检索,答案合并(适合对比类:“A 和 B 分别是什么”);Sequential(串行)——子问题之间有依赖,前一个子问题的答案是后一个的输入(适合多跳:“X 的负责人毕业于哪所大学”,先查 X 的负责人是谁,再查此人的母校);Parallel with Merge——子问题独立但答案需要统一视角合并(适合综合类:“从成本、性能、安全三个维度评估方案”)。工程上 Tree 和 Parallel 可以用 fan-out 并行加速,Sequential 只能串行,延迟是子问题数量的线性倍。
Q4: 怎么评估改写质量?
A: 三个层面。语义保真度——改写后的 query 和原始 query 的语义是否一致(可以用 embedding cosine 衡量,阈值 0.85 以上);检索提升度——用改写后的 query 检索,MRR / Recall@K 是否优于原始 query(这是最直接的端到端指标);人工抽检——抽样看改写是否偏离原意、过度泛化、丢失关键信息。生产环境中最实用的是检索提升度——如果改写后 MRR 没提升甚至下降,那改写就是失败的,不管语义保真度多高。
Q5: 意图分类有哪些常见维度?
A: 至少三个正交维度。语义意图(intent):factoid(事实型)/ procedural(步骤型)/ comparison(对比型)/ opinion(观点型)/ chitchat(闲聊)——决定答案的生成格式。复杂度(complexity):SIMPLE / MEDIUM / COMPLEX——决定检索深度(一次检索 vs agentic 循环)。精确度(specificity):FUZZY / NORMAL / PRECISE——决定检索策略(FUZZY 用 HyDE 弥补语义鸿沟,PRECISE 追加 BM25 关键词精确匹配)。这三个维度组合起来就能覆盖绝大多数路由决策。
Q6: Query Routing 用 LLM 还是规则?什么时候用哪个?
A: 不是非此即彼,最佳实践是级联——规则先行 + LLM 兜底。规则抓高确定性的模式(“你好”是闲聊、“总结上面的对话”是元对话、“我是一名工程师”是身份声明),命中就短路,零 LLM 成本、零延迟。没命中的交给 LLM 做精细判断——意图、复杂度、范畴一次调用全出来。规则的设计原则是 precision > recall:宁可漏判(交给 LLM),不可误判(把知识问答错认为闲聊)。
Q7: Multi-Query 和 RAG-Fusion 的区别?
A: Multi-Query 是”生成多个查询变体,分别检索,结果取并集”——覆盖面广但结果可能有大量重复。RAG-Fusion 在 Multi-Query 基础上加了 RRF(Reciprocal Rank Fusion)合并——只看排名不看分数,把多路结果融合成一个去重排序的列表。RAG-Fusion 严格优于裸 Multi-Query,因为 RRF 解决了”多路分数不可比”和”结果重复”两个问题。但两者共同的缺点是成本高——N 个变体意味着 N 次检索调用。
Q8: 指代消解在 RAG 场景中为什么特别重要?
A: 多轮对话中用户大量使用指代——“它""这个""那种""刚才说的”。如果不做指代消解,检索的是”它怎么配置”这样的无意义 query。更隐蔽的情况是用户指代文档本身——“这几份文档讲了什么""对比第一份和第三份”——这需要结合用户当前选中的文档列表才能补全。指代消解的输入不只是 query,还需要对话历史和文档上下文。
二、DocMind 实践#
30 秒口述版#
“DocMind 的 Query Understanding 做了三个核心优化。第一,合并调用:原来 5 个独立组件(QueryRewriter / QueryRouter / QueryProfiler / QueryDecomposer / ExplicitMemoryExtractor)各调一次 LLM,我合并成
QueryUnderstandingService一次调用产出 10 个结构化字段——改写后查询、intent、complexity、specificity、timeAware、memoryAware、isAmbiguous、memoryWriteHints、scope、scopeConfidence,LLM 调用从 2 次降到 1 次,省约 250ms。第二,确定性规则兜底:applyDeterministicSignals用正则匹配’分别/对比/vs/多问号’等多焦点强信号,命中就把 complexity 从 SIMPLE 升级到 MEDIUM,触发率 100%(对比小模型 60%)。第三,两级 scope 路由:Tier-0 纯正则快路径(MetaIntentDetector,闲聊/元对话/身份声明/KB 元信息,<1ms)+ Tier-1 LLM 合并路由(scope 与 classification 同一次调用产出),6 种范畴短路非检索 query。HyDE 仅在 specificity=FUZZY 时启用,且双路检索——向量路用假设文档 embedding,BM25 路保留原始 query 关键词。“
详细展开#
背景/痛点(Situation)#
DocMind 初版的查询理解层由 5 个独立组件构成(QueryRewriter / QueryRouter / QueryProfiler / QueryDecomposer / ExplicitMemoryExtractor),每个组件独立调用一次 LLM。问题有三:
- 延迟不可接受:5 次 LLM 调用串行执行,仅理解层就消耗 1-2 秒,占端到端延迟的 40%+
- 信号冗余与冲突:各组件独立判断,QueryRouter 说”简单”但 QueryProfiler 说”复杂”,下游无法调和
- 路由缺失:没有前置 scope 判断,闲聊问候也走完整检索管线,白白消耗向量检索和 LLM 资源
行动(Action)#
Action 1:合并为 QueryUnderstandingService 单次 LLM 调用
将 5 个组件的能力合并到一个精心设计的 prompt(prompts/query_understanding.txt),通过结构化 JSON 约定让小模型一次调用输出 10 个字段:
rewritten:改写后的独立检索查询(已消解指代/省略)intent:factoid / procedural / comparison / opinion / chitchatcomplexity:SIMPLE / MEDIUM / COMPLEXspecificity:FUZZY / NORMAL / PRECISEtimeAware/memoryAware:时效性和记忆依赖信号isAmbiguous:模糊查询标记(触发保守工具集)memoryWriteHints:用户声明的偏好原文scope+scopeConfidence:范畴决策(Phase 5 合并模式,scope 和 classification 同一次调用产出)
解析层做了三重防御:
extractJsonObject():容忍 markdown 围栏、前置说明文本,提取首个 JSON 对象sanitizeEnum():非法 enum 值退默认(intent 退 factoid,complexity 退 MEDIUM)sanitizeMemoryHints():长度上限 120 字 + 注入检测(<script/ignore previous等直接丢弃)
降级策略:LLM 调用失败 → QueryClassification.fallback()(isAmbiguous=true,触发下游走保守混合工具集,保证可用性)。
Action 2:确定性规则后处理(applyDeterministicSignals)
小模型对多焦点/多步问题的复杂度评估偶尔失准(实测触发率约 60%)。新增纯正则后处理:
正则模式:
"分别(说|讲|介绍|对比|分析|阐述)"
"对比.*(和|与|以及|vs)"
".*的区别(是什么)?"
"(及其|以及|另外|此外).*(吗|呢|?|?|怎么|如何|是什么|有哪些)"
"vs|VS|这几份|两份文档|三份文档"
触发条件(二选一):
① 正则命中
② 问号数 ≥ 2
效果:
SIMPLE → MEDIUM(确保路由到 agentic 循环而非一次性检索)
已是 MEDIUM/COMPLEX 则不变plaintext设计原则:只升不降、只兜底不覆盖。规则不替代 LLM 判断,只在 LLM 漏判时补位。
Action 3:两级 scope 路由(Adaptive RAG 范式)
用户输入
│
├─ Tier-0:MetaIntentDetector(纯正则,<1ms)
│ ├─ "你好/谢谢/在吗" → CHITCHAT(短路)
│ ├─ "我是一名后端工程师" → CHITCHAT + 记忆写入(短路)
│ ├─ "总结上面的对话" → META_CONVERSATION(短路)
│ ├─ "有哪些知识库" → KB_META(短路,查 DB 回答)
│ └─ 未命中 → 进入 Tier-1
│
└─ Tier-1:LLM 合并路由(~250ms,scope 与 classification 同一次调用)
├─ KNOWLEDGE_QUERY → 进入 RAG 检索主路径
├─ CHITCHAT / META_CONVERSATION / KB_META → 短路
├─ OUT_OF_SCOPE → 礼貌拒答
└─ 失败 / 未决 → 降级 KNOWLEDGE_QUERY(宁可多检索不可漏检索)plaintextTier-0 的设计原则:precision > recall。只在规则极有把握时命中(闲聊限制 ≤8 字 + 精确正则匹配,身份声明限制 ≤25 字 + 无问号),误判代价远高于漏判(多一次 LLM 调用 vs 把知识问答错路由到闲聊)。
Tier-1 的合并优化:scope 与 classification 在同一次 LLM 调用中产出,结果缓存到 AgentState.cachedUnderstanding,下游 runReActLoop 直接复用,不二次调用。整条链路共 1 次 LLM 调用(Tier-0 命中时 0 次)。
Action 4:HyDE 条件启用 + 双路检索
HyDE 仅在 specificity=FUZZY 时启用(概念性/开放性问题),精确查询不启用(避免假设跑偏引入噪声)。启用后采用双路策略:
- 向量路:用
HyDEGenerator.generate()生成假设文档(150-300 字),用其 embedding 做 Milvus ANN 检索 - BM25 路:保留原始 query 做 Lucene BM25 检索,保持关键词精准性
两路结果通过 RRF 融合 → Cross-Encoder 统一精排,互补性最强。
Action 5:三路路径决策(routePath)
Query Understanding 的分类信号驱动下游三选一:
① SELECTED_DOC:命中文档/摘要意图("总结这份文档")+ kbIds 在直读区间
→ DocumentDirectReader 直接读 chunk,跳过检索
② RULE_PLANNER:SIMPLE + 非歧义
→ RetrievalPlanner 规则引擎选工具,一次性检索(快且省)
③ AGENTIC:COMPLEX / MEDIUM / 歧义 / 多焦点
→ AgenticSearchOrchestrator LLM 工具调用循环,模型自驱拆解/多跳/补 Webplaintext结果(Result)#
- LLM 调用次数:理解层从 2 次降到 1 次(Tier-0 命中时 0 次),省约 250ms
- 多焦点检测触发率:从小模型 ~60% 提升到规则兜底后 100%
- scope 短路覆盖 6 种范畴(META_CONVERSATION / CHITCHAT / KB_META / KNOWLEDGE_QUERY / OUT_OF_SCOPE / TASK_EXECUTION),非检索 query 零 LLM 检索成本
- HyDE 双路策略确保模糊查询和精确查询各有最优检索路径
- 降级策略保证 LLM 不可用时系统仍可运行(
fallback→isAmbiguous=true→ 保守工具集)
代码锚点(表格)#
| 能力 | 类/方法 | 文件路径 |
|---|---|---|
| 查询理解主入口 | QueryUnderstandingService.understand() | service/rag/QueryUnderstandingService.java |
| LLM 分类 + 结构化解析 | QueryUnderstandingService.classify() / parseClassification() | service/rag/QueryUnderstandingService.java |
| 确定性规则后处理 | QueryUnderstandingService.applyDeterministicSignals() | service/rag/QueryUnderstandingService.java |
| 多焦点强信号正则 | STRONG_COMPLEX_PATTERN | service/rag/QueryUnderstandingService.java |
| 分类结果 record | QueryClassification (10 字段) | service/rag/QueryClassification.java |
| 降级默认值 | QueryClassification.fallback() | service/rag/QueryClassification.java |
| Tier-0 正则快路径 | MetaIntentDetector.detect() | agent/MetaIntentDetector.java |
| 两级 scope 路由级联 | DocMindAgent.decideScope() / tier0RuleScope() / tier1LlmScope() | agent/DocMindAgent.java |
| scope 合并模式 | DocMindAgent.decideViaMergedUnderstanding() | agent/DocMindAgent.java |
| HyDE 假设文档生成 | HyDEGenerator.generate() | service/rag/HyDEGenerator.java |
| HyDE 条件启用 | SupervisorAgent.oneShotRetrieval() (specificity=FUZZY 时) | agent/supervisor/SupervisorAgent.java |
| HyDE 双路检索 | RetrievalWorker.execute() (vectorQuery vs query) | agent/worker/RetrievalWorker.java |
| 三路路径决策 | DocMindAgent.routePath() | agent/DocMindAgent.java |
| 路径决策结果 | PathDecision (SELECTED_DOC / RULE_PLANNER / AGENTIC) | agent/PathDecision.java |
| 规则引擎工具选择 | RetrievalPlanner.plan() | service/rag/RetrievalPlanner.java |
| 范畴决策 record | ScopeDecision (6 种 Scope) | agent/ScopeDecision.java |
| 理解阶段缓存复用 | AgentState.cachedUnderstanding + stageQueryUnderstanding() | agent/DocMindAgent.java |
| prompt 模板 | query_understanding.txt / hyde_generation.txt | resources/prompts/ |
| memoryHints 防注入 | QueryUnderstandingService.sanitizeMemoryHints() | service/rag/QueryUnderstandingService.java |
| 时效性双源校验 | RetrievalPlanner.containsTimeKeyword() | service/rag/RetrievalPlanner.java |
量化数据(表格)#
| 指标 | 改造前 | 改造后 | 备注 |
|---|---|---|---|
| 理解层 LLM 调用次数 | 2 次(分类 + 路由各一次) | 1 次(合并模式),Tier-0 命中时 0 次 | scope + classification 同一次调用产出 |
| 理解层延迟 | ~500ms(2 次 LLM) | ~250ms(1 次 LLM),<1ms(Tier-0 命中) | 小模型调用约 250ms/次 |
| 多焦点检测触发率 | ~60%(纯 LLM 判断) | 100%(规则兜底) | applyDeterministicSignals 正则补漏 |
| scope 范畴 | 无前置路由 | 6 种范畴短路 | META_CONVERSATION / CHITCHAT / KB_META / KNOWLEDGE_QUERY / OUT_OF_SCOPE / TASK_EXECUTION |
| 分类字段数 | 分散在 5 个组件 | 10 个结构化字段一次输出 | QueryClassification record |
| 改写长度上限 | 无限制 | 200 字硬截断 | 防止 LLM 输出过长改写 |
| memoryHints 长度上限 | 无限制 | 120 字 + 注入检测 | 防止提示注入写入 Redis 长期记忆 |
| Tier-0 精度 | N/A | >95%(高确定性匹配) | precision > recall 设计原则 |
| HyDE 假设文档长度 | N/A | 150-300 字 | 仅 FUZZY 查询启用 |
| KB 元数据上下文 | 无 | 最多 8 份文档简要信息注入 prompt | 用于指代消解(“这几份文档”) |
三、追问应对#
面试官想听到的信号#
- 理解合并调用的工程价值——不是”把代码写在一起”,而是精心设计 prompt 让一次 LLM 调用覆盖多个正交维度的信号输出
- 确定性规则 vs LLM 的分工——知道什么时候该用规则(高确定性、零成本、100% 可复现)、什么时候该用 LLM(边界模糊、需要语义理解)
- 降级策略的完备性——LLM 失败不是系统失败,fallback 路径保证可用性
- HyDE 的条件启用思路——不是”有就用”,而是根据 specificity 判断是否需要,且双路互补(向量路用假设 embedding / BM25 路保原始 query)
- Adaptive Routing 的实际落地——不只是”简单问题简单处理”,而是具体到 Tier-0/Tier-1 级联、3 路路径决策、缓存复用等工程细节
追问预判与应答#
Q1: 为什么不用 Multi-Query 方案,而是只做单次改写?
A: 三个原因。第一,成本——Multi-Query 生成 N 个变体意味着 N 次检索调用,对于实时 Q&A 系统延迟不可接受。第二,我们的管线已经有多路召回(向量 + BM25 + 可选 Web),本身就是”多角度”检索,再加 Multi-Query 收益递减。第三,对于真正需要多角度的复杂问题,我们交给 agentic 循环——由模型自驱地决定需要搜什么、搜几次、什么时候停,比静态的 Multi-Query 更灵活也更精准。单次改写 + agentic 循环的组合,在保持低延迟的同时覆盖了 Multi-Query 的场景。
Q2: applyDeterministicSignals 正则会不会误升级?比如用户问”什么是对比学习”,包含”对比”但其实是简单问题。
A: 不会。正则模式要求”对比”后面跟”和/与/以及/vs”(
对比.*(和|与|以及|vs)),“什么是对比学习”不会命中。我们的正则是针对多焦点句式结构而非单个关键词——“分别说 A 和 B""A 和 B 的区别""A 以及 B 怎么用”这类句式强烈暗示需要对比两个主题。即使偶尔误升级(SIMPLE→MEDIUM),代价也只是多走一轮 agentic 循环,不会影响答案质量,只是多花一点延迟。而漏检的代价是复杂问题只做一次检索,答案覆盖不全。只升不降、宁可多检索不可漏检索。
Q3: Tier-0 和 Tier-1 的 scope 判断会冲突吗?
A: 不会,因为是级联而非并行。Tier-0 命中就短路,不进入 Tier-1。Tier-0 的设计原则是 precision > recall——只在规则极有把握时返回结论(闲聊限 ≤8 字精确匹配,身份声明限 ≤25 字无问号),拿不准就返回 null 交给 Tier-1。两级的覆盖范围是互补的:Tier-0 抓”100% 确定”的,Tier-1 处理”需要语义理解才能判断”的。
Q4: 为什么 scope 和 classification 要合并到同一次 LLM 调用?分开调用不是更清晰吗?
A: 分开调用确实更清晰,但代价是多一次 LLM 调用(~250ms)。scope 和 classification 的输入完全一样(用户问题 + 对话历史 + KB 列表),判断逻辑也高度相关(scope 需要理解意图才能路由,classification 需要理解意图才能分类),让同一个 LLM 同时输出两组信号是自然的。合并后还有一个隐藏收益:scope 和 intent 的判断是互相一致的(LLM 在同一个推理过程中产出),分开调用两次 LLM 可能给出矛盾结论。缓存到
cachedUnderstanding后下游无缝复用,零额外开销。
Q5: QueryClassification.fallback() 的 isAmbiguous=true 会不会导致所有降级查询都走最重的 agentic 路径?
A: 不会。
isAmbiguous=true在routePath()中确实会路由到 AGENTIC 模式,但这是故意的——降级意味着我们对 query 的理解不充分,用保守策略(让模型自己决定怎么检索)比用激进策略(基于不准确的分类做规则路由)更安全。同时isAmbiguous=true也会被RetrievalPlanner用来追加 keyword_search(BM25 保底防漏召)。降级频率在生产环境中 <2%(小模型调用很稳定),对整体性能影响可忽略。
Q6: HyDE 的假设文档如果完全跑偏怎么办?
A: 三层防御。第一,条件启用——只在
specificity=FUZZY时启用 HyDE,精确查询不用(精确查询的 query 本身就包含足够的检索信号,HyDE 反而可能模糊掉精确关键词)。第二,双路互补——向量路用假设文档 embedding,BM25 路保留原始 query 关键词,两路通过 RRF 融合。即使假设跑偏导致向量路召回垃圾,BM25 路仍能兜住。第三,生成失败回退——HyDEGenerator.generate()内部有 try-catch,生成为空或抛异常时直接返回原始 query,不影响检索流程。
Q7: prompt 中注入 KB 元数据(文档名、分类)做指代消解,有没有 prompt 注入风险?
A: 有意识地做了防御。KB 元数据来源是数据库中的
kb_knowledge_base表,不是用户直接输入,注入风险较低。但文档名是用户上传时填的,理论上可以包含恶意 prompt。防御措施:safeName()限制 60 字截断,renderKbListForPrompt()限制最多 8 份文档(MAX_KB_LIST_FOR_PROMPT),sanitizeMemoryHints()对 LLM 回填的 memoryWriteHints 做注入检测(<script/ignore previous等直接丢弃)。更彻底的方案是对文档名做白名单字符过滤,这是后续可以加固的点。
Q8: 如果要支持多语言查询(比如英文问题查中文文档),Query Understanding 层需要怎么改?
A: 两个层面的改动。第一,改写层面——prompt 中增加”如果用户用英文提问但知识库是中文,请将查询翻译为中文并保留关键英文术语”的指令,让 LLM 在改写时同时做跨语言对齐。第二,检索层面——HyDE 天然适合跨语言场景:英文 query → LLM 生成中文假设文档 → 用中文 embedding 检索中文文档。BM25 路需要同时检索英文关键词和中文翻译。但更根本的方案是用多语言 embedding 模型(如 BGE-M3),让不同语言的语义在同一向量空间对齐。当前 DocMind 用的 text-embedding-v3 已经支持中英双语,基础能力具备。
Q9: 实测中 LLM 输出的 JSON 解析失败率是多少?有什么优化手段?
A: 小模型(qwen-plus)的 JSON 遵从度很高,解析失败率 <1%。但我们做了三层防御确保稳定:第一,
extractJsonObject()容忍 markdown 围栏和前置文本——LLM 有时会输出 ````json … ``` 包裹或加一句”以下是分析结果”,正则提取首个{...}即可。第二,sanitizeEnum()对非法 enum 值退默认而非抛异常——比如 LLM 输出"intent": "question"不在合法集合内,退到factoid。第三,整个 classify 包在 try-catch 里,任何异常走fallback()。优化手段方面,prompt 中显式标注”不要 markdown 代码块、不要解释文字”,比依赖 BeanOutputConverter 的 schema 描述更稳(对中文模型尤其如此)。
反击引导#
- “说到 scope 路由,DocMind 的 6 种范畴实际上就是 Adaptive-RAG 论文的工程落地——简单闲聊零成本短路、中等问题一次性检索、复杂问题 agentic 循环。如果感兴趣,我可以展开讲 agentic 循环是怎么用 Spring AI ToolCallingManager 实现 LLM 自驱检索的。”
- “多焦点检测的正则兜底其实反映了一个通用工程原则:小模型容易在边界 case 上失准,用确定性规则做安全网。这个思路在 CRAG 评分降级、reranker 超时降级上也有体现,形成了 DocMind 全链路降级策略的核心范式。”
- “HyDE 的条件启用引出一个更大的话题:RAG 管线的每个组件都不应该 always-on,而是根据上游信号动态开关。specificity 决定 HyDE 开关、complexity 决定检索路径深度、timeAware 决定是否补 Web 搜索——这就是 Query Understanding 真正的价值:不只是改写 query,而是为整条管线提供决策信号。”