概述

在智能客服问答系统中,检索(Retrieval) 是整个系统的第一道关卡——它的任务是从海量知识库中快速找出与用户问题最相关的候选文档片段,再由下游的 LLM 进行最终回答生成。检索质量直接决定了整个问答系统的天花板。

本文深入剖析我们为智能问答系统设计的检索架构。系统采用了三层检索模式 × 五种匹配算法 × 两种融合策略 × 九级渐进式文件名匹配 × Cross-encoder 精排的立体化设计。下面逐一拆解。


一、系统概览

整个检索管线从顶层到底层分为三个层次:

用户问题
    │
    ▼
┌─────────────────────────────┐
│     检索模式 (Mode)         │  ← navigate / index / direct
├─────────────────────────────┤
│     匹配算法 (Matcher)      │  ← gram / bm25 / vector / hybrid / ensemble
├─────────────────────────────┤
│     多路融合 (Fusion)       │  ← weighted / rrf
├─────────────────────────────┤
│     文件名模糊匹配          │  ← 9级渐进匹配
├─────────────────────────────┤
│     Cross-encoder 精排      │  ← BAAI/bge-reranker-v2-m3
├─────────────────────────────┤
│     多级缓存                │  ← encode / bm25 / splade / colbert
└─────────────────────────────┘

二、三层检索模式

retriever.py 中定义了三种检索模式,通过配置 INDEX_READ_MODE 切换:

2.1 Navigate 模式(推荐)

这是系统默认且最推荐的模式。核心思想是:让 LLM 先读索引导航,理解知识库的整体结构和分区信息,再决定从哪些索引区块中检索。

Navigate 模式流程:
LLM 读取索引导览 → 理解知识库分区
         │
         ▼
  Map-Reduce 选项控制并发
  (INDEX_READ_MODE 配置)
         │
         ▼
  在选定的索引分区中执行检索

关键特性:

  • 避免 LLM 直接面对海量原始文本,解决"注意力溃散"问题
  • Map-Reduce 模式可将大索引拆分为多个子任务并行处理
  • 适合知识库分区明确、文档结构清晰的场景

2.2 Index 模式

两层管线设计:

第一阶段(粗筛): BM25 / Gram / Vector 快速过滤
         │
         ▼
第二阶段(精排): LLM 对粗筛结果进行重排序
  • 粗筛阶段:使用轻量算法快速从海量文档中过滤出 Top-K 候选
  • 精排阶段:LLM 利用语义理解对候选结果进行精确排序
  • 适合对精度要求高、但可接受一定延迟的场景

2.3 Direct 模式

最直接的方案——将全文内容直接喂给 LLM:

全文直给 → LLM 自行理解并回答
(DIRECT_MAX_TOTAL_CHARS 控制截断)
  • 实现最简单,省去了复杂的检索逻辑
  • 受限于上下文窗口大小,DIRECT_MAX_TOTAL_CHARS 控制最大输入长度
  • 适合小型知识库或对实时性要求极高的场景

三、五种匹配算法

matcher.py 是实现检索引擎的核心模块,提供了从字符到语义、从单一到混合的完整算法家族。

3.1 Gram 匹配(2-gram)

原理:将查询和文档都拆分为 2-gram(连续两个字符的组合),通过 Jaccard 相似度计算匹配分数。

例子: "申请" → {"申", "申请", "请"}
     "审批" → {"审", "审批", "批"}

适用场景:中文文本的字符级模糊匹配,对拼写错误和局部字符变化有较好的容忍度。速度快,适合做第一道粗筛。

3.2 BM25 匹配

原理:经典的 BM25Okapi 概率检索模型,对 TF-IDF 做了非线性优化。

# 伪代码: BM25 概率检索
# BM25 对词频做了饱和处理(不是简单的 TF-IDF 线性累加)
# 加入了文档长度归一化因子(短文档不容易被稀释)

bm25 = build_bm25_index(corpus)   # 对文档集合建立 BM25 索引
scores = bm25.search(query)        # 检索并返回排序结果

核心优势:

  • 成熟的工业级算法,经过数十年验证
  • 对高频词的惩罚机制,避免常见词干扰
  • 文档长度归一化,避免长文档天然占优
  • 计算效率高,适合海量文档粗筛

