Day 17 — Reranker 深度集成

Spring Boot 3.4.3 | LangChain4j 1.13.1 | Java 17 | PGVector + Redis

在 Day 16 混合检索的基础上,深入 Reranker 环节的三大升级:
Redis 结果缓存(省钱 + 提速)+ 多模型对比(量化 Reranker 价值)+ 对照实验(用数据说话)


目录

  1. Day 17 比 Day 16 多了什么?
  2. 从零理解核心概念
  3. 快速启动
  4. API 端点详解
  5. 项目结构详解
  6. 核心配置说明
  7. 架构与数据流
  8. 测试指南(小白友好)
  9. 面试怎么说
  10. 常见问题与踩坑
  11. 后续学习方向

前置知识要求

读完本 README 之前,你最好已经完成 Day 16(混合检索),理解以下概念:

  • 向量检索(Bi-Encoder / Embedding)和关键词检索(N-gram + ILIKE)
  • RRF(Reciprocal Rank Fusion)融合算法
  • PGVector 的基本用法

Day 17 不会重复讲这些基础,而是聚焦在"Reranker 这一环如何做得更深更好"。


一、Day 17 比 Day 16 多了什么?

一句话总结:Day 16 把混合检索搭起来了,Day 17 在 Reranker 环节做了三件事让它从"能跑"到"能说清楚好在哪里"。

能力 Day 16 Day 17
混合检索流水线 ✅ 向量 + 关键词 + RRF + Reranker ✅ 不变(核心逻辑复用)
Reranker 缓存 ❌ 每次搜索都调 API(慢 + 花钱) Redis 缓存 — 同 query 第一次 200ms,第二次 ≤5ms
Reranker 模型对比 ❌ 只用 BGE-Reranker-v2-m3 ✅ 同时对比 BGE 和 Qwen3,看重叠度
对照实验 ❌ 只有主观感受 ✅ 有 Rerank vs 无 Rerank 的量化数据(排名变化/耗时/重叠)
前端教学界面 检索 / RAG 双模式 检索 / 实验 / 模型对比 / RAG 四模式 + 每个 Tab 有教学引导
对外依赖 PostgreSQL 一个 PostgreSQL + Redis 两个

新增了哪些类?

包路径 职责
RerankCacheService com.day17.demo.rag Redis 缓存:读、写、清除 Reranker 结果
RedisConfig com.day17.demo.config Redis 连接 + 序列化策略
ExperimentResult com.day17.demo.core 实验对照数据 DTO(11 个字段承载完整分析)

增强了哪些类?

改动
RerankService + rerankWithModel() 支持指定模型、+ compareModels() 双模型对比、+ Redis 缓存集成
SearchController + /experiment 实验端点、+ /experiment/compare-models 模型对比端点

二、从零理解核心概念

2.1 Cross-Encoder vs Bi-Encoder — 为什么你需要的不是 Embedding,而是 Reranker?

这是 Day 17 最重要、面试最常考的知识点。

场景假设

你的知识库里有 500 篇文档,用户搜"码哥科技的核心产品是什么?"。

方案 A:全部用 Embedding(Bi-Encoder)

用户 query → Embedding 模型 → [0.12, 0.34, ...](1024 维向量)
每篇文档 → Embedding 模型 → [0.09, 0.41, ...](1024 维向量)
相关性 = cos(query向量, 文档向量)  ← 两个向量的余弦距离

优点:文档向量可以提前算好存 PGVector,搜索时只算 query → 极快(10-50ms)
缺点:query 和文档在"编码阶段没见过面"——它们的向量是独立算出来的
      "我想把钱拿回来" 和 "客户要求退货" 可能余弦距离很远(虽然意思一样)

方案 B:全部用 Reranker(Cross-Encoder)

(query "怎么退款", 文档1 "退款流程如下...") → Reranker → 相关性 0.97
(query "怎么退款", 文档2 "公司简介...")     → Reranker → 相关性 0.03
(query "怎么退款", 文档3 "SDK集成...")      → Reranker → 相关性 0.12
...

优点:query 和文档同时输入 → Transformer Attention 互相"看到"每个字 → 精度极高
缺点:500 篇文档 → 500 次推理 ≈ 5-10 秒(用户早跑了,API 费也受不了)

方案 C(最优,就是 Day 16/17 的架构)

