一、混合检索(向量 + 关键词)
1. 纯向量检索的局限
- 语义相近但答案靠后:向量相似度高不代表文档真正包含答案。
- 精确关键词匹配弱:用户搜索特定术语时,向量检索可能漏掉精确匹配的文档。
- 多路召回更可靠:结合向量检索和关键词检索,取并集后融合排序。
2. 混合检索的实现(以PGVector为例)
sql
-- 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排序)。
java
@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流程
java
@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的文档重排序。
java
@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 模型进行文档重排序。
java
@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 对检索结果按相关性重新排序。
java
/**
* 基于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集成方式
方式一:手动编排
java
@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集成
java
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 中。
java
@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. 自定义多查询改写服务
生成多个改写版本,分别检索后取并集,进一步提升召回率。
java
@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. 多查询检索流程
java
@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检索流程,避免浪费资源和产生无关检索结果。
java
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
java
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) 后,无检索结果时仍能正常生成回答。
java
@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. 基于引用验证的幻觉检测
幻觉检测的核心思路:检查模型生成的答案是否完全基于参考资料,识别不受支持的说法。
java
@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流程集成
java
@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完整优化流程
核心流程
查询改写 → 检索 → 重排 → 增强 → 生成
完整配置示例
java
@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 列表拼接为一段完整的上下文文本。
java
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 信息。
java
/**
* 从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改写、闲聊感知、查询增强和幻觉检测,可显著提升知识库问答系统的检索效果与答案质量。
建议:在实际应用中分阶段引入上述机制,持续优化用户体验和结果可靠性。