3.3 Vector 向量匹配

原理:使用 SentenceTransformer 将查询和文档编码为稠密向量,通过余弦相似度计算语义匹配度。

# 伪代码: 向量匹配示意
# 使用 SentenceTransformer 将文本编码为向量
model = load_vector_model("multilingual-embedding-model")
query_vec = encode_text(model, query)
doc_vec = encode_text(model, doc)
score = cosine_similarity(query_vec, doc_vec)  # 0~1 越接近 1 越相关

核心优势:

  • 理解语义而非字面匹配 — "怎么做红烧肉"和"红烧肉的做法"语义相近
  • 跨语言对齐能力(使用多语言模型时)
  • 对同义词、近义词、句式变换天然鲁棒

3.4 Hybrid 混合匹配

原理:将 BM25 和 Vector 的分数通过加权求和融合。

score = HYBRID_BM25_WEIGHT * bm25_norm_score + HYBRID_VECTOR_WEIGHT * vector_norm_score

可配置权重:

  • HYBRID_BM25_WEIGHT:BM25 的贡献权重(默认 0.3)
  • HYBRID_VECTOR_WEIGHT:向量检索的贡献权重(默认 0.7)

设计意图:

  • BM25 保证关键词精确匹配的召回率
  • Vector 补充语义相关但字面不匹配的结果
  • 权重可调节,适应不同业务场景(精确匹配优先 vs 语义理解优先)

3.5 Ensemble 集成匹配

原理:Gram + BM25 + Vector 三路融合,引入了 consensus_bonus(一致性奖励) 机制。

# 伪代码示意
gram_score = gram(query, doc)
bm25_score = bm25(query, doc)
vector_score = vector(query, doc)

# 归一化后融合
consensus_count = count_agreed_algorithms()
bonus = consensus_bonus * consensus_count
final_score = (gram_score + bm25_score + vector_score) / 3 + bonus

核心创新点:

  • 三路互补:Gram 抓字符模式、BM25 抓关键词、Vector 抓语义
  • 一致性奖励:多个算法同时命中的结果获得额外加分,提高高置信结果的排序位置
  • 比 Hybrid 更稳健——单一算法偏差被其他算法平均化

算法选择指南

算法 速度 精度 语义理解 容错能力 推荐场景
Gram ⚡⚡⚡⚡⚡ ★★☆ 字符级 第一道粗筛
BM25 ⚡⚡⚡⚡ ★★★ 关键词精确匹配
Vector ⚡⚡ ★★★★★ 语义搜索
Hybrid ⚡⚡⚡ ★★★★ 部分 默认通用方案
Ensemble ★★★★★ 极高 高精度要求

四、多路融合策略

当使用多个匹配算法时,需要将不同算法的分数统一合并。系统提供了两种融合策略(由 fusion 参数控制):

4.1 Weighted 加权融合

原理:先将各算法得分归一化到 [0,1] 区间,再按预设权重加权求和。

步骤:
1. Min-Max 归一化各算法分数
2. 加权求和: final = Σ(weight_i × score_i)
3. 按 final 降序排列

优点:保留了各算法的分数差异信息,对高分结果更敏感。

4.2 RRF(Reciprocal Rank Fusion)

原理:不看具体分数,只看每个文档在各算法结果中的排名位置。

# RRF 核心公式
score(d) = Σ 1 / (k + rank_i(d))
# rank_i(d) 是文档 d 在算法 i 中的排名
# k 是 RRF_K 参数(通常为 60)

优点:

  • 不需要跨算法的分数归一化(不同算法的分数分布可能差异极大)
  • 对异常值不敏感——某个算法给了一个离谱的高分也不会破坏结果
  • 实现简单,效果稳定

RRF_K 参数:控制排名倒数的衰减速度。k 越小,排名靠前的文档权重越大;k 越大,排名差异的影响越小。

何时选择哪种融合?

问:不同算法的分数分布差异很大?
答:用 RRF,跳过归一化环节

问:分数可控且分布稳定?
答:用 Weighted,保留分数信息

问:需要极端精确的 Top-1?
答:用 RRF(对排名更敏感)

五、文件名的九级渐进式模糊匹配

除了正文内容检索,系统还实现了文件名的多级模糊匹配——这在实际运维中非常有用:用户经常用自己的记忆或上下文中的片段来寻找某个特定文档。