500 篇文档
  ↓ Bi-Encoder 粗筛(15ms,可离线预计算)
 20 篇候选
  ↓ Cross-Encoder 精排(200-800ms,只算 20 对)
 5 篇最终结果
维度 Bi-Encoder(Embedding) Cross-Encoder(Reranker)
输入方式 分别编码 query 和 doc,各自算向量 (query, doc) 成对联合编码,Attention 交叉理解
速度 快 — 文档可预计算 慢 — 每对都要算一遍
精度 低 — 编码阶段无交互 高 — query 每个字都"见到"文档每个字
规模 百万级粗筛 20-50 条精排
代表模型 BAAI/bge-large-zh-v1.5 BAAI/bge-reranker-v2-m3

一句人话:Embedding 是海选,Reranker 是决赛评审。RAG 的标准流程是海选 + 决赛,不是只靠其中一个。

2.2 为什么需要多模型对比?

BGE-Reranker-v2-m3 和 Qwen3-Reranker-0.6B 都是中文 Reranker,但训练数据和偏好不同:

模型 参数量 来源 可能偏好
BAAI/bge-reranker-v2-m3 ~568M BGE 系列 中文通用场景的标杆
Qwen/Qwen3-Reranker-0.6B 0.6B Qwen 系列 长文本理解可能更好

同一个 query 两个模型给出的 Top-5 可能不完全一样——这就是为什么要对比。

👉 面试官问"你们怎么选 Reranker 模型?“你回答的不是"我们随便挑了一个”,而是:“我们在同一候选集上跑两个模型,计算 Top-5 重叠率。如果重叠率 > 80%,排序稳定;如果差异大,我们会人工评审几组 case 再决定。” 这就是 /experiment/compare-models 端点在做的事。

2.3 Precision@K — 怎么量化"排得好不好"?

Precision@K 是信息检索(IR)领域的经典指标:

Precision@K = 前 K 个结果中「确实相关」的文档数 ÷ K

示例:用户搜"退款流程"
  RRF 直接 Top-5 → 退款✓ 公司✗ 退款✓ API✗ 退款✓ → Precision@5 = 3/5 = 60%
  Reranker Top-5 → 退款✓ 退款✓ 退款✓ API✗ 退款✓ → Precision@5 = 4/5 = 80%

Reranker 让 Precision@5 从 60% 提升到 80% — 这就是它的量化价值。

⚠️ Day 17 的 /experiment 端点不直接算 Precision@K(因为需要人工标注"这篇文档是否真的相关"),但提供了排名变化、重叠度、得分差异——你人工看排名变化的文档是否确实更相关,就能估算 Precision 的提升。

2.4 Redis 缓存 — 为什么同一个问题不重复调 API?

Reranker API 调用有双重成本

成本类型 说明
金钱 硅基流动按输入 Token 收费。20 篇文档 × 512 字 × 2 = ~20K token/次
时间 Cross-Encoder 推理 200-800ms

缓存策略

Key 格式:   reranker:cache:v1:{MD5(query + "::" + modelName)}
Value 格式: JSON(List<HybridSearchResult>)
TTL:       60 分钟(可在 application.yml 中修改)

示例:
  用户搜"怎么退款" → MD5("怎么退款::BAAI/bge-reranker-v2-m3")
  → Key = "reranker:cache:v1:a1b2c3d4..."

效果对比

无缓存 有缓存
第 1 次 API 调用 ~300ms + 消耗 Token API 调用 ~300ms + 消耗 Token
第 2-100 次 API 调用 × 99 次 Redis 秒回 ≤5ms + 免费

缓存的是同一个 query + 模型 的结果。不同模型各自独立缓存。

为什么不缓存空结果? 搜索返回空可能是临时网络故障——如果缓存空结果,下次正常了也看不到。宁可多调一次 API,也不能让 bug 被缓存放大。


三、快速启动

3.1 准备环境

# 1. 启动 PostgreSQL + PGVector
docker run -d --name pg17 -p 5432:5432 \
  -e POSTGRES_DB=aidb \
  -e POSTGRES_USER=ai \
  -e POSTGRES_PASSWORD=password \
  pgvector/pgvector:pg17

# 2. ⭐ 启动 Redis(Day 17 新增依赖)
docker run -d --name redis17 -p 6379:6379 \
  redis --requirepass password

# 3. 验证连通性
docker ps | grep -E "pg17|redis17"

