ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

第二章:5、RAG进阶优化

第二章:5、RAG进阶优化

一、混合检索(向量 + 关键词)

1. 纯向量检索的局限

  • 语义相近但答案靠后:向量相似度高不代表文档真正包含答案。
  • 精确关键词匹配弱:用户搜索特定术语时,向量检索可能漏掉精确匹配的文档。
  • 多路召回更可靠:结合向量检索和关键词检索,取并集后融合排序。

2. 混合检索的实现(以PGVector为例)

-- PGVector 全文检索 + 向量检索 SQL SELECT * FROM knowledge_base WHERE tsv @@ to_tsquery('chinese', :query) -- 关键词匹配 ORDER BY ts_rank(tsv, to_tsquery('chinese', :query)) DESC, -- 关键词相关性 embedding <=> :query_vector -- 向量相似度 LIMIT :topK;

3. Java端混合检索逻辑

核心思路:向量检索与关键词检索并行,结果融合去重(RRF排序)。
@Service public class HybridSearchService { @Resource private VectorStore vectorStore; @Resource private JdbcTemplate jdbcTemplate; public List<Document> hybridSearch(String query, int topK) { // 1. 向量检索 List<Document> vectorResults = vectorStore.similaritySearch( SearchRequest.builder().query(query).topK(topK).build() ); // 2. 关键词检索(PGVector全文搜索) String sql = """ SELECT id, content, ts_rank(tsv, to_tsquery('chinese', ?)) AS rank FROM knowledge_base WHERE tsv @@ to_tsquery('chinese', ?) ORDER BY rank DESC LIMIT ? """; List<Document> keywordResults = jdbcTemplate.query(sql, ...); // 3. RRF融合排序(Reciprocal Rank fusion) return rrfMerge(vectorResults, keywordResults, topK); } /** * RRF(Reciprocal Rank Fusion)融合排序 * 对两个检索结果按排名的倒数加权融合,得到最终排序 */ private List<Document> rrfMerge( List<Document> vectorResults, List<Document> keywordResults, int topK) { double k = 60.0; // RRF常量 Map<String, Double> scores = new HashMap<>(); Map<String, Document> docMap = new HashMap<>(); // 向量检索结果按排名加权 for (int i = 0; i < vectorResults.size(); i++) { String id = vectorResults.get(i).getId(); double score = 1.0 / (k + i + 1); scores.merge(id, score, Double::sum); docMap.put(id, vectorResults.get(i)); } // 关键词检索结果按排名加权 for (int i = 0; i < keywordResults.size(); i++) { String id = keywordResults.get(i).getId(); double score = 1.0 / (k + i + 1); scores.merge(id, score, Double::sum); docMap.put(id, keywordResults.get(i)); } // 按融合分数降序取topK return scores.entrySet().stream() .sorted(Map.Entry.<String, Double>comparingByValue().reversed()) .limit(topK) .map(e -> docMap.get(e.getKey())) .toList(); } }

4. 集成到RAG流程

@GetMapping("/ask-hybrid") public String askHybrid(@RequestParam String question) { // Step 1: 混合检索 List<Document> docs = hybridSearchService.hybridSearch(question, 10); // Step 2: 生成答案 String context = buildContext(docs); return chatClient.prompt() .user("参考资料:\n" + context + "\n\n问题:" + question) .call().content(); }

二、Rerank重排序

Rerank 是对检索结果按查询相关性进行二次精排的过程。粗检索阶段多召回(topK=20),Rerank阶段精排取Top5。

1. DashScope Rerank(qwen3-rerank)

通过阿里云 DashScope API 调用 qwen3-rerank 模型,实现基于API的文档重排序。
@Service public class DashScopeDocumentReranker implements DocumentPostProcessor { private final RestTemplate restTemplate; private final String apiKey; private final String modelName; private final int topN; public DashScopeDocumentReranker(String apiKey, String modelName, int topN) { this.restTemplate = new RestTemplate(); this.apiKey = apiKey; this.modelName = modelName; this.topN = topN; } @Override public List<Document> process(Query query, List<Document> documents) { if (documents == null || documents.size() <= 1) { return documents; } List<String> texts = documents.stream().map(Document::getText).toList(); Map<String, Object> body = Map.of( "model", modelName, "query", query.text(), "documents", texts, "top_n", Math.min(topN, documents.size()) ); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("Authorization", "Bearer " + apiKey); try { ResponseEntity<RerankResponse> response = restTemplate.postForEntity( "https://dashscope.aliyuncs.com/api/v1/services/rerank", new HttpEntity<>(body, headers), RerankResponse.class ); return reorder(documents, response.getBody()); } catch (Exception e) { log.warn("DashScope rerank调用失败: {}", e.getMessage()); return documents; } } /** * 按rerank返回的相关性顺序重排文档(结果已按relevance_score降序排列) */ private List<Document> reorder(List<Document> documents, RerankResponse response) { if (response == null || response.results == null || response.results.isEmpty()) { return documents; } List<Document> reranked = new ArrayList<>(); for (RerankResult result : response.results) { if (result.index != null && result.index >= 0 && result.index < documents.size()) { reranked.add(documents.get(result.index)); } } return reranked.isEmpty() ? documents : reranked; } // ===== qwen3-rerank API 响应结构 ===== static class RerankResponse { public List<RerankResult> results; } static class RerankResult { @JsonProperty("relevance_score") public Double relevanceScore; public Integer index; } }

2. 硅基流动 Rerank(BAAI/bge-reranker-v2-m3)

调用硅基流动平台的 bge-reranker-v2-m3 模型进行文档重排序。
@Service public class RerankService { private static final String API_URL = "https://api.siliconflow.cn/v1/rerank"; private final RestTemplate restTemplate; private final ObjectMapper objectMapper; @Value("${spring.ai.siliconflow-api-key}") private String apiKey; public RerankService(RestTemplateBuilder restTemplateBuilder) { this.restTemplate = restTemplateBuilder .connectTimeout(Duration.ofSeconds(10)) .readTimeout(Duration.ofSeconds(30)) .build(); this.objectMapper = new ObjectMapper(); } public List<Document> rerank(String query, List<Document> documents, int topN) { if (documents == null || documents.isEmpty()) { return List.of(); } if (documents.size() == 1) { return documents; } List<String> texts = documents.stream().map(Document::getText).toList(); Map<String, Object> body = Map.of( "model", "BAAI/bge-reranker-v2-m3", "query", query, "documents", texts, "top_n", Math.min(topN, documents.size()) ); try { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("Authorization", "Bearer " + apiKey); HttpEntity<Map<String, Object>> request = new HttpEntity<>(body, headers); ResponseEntity<Map> response = restTemplate.postForEntity( API_URL, request, Map.class ); @SuppressWarnings("unchecked") List<Map<String, Object>> results = (List<Map<String, Object>>) response.getBody().get("results"); if (results == null || results.isEmpty()) { return documents; } return results.stream().map(r -> { int index = (int) r.get("index"); Document doc = documents.get(index); doc.getMetadata().put("rerankScore", r.get("relevance_score")); return doc; }).toList(); } catch (Exception e) { log.warn("硅基流动Rerank调用失败: {}", e.getMessage()); return documents.stream().limit(topN).toList(); } } }

3. LLM文档重排器(自定义实现)

Spring AI 2.0.1 没有内置 reranker,可以用 LLM 对检索结果按相关性重新排序。
/** * 基于LLM的文档重排器(实现DocumentPostProcessor接口) */ public class LlmDocumentReranker implements DocumentPostProcessor { private static final Pattern INDEX_PATTERN = Pattern.compile("\\d+"); private final ChatClient chatClient; private final int maxDocPreviewLength; public LlmDocumentReranker(ChatClient.Builder chatClientBuilder, int maxDocPreviewLength) { this.chatClient = chatClientBuilder.build(); this.maxDocPreviewLength = maxDocPreviewLength; } @Override public List<Document> process(Query query, List<Document> documents) { if (documents == null || documents.size() <= 1) { return documents; } StringBuilder docList = new StringBuilder(); for (int i = 0; i < documents.size(); i++) { String text = documents.get(i).getText(); if (text == null) { text = ""; } if (text.length() > maxDocPreviewLength) { text = text.substring(0, maxDocPreviewLength) + "..."; } docList.append("[").append(i).append("] ").append(text).append("\n\n"); } String prompt = """ 你是一个文档相关性排序助手。请根据文档与查询的相关程度重新排序。 查询:%s 文档列表: %s 请只返回文档编号的排序结果,用英文逗号分隔。 例如:2,0,3,1 """.formatted(query.text(), docList); String result = chatClient.prompt().user(prompt).call().content(); return reorder(documents, result); } /** * 解析LLM返回的编号序列,按顺序重排文档。解析失败时返回原顺序。 */ private List<Document> reorder(List<Document> documents, String llmOutput) { if (llmOutput == null || llmOutput.isBlank()) { return documents; } Matcher matcher = INDEX_PATTERN.matcher(llmOutput); List<Integer> indices = new ArrayList<>(); while (matcher.find()) { int idx = Integer.parseInt(matcher.group()); if (idx >= 0 && idx < documents.size() && !indices.contains(idx)) { indices.add(idx); } } if (indices.size() != documents.size()) { return documents; } List<Document> reranked = new ArrayList<>(documents.size()); for (int idx : indices) { reranked.add(documents.get(idx)); } return reranked; } }

4. Rerank集成方式

方式一:手动编排
@GetMapping("/ask-rerank") public String askWithRerank(@RequestParam String question) { // Step 1: 粗检索(多召回) List<Document> candidates = vectorStore.similaritySearch( SearchRequest.builder().query(question).topK(20).build() ); // Step 2: Rerank精排(取Top5) List<Document> reranked = rerankService.rerank(question, candidates, 5); // Step 3: 生成答案 String context = buildContext(reranked); return chatClient.prompt() .user("参考资料:\n" + context + "\n\n问题:" + question) .call().content(); }
方式二:Advisor集成
RetrievalAugmentationAdvisor ragAdvisor = RetrievalAugmentationAdvisor.builder() .documentRetriever(VectorStoreDocumentRetriever.builder() .vectorStore(vectorStore) .similarityThreshold(0.7) .topK(6) .build()) .documentPostProcessors(new DashScopeDocumentReranker(apiKey, "qwen3-rerank", 3)) .build();

三、Query改写与多查询扩展

1. Query改写的必要性

  • 用户提问可能口语化、模糊或含指代词,直接用于检索效果差。
  • 通过LLM改写为更适合知识库检索的形式,提升召回率。

2. RewriteQueryTransformer(Spring AI内置)

Spring AI 2.0 提供了内置的查询改写器,可直接集成到 Advisor 中。
@Bean public RetrievalAugmentationAdvisor ragAdvisorWithRewrite( ChatClient.Builder chatClientBuilder, VectorStore vectorStore) { RewriteQueryTransformer rewriteTransformer = RewriteQueryTransformer.builder() .chatClientBuilder(chatClientBuilder) .targetSearchSystem("vector store") .build(); return RetrievalAugmentationAdvisor.builder() .queryTransformers(rewriteTransformer) .documentRetriever(VectorStoreDocumentRetriever.builder() .vectorStore(vectorStore) .topK(5) .build()) .build(); }

3. 自定义多查询改写服务

生成多个改写版本,分别检索后取并集,进一步提升召回率。
@Service public class QueryRewriteService { @Resource private ChatClient ragManualChatClient; private final ObjectMapper objectMapper; public QueryRewriteService() { this.objectMapper = new ObjectMapper(); } public List<String> rewrite(String originalQuery, String conversationHistory) { String prompt = String.format(""" 你是一个搜索查询优化专家。请将用户的问题改写为更适合在知识库中检索的形式。 要求: 1. 生成3个不同角度的改写版本 2. 用JSON数组格式返回 3. 保留原始问题的核心意图 4. 如果有对话历史,结合上下文补全指代 对话历史:%s 用户问题:%s 请直接返回JSON数组,不要包含其他文字。 """, conversationHistory, originalQuery); String result = ragManualChatClient.prompt().user(prompt).call().content(); return parseJsonArray(result); } /** * 将大模型返回的JSON数组字符串解析为List<String> * 大模型返回格式示例:["改写版本1", "改写版本2", "改写版本3"] */ private List<String> parseJsonArray(String json) { if (json == null || json.isBlank()) { return List.of(); } try { String cleaned = json.trim(); if (cleaned.startsWith("```json")) { cleaned = cleaned.substring(7); } else if (cleaned.startsWith("```")) { cleaned = cleaned.substring(3); } if (cleaned.endsWith("```")) { cleaned = cleaned.substring(0, cleaned.length() - 3); } cleaned = cleaned.trim(); return objectMapper.readValue(cleaned, new TypeReference<List<String>>() {}); } catch (Exception e) { return Arrays.stream(json.split("\n")) .map(String::trim) .filter(s -> !s.isEmpty()) .map(s -> s.replaceAll("^\\d+[\\.\\)\u3001]\u0020*", "")) .filter(s -> !s.startsWith("[")) .toList(); } } }

4. 多查询检索流程

@GetMapping("/ask-multi-query") public Flux<String> askMultiQuery( @RequestParam String message, @RequestParam(defaultValue = "default") String conversationId) { // Step 1: 改写问题(生成3个版本) List<String> queries = queryRewriteService.rewrite(message, ""); // Step 2: 分别检索并去重 Set<String> seen = new HashSet<>(); List<Document> allDocs = new ArrayList<>(); for (String q : queries) { List<Document> docs = knowledgeService.search(q, 5, 0.6); for (Document doc : docs) { if (seen.add(doc.getId())) { allDocs.add(doc); } } } // Step 3: 生成答案 String context = buildContext(allDocs); return chatClient.prompt() .user("参考资料:\n" + context + "\n\n问题:" + message) .stream().content(); }

四、闲聊感知查询改写

问题背景:用户可能输入闲聊内容(如 "你好"),此时不应触发RAG检索流程,避免浪费资源和产生无关检索结果。
public class ChitchatAwareQueryTransformer implements QueryTransformer { private final RewriteQueryTransformer delegate; public ChitchatAwareQueryTransformer(ChatClient.Builder chatClientBuilder) { this.delegate = RewriteQueryTransformer.builder() .chatClientBuilder(chatClientBuilder) .targetSearchSystem("vector store") .build(); } @Override public Query transform(Query query) { String text = query.text(); if (isChitchat(text)) { return query; // 闲聊直接透传 } return delegate.transform(query); // 知识性问题交给改写器 } private boolean isChitchat(String text) { if (text == null || text.isBlank()) return true; String trimmed = text.trim(); if (trimmed.length() <= 6) return true; String lower = trimmed.toLowerCase(); return lower.matches(".*(你好|您好|hi|hello|谢谢|感谢|再见|你是谁).*") && !lower.matches(".*(什么是|怎么|如何|为什么|区别|原理|实现).*"); } }

集成到RAG Advisor

RetrievalAugmentationAdvisor ragAdvisor = RetrievalAugmentationAdvisor.builder() .queryTransformers(new ChitchatAwareQueryTransformer(ChatClient.builder(chatModel))) .documentRetriever(VectorStoreDocumentRetriever.builder() .vectorStore(vectorStore) .similarityThreshold(0.7) .topK(6) .build()) .build();

五、查询增强与空上下文处理

ContextualQueryAugmenter负责将检索到的文档与用户查询拼装为完整的 Prompt。

allowEmptyContext 配置

  • 默认情况下,无检索结果时会抛出异常。
  • 设置 allowEmptyContext(true) 后,无检索结果时仍能正常生成回答。
@Bean public RetrievalAugmentationAdvisor ragAdvisor( VectorStore vectorStore) { return RetrievalAugmentationAdvisor.builder() .documentRetriever(VectorStoreDocumentRetriever.builder() .vectorStore(vectorStore) .topK(5) .build()) .queryAugmenter(ContextualQueryAugmenter.builder() .allowEmptyContext(true) // 允许空上下文 .build()) .build(); }

六、幻觉检测与答案质量保障

1. 基于引用验证的幻觉检测

幻觉检测的核心思路:检查模型生成的答案是否完全基于参考资料,识别不受支持的说法。
@Service public class HallucinationDetector { private final ChatClient chatClient; private final ObjectMapper objectMapper; public HallucinationDetector(ChatClient.Builder builder) { this.chatClient = builder .defaultSystem("你是一个严格的事实核查专家。") .build(); this.objectMapper = new ObjectMapper(); } public Map<String, Object> detect(String answer, String context) { String prompt = String.format(""" 请判断以下答案是否完全基于参考资料得出。 参考资料:%s 模型答案:%s 请用JSON格式返回: {"reliable": true/false, "reason": "判断理由", "unsupportedClaims": [...]} """, context, answer); String result = chatClient.prompt().user(prompt).call().content(); return parseResult(result); } /** * 将大模型返回的幻觉检测结果JSON字符串解析为Map */ private Map<String, Object> parseResult(String json) { Map<String, Object> defaultResult = Map.of( "reliable", true, "reason", "解析失败,默认标记为可信" ); if (json == null || json.isBlank()) { return defaultResult; } try { String cleaned = json.trim(); if (cleaned.startsWith("```json")) { cleaned = cleaned.substring(7); } else if (cleaned.startsWith("```")) { cleaned = cleaned.substring(3); } if (cleaned.endsWith("```")) { cleaned = cleaned.substring(0, cleaned.length() - 3); } cleaned = cleaned.trim(); JsonNode node = objectMapper.readTree(cleaned); boolean reliable = node.has("reliable") && node.get("reliable").asBoolean(); String reason = node.has("reason") ? node.get("reason").asText() : ""; List<String> unsupportedClaims = new ArrayList<>(); if (node.has("unsupportedClaims") && node.get("unsupportedClaims").isArray()) { for (JsonNode claim : node.get("unsupportedClaims")) { unsupportedClaims.add(claim.asText()); } } return Map.of("reliable", reliable, "reason", reason, "unsupportedClaims", unsupportedClaims); } catch (Exception e) { return defaultResult; } } }

2. RAG流程集成

@GetMapping("/ask-safe") public Map<String, Object> askSafe( @RequestParam String message, @RequestParam(defaultValue = "default") String conversationId) { // Step 1: 检索文档 List<Document> docs = vectorStore.similaritySearch( SearchRequest.builder().query(message).topK(5).build() ); // Step 2: 生成答案 String context = buildContext(docs); String answer = chatClient.prompt() .user("参考资料:\n" + context + "\n\n问题:" + message) .call().content(); // Step 3: 幻觉检测 Map<String, Object> result = hallucinationDetector.detect(answer, context); // Step 4: 组装返回 Map<String, Object> response = new HashMap<>(); response.put("answer", answer); response.put("reliable", result.get("reliable")); response.put("reason", result.get("reason")); response.put("unsupportedClaims", result.get("unsupportedClaims")); return response; }

3. System Prompt约束建议

  • 只基于提供的参考资料回答问题,不要编造信息。
  • 如果参考资料中没有相关内容,请明确告知用户。
  • 引用参考资料时标注来源。

七、RAG Advisor完整优化流程

核心流程

查询改写 → 检索 → 重排 → 增强 → 生成

完整配置示例

@Bean public ChatClient ragOptimizedChatClient( ChatClient.Builder builder, VectorStore vectorStore, ChatModel chatModel) { String promptTemplate = """ 你是一个知识库助手。请严格遵循以下规则: 1. 只基于提供的参考资料回答问题,不要编造信息 2. 如果参考资料中没有相关内容,请明确告知用户 3. 引用参考资料时标注来源 参考资料: {context} 问题:{question} """; RetrievalAugmentationAdvisor advisor = RetrievalAugmentationAdvisor.builder() .queryTransformers(new ChitchatAwareQueryTransformer(builder)) .documentRetriever(VectorStoreDocumentRetriever.builder() .vectorStore(vectorStore) .similarityThreshold(0.7) .topK(6) .build()) .documentPostProcessors(new DashScopeDocumentReranker(apiKey, "qwen3-rerank", 3)) .queryAugmenter(ContextualQueryAugmenter.builder() .allowEmptyContext(true) .build()) .build(); return builder.defaultAdvisors(advisor).build(); }

八、常用工具方法

buildContext:文档列表拼接为上下文字符串

将检索到的 Document 列表拼接为一段完整的上下文文本。
private String buildContext(List<Document> documents) { if (documents == null || documents.isEmpty()) { return ""; } StringBuilder sb = new StringBuilder(); for (int i = 0; i < documents.size(); i++) { Document doc = documents.get(i); sb.append("--- 文档").append(i + 1).append(" ---\n"); Object fileName = doc.getMetadata().get("fileName"); if (fileName != null) { sb.append("来源:").append(fileName).append("\n"); } sb.append(doc.getText()).append("\n\n"); } return sb.toString(); }

extractCitations:从响应中提取引用来源

解析 Spring AI ChatResponse 中的 citations 信息。
/** * 从ChatResponse中提取引用的文档信息 */ private List<Map<String, Object>> extractCitations(ChatResponse response) { List<Map<String, Object>> citations = new ArrayList<>(); if (response == null || response.getMetadata() == null) { return citations; } Map<String, Object> metadata = response.getMetadata(); Object docsObj = metadata.get("documents"); if (docsObj instanceof List<?>) { for (Object obj : (List<?>) docsObj) { if (obj instanceof Document doc) { Map<String, Object> citation = new HashMap<>(); citation.put("content", doc.getText()); citation.put("metadata", doc.getMetadata()); citations.add(citation); } } } return citations; }

总结

结合混合检索、Rerank重排序、Query改写、闲聊感知、查询增强和幻觉检测,可显著提升知识库问答系统的检索效果与答案质量。
建议:在实际应用中分阶段引入上述机制,持续优化用户体验和结果可靠性。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进