springboot+langchain4j实战:Day17-----Reranker 深度集成
Day 17 — Reranker 深度集成
Spring Boot 3.4.3 | LangChain4j 1.13.1 | Java 17 | PGVector + Redis
在 Day 16 混合检索的基础上,深入 Reranker 环节的三大升级:
Redis 结果缓存(省钱 + 提速)+ 多模型对比(量化 Reranker 价值)+ 对照实验(用数据说话)
目录
前置知识要求
读完本 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": [ ← 路径 A:RRF 直接 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%)"
}
}
实验分析方法(按优先级排序):
-
看
overlapCount— Top-5 中两条路径有多少条相同?- 5/5:Reranker 和 RRF 意见一致(Reranker 在这个 query 上没发挥价值)
- 2/5:Reranker 选出了完全不同的结果(增量价值大)
-
看
rankChanges— 有哪些文档因 Reranker 升了/降了?- 第一名被降级 → Reranker 在纠正 RRF 的排序偏差
- 排名不变 → RRF 本身已经排得很好
-
看
rerankApiTimeMs— Reranker 增加了多少延迟?- 50-100ms:缓存命中(秒回)
- 200-800ms:首次 API 调用(可接受)
-
2000ms:网络问题或知识库太大
-
看
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 缓存"
}
}
分析维度:
- 看 Top-1 — 两个模型排在第一的是不是同一篇文档?
- 看重叠度 — 手动比较 Top-5 的有多少条相同
- 看得分差异 — 哪个模型的 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 这部分基本就吃透了。
更多推荐
所有评论(0)