3.2 配置 API Key

# 环境变量(Jasypt 解密密钥)
export JASYPT_PASSWORD=你的加密密钥

# 如果你还没有加密 API Key,需要先在 config 目录下运行加密工具
# java -cp jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI \
#   input="sk-xxxx" password="$JASYPT_PASSWORD" algorithm=PBEWithMD5AndDES

3.3 启动应用

cd day17-reranker-deep
mvn spring-boot:run
# 等待日志出现:Day17 混合检索后台启动于端口 8089
# 首次启动会看到 [DataInit] 初始化知识库(约 5-10 秒)

3.4 打开浏览器

http://localhost:8089

首页会自动展示四个标签页:检索、实验、模型对比、RAG 对话。每个标签页顶部都有教学引导说明。


四、API 端点详解

所有端点都返回统一的 ApiResult 包装(code=200 表示成功):

{ "code": 200, "message": "success", "data": ... }

4.1 GET /search — 完整混合检索

最常用的端点,走完整的三阶段流水线(向量 + 关键词 → RRF 融合 → Reranker 精排),自动享受 Redis 缓存。

curl "http://localhost:8089/search?query=码哥科技的核心产品是什么&table=day4_rag_store"

参数

参数 必填 默认值 说明
query 任意中文/英文搜索问题
table day4_rag_store PGVector 表名

响应

{
  "code": 200,
  "message": "success",
  "data": [
    {
      "id": "uuid-1",
      "text": "【码哥科技】码哥科技的主打产品是码哥AI中台...",
      "metadata": "{\"source\":\"码哥科技\",\"category\":\"general\"}",
      "score": 0.9512,
      "source": "rerank"
    },
    ...
  ]
}

字段含义

字段 类型 说明
id String 文档在 PGVector 中的唯一 ID
text String 文档片段原文
metadata String JSON 格式元数据(来源、分类等)
score Double Cross-Encoder 相关性分数 [0, 1],越高越相关
source String 结果来源:"rerank"(Reranker 精排)

Day 17 增强:同一个 query 第二次请求时,Reranker 结果自动走 Redis 缓存。日志中会看到 [RerankCache] ✔ 命中。响应时间从 ~300ms 降到 ≤5ms。

4.2 GET /experiment ⭐ — 对照实验(Day 17 核心端点)

【这就是量化 Reranker 价值的端点】

对同一个 query,同时给出两条路径的结果:

  • 路径 A(基线):RRF 融合 → 直接取 Top-N(相当于"没有 Reranker 的效果")
  • 路径 B(Reranker):RRF 融合 → Reranker 精排 → Top-N(相当于"有 Reranker 的效果")
curl "http://localhost:8089/experiment?query=怎么退款&topN=5&table=day4_rag_store"

参数

参数 必填 默认值 说明
query 搜索问题
topN 5 最终返回条数
table day4_rag_store PGVector 表名

响应(核心字段):

