springboot+langchain4j实战Day 16 — 混合检索 + Reranker 重排序

发布时间:2026/6/27 9:36:04
springboot+langchain4j实战Day 16 — 混合检索 + Reranker 重排序 Day 16 — 混合检索 Reranker 重排序一、这是什么一个知识库检索引擎。你问一个问题它从几千篇文档中找出最相关的 5 篇按相关性排序返回。跟 Day 4纯向量检索的区别Day 4: 问句 → Embedding 向量 → 余弦距离排序 → 结果 Day 16: 问句 → 两路并行检索 → 融合排序 → 精排 → 结果 ├─ 向量语义 └─ 关键词精确简单说Day 4 只用一种方式找文档Day 16 用两种方式找然后互相印证最后再请一个裁判精挑细选。二、从零理解混合检索2.1 一个搜索场景假设你的知识库里有几千篇公司文档。你想查码哥科技退款流程系统会怎么做2.2 纯向量检索怎么做Day 4 的方式一问码哥科技退款流程 │ ▼ BGE Embedding 模型 [0.0123, 0.8456, -0.3210, ...] ← 1024 个浮点数 │ ▼ PGVector 余弦距离 找到语义最接近的 20 篇文档问题在于Embedding 模型把每个词都变成了一组数字。码哥科技这个词它可能没见过——训练数据里没有你公司的名字。所以它只能猜猜对是运气猜错是常态。向量检索擅长意思差不多的东西但不擅长一模一样的东西。2.3 关键词检索补什么关键词检索不关心语义只管字面匹配一问码哥科技退款流程 │ ▼ 切词去掉标点 → 码哥科技退款流程 │ 2-gram: [码哥, 哥科, 科技, 技退, 退款, 款流, 流程] │ 3-gram: [码哥科, 哥科技, 科技退, ...] ▼ 在数据库里查哪些文档包含退款 │ ▼ ILIKE %退款% 找到所有含退款二字的文档不管退款是什么意思只要文档里出现了这两个字就算命中。专有名词、编号、人名——这些向量模型不好处理的东西关键词精确匹配直接搞定。2.4 为什么不只用一种问题类型纯向量纯关键词混合“产品有哪些功能”✅ 语义泛化❌ 功能太泛✅“API_KEY 配置在哪”❌ 专有名词不准✅ 精确命中✅“李四的退款”❌ 人名泛化差✅ “李四”“退款”✅“怎么提升用户体验”✅ 语义理解好❌ 关键词覆盖不全✅向量负责语义泛化“意思差不多”关键词负责精确命中“一模一样”——两条腿走路。2.5 两条路结果怎么合并两路都返回了 Top-20但它们的分数不在同一个世界向量路分数是 0.6~0.9余弦相似度关键词路分数是 0~8命中了几条 N-gram直接加权0.6 × 0.7 3 × 0.3权重怎么调都不对。RRF 的思路非常巧妙不看分数绝对值只看排名位置。假设问句码哥科技的产品 向量检索排名: 关键词检索排名: 第1名文档A (0.92分) 第1名文档C (6分) 第2名文档B (0.88分) 第2名文档A (5分) 第3名文档D (0.71分) 第3名文档B (3分) RRF 融合 文档A 1/(601) 1/(602) 0.0164 0.0161 0.0325 ← 两路都靠前胜出 文档B 1/(602) 1/(603) 0.0161 0.0159 0.0320 文档C 1/(6021) 1/(601) 0.0123 0.0164 0.0287 ← 向量路没排进去 文档D 1/(603) 1/(6021) 0.0159 0.0123 0.0282RRF 分数 1/(k 排名)两路累加。k60 是经典常数让分数曲线平滑。2.6 RRF 融合后还不够准RRF 只是合并两路排名完全没有理解文档内容和问题的关系。排第一的文档可能只是运气好。这时候Reranker上场。它是一个专门的打分模型把(问题, 文档)成对地喂进去让模型仔细读后打分输入 问题码哥科技的核心产品是什么 文档码哥科技成立于2023年核心产品包括码哥AI中台、智能客服平台... Reranker 内部 → 让问题里的核心产品注意到文档里的AI中台 → 让文档里的智能客服回看问题里的产品 → 双向交互 → 输出 0.9974 分 而嵌入模型Bi-Encoder做不到这点——它把问题和文档分别编码两人从未见过面。所以要分三步走粗筛向量关键词→ 合并RRF→ 精挑Reranker。三、完整流水线用户问句码哥科技的核心产品是什么 │ ├──→ 阶段1双路召回 │ ├─ 向量路Embedding → PGVector 余弦距离 → Top-20 │ └─ 关键词路N-gram 切词 → ILIKE 模糊匹配打分 → Top-20 │ ├──→ 阶段2RRF 融合 │ score(doc) 1/(60向量排名) 1/(60关键词排名) │ → 去重 → 按 RRF 分数重新排序 │ └──→ 阶段3Reranker 精排 POST /v1/rerankCross-Encoder 联合编码 → relevance_score 排序 → Top-5四、技术栈组件版本/型号用途Spring Boot3.4.3应用框架Java17运行语言LangChain4j1.13.1LLM/Embedding 模型调用langchain4j-pgvector1.13.1-beta23PGVector 向量存储集成PGVectorPostgreSQL 17 扩展向量存储与相似度查询EmbeddingBAAI/bge-large-zh-v1.5文本→1024 维向量RerankerBAAI/bge-reranker-v2-m3Cross-Encoder 精排DeepSeek-V3硅基流动托管大语言模型RAG 对话Jasypt3.0.5配置文件加密API Key 保护五、项目结构day16-hybrid-rag/ ├── pom.xml # Maven 依赖SB 3.4.3, LC4j 1.13.1 ├── README.md ├── docs/ │ ├── day16-reference.md # 速查手册字段、配置、API、术语 │ └── day16-ai-concepts-teaching.md # 从零教学每个 AI 概念逐层拆解 └── src/main/ ├── java/com/day16/demo/ │ ├── Day16Application.java # SpringBootApplication 启动入口 │ ├── config/ │ │ ├── ChatModelConfig.java # 3 个 BeanChat Streaming Embedding │ │ └── DataInitializer.java # 启动时自动向量化 classpath:docs/ 下的文档 │ ├── controller/ │ │ └── SearchController.java # GET /search纯检索 GET /rag/chatRAG 对话 │ ├── core/ │ │ └── HybridSearchResult.java # 统一检索结果 DTO │ ├── dto/ │ │ └── ApiResult.java # 统一响应体 {code, message, data} │ └── rag/ │ ├── HybridSearchService.java # 三阶段编排向量N-gramRRFReranker │ └── RerankService.java # 直调硅基流动 /v1/rerank 接口 └── resources/ ├── application.yml # 配置端口 8088 / API Key / 数据源 ├── schema.sql # 建表 DDLday4_rag_store 索引 ├── data.sql # 示例 SQL保留备用 ├── docs/ # 知识库种子文档4 篇 .txt └── static/ └── index.html # 检索前端检索模式 RAG 对话模式六、核心实现6.1 关键词检索中文 N-gram无分词器方案不依赖 jieba/HanLP 等重量级分词器使用滑动窗口切词// 输入码哥科技的核心产品是什么// cleanQuery() → 去标点、英文、数字、停用单字// 码哥科技核心产品// generateNgrams() → 2-4 字滑动窗口取前 8 个// [码哥, 哥科, 科技, 技核, 核心, 心产, 产品, 码哥科]每条 N-gram 生成一条 SQL CASE WHEN 条件累加计分-- 最终分数 命中次数 × 长度惩罚score(CASEWHENtextILIKE%科技%THEN1ELSE0ENDCASEWHENtextILIKE%核心%THEN1ELSE0ENDCASEWHENtextILIKE%产品%THEN1ELSE0END...)*(1.0/(1length(text)/500.0))长度惩罚1/(1len/500)50 字片段命中 3 个词条 5000 字文章命中 3 个词条。短片段更可能是精确答案。6.2 RRF 融合公式rrf_score(doc) 1/(k 向量排名) 1/(k 关键词排名)k60。为什么用排名而不是原始分数两路的分数不在同一量纲余弦距离 vs N-gram 命中计数直接加权怎么调都不稳。排名是对齐的唯一方式。为什么 k60TREC 论文的经典取值。k 越大排名差异越不重要60 是适度关注排名的平衡点。6.3 Reranker 精排POSThttps://api.siliconflow.cn/v1/rerank{model:BAAI/bge-reranker-v2-m3,query:码哥科技的核心产品是什么,documents:[文档1文本...,文档2文本...,...],top_n:5}← 返回[{index:2,relevance_score:0.9974},{index:0,relevance_score:0.9891},...]Cross-Encoder 把 (query, document) 成对输入通过 Transformer 自注意力让两者交互——比向量检索Bi-Encoder精确得多。为什么只对 Top-20 做 Rerank 而不是全部文档Cross-Encoder 必须现场计算不能预先编码。2697 条全跑一次要好几秒20 条只要几百毫秒。6.4 降级策略Reranker API 调用失败时不抛异常降级返回 RRF 融合后的原始结果截断到 topNtry{// 调 Reranker API → 解析响应}catch(Exceptione){log.error([Rerank] 失败{},e.getMessage());// 优雅降级RRF 的结果也比没有强returncandidates.stream().limit(topN).collect(Collectors.toList());}原则部分功能坏了 ≠ 整体服务不可用。对用户来说返回 5 条还行的结果远好过报 500 错误。6.5 知识库数据初始化DataInitializer在应用启动时自动执行检查day4_rag_store是否已有数据 → 有则跳过幂等无数据时读取classpath:docs/下所有.txt文件按段落切分为 ≤300 字的片段分批调用 Embedding API 向量化批量写入 PGVector数据只初始化一次。如需强制重建先清空表再重启。6.6 API Key 加密生产环境中敏感信息不写明文。application.yml中 API Key 使用ENC(...)密文形式存储由 Jasypt 在启动时解密siliconflow:api-key:ENC(K1krO1oc4nbWKTGQ/ZUQCVYs/HWQaD206ux3weV1UVdCYyuOLi3fmtgIAlFcAbonB5rNZYMci0wic5lU4Yw)启动时需设置环境变量JASYPT_PASSWORD。七、API7.1 GET /search — 纯检索curlhttp://localhost:8088/search?query码哥科技的核心产品tableday4_rag_store参数类型必填默认值说明queryString✅—用户问题tableString❌day4_rag_storePGVector 表名返回{code:200,message:success,data:[{id:uuid-xxx,text:码哥科技成立于2023年核心产品包括码哥AI中台...,metadata:{\source\:\码哥科技\},score:0.9974,source:rerank},...]}source字段标识该结果由哪个阶段产生rerankReranker 精排后、rrfRRF 融合但未重排、vector纯向量、keyword纯关键词。7.2 GET /rag/chat — RAG 对话curlhttp://localhost:8088/rag/chat?message码哥科技有多少员工tableday4_rag_store返回检索到的文档 组装好的 RAG Prompt可直接发给任意 LLM{code:200,data:{query:码哥科技有多少员工,documents:[...],prompt:你是一个知识库助手。请根据以下参考资料回答用户问题...\n\n 参考资料 \n【资料1】码哥科技...,hint:将此 prompt 发送给 LLM 即可获得 RAG 增强回答}}八、与 Day 4 的效果对比维度Day 4纯向量Day 16混合检索Rerank召回方式单一向量向量 N-gram 关键词双路排序方式余弦距离RRF 融合 → Cross-Encoder 精排专有名词容易漏N-gram 精确命中Top-1 得分0.6~0.8余弦0.99Reranker relevance降级能力无每层独立降级知识库初始化手动启动时自动向量化九、踩坑记录#现象根因修复1启动报NoSuchBeanDefinitionException: DataSourcepom.xml 只有postgresql驱动缺spring-boot-starter-jdbc添加spring-boot-starter-jdbc2Reranker 返回 200 但反序列化报错硅基流动 API 返回了 DTO 未定义字段DTO 加JsonIgnoreProperties(ignoreUnknown true)3关键词检索始终返回 0 条原用正则切分 停用词过滤中文无空格导致全句匹配改为滑动窗口 N-gram2-4 字4String.format占位符语法错误误用 Python 风格{:.4f}改为%.4f5IDE 自动导入javax.naming.directory.SearchResult自定义类SearchResult与 JDK 同名重命名为HybridSearchResult6dev.langchain4j.service.Result被误导入自定义类Result与框架同名重命名为ApiResult十、启动方式# 1. 确认 PostgreSQL PGVector 已运行dockerps|grepai-postgres# 2. 设置 Jasypt 解密密钥exportJASYPT_PASSWORDday16-secret-key# 3. 编译 启动cdday16-hybrid-rag mvn clean compile spring-boot:run-DskipTests# 4. 测试curlhttp://localhost:8088/search?query码哥科技tableday4_rag_store# 5. 打开前端页面# 浏览器访问 http://localhost:8088效果实测十一、进一步阅读文档内容适合谁docs/day16-reference.md所有字段、配置、接口、术语的速查手册写代码时随时查docs/day16-ai-concepts-teaching.md每个 AI 概念从零拆解LLM、Token、Embedding、RAG、RRF…零 AI 基础的同学