匹配度从精确到模糊,分为 9 个级别

级别 策略 描述 示例
L1 精确匹配 完全一致 “系统文档操作手册.md” → 精确命中
L2 容错匹配 忽略大小写、全半角、空格 “云 文档 手册” → 命中
L3 子串匹配 查询是文件名的一部分 “文档” → 命中
L4 BM25 匹配 关键词概率检索 “文档 操作” → 计算分数
L5 拼音匹配 中文拼音模糊匹配 “yun shipu” → 命中"系统文档"
L6 Gram 匹配 2-gram 字符匹配 “文档” vs “文档系统”
L7 Hybrid 匹配 BM25 + Vector 混合 关键词+语义综合
L8 Vector 匹配 向量语义匹配 语义相近的文件名
L9 Ensemble 匹配 三路集成+一致性奖励 最高召回率

设计思路:从精确逐步放宽到模糊,一旦某一级找到足够结果就不再继续下一级。这样既能保证精确查询的快速响应,又能在模糊查询时尽可能"捞"回相关文档。


六、Cross-encoder 精排(Reranker)

粗筛得到的候选结果集虽然覆盖率高,但排序可能不够精确。为此系统引入了 Cross-encoder 精排模块 reranker.py

技术选型

使用 BAAI/bge-reranker-v2-m3 作为 reranker 模型:

# 伪代码: Cross-encoder 精排示意
# 使用 Cross-encoder 模型对 query-doc 对进行相关性打分
model = load_cross_encoder_model("reranker-model")

# 将 query 和 doc 拼接输入,输出相关性分数
pairs = [(query, doc1), (query, doc2), ...]
scores = model.predict(pairs)  # 每个 pair 得到一个相关性分数

# 按分数降序排列,取 top_n 作为精排结果

为什么需要 Cross-encoder?

Bi-encoder(向量检索) Cross-encoder(精排)
编码方式 Query 和 Doc 独立编码 Query 和 Doc 拼接编码
交互层级 浅层(点积/余弦) 深层(交叉注意力)
推理速度 快(可预计算) 慢(必须实时计算)
精度 中等

典型管线

Bi-encoder 检索出 Top-100 候选
         ↓
Cross-encoder 对 Top-100 逐一打分重排
         ↓
Top-5 送入 LLM 生成最终回答

RERANKER_MODE 控制

  • reranker.disabled:关闭精排,仅用粗筛结果
  • reranker.enabled:启用精排,对粗筛 Top-K 结果重排
  • 可在配置中动态开关,适应不同负载场景

七、索引读取增强:Map-Reduce 模式

在处理大规模知识库时,单一索引可能因为内容过度集中而导致 LLM 的注意力溃散(attention dilution)——即模型无法从海量信息中聚焦到真正相关的部分。

Map-Reduce 解决方案

大索引
    │
    ├── Map 分区 1 → 检索候选
    ├── Map 分区 2 → 检索候选
    ├── Map 分区 3 → 检索候选
    └── Map 分区 N → 检索候选
            │
            ▼
        Reduce 合并 → 去重 → 排序

实现要点:

  • INDEX_READ_MODE 控制是否启用 Map-Reduce
  • 每个分区独立检索,互不干扰
  • Reduce 阶段合并结果,去重后按综合分数排序
  • 可以配合 Navigate 模式,让 LLM 先决定访问哪些分区

为什么需要 Map-Reduce?

  1. 注意力聚焦:每个分区内容相关且紧凑,LLM 更容易聚焦
  2. 并行加速:多个分区可以并发检索,减少总延迟
  3. 容错性:某个分区检索失败不影响其他分区
  4. 可水平扩展:知识库增长只需增加分区,无需改造架构

八、缓存机制设计

为了加速重复查询,系统实现了多级缓存,覆盖了从向量计算到索引构建的各个环节:

8.1 _encode_cache — 向量编码缓存

# 缓存 SentenceTransformer 的编码结果
# 相同文本避免重复编码
_cache = {}  # {text: vector}

def encode(text):
    if text in _encode_cache:
        return _encode_cache[text]
    vector = model.encode(text)
    _encode_cache[text] = vector
    return vector

