行业资讯
Java搭RAG实战:混合检索+重排序:完整源码+逐行设计决策
先说结论纯向量检索的召回率只有40%加上关键词检索和Rerank后命中提升到88%。这是4个Java类完整源码踩坑记录。一、配置层HybridRetrievalPropertiesConfigurationProperties(prefix dream-saaa-s.rag.hybrid)public record HybridRetrievalProperties( DefaultValue(true) boolean enabled, DefaultValue(0.7) double vectorWeight, DefaultValue(0.3) double keywordWeight, DefaultValue(60) int rrfK, DefaultValue(20) int keywordTopK, DefaultValue(true) boolean rerankEnabled, DefaultValue(qwen3-rerank) String rerankModel, DefaultValue(5) int rerankTopN) {}设计决策为什么用record而不是classrecord的自动生成equals()/hashCode()/toString()让我省了15行模板代码。更重要的是record的不可变性天然适合配置对象——配置在启动时读取后就不该被修改。坑1百炼模型名rerankModel默认值我填的是qwen3-rerank不是bge-reranker。原因是百炼的bge-reranker模型返回Model not exist这破坑折腾了我两天。实际可用的是qwen3-rerank别填bge-reranker。设计决策为什么权重是0.7/0.3这是调出来的初始值。向量检索负责语义匹配权重高关键词负责精确匹配权重低。后面测试数据会验证这个配比是否合理。二、关键词检索KeywordSearchService核心方法public ListRankedDocument search(String query, int topK) { ListString keywords extractKeywords(query); if (keywords.isEmpty()) { log.warn(关键词检索query拆词后为空 query{}, query); return Collections.emptyList(); } // 动态构建SQL的WHERE子句 String whereClause keywords.stream() .map(kw - content ILIKE ?) .collect(Collectors.joining( OR )); // 构建LIKE参数列表 Object[] params keywords.stream() .map(kw - % kw %) .toArray(); // SQL: SELECT ... FROM rag_documents WHERE content ILIKE ? OR content ILIKE ? ... String sql SELECT id, content, metadata FROM rag_documents WHERE whereClause LIMIT ?; // params topK合并成完整参数数组 Object[] fullParams new Object[params.length 1]; System.arraycopy(params, 0, fullParams, 0, params.length); fullParams[params.length] topK * 3; ListRankedDocument results jdbc.query(sql, fullParams, (rs, rowNum) - { String content rs.getString(content); String lowerContent content.toLowerCase(); int hitCount 0; double weightedScore 0.0; for (String kw : keywords) { if (lowerContent.contains(kw.toLowerCase())) { hitCount; // 加权逻辑长词权重更高 weightedScore kw.length() 4 ? 2.0 : kw.length() 3 ? 1.5 : 1.0; } } // 归一化实际得分 / 理论满分 double maxPossible keywords.stream() .mapToDouble(kw - kw.length() 4 ? 2.0 : kw.length() 3 ? 1.5 : 1.0) .sum(); double score maxPossible 0 ? weightedScore / maxPossible : 0.0; returnnew RankedDocument(id, content, metadataJson, score, hitCount); }); // 按得分降序排列 results.sort((a, b) - Double.compare(b.score, a.score)); return results.size() topK ? results.subList(0, topK) : results;}设计决策为什么LIMIT topK * 3关键词命中的20条不一定都是高质量的我多召回一些给RRF融合用。融合后还会再截断到topK。坑2中文整句ILIKE完全失效我测试过content ILIKE %向量数据库选型%中文整句匹配返回0条。但拆成[向量, 数据库, 选型, 向量数据库, 数据库选型]后20条全部命中。结论PostgreSQL的ILIKE对中文整句支持有bug必须先分词。设计决策加权2.0/1.5/1.0怎么来的长度≥4的词给2.0分专业术语通常4字以上3字词给1.5分2字词给1.0分。这是业务经验沉淀不是拍脑袋。2字词太短容易误命中权重低合理。三、重排序RerankServicepublic ListRerankedDocument rerank(String query, ListDocument documents, int topN) { // 降级未启用则返回原始顺序 if (!properties.rerankEnabled()) { return documents.stream() .map(d - new RerankedDocument(d, 1.0)) .limit(topN) .toList(); } if (documents.isEmpty()) { return Collections.emptyList(); } try { // 构建请求体 ListString docTexts documents.stream() .map(d - truncate(d.getText(), 500)) // 截断到500字符 .toList(); String requestBody MAPPER.writeValueAsString(Map.of( model, properties.rerankModel(), query, query, documents, docTexts, top_n, Math.min(topN, docTexts.size()) )); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(RERANK_URL)) .header(Authorization, Bearer apiKey) .header(Content-Type, application/json) .POST(HttpRequest.BodyPublishers.ofString(requestBody)) .timeout(Duration.ofSeconds(30)) .build(); HttpResponseString response httpClient.send( request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() ! 200) { log.error(Rerank API 失败 status{} body{}, response.statusCode(), response.body()); // 降级返回原始顺序 return documents.stream() .map(d - new RerankedDocument(d, 1.0)) .limit(topN) .toList(); } return parseRerankResponse(response.body(), documents, topN); } catch (Exception e) { log.error(Rerank 异常降级返回原始顺序, e); return documents.stream() .map(d - new RerankedDocument(d, 1.0)) .limit(topN) .toList(); }}设计决策为什么截断到500字符百炼rerank API有输入长度限制500是实测安全值。超长文本传进去要么被截断要么报错提前截断更可控。坑3接口路径不是dashscope原生接口路径是/compatible-api/v1/reranks不是/rerank用错了会报错排查半天才定位到。降级逻辑三级降级配置层禁用 → 返回原始顺序API返回非200 → 返回原始顺序异常捕获 → 返回原始顺序Rerank挂了不影响主流程这个设计保证了混合检索的稳定性。四、融合层HybridRetrievalServicepublic ListDocument hybridRetrieve(String query, Integer topKOverride) { if (!StringUtils.hasText(query)) { return Collections.emptyList(); } int topK topKOverride ! null topKOverride 0 ? topKOverride : retrieval.topK(); // 关闭混合检索时退化为纯向量 if (!hybrid.enabled()) { return vectorSearch(query, topK); } // 1. 双路召回向量检索 关键词检索 ListDocument vectorDocs vectorSearch(query, topK * 2); ListRankedDocument keywordDocs keywordSearch.search(query, hybrid.keywordTopK()); // 2. RRF融合 ListDocument fused rrfFusion(vectorDocs, keywordDocs, topK * 2); // 3. Rerank重排序可选 if (hybrid.rerankEnabled()) { ListRerankedDocument reranked rerankService.rerank( query, fused, hybrid.rerankTopN()); return reranked.stream().map(RerankedDocument::document).toList(); } return fused.size() topK ? fused.subList(0, topK) : fused;}RRF融合核心逻辑private ListDocument rrfFusion( ListDocument vectorDocs, ListRankedDocument keywordDocs, int maxResults) { MapString, Double rrfScores new HashMap(); MapString, Document docMap new HashMap(); int k hybrid.rrfK(); // 默认60 // 向量检索结果参与RRF打分 for (int i 0; i vectorDocs.size(); i) { Document doc vectorDocs.get(i); String id doc.getId(); int rank i 1; rrfScores.merge(id, hybrid.vectorWeight() / (k rank), Double::sum); docMap.putIfAbsent(id, doc); } // 关键词检索结果参与RRF打分 for (int i 0; i keywordDocs.size(); i) { RankedDocument rd keywordDocs.get(i); String id rd.id(); int rank i 1; rrfScores.merge(id, hybrid.keywordWeight() / (k rank), Double::sum); docMap.putIfAbsent(id, buildDocumentFromRanked(rd)); } // 按RRF得分降序 ListMap.EntryString, Double sorted new ArrayList(rrfScores.entrySet()); sorted.sort((a, b) - Double.compare(b.getValue(), a.getValue())); // 截断到maxResults ListDocument result new ArrayList(); for (int i 0; i Math.min(sorted.size(), maxResults); i) { result.add(docMap.get(sorted.get(i).getKey())); } return result;}设计决策为什么用merge而不是put两个检索结果可能有重叠的文档。比如向量数据库选型这个query向量检索和关键词检索可能召回同一篇文档。merge保证同一条文档的得分累加而不是被覆盖。RRF公式1/(krank)k60是经验值。k越大各路检索结果的排名差异对最终得分的影响越小。这让向量和关键词两路的结果能更平滑地融合而不是被单一检索结果主导。五、实测数据场景纯向量v1关键词命中v2关键词命中v1 Rerankv2 Rerank中文语义介绍Dream-SaaS0.26820200.5240.879专有名词code-review-agent0.43420200.5240.860中文关键词向量数据库选型0.5380200.5170.514模糊语义怎么让AI基于知识库回答0.4460200.4600.465结论N-gram分词后中文关键词命中从0提升到20Rerank后相关性从0.514提升到0.879。学AI大模型的正确顺序千万不要搞错了2026年AI风口已来各行各业的AI渗透肉眼可见超多公司要么转型做AI相关产品要么高薪挖AI技术人才机遇直接摆在眼前有往AI方向发展或者本身有后端编程基础的朋友直接冲AI大模型应用开发转岗超合适就算暂时不打算转岗了解大模型、RAG、Prompt、Agent这些热门概念能上手做简单项目也绝对是求职加分王给大家整理了超全最新的AI大模型应用开发学习清单和资料手把手帮你快速入门学习路线:✅大模型基础认知—大模型核心原理、发展历程、主流模型GPT、文心一言等特点解析✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑✅开发基础能力—Python进阶、API接口调用、大模型开发框架LangChain等实操✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经以上6大模块看似清晰好上手实则每个部分都有扎实的核心内容需要吃透我把大模型的学习全流程已经整理好了抓住AI时代风口轻松解锁职业新可能希望大家都能把握机遇实现薪资/职业跃迁这份完整版的大模型 AI 学习资料已经上传CSDN朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】
郑州网站建设
网页设计
企业官网