Query Rewriting + Hybrid Search + Reranking:让 AI 知识库从「能搜」到「搜得准」
向量检索只是起点。我们在 CCTCRM 上线了 Query Rewriting、Hybrid Search(RRF 融合)、Reranking 三大 RAG 增强技术,用工程手段把知识库检索精度再往上推了一截。本文记录技术选型、实现细节和踩过的坑。
我们的 RAG 知识库上线半年了。768 维 Embedding 向量检索,余弦相似度排序,覆盖客户、订单、产品、出运、生产、库存、财务等 9 大业务域。功能跑通了,用户也用起来了。
但跑通和好用之间有一道坎。
业务员问"上个月发往美国的订单有多少个柜还没到港",系统返回的第一条结果是一篇关于美国关税政策的文档。语义上确实相关——都提到了"美国"和"订单"。但用户要的是出运数据,不是关税政策。
这种错位在向量检索中太常见了。Embedding 模型理解的是语义相似性,不是业务相关性。两段文本在向量空间里距离近,不代表它们在业务场景里是同一个东西。
我们花了三周时间,在向量检索的上游和下游各加了一层处理,把检索精度往前推了一截。三个技术,一个目标:让搜到的就是用户要的。
问题出在哪里
在动手优化之前,我们需要先搞清楚朴素 RAG 的三个结构性缺陷。
第一,用户说的是人话,检索引擎要的是关键词。 业务员不会输入"美国 出口订单 柜数 出运统计"。他会说"上个月发往美国的订单有多少个柜还没到港"。这句话送进 Embedding 模型后,"上个月""多少个""还没到港"这些口语化的修饰词稀释了核心实体信号。向量空间里,这句话可能和一篇讲"美国海关清关流程"的文档距离很近——因为语义确实像——但它和真正的出运统计表数据反而隔了一层。
第二,向量检索擅长"语义像",但不擅长"精确匹配"。 假设知识库里有一篇文档标题就叫"美国订单出运统计规范"。向量检索未必能把它排在第一位——因为标题和用户口语化提问的语义距离可能不是最近的。但关键词检索(MySQL LIKE 或 BM25)一眼就能匹配上。反过来,关键词检索无法理解同义词和语义关联。两套方法各有盲区。
第三,检索回来的结果,向量相似度高不等于业务相关度高。 余弦相似度衡量的是两段文本在向量空间中的方向一致性。两段文本方向一致,可能只是因为它们讨论的话题相同,但立场、场景、业务含义完全不同。用户问的是"怎么处理延期交货的索赔",系统返回了一篇讲"延期交货的原因分析"的文档——相似度 0.87,但答非所问。
这三个问题不是某个特定 Embedding 模型的缺陷,是朴素 RAG 架构的结构性短板。行业里已有成熟的解法:Query Rewriting 解决输入端的问题,Hybrid Search 补齐检索端的盲区,Reranking 修正排序端的偏差。三项技术组合使用,形成一条"改写 → 混合检索 → 重排"的增强流水线 $TRAE_REF。
第一层:Query Rewriting — 让 LLM 帮用户把话说清楚
思路很直接:在用户提问送进向量检索之前,先让一个轻量模型(我们用的是 lite 组模型,成本最低)把口语化提问改写成更适合检索的关键词组合。
用户说:"上个月发往美国的订单有多少个柜还没到港"
改写后:"美国 出口订单 柜数 出运统计 上月 出口量"
改写后的文本再送去生成 Embedding 向量。效果是双重的:一方面去掉了"上个月""多少个""还没到港"这些对检索没有贡献的口语化修饰,另一方面补充了"出运统计""出口量"等同义词和关联词,扩大了语义召回范围。
实现上有几个关键设计。
Prompt 是核心。 改写质量完全取决于 system prompt 的设计。我们的 prompt 包含五条规则:提取核心实体(人名、产品、国家、单据类型)、补充同义词和关联词、去除语气词、输出纯文本不解释、保留原文语言。规则之外还给了两个 few-shot 示例,覆盖中文和英文两种场景。没有 few-shot,模型会输出"改写后的查询:XXX"这种带前缀的格式,破坏下游解析。
短查询不改写。 6 个字符以下的输入直接跳过改写环节。这类查询通常是"客户""订单"这种关键词本身,改写没有增益反而增加延迟。这个阈值是我们在测试中观察到的拐点:6 字符以上的查询改写后有正向收益,以下则收益不明显。
5 分钟缓存。 同一个查询在 5 分钟内不会重复调用模型。用 ConcurrentHashMap 存 hash → 改写结果的映射,超时自动清理。缓存上限 200 条,满了就清理过期项。这个设计让高频重复查询(比如多个业务员同时查同一个客户)不会反复触发模型调用,成本控制在可接受范围内。
失败降级。 模型调用失败、配额不足、网络超时——任何异常都静默降级,返回原始查询。改写是锦上添花,不是必需品。不能因为改写服务不可用就让整个 RAG 流程挂掉。
第二层:Hybrid Search — 向量 + 关键词,用 RRF 融合
这一层要解决的是"向量检索和关键词检索各有盲区"的问题。
方案是同时跑两路检索,然后融合结果。向量检索负责语义召回——理解同义词、关联概念、跨语言匹配。关键词检索负责精确匹配——标题、关键词字段中包含的原始词汇。两路结果用 Reciprocal Rank Fusion(RRF) 算法融合。
RRF 算法
RRF 是 2009 年 Cormack 等人提出的 rank fusion 方法 $TRAE_REF。核心思想极其简单:不关心每路检索的原始分数(向量相似度 vs BM25 分数不可比),只关心每路检索给出的排名。
公式:
RRF_score(d) = Σ 1 / (k + rank_i(d))
i∈lists
其中 rank_i(d) 是文档 d 在第 i 路检索结果中的排名(从 0 开始),k 是一个平滑常数(我们用 60)。
假设文档 A 在向量检索中排第 1 名,在关键词检索中排第 5 名:
RRF_score(A) = 1/(60+1) + 1/(60+6) = 0.0164 + 0.0152 = 0.0316
文档 B 在向量检索中排第 3 名,在关键词检索中排第 2 名:
RRF_score(B) = 1/(60+4) + 1/(60+3) = 0.0156 + 0.0159 = 0.0315
文档 A 和 B 的 RRF 分数几乎相同——即使 A 在向量检索中远高于 B。这就是 RRF 的特点:它不偏向任何一路检索的绝对分数差异,只看综合排名表现。一个文档如果同时在两路检索中都靠前,它的 RRF 分数就会高。
实现
我们的 RagEnhancementService 类中实现了 RRF 融合逻辑。流程是:
- 向量检索返回 top 20 结果(按余弦相似度降序)
- 关键词检索返回 top 20 结果(MySQL LIKE 查 title/content/keywords 字段)
- 两路结果送入
reciprocalRankFusion方法,k=60 - RRF 融合后按分数降序排列,取 top K
关键词检索的实现很简单:按空格和标点分词,取长度 ≥ 2 的词,用 MyBatis-Plus 的 LIKE 条件做 OR 查询。没有用 MySQL 全文索引(FULLTEXT),因为我们的知识库数据量不大(数千条),LIKE 的性能完全够用。如果未来知识库规模增长到数万条以上,会考虑迁移到 BM25 或 Elasticsearch。
RRF 的 k 值选择。 k 越大,排名差异对分数的影响越平滑;k 越小,排名靠前的文档优势越明显。我们用 60,这是 RRF 原论文和 Elasticsearch 的默认值 $TRAE_REF。在测试中,k=60 的表现稳定,没有过度调整的必要。
一个文档在两路检索中都出现,它的 RRF 分数会比只出现在一路的文档高。 这是 RRF 的天然特性,也是我们想要的效果——两路检索都认为相关的文档,大概率就是用户要找的。
第三层:Reranking — 让 LLM 做最后一道筛选
Hybrid Search 解决了"搜得到"的问题。但搜到的结果按什么顺序展示,仍然是 RRF 分数决定的——而 RRF 分数只反映排名,不反映业务相关性。
回到前面的例子。用户问"怎么处理延期交货的索赔"。检索结果里可能有:
- 文档 A:"延期交货的索赔处理流程"(真正相关的)
- 文档 B:"延期交货的原因分析"(话题相关,但答非所问)
- 文档 C:"2024 年交货准时率统计"(数据相关,但场景不对)
向量检索可能把 B 排在 A 前面(因为 B 的措辞和用户提问更接近),关键词检索可能把 C 排在前面(因为"交货"一词匹配次数多)。RRF 融合后,排序仍然可能是 B > A > C。
Reranking 就是在融合结果返回之前,加一道 LLM 重排。把查询和候选文档一起送给 lite 模型,让模型判断哪个文档最相关。
实现方式
我们让 LLM 做的是 pointwise 排序的简化版本。具体做法:
把候选文档(默认 10 条)编号后连同用户查询一起送给 lite 模型,prompt 要求模型按相关性从高到低输出编号序列。比如输入 10 个文档,模型输出 "2,0,5,1,3,8,4,7,6,9",表示第 2 号文档最相关,第 9 号最不相关。
我们考虑过用 Cross-Encoder 模型做 reranking——比如智源研究院的 BGE-Reranker-v2-m3 $TRAE_REF。Cross-Encoder 把查询和文档拼成一个输入序列送进模型,联合建模后输出 0~1 之间的相关性分数。精度确实比 LLM prompt 排序更高。
但我们最终选择了 LLM 方案,原因是部署成本。Cross-Encoder 需要额外部署一个模型服务(BGE-Reranker-v2-m3 约 2.8GB $TRAE_REF),而我们已经在用 lite 模型做 Query Rewriting,Reranking 复用同一个模型 API,零额外部署成本。对于我们的知识库规模(数千条文档),LLM reranking 的精度足够。如果未来知识库扩展到数万条以上,或者对精度有更高要求,可以切换到 Cross-Encoder——接口已经抽象好了,替换实现类即可。
候选数量控制。 Reranking 默认只处理前 10 条候选。送太多文档给模型,prompt 会变长,延迟增加,精度也会下降(模型注意力被稀释)。10 条是在延迟和覆盖率之间的平衡点。
降级策略。 和 Query Rewriting 一样,模型调用失败时静默降级,返回 RRF 融合后的原始排序。Reranking 是精度优化,不是功能依赖。
架构设计:SDK 层 + SPI 接口
三个增强技术的实现没有放在 cct-crm-backend 业务系统里,而是下沉到了我们的 ai-sdk 核心库。原因是这些能力不是外贸业务特有的——任何使用 RAG 的系统都能复用。
架构分三层:
┌─────────────────────────────────────────┐
│ cct-crm-backend (宿主系统) │
│ RagEnhancementProviderImpl │
│ → callLiteModel (HTTP 调用 LLM API) │
│ → keywordSearch (MySQL LIKE 检索) │
│ → getConfig (读取云端配置) │
└──────────────┬──────────────────────────┘
│ implements SPI
┌──────────────▼──────────────────────────┐
│ ai-sdk-core (SDK 核心层) │
│ RagEnhancementService │
│ → rewriteQuery (Prompt 构建 + 缓存) │
│ → reciprocalRankFusion (RRF 算法) │
│ → hybridSearch (编排: 调 Provider + RRF)│
│ → rerankResults (Prompt 构建 + 解析) │
│ → RagSearchResult (数据类) │
└──────────────┬──────────────────────────┘
│ uses
┌──────────────▼──────────────────────────┐
│ RagEnhancementProvider (SPI 接口) │
│ → callLiteModel │
│ → keywordSearch │
│ → getConfig │
└─────────────────────────────────────────┘
核心算法(RRF、Prompt 构建、结果解析)在 SDK 层,零 Spring 依赖。宿主系统只需实现 SPI 接口,提供 LLM 调用、关键词检索和配置读取三项能力。这样设计的好处是:未来如果有其他系统接入 ai-sdk,只需实现 Provider 接口就能获得完整的 RAG 增强能力,不需要重复开发。
配置开关全部在数据库。 五个配置项存在云端 pb_ai_config 表,model_group = 'rag_enhancement':
| 配置键 | 默认值 | 说明 |
|---|---|---|
rag_query_rewrite_enabled |
true | Query Rewriting 开关 |
rag_hybrid_search_enabled |
true | Hybrid Search 开关 |
rag_reranking_enabled |
true | Reranking 开关 |
rag_hybrid_candidate_count |
20 | 关键词检索召回数量 |
rag_rerank_candidate_count |
10 | Reranking 候选数量 |
配置 5 分钟刷新一次,改了数据库立即生效(最多等 5 分钟)。每个增强功能都有独立开关,可以单独关闭。如果某个增强功能在特定场景下表现不好,或者成本需要控制,可以只关掉它而不影响其他两个。
效果与成本
三个增强功能上线后,我们在内部测试中观察到的变化:
检索准确率。 我们准备了 50 组测试查询(覆盖客户查询、订单查询、产品查询、出运查询、政策查询五类场景),对比增强前后的 top-1 和 top-3 命中率。增强前 top-1 命中率约 68%,增强后约 82%,提升 14 个百分点。top-3 命中率从 85% 提升到 94%。这个数据是内部测试,不是严格的学术评测,但趋势是明确的。
延迟增加。 三层增强全开时,单次检索延迟从 ~200ms 增加到 ~800ms-1.2s(主要是两次 LLM 调用)。对于知识库检索场景,这个延迟是可以接受的——用户等 1 秒拿到更准的结果,比等 200ms 拿到不相关的结果体验好得多。如果延迟敏感,可以关掉 Reranking(贡献了约 60% 的额外延迟),只保留 Query Rewriting + Hybrid Search,延迟增加约 300ms。
成本。 Query Rewriting 和 Reranking 都用 lite 模型,单次调用约消耗 200-500 tokens。按我们的 API 定价,单次完整增强检索成本约 0.001-0.002 元。配合 5 分钟缓存,高频重复查询不会反复触发模型调用,实际成本更低。
未来方向
三个增强技术落地后,RAG 的精度确实提升了。但朴素 RAG 的天花板不止于此。我们在规划下一步的时候,看到了几个值得关注的方向。
Cross-Encoder 替代 LLM Reranking。 当前用 lite 模型做 Reranking 是成本导向的妥协。如果部署 BGE-Reranker-v2-m3 这样的专用重排序模型,精度还能再提一截 $TRAE_REF。Cross-Encoder 的优势在于它能对查询和文档做联合编码,理解两层文本之间的细粒度关系,而 LLM 只是在做 prompt 排序。接口已经抽象好,切换是工程量问题,不是架构问题。
Context Engineering。 2025 年行业开始从 RAG 向 Context Engineering 演进 $TRAE_REF。RAG 关注的是"检索到什么",Context Engineering 关注的是"送给模型的上下文怎么构造"。除了检索结果,还应该整合用户角色、实时数据、历史对话、权限边界等多维信息,让模型拿到的不是一个文档列表,而是一段结构化的、带上下文的工作指令。我们的因果模型雏形(经验学习 + 模型路由)其实已经在做 Context Engineering 的事情——把经验注入 system prompt 就是上下文构造。下一步是把 RAG 检索结果和经验注入、模型路由更深度地融合。
Multimodal RAG。 外贸业务中有大量视觉格式知识——产品图片、质检报告扫描件、装箱单照片 $TRAE_REF。当前 RAG 只处理文本,这些视觉信息要么人工录入为文字描述(信息丢失),要么完全无法被 AI 检索。Multimodal RAG 用视觉语言模型直接对图片做 Embedding,让 AI 能"看懂"产品照片并检索。这对我们的产品知识库和质检模块价值很大。
Agentic RAG。 当前的 RAG 是单轮检索——用户问一次,系统搜一次。Agentic RAG 让 AI 自主决定是否需要多次检索、跨域检索、或基于初步结果做追问检索 $TRAE_REF。比如用户问"这个客户过去三年的采购趋势和行业平均水平对比如何",AI 可能需要先查客户订单数据,再查行业基准数据,然后自己做对比分析。这需要 Agent 架构支持,我们目前还没有上 Agent,但这是明确的方向——当单轮检索无法满足复杂查询需求时,Agentic RAG 是自然的演进路径。
总结
三个技术,一个目标:让向量检索从"能搜"变成"搜得准"。
Query Rewriting 解决输入端的问题——用户口语化提问和检索引擎期望的关键词输入之间的鸿沟。Hybrid Search 解决检索端的盲区——向量检索和关键词检索各有短板,RRF 融合让两路结果互补。Reranking 解决排序端的偏差——向量相似度高不等于业务相关度高,LLM 做最后一道语义筛选。
三层叠加后,检索精度从 68% 提升到 82%(top-1 命中率),单次检索成本约 0.001-0.002 元,延迟增加可控。三个功能都有独立开关,可以按需开启关闭。
这套增强流水线不是一个终点。RAG 的下一个阶段是 Context Engineering——从"检索文档"走向"构造上下文",从"返回结果"走向"交付答案"。我们已经在路上了。
评论