一、检索效果差,问题往往出在查询端
很多团队把RAG调优的重点放在分块和向量库上,却忽略了一个事实:用户很少会用"文档的语言"提问。真实查询往往短、口语化、带指代,甚至一个问题里藏着多个意图。查询端不处理,召回率天花板就在那里。
| 查询形态 | 示例 | 对检索的影响 |
|---|---|---|
| 短查询 | "发票" | 语义向量区分度低,召回一堆噪声 |
| 口语化 | "那个报税的系统咋整" | 与文档术语"企业所得税申报"不匹配 |
| 指代模糊 | "它和上个月说的方案一样吗" | 缺少上下文,检索完全跑偏 |
| 复合意图 | "对比A和B的定价,再给出部署建议" | 单一查询向量无法覆盖两个主题 |
二、意图识别:先判断"该不该检索"
不是所有问题都需要检索。闲聊、问候、直接计算类问题走检索只会引入噪声。用规则兜底 + LLM分类的混合方案,兼顾准确率和成本:
import re
INTENT_KEYWORDS = {
"greeting": ["你好", "hi", "hello", "在吗"],
"calculation": ["计算", "等于", "多少加", "换算"],
"chitchat": ["谢谢", "再见", "你是谁"],
}
def rule_intent(query: str) -> str | None:
"""规则层:快速识别无需检索的意图"""
for intent, words in INTENT_KEYWORDS.items():
if any(w in query for w in words):
return intent
return None
def classify_intent(query: str, llm) -> str:
"""LLM层:兜底分类,返回 retrieval / greeting / calculation / chitchat"""
rule_result = rule_intent(query)
if rule_result:
return rule_result
prompt = f"""判断以下用户问题的意图,只输出一个词:
- retrieval:需要查资料才能回答
- greeting:打招呼
- calculation:直接计算即可
- chitchat:闲聊
问题:{query}
意图:"""
resp = llm.chat(prompt, max_tokens=10, temperature=0)
intent = resp.strip().lower()
return intent if intent in ("retrieval", "greeting", "calculation", "chitchat") else "retrieval"
规则层零成本秒回,只有规则覆盖不到时才调用LLM,约70%的流量不需要过LLM分类器。
三、查询改写:把口语翻译成"文档的语言"
识别为 retrieval 后,下一步是改写。三种高频改写策略:
def rewrite_query(query: str, history: list, llm) -> str:
"""查询改写:补全指代 + 术语归一 + 必要扩展"""
prompt = f"""你是一名检索专家。请将用户的模糊问题改写为适合向量检索的清晰查询。
要求:
1. 结合对话历史补全指代(它/这个/上个月)
2. 将口语表达转换为书面术语
3. 保留核心实体,不添加原文没有的事实
4. 只输出改写后的查询,不要解释
对话历史:
{chr(10).join(history[-4:])}
用户问题:{query}
改写结果:"""
rewritten = llm.chat(prompt, max_tokens=100, temperature=0.2).strip()
# 兜底:改写失败或返回空则用原查询
return rewritten if len(rewritten) >= 2 else query
改写质量的验收标准:改写前后向量相似度应保持在0.7以上——如果改得面目全非,说明LLM在编造信息,宁可退回原查询。
四、子问题分解:复杂问题拆着问
对复合意图查询,先用LLM拆解成子问题,分别检索,再让LLM基于多路证据综合回答:
def decompose_query(query: str, llm) -> list[str]:
prompt = f"""把下面的复杂问题拆解为2-4个独立的子问题。
要求:每个子问题只问一件事,且都能独立检索资料回答。
问题:{query}
子问题(每行一个):"""
lines = [l.strip("- ").strip() for l in llm.chat(prompt, max_tokens=200).splitlines()]
return [l for l in lines if l and len(l) > 3][:4]
def multi_hop_retrieve(query: str, llm, retriever, top_k=3):
"""多跳检索:分解 -> 分别召回 -> 汇总证据"""
sub_questions = decompose_query(query, llm)
evidence = []
for sq in sub_questions:
# 每个子问题独立改写后检索
rq = rewrite_query(sq, [], llm)
evidence.extend(retriever.search(rq, k=top_k))
return deduplicate(evidence), sub_questions
子问题分解适合对比类、方案类、流程类问题;简单事实类问题直接检索即可,不要过度设计。
五、查询路由:不同查询走不同通道
路由是查询改写的最后一环:把改写好、分解好的查询分发到最合适的检索通道。
class QueryRouter:
def __init__(self, vector_store, keyword_index, sql_engine=None):
self.vector_store = vector_store # 语义检索
self.keyword_index = keyword_index # BM25 关键词检索
self.sql_engine = sql_engine # 结构化查询
def route(self, query: str) -> str:
# 规则:含明确数值/筛选词 -> 结构化查询
if re.search(r"\d{4}年|\d+月|大于|小于|排名|TOP", query):
return "sql"
# 含专有名词/型号/编号 -> 关键词检索优先
if re.search(r"[A-Z]{2,}|型号|编号|版本", query):
return "keyword"
# 默认走混合检索
return "hybrid"
def search(self, query: str, top_k=5):
channel = self.route(query)
if channel == "sql" and self.sql_engine:
return self.sql_engine.query(query)
if channel == "keyword":
return self.keyword_index.search(query, k=top_k)
# hybrid: 向量 + BM25 结果融合(RRF)
return rrf_fusion(
self.vector_store.search(query, k=top_k * 2),
self.keyword_index.search(query, k=top_k * 2),
top_k,
)
路由策略要与你的知识库形态匹配:文档型知识库以 hybrid 为主;库表型数据一定要接 SQL 通道,别硬塞进向量库。
六、生产落地建议
| 环节 | 推荐配置 | 延迟预算 | 收益 |
|---|---|---|---|
| 意图识别 | 规则 + LLM兜底 | +0~200ms | 过滤30%无效检索 |
| 查询改写 | 轻量模型 | +200~500ms | Recall@K 提升15~25% |
| 子问题分解 | 仅复杂问题触发 | +500ms~1s | 复合问题准确率翻倍 |
| 查询路由 | 规则为主 | +0ms | 各通道各司其职 |
**关键原则**:查询改写链路每一环都要有"兜底回退"。任何一环失败或超时,直接使用原始查询继续检索,绝不让改写链路本身成为可用性瓶颈。建议对改写质量做A/B抽样评估,用真实用户查询日志持续迭代。