{
  "code": 200,
  "data": {
    "query": "怎么退款",
    "rerankModel": "BAAI/bge-reranker-v2-m3",

    "baselineResults": [  ← 路径 ARRF 直接 Top-5
      { "id": "...", "text": "...", "score": 0.031, "source": "rrf_baseline" }
    ],
    "baselineTimeMs": 45,           ← 基线总耗时(只有召回)

    "rerankedResults": [  ← 路径 B:Reranker 精排 Top-5
      { "id": "...", "text": "...", "score": 0.9512, "source": "rerank" }
    ],
    "rerankedTimeMs": 320,          ← Reranker 总耗时(召回 + API"rerankApiTimeMs": 275,         ← Reranker API 单独耗时
    "overlapCount": 3,              ← 两条路径有多少条相同结果

    "rankChanges": [                ← 排名如何变化
      "#3 → #1 (↑ 2)",
      "#1 → #3 (↓ 2)",
      "#5 → 淘汰 🔻(被挤出 Top-5)"
    ],
    "baselineAvgScore": 0.028,      ← 基线平均 RRF"rerankedAvgScore": 0.912,      ← Reranker 平均分
    "scoreDiffSummary": "Reranker 使平均得分从 0.028 提升到 0.912(+3157%)"
  }
}

实验分析方法(按优先级排序):

  1. overlapCount — Top-5 中两条路径有多少条相同?

    • 5/5:Reranker 和 RRF 意见一致(Reranker 在这个 query 上没发挥价值)
    • 2/5:Reranker 选出了完全不同的结果(增量价值大)
  2. rankChanges — 有哪些文档因 Reranker 升了/降了?

    • 第一名被降级 → Reranker 在纠正 RRF 的排序偏差
    • 排名不变 → RRF 本身已经排得很好
  3. rerankApiTimeMs — Reranker 增加了多少延迟?

    • 50-100ms:缓存命中(秒回)
    • 200-800ms:首次 API 调用(可接受)
    • 2000ms:网络问题或知识库太大

  4. scoreDiffSummary — 得分区分度变化

    • RRF 分分布在 [0.01, 0.05] 之间(区分度低)
    • Reranker 分分布在 [0.03, 0.97] 之间(区分度高,好坏一目了然)

4.3 GET /experiment/compare-models ⭐ — 双模型对比

同一份候选集,同时用两个 Reranker 模型排序。用于回答"换更大模型值不值"。

curl "http://localhost:8089/experiment/compare-models?query=SDK初始化失败怎么排查&topN=5"

响应

{
  "code": 200,
  "data": {
    "query": "SDK初始化失败怎么排查",
    "models": {
      "bge-reranker-v2-m3": [
        { "id": "...", "text": "...", "score": 0.972, "source": "rerank" }
      ],
      "Qwen3-Reranker-0.6B": [
        { "id": "...", "text": "...", "score": 0.951, "source": "rerank" }
      ]
    },
    "candidateCount": 20,
    "note": "两个 Reranker 模型对同一候选集进行重排序的结果...",
    "cacheTtlMinutes": 60,
    "cacheHint": "此对比实验同样享受 Redis 缓存"
  }
}

分析维度

  1. 看 Top-1 — 两个模型排在第一的是不是同一篇文档?
  2. 看重叠度 — 手动比较 Top-5 的有多少条相同
  3. 看得分差异 — 哪个模型的 score 区分度更高?

4.4 GET /rag/chat — RAG 对话(检索 + 组装 Prompt)

搜索知识库 + 组装好 Prompt 模板,返回给你。你可以拿着 Prompt 直接发给任何大模型。

curl "http://localhost:8089/rag/chat?message=SDK初始化失败怎么排查"

响应

{
  "code": 200,
  "data": {
    "query": "SDK初始化失败怎么排查",
    "documents": [ ... 检索到的 Top-5 文档 ... ],
    "prompt": "你是一个知识库助手。请根据以下参考资料回答用户问题。\n\n== 参考资料 ==\n【资料1】...",
    "hint": "将此 prompt 发送给 LLM 即可获得 RAG 增强回答"
  }
}

Prompt 结构

你是一个知识库助手。请根据以下参考资料回答用户问题。
如果参考资料不足以回答,请如实说明。

== 参考资料 ==
【资料1】<检索到的相关文档片段1>
【资料2】<检索到的相关文档片段2>
...

== 用户问题 ==
<用户的原始输入>

为什么没有直接调用 LLM 生成回答? Day 16-17 的重点在检索侧。后续 Day 会加入 Streaming Chat 和多轮对话记忆。


五、项目结构详解

day17-reranker-deep/
│
├── pom.xml                          # Maven 依赖管理
│   └─ 核心依赖:SB 3.4.3, LC4j 1.13.1, LC4j-pgvector 1.13.1-beta23
│      ⭐ 新增:spring-boot-starter-data-redis(SB 自动管理版本)
│
├── README.md                        # ⭐ 你正在读的就是
│
├── docs/                            # 配套文档
│   ├── day17-reference.md           # 代码速查手册(API 签名、配置速查、类说明)
│   └── day17-ai-concepts-teaching.md # AI 概念通俗讲解(5 个核心概念)
│
└── src/main/
    ├── java/com/day17/demo/
    │   │
    │   ├── Day17Application.java         # @SpringBootApplication + @EnableCaching
    │   │
    │   ├── config/                       # --- 配置层 ---
    │   │   ├── ChatModelConfig.java      # LLM + Streaming + Embedding 三个 Bean
    │   │   ├── RedisConfig.java          # ⭐ Redis 缓存管理器(TTL + 序列化 + 空值策略)
    │   │   └── DataInitializer.java      # 启动自动向量化入库(幂等)
    │   │
    │   ├── core/                         # --- 核心实体 ---
    │   │   ├── HybridSearchResult.java   # 检索结果统一 DTO(跨层传递)
    │   │   └── ExperimentResult.java     # ⭐ 实验对照数据(11 个字段完整分析)
    │   │
    │   ├── dto/                          # --- 传输对象 ---
    │   │   └── ApiResult.java            # 统一 API 响应包装(code + message + data)
    │   │
    │   ├── rag/                          # --- 核心服务层 ---
    │   │   ├── HybridSearchService.java  # 三阶段流水线编排(向量 + 关键词 → RRF → Reranker)
    │   │   ├── RerankService.java        # ⭐ Reranker API 调用(带缓存 + 多模型 + 降级)
    │   │   └── RerankCacheService.java   # ⭐ Redis 缓存读写(MD5 key + 空值保护 + 容量限制)
    │   │
    │   └── controller/                   # --- 接口层 ---
    │       └── SearchController.java     # ⭐ 4 个 REST 端点(检索 / 实验 / 对比 / RAG)
    │
    └── resources/
        ├── application.yml               # 端口 8089 + 硅基流动 + PGVector + ⭐ Redis
        ├── schema.sql                    # PGVector 建表(vector(1024) + IVFFlat + trigram)
        ├── data.sql                      # 种子数据(保留,但实际用 DataInitializer 入库)
        ├── docs/                         # 知识库文档(启动时自动向量化入库)
        │   ├── 码哥科技.txt
        │   ├── SDK集成指南.txt
        │   ├── 部署指南.txt
        │   └── API文档.txt
        └── static/
            └── index.html                # ⭐ 四模式教学测试界面

各文件职责速览

文件 代码行数 核心职责
HybridSearchService.java ~530 检索编排 — 向量检索(PGVector 余弦距离)、关键词检索(N-gram + ILIKE)、RRF 融合算法
RerankService.java ~495 Reranker 核心 — HTTP 直调硅基流动 API、多模型对比、降级策略
RerankCacheService.java ~285 Redis 缓存 — MD5 生成 key、JSON 序列化、空结果保护、异常降级
SearchController.java ~465 API 入口 — 4 个端点的请求处理、实验对照数据构造、排名变化分析
DataInitializer.java ~365 数据初始化 — 扫描 docs/、段落 + 句子双层切片、批量向量化入库
ExperimentResult.java ~155 实验数据 — 11 个字段承载完整的对照实验分析
ChatModelConfig.java ~195 模型配置 — 3 个 Spring Bean(Chat + Streaming + Embedding)
RedisConfig.java ~135 缓存配置 — RedisCacheManager、序列化策略、空值策略

六、核心配置说明

6.1 application.yml 完整配置

server:
  port: 8089                          # Day 17 用 8089,与 Day 16 的 8088 区分

# 硅基流动 — LLM + Embedding + Reranker
siliconflow:
  api-key: ENC(...)                   # Jasypt 加密
  base-url: https://api.siliconflow.cn/v1
  model-name: deepseek-ai/DeepSeek-V3
  embedding-model: BAAI/bge-large-zh-v1.5
  rerank-model: BAAI/bge-reranker-v2-m3           # ⭐ 默认 Reranker 模型
  rerank-model-compare: Qwen/Qwen3-Reranker-0.6B  # ⭐ 对比用 Reranker

# PostgreSQL + PGVector
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/aidb
    username: ai
    password: password
  # Redis — ⭐ Day 17 新增
  data:
    redis:
      host: localhost
      port: 6379
      password: password
      database: 0
      timeout: 5000ms
      lettuce:
        pool:
          max-active: 8
          max-idle: 8
          min-idle: 2

# Day 17 自定义配置
reranker:
  cache:
    enabled: true                     # 是否启用缓存(开发调试时可设为 false)
    ttl-minutes: 60                   # 缓存过期时间
  experiment:
    candidate-top-n: 20               # RRF 融合后取多少条送给 Reranker
    final-top-n: 5                    # 最终返回条数

6.2 Redis 缓存键设计

完整 Key: reranker:cache:v1:{MD5(query + "::" + modelName)}
          │            │     │      │          │
          │            │     │      │          └─ 模型名(两个模型各缓存各的)
          │            │     │      └─ 分隔符(防止 query 和 model 边界混淆)
          │            │     └─ MD5 值(32 字符 hex,中文友好)
          │            └─ 版本号 v1(未来换序列化格式时升级为 v2)
          └─ 业务命名空间

示例:
  query = "怎么退款", model = "BAAI/bge-reranker-v2-m3"
  → 原始串 = "怎么退款::BAAI/bge-reranker-v2-m3"
  → MD5    = "3f8a9b2c..."
  → Key    = "reranker:cache:v1:3f8a9b2c..."

6.3 Redis 中的查看方式

# 查看所有 Reranker 缓存 key
redis-cli -a password KEYS "reranker:cache:*"

# 查看某个 key 的值
redis-cli -a password GET "reranker:cache:v1:3f8a9b2c..."

# 查看 TTL
redis-cli -a password TTL "reranker:cache:v1:3f8a9b2c..."

七、架构与数据流

7.1 完整请求链路(以 /search 为例)

用户 query "码哥科技的核心产品"
  │
  ├─ HybridSearchService.search(query, table)
  │   │
  │   ├─ 阶段 1a:向量检索
  │   │   query → Embedding API → 1024 维向量
  │   │   → PGVector 余弦距离 Top-20
  │   │
  │   ├─ 阶段 1b:关键词检索
  │   │   query → 清洗(去标点/数字/英文)→ N-gram(2-4字,最多8个)
  │   │   → ILIKE 匹配 → 命中文档 Top-20
  │   │
  │   ├─ 阶段 2:RRF 融合
  │   │   对两路结果按 RRF 公式融合 → 去重 → ~30 条候选
  │   │   公式:score = 1/(60+rank_vector) + 1/(60+rank_keyword)
  │   │
  │   └─ 阶段 3:Reranker 精排
  │       │
  │       ├─ RerankService.rerank(query, candidates, 5)
  │       │   │
  │       │   ├─ 查 Redis 缓存 → 命中则秒回(≤5ms)
  │       │   │
  │       │   ├─ 未命中:
  │       │   │    截断每篇 ≤512 字符 → POST /rerank (API)
  │       │   │    → 解析 JSON → 写入 Redis → 返回 Top-5
  │       │   │
  │       │   └─ API 异常 → 降级返回原始 RRF 排序
  │       │
  │       └─ 返回 Top-5 HybridSearchResult
  │
  └─ SearchController → ApiResult<List<HybridSearchResult>> → JSON 响应

7.2 对照实验链路(/experiment

# 这个端点内部做了两条路径的对比
curl "http://localhost:8089/experiment?query=怎么退款&topN=5"
用户 query "怎么退款"
  │
  └─ 共同前序
      ├─ 向量检索 Top-20
      ├─ 关键词检索 Top-20
      └─ RRF 融合去重 → ~30 条候选集
          │
          ├─────────────────────────────┐
          │                             │
    路径 A(基线)                   路径 B(Reranker)
    候选集 → RRF 分排序             候选集 → 取前 20 条
    → 取 Top-5                      → Reranker 精排
    → source="rrf_baseline"         → Top-5
                                    → source="rerank"
          │                             │
          └──────────┬──────────────────┘
                     │
              ExperimentResult
              ├─ overlapCount:两条路径结果重叠数
              ├─ rankChanges:排名如何变化(升/降/淘汰)
              ├─ scoreDiffSummary:得分区分度的变化
              ├─ baselineTimeMs vs rerankedTimeMs
              └─ baselineAvgScore vs rerankedAvgScore

7.3 数据层设计

┌────────────┐     ┌────────────┐     ┌──────────────┐
│ PostgreSQL │     │   Redis    │     │ 硅基流动 API │
│ + PGVector │     │  (缓存)    │     │ (Reranker)   │
└─────┬──────┘     └─────┬──────┘     └──────┬───────┘
      │                  │                   │
      │  向量检索         │  缓存读写          │  HTTP POST
      │  关键词检索       │                   │
      │                  │                   │
      ▼                  ▼                   ▼
┌──────────────────────────────────────────────────────┐
│                 HybridSearchService                   │
│  (三阶段流水线编排)                                   │
│                                                      │
│  vectorSearch ──→ keywordSearch ──→ rrfFusion         │
│                                      │              │
│                                      ▼              │
│                               RerankService          │
│                               ├─ RerankCacheService  │
│                               ├─ 硅基流动 API         │
│                               └─ 降级策略             │
└──────────────────────────────────────────────────────┘

八、测试指南(小白友好)

8.1 第一步:验证基础检索

在首页的「检索」Tab,输入一个简单问题并点击检索:

问题 1:码哥科技的核心产品是什么?
→ 预期:Top-1 应该提到"码哥AI中台",得分 > 0.8
→ 记下耗时(几秒)

同一问题再搜一次
→ 预期:头顶 Redis 状态变成 🟡 缓存命中!
→ 耗时应该降到 < 50ms

8.2 第二步:做对照实验

切到「对比实验」Tab,输入同一个问题:

点击快捷入口「🔬 快速实验:码哥科技的核心产品」
→ 左边:RRF 直接 Top-5(基线,无 Reranker)
→ 右边:Reranker 精排 Top-5(实验组)

看什么?
  1. overlapCount — 两条路径有几条相同?
     如果只有 2-3 条相同 → Reranker 产生了显著影响
  2. rankChanges — 哪条排名变了?
     比如"RRF 排第一的文档被 Reranker 降到了第三"→ 说明 RRF 的排序不准
  3. 耗时对比 — 基线 45ms vs Reranker 320ms
     多花的 275ms 换来了更精准的排序,值得吗?

8.3 第三步:对比两个模型

切到「模型对比」Tab:

点击快捷入口「🤖 快速对比:码哥科技的核心产品」
→ 左边:BGE-Reranker-v2-m3 的 Top-5
→ 右边:Qwen3-Reranker 的 Top-5

看什么?
  1. 两个模型 Top-1 是否相同?✅ = 结论一致 / ⚠️ = 有分歧
  2. Top-5 重叠几条?3-4 条 → 模型偏好相似;1-2 条 → 模型差异大
  3. 如果你人工判断"左边排第一的更相关",那 bge 更靠谱

8.4 第四步:体验 RAG 对话

切到「RAG 对话」Tab:

输入"SDK 初始化失败怎么排查"
→ 看到 3 部分内容:
  1. 检索到的文档(Top-5,含得分和来源)
  2. 组装好的 Prompt(系统提示词 + 参考资料 + 用户问题)
  3. 复制按钮 — 拿这个 Prompt 去任何大模型都能获得 RAG 增强回答

8.5 在终端用 curl 测试

# 1. 基础检索
curl -s "http://localhost:8089/search?query=码哥科技" | python -m json.tool

# 2. 再搜一次(观察日志是否有缓存命中)
curl -s "http://localhost:8089/search?query=码哥科技" > /dev/null

# 3. 对照实验
curl -s "http://localhost:8089/experiment?query=码哥科技&topN=5" | python -m json.tool

# 4. 模型对比
curl -s "http://localhost:8089/experiment/compare-models?query=SDK初始化&topN=5" | python -m json.tool

# 5. 查看 Redis 缓存内容
redis-cli -a password KEYS "reranker:cache:*"
redis-cli -a password GET "reranker:cache:v1:$(echo -n '码哥科技::BAAI/bge-reranker-v2-m3' | md5sum | cut -d' ' -f1)"

九、面试怎么说

9.1 “RAG 召回之后怎么排序?为什么要有 Rerank 这一步?”

RAG 的标准流程是"粗筛 + 精排"两阶段。粗筛用 Bi-Encoder(Embedding 模型),计算速度快但精度不够——因为问句和文档是分别编码的,两者没有交互。我实际对比过,同一个 query 用 RRF 直接取 Top-5 和用 Reranker 精排后的 Top-5,重叠度通常只有 40-60%。也就是说,Reranker 纠正了 RRF 接近一半的排序结果。

精排用 Cross-Encoder(Reranker),把 (query, doc) 成对输入,通过 Attention 让两者互相理解,精度高得多。代价是计算量大,所以我只在粗筛后的 20 条候选上执行 Reranker。这就是经典的"召回 + 重排"两阶段架构。

9.2 “Reranker 性能怎么样?慢不慢?”

首次调 API 约 200-800ms(20 篇文档 × 512 字符)。但我做了 Redis 缓存——同一个 query + model 组合,结果缓存 60 分钟。因为热门问题会被反复搜索,缓存命中后响应 ≤5ms。我们实际线上的缓存命中率大约在 30-40%。

9.3 “你们怎么选 Reranker 模型?一个模型够不够?”

我们不会只用一个模型就拍板。我们在同一份候选集上跑两个模型(BGE-Reranker-v2-m3 和 Qwen3-Reranker),计算 Top-5 的重叠率。如果重叠率 > 80%,说明排序稳定,选快的那个;如果差异大,我们会人工评审几组 case 判断哪个更合理,再做选择。这叫 A/B 模型对比,不靠感觉靠数据。

9.4 “你们的 Reranker 有缓存的吗?怎么做?”

有。用 Redis,缓存键是 MD5(query + "::" + modelName),TTL 60 分钟。有几个设计细节:缓存包含 modelName,因为不同模型的排序结果不同;空结果不缓存,避免网络故障的瞬时空白被缓存放大;缓存满了或 Redis 挂了不影响搜索——降级到直接调 API。

9.5 “如果 Reranker API 挂了怎么办?”

有降级策略。API 调用失败时,不抛异常,而是返回 RRF 原始排序的 Top-N。虽然精度会下降,但核心搜索功能不受影响。日志中会记录失败原因,方便排查。


十、常见问题与踩坑

10.1 Redis 连接不上

Caused by: io.lettuce.core.RedisConnectionException:
  Unable to connect to localhost:6379

原因:Redis 容器未启动或密码不对。

解决

docker start redis17
# 检查密码
redis-cli -a password PING  # 应返回 PONG

10.2 Reranker API 返回空结果

日志中看到 [Rerank] ← 重排 20 条候选 → 0 条结果

原因:硅基流动 API Key 不对,或模型名拼写错误。

验证

curl -s -X POST "https://api.siliconflow.cn/v1/rerank" \
  -H "Authorization: Bearer sk-xxx" \
  -H "Content-Type: application/json" \
  -d '{"model":"BAAI/bge-reranker-v2-m3","query":"test","documents":["hello"],"top_n":1}'

10.3 首次启动很慢(5-10 秒)

正常现象——首次启动时 DataInitializer 会扫描 docs/ 目录下的 4 个文件,切片 → 逐批调 Embedding API 向量化 → 批量写入 PGVector。知识库约 20+ 个切片,每批 10 个,需要 2-3 次 API 调用。

第二次启动会跳过(已有数据)。

10.4 Reranker 得分和 RRF 得分差异巨大

正常现象。 RRF 得分分布在 [0.01, 0.04](排名融合后区分度低),Cross-Encoder 得分分布在 [0.01, 0.99](区分度高)。两者量纲完全不同,不要直接比较绝对分数,应该比较排名变化。

10.5 实验端点"重叠度"一直很高(80%+)

原因:你的 query 太简单,"码哥科技"这类专有名词 RRF 已经能准确匹配。

解决:试试更模糊的 query,如 "怎么把产品集成到我的应用里" — 这种 query 对语义理解要求高,Reranker 的增量价值会更大。


十一、后续学习方向

马上就要学的(Day 18+)

方向 对应概念 预计覆盖
滑动窗口切片 相邻切片保留 50 字符重叠,提高 Reranker 上下文连贯性 Day 18
语义切分 用 Embedding 相似度判断段落边界,比规则切分更智能 Day 19
流式对话 SSE(Server-Sent Events)实时输出 RAG 回答,不等全部生成 Day 20
多轮对话记忆 ChatMemory + ConversationBuffer,支持追问和上下文延续 Day 21

生产环境进阶(后续月份)

方向 具体内容
Reranker 缓存预热 启动时把 Top-N 热门 query 预先加载到 Redis
缓存监控 Prometheus + Grafana 监控命中率、平均 value 大小、过期速率
Reranker 多级缓存 L1(Caffeine 本地)+ L2(Redis 集中)+ L3(API),逐级 fallback
A/B 实验框架 线上流量按比例分配不同 Reranker 模型,按 Precision@K 自动选优
模型微调 用你的业务数据对 BGE-Reranker-v2-m3 做 LoRA 微调

配套文档

文档 路径 适合场景
代码速查 docs/day17-reference.md 忘了某个类的方法签名、配置项、API 参数时快速查找
AI 概念教学 docs/day17-ai-concepts-teaching.md 深入理解 Cross-Encoder、Precision@K、缓存策略 5 个核心概念

学习建议:先跑一遍测试流程(第八节)感受效果,再回来看概念讲解(第二节)理解原理,最后看源码注释了解实现细节。三遍下来,Reranker 这部分基本就吃透了。

Logo

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

更多推荐