8.2 _bm25_cache — BM25 索引缓存

  • 缓存已经构建好的 BM25 索引(词频矩阵 + 文档列表)
  • 避免每次查询都重新构建 BM25 索引
  • 当知识库更新时自动失效

8.3 _splade_cache — SPLADE 稀疏向量缓存

  • SPLADE 模型将文本编码为稀疏向量(词汇权重分布)
  • 缓存稀疏向量的计算结果,加速重复查询

8.4 _colbert_cache — ColBERT 细粒度匹配缓存

  • ColBERT 使用"后期交互"(late interaction)进行细粒度匹配
  • 缓存 token-level 的向量表示,加速交互计算

缓存策略总结

缓存 缓存对象 目的 失效条件
_encode_cache 文本 → 向量 避免重复编码 进程重启
_bm25_cache BM25 索引 避免重复构建 知识库更新
_splade_cache SPLADE 稀疏向量 加速稀疏检索 模型切换
_colbert_cache ColBERT token 向量 加速后期交互 模型切换

九、整体工作流串联

将以上所有组件串联起来,一次完整的检索流程如下:

用户提问: "如何在系统文档中创建新文档?"
    │
    ▼
1. 选择检索模式 (navigate / index / direct)
    │  navigate: LLM 读索引导航 → 选择索引分区
    │
    ▼
2. 文件名模糊匹配 (9级渐进)
    │  检查是否有文件名对应文档
    │
    ▼
3. 执行匹配算法
    │  ┌─ Gram (2-gram 字符匹配)
    │  ├─ BM25 (概率检索)
    │  ├─ Vector (语义检索)
    │  ├─ Hybrid (BM25 × W1 + Vector × W2)
    │  └─ Ensemble (Gram + BM25 + Vector + 一致性奖励)
    │
    ▼
4. 多路融合
    │  Weighted 或 RRF 合并各路分数
    │
    ▼
5. Cross-encoder 精排
    │  BAAI/bge-reranker-v2-m3 逐一对候选打分
    │
    ▼
6. 缓存写入
    │  缓存编码结果和索引
    │
    ▼
7. Top-K 结果送入 LLM
    │  LLM 结合上下文生成最终回答
    │
    ▼
    "您可以进入系统B → 文档管理 → 新建文档..."

十、性能考量与最佳实践

延迟分析

阶段 耗时估计(1000文档库) 说明
Gram 匹配 < 1ms 纯字符串操作
BM25 匹配 1-3ms 预索引后很快
Vector 编码 50-100ms 模型推理耗时
Hybrid 融合 < 1ms 简单加权计算
Ensemble 融合 < 1ms 三路分数合并
Cross-encoder 重排(×10) 200-500ms 逐个拼接推理
LLM 生成回答 1-3s 取决于模型大小

配置建议

小型知识库 (< 1000 文档):
  → Direct 模式 或 Index + BM25 即可
  → 无需 Cross-encoder

中型知识库 (1000-10000 文档):
  → Navigate 模式 + Hybrid 或 Ensemble
  → 建议开启 Cross-encoder

大型知识库 (> 10000 文档):
  → Navigate + Map-Reduce + Ensemble
  → 必须开启 Cross-encoder
  → 建议开启所有缓存

总结

本文详细拆解了智能客服问答系统的检索架构,核心设计理念可以总结为 “多路并行、层层递进、由粗到精”

  1. 三层检索模式提供了从"让 LLM 导航"到"全文直给"的灵活选择
  2. 五种匹配算法覆盖了字符级(Gram)、关键词级(BM25)、语义级(Vector)以及混合级(Hybrid/Ensemble)的全频谱需求
  3. Weighted/RRF 双融合策略解决了多算法分数归一化和融合的问题
  4. 九级文件名渐进匹配将模糊匹配细粒度分级,兼顾速度和召回率
  5. Cross-encoder 精排在最后关口用注意力机制确保 Top 结果的准确性
  6. 四层缓存设计大幅减少了重复计算的开销

这套架构已经在多个产品线的知识库问答中得到了验证,能够稳定处理数万级别的文档检索,既保证了召回率,也保证了排序的精准度。

下一篇文章预告:我们将深入探讨 FAQ 系统中的"答案生成与引用溯源"模块——当检索到相关文档后,如何让 LLM 生成准确、可追溯、无幻觉的回答。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