RAG 实战教学完全指南
1. RAG 是什么?为什么需要它?
1.1 大模型的三个致命短板
| 痛点 | 表现 | 根源 |
|---|---|---|
| 知识滞后 | 问最新政策,回答停留在训练截止日期 | 训练数据有截止时间 |
| 幻觉问题 | 一本正经地编造不存在的 API、数据 | 概率生成,无事实校验 |
| 私有知识盲区 | 问公司内部文档,完全不知道 | 训练集不包含企业私有数据 |
1.2 RAG 如何解决?
用户提问 → [检索] → 从知识库找到相关文档 → [增强] → 文档+问题拼成 Prompt → [生成] → LLM 回答
核心思想:不让 LLM 凭空编造,先把相关文档"塞"进 Prompt 里,让它基于事实回答。
1.3 一个对比
| 场景 | 纯 LLM | RAG 增强 |
|---|---|---|
| "公司的报销流程是什么?" | 编造通用流程 | 检索公司内部制度文档,精确回答 |
| "这款产品电池能用多久?" | 猜测典型值 | 检索产品说明书,引用具体参数 |
| "Spring AI 2.0 新特性?" | 不知道(训练数据旧) | 检索最新官方文档,实时回答 |
2. RAG 核心工作流程
2.1 离线阶段(知识入库)
原始文档 → 文档加载 → 文本分块 → 向量嵌入 → 向量数据库存储
↓ ↓ ↓ ↓ ↓
PDF/Word Document Token Embedding Milvus /
Markdown Reader Splitter Model Redis / ES
2.2 在线阶段(问答检索)
用户提问 → 查询重写 → 向量检索 → 文档后处理 → Prompt 增强 → LLM 生成 → 返回回答
↓ ↓ ↓ ↓ ↓ ↓ ↓
"怎么退款" 压缩历史 相似度计算 去重+排序 "根据以下 ChatModel 最终答案
topK 返回 时效过滤 上下文..."
2.3 模块化 RAG 四阶段
│ Pre-Retrieval │ Retrieval │ Post-Retrieval │ Generation
│ 预检索 │ 检索 │ 后检索 │ 生成 │
│ 查询重写 │ 向量检索 │ 去重 │ 上下文增强 │
│ 查询扩展 │ 混合检索 │ 重排序 │ 空上下文处理 │
│ 查询压缩 │ 元数据过滤 │ 内容压缩 │ 流式输出 │
│ 查询翻译 │ 文档合并 │ 时效过滤 │ 引用标注 │
3. 技术选型与架构设计
3.1 推荐技术栈
| 层级 | 技术选择 | 说明 |
|---|---|---|
| 应用框架 | Spring Boot 3.2+ | 企业级 Java 后端 |
| AI 框架 | Spring AI Alibaba 1.1.x | Spring AI + 阿里云 DashScope 扩展 |
| LLM | 通义千问 (DashScope) | 国内可用,中文理解最佳 |
| Embedding | DashScope text-embedding-v3 | 1536 维,中文效果优秀 |
| 向量数据库 | Milvus / Elasticsearch | 生产级向量检索 |
| 文档解析 | Apache Tika + Spring AI | 支持 PDF / Word / Markdown |
| 缓存 | Redis / Caffeine | 常见问题缓存 |
3.2 项目依赖 (Maven)
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.0</version> </parent> <dependencies> <!-- Spring AI Alibaba DashScope Starter --> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter-dashscope</artifactId> <version>1.1.2.0</version> </dependency> <!-- Spring AI Alibaba Graph Core(Agent Framework 核心) --> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-graph-core</artifactId> <version>1.0.0.3</version> </dependency> <!-- Spring AI Chat Client 自动配置 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-autoconfigure-model-chat-client</artifactId> </dependency> <!-- Spring AI RAG 支持 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-rag</artifactId> </dependency> <!-- Elasticsearch 向量存储 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-elasticsearch-store-spring-boot-starter</artifactId> </dependency> <!-- 文档解析 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-tika-document-reader</artifactId> </dependency> <!-- Spring Boot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>3.3 配置文件 (application.yml)
spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: model: qwen-max-latest temperature: 0.0 embedding: model: text-embedding-v3 elasticsearch: vector-store: uris: http://localhost:9200 index-name: knowledge_base dimensions: 1536 distance-type: COSINE index-type: HNSW rag: chunk: size: 512 overlap: 64 retrieval: top-k: 5 similarity-threshold: 0.7 agent: max-iterations: 104. 环境搭建
4.1 启动 Elasticsearch(带向量支持)
docker run -d \ --name es-vector \ -p 9200:9200 \ -e "discovery.type=single-node" \ -e "xpack.security.enabled=false" \ docker.elastic.co/elasticsearch/elasticsearch:8.12.04.2 验证连接
@SpringBootTest class VectorStoreConnectionTest { @Autowired private VectorStore vectorStore; @Test void testConnection() { List<Document> results = vectorStore.similaritySearch( SearchRequest.builder() .query("测试连接") .topK(1) .build() ); System.out.println("连接成功,返回 " + results.size() + " 条结果"); } }5. 文档处理 Pipeline
5.1 ETL 完整流程
@Component public class DocumentEtlPipeline { private final VectorStore vectorStore; private final EmbeddingModel embeddingModel; public DocumentEtlPipeline(VectorStore vectorStore, EmbeddingModel embeddingModel) { this.vectorStore = vectorStore; this.embeddingModel = embeddingModel; } /** * 完整的 ETL 流程:加载 → 分块 → 嵌入 → 存储 */ public void processDocuments(String directoryPath) throws IOException { List<File> files = Files.list(Paths.get(directoryPath)) .map(Path::toFile) .filter(this::isSupportedFormat) .collect(Collectors.toList()); for (File file : files) { processSingleDocument(file); } } private void processSingleDocument(File file) { // Step 1: Extract - 文档加载 List<Document> documents = loadDocument(file); // Step 2: Transform - 文本分块 List<Document> chunks = splitDocuments(documents); // Step 3: Load - 写入向量库 vectorStore.add(chunks); System.out.printf("文档 %s 处理完成,共 %d 个分块%n", file.getName(), chunks.size()); } private List<Document> loadDocument(File file) { return switch (getExtension(file)) { case "pdf" -> new PagePdfDocumentReader(file).get(); case "md" -> new MarkdownDocumentReader( new FileSystemResource(file.getPath())).get(); case "docx" -> new TikaDocumentReader(file).get(); default -> throw new UnsupportedOperationException( "不支持的文件格式: " + file.getName()); }; } private List<Document> splitDocuments(List<Document> docs) { TokenTextSplitter splitter = new TokenTextSplitter(); splitter.setChunkSize(512); splitter.setChunkOverlap(64); return splitter.apply(docs); } private boolean isSupportedFormat(File file) { String ext = getExtension(file); return Set.of("pdf", "md", "docx", "txt").contains(ext); } private String getExtension(File file) { String name = file.getName(); int dot = name.lastIndexOf('.'); return dot > 0 ? name.substring(dot + 1).toLowerCase() : ""; } }5.2 分块策略选择指南
| 策略 | 组件 | 适用场景 | 示例 |
|---|---|---|---|
| Token 分块 | TokenTextSplitter(512, 64) | 需要精确控制 LLM 上下文窗口 | 产品手册、技术文档 |
| 固定长度 | FixedTextSplitter(1000, 200) | 无明显结构的纯文本 | 日志、聊天记录 |
| 语义分块 | SemanticSplitter | 结构化文档(含表格、标题) | 法律文书、学术论文 |
5.3 元数据设计(重要!)
好的元数据设计是高效检索的基础:
private Document enrichMetadata(Document doc, File source) { doc.getMetadata().put("source", source.getName()); doc.getMetadata().put("type", determineDocType(source)); doc.getMetadata().put("timestamp", Instant.now().toString()); doc.getMetadata().put("department", "客服部"); // 业务字段 doc.getMetadata().put("productId", "PROD-001"); // 业务字段 doc.getMetadata().put("version", "v2.1"); // 版本管理 return doc; }元数据设计原则:
source和timestamp是必选字段- 业务字段用驼峰命名(如
productId) - 避免存入过长的文本内容(元数据应轻量)
6. 向量嵌入与存储
6.1 嵌入模型配置
@Configuration public class EmbeddingConfig { @Bean public EmbeddingModel embeddingModel(DashScopeApi dashScopeApi) { return new DashScopeEmbeddingModel(dashScopeApi); } }6.2 向量库索引初始化
@Configuration public class VectorStoreConfig { @Bean public VectorStore vectorStore(ElasticsearchVectorStoreProperties properties) { return new ElasticsearchVectorStore( properties.getVectorIndexConfig(), properties.getRestClient() ); } }6.3 批量写入优化
public void batchIngest(List<Document> allChunks, int batchSize) { // 按 1000 条/批次写入,避免单次写入过多 for (int i = 0; i < allChunks.size(); i += batchSize) { int end = Math.min(i + batchSize, allChunks.size()); List<Document> batch = allChunks.subList(i, end); vectorStore.add(batch); System.out.printf("批次 %d/%d 写入完成%n", (i / batchSize) + 1, (allChunks.size() + batchSize - 1) / batchSize); } }6.4 关键参数速查
| 参数 | 推荐值 | 说明 |
|---|---|---|
| dimensions | 1536 (DashScope v3) / 1536 (OpenAI) | 向量维度,建索引后不可改 |
| distance-type | COSINE | 余弦相似度,适合文本语义匹配 |
| index-type | HNSW | 分层可导航小世界,检索性能最优 |
| ef-construction | 200 | HNSW 构建参数,越大精度越高但速度越慢 |
| m | 16 | HNSW 每个节点的连接数 |
7. 检索策略设计
7.1 基础向量检索
@Component public class KnowledgeRetriever { private final VectorStore vectorStore; public KnowledgeRetriever(VectorStore vectorStore) { this.vectorStore = vectorStore; } /** * 基础向量检索 */ public List<Document> search(String query, int topK, double threshold) { return vectorStore.similaritySearch( SearchRequest.builder() .query(query) .topK(topK) .similarityThreshold(threshold) .build() ); } }7.2 带元数据过滤的检索
/** * 按产品 ID 过滤的检索 */ public List<Document> searchByProduct(String query, String productId) { FilterExpression filter = new FilterExpressionBuilder() .eq("productId", productId) .build(); return vectorStore.similaritySearch( SearchRequest.builder() .query(query) .topK(5) .similarityThreshold(0.7) .filterExpression(filter) .build() ); } /** * 多条件组合过滤 */ public List<Document> advancedSearch(String query, String dept, String type) { FilterExpression filter = new FilterExpressionBuilder() .eq("department", dept) .and() .eq("type", type) .build(); return vectorStore.similaritySearch( SearchRequest.builder() .query(query) .topK(8) .filterExpression(filter) .build() ); }7.3 动态过滤(多租户场景)
/** * 动态过滤表达式 — 根据当前租户自动过滤 */ @Component public class TenantAwareRetriever { private final VectorStore vectorStore; public List<Document> search(String query) { FilterExpression filter = () -> new FilterExpressionBuilder() .eq("tenantId", TenantContext.getCurrentTenant()) .build(); return vectorStore.similaritySearch( SearchRequest.builder() .query(query) .topK(5) .filterExpression(filter) .build() ); } }7.4 检索策略对比
| 策略 | 召回率 | 精确率 | 延迟 | 适用场景 |
|---|---|---|---|---|
| 纯向量检索 | 高 | 中 | 低 | 语义模糊的问题 |
| 纯关键词检索 | 低 | 高 | 极低 | 精确 ID/编码查询 |
| 混合检索 (7:3) | 高 | 高 | 中 | 通用场景(推荐) |
| 语义路由 | 高 | 高 | 低 | 问题类型明确时 |
8. RAG Advisor 配置
8.1 快速入门:QuestionAnswerAdvisor
@RestController public class SimpleRagController { private final ChatClient chatClient; public SimpleRagController(ChatModel chatModel, VectorStore vectorStore) { var advisor = QuestionAnswerAdvisor.builder(vectorStore) .searchRequest(SearchRequest.builder() .similarityThreshold(0.7) .topK(5) .build()) .build(); this.chatClient = ChatClient.builder(chatModel) .defaultAdvisors(advisor) .build(); } @PostMapping("/rag/ask") public String ask(@RequestBody QuestionRequest request) { return chatClient.prompt() .user(request.getQuestion()) .call() .content(); } }8.2 进阶配置:RetrievalAugmentationAdvisor
生产级推荐配置,支持全链路自定义:
@Configuration public class RagConfiguration { @Bean public Advisor ragAdvisor(VectorStore vectorStore, ChatClient.Builder chatClientBuilder) { return RetrievalAugmentationAdvisor.builder() // 1. 查询重写:压缩对话历史,提出独立查询 .queryTransformers( CompressionQueryTransformer.builder() .chatClientBuilder(chatClientBuilder) .build(), RewriteQueryTransformer.builder() .chatClientBuilder(chatClientBuilder) .build() ) // 2. 文档检索:向量检索 + 元数据过滤 .documentRetriever( VectorStoreDocumentRetriever.builder() .vectorStore(vectorStore) .similarityThreshold(0.65) .topK(6) .build() ) // 3. 检索后处理:去重 + 排序 + 时效过滤 .documentPostProcessors( new DuplicateRemoverPostProcessor(), new RelevanceRankerPostProcessor(0.7) ) // 4. 查询增强:拼接上下文 + 问题 .queryAugmenter( ContextualQueryAugmenter.builder() .allowEmptyContext(false) .build() ) .build(); } }8.3 自定义 Prompt 模板
private static final String RAG_PROMPT_TEMPLATE = """ 你是一个专业的知识库助手。请严格基于以下上下文信息回答问题。 上下文信息: --------------------- {context} --------------------- 回答规则: 1. 仅使用上下文中明确提供的信息 2. 如果上下文中没有相关信息,直接回答"根据现有资料无法回答该问题" 3. 引用具体数据时需标注来源 4. 回答应简洁、准确、有条理 用户问题: {question} """; @Bean public Advisor ragAdvisorWithCustomPrompt(VectorStore vectorStore, ChatClient.Builder chatClientBuilder) { return RetrievalAugmentationAdvisor.builder() .documentRetriever( VectorStoreDocumentRetriever.builder() .vectorStore(vectorStore) .topK(6) .build() ) .queryAugmenter( ContextualQueryAugmenter.builder() .promptTemplate(RAG_PROMPT_TEMPLATE) .allowEmptyContext(false) .build() ) .build(); }9. 查询增强与后处理
9.1 查询变换策略
/** * 查询变换工具类 */ @Component public class QueryTransformService { private final ChatClient.Builder chatClientBuilder; /** * 查询压缩:去除对话历史中的冗余 * 输入: "我之前问了你关于报销的事...那出差住宿标准是什么?" * 输出: "出差住宿标准是多少?" */ public Query compressQuery(String conversationHistory, String currentQuery) { CompressionQueryTransformer transformer = CompressionQueryTransformer.builder() .chatClientBuilder(chatClientBuilder) .build(); return transformer.transform( new Query(conversationHistory + "\n" + currentQuery) ); } /** * 多查询扩展:生成语义变体提升覆盖率 * 输入: "如何申请报销" * 输出: ["如何申请报销", "报销申请步骤", "费用报销流程说明"] */ public List<Query> expandQueries(String query) { MultiQueryExpander expander = MultiQueryExpander.builder() .chatClientBuilder(chatClientBuilder) .numberOfQueries(3) .includeOriginal(true) .build(); return expander.expand(new Query(query)); } }9.2 自定义文档后处理器
时效过滤处理器
/** * 过滤超过 30 天的过期文档 */ public class FreshnessFilterPostProcessor implements DocumentPostProcessor { private static final Duration MAX_AGE = Duration.ofDays(30); @Override public List<Document> process(List<Document> documents) { Instant cutoff = Instant.now().minus(MAX_AGE); return documents.stream() .filter(doc -> { String timestamp = doc.getMetadata().get("timestamp"); if (timestamp == null) return true; return Instant.parse(timestamp).isAfter(cutoff); }) .collect(Collectors.toList()); } } /** * 对过长文档进行关键信息提取,避免 Prompt 超出 Token 上限 */ @Component public class ContentCompressorPostProcessor implements DocumentPostProcessor { private static final int MAX_CONTENT_LENGTH = 2000; @Override public List<Document> process(List<Document> documents) { return documents.stream() .map(doc -> { if (doc.getText().length() > MAX_CONTENT_LENGTH) { String compressed = doc.getText().substring(0, MAX_CONTENT_LENGTH) + "\n... [内容过长,已截断]"; return new Document(compressed, doc.getMetadata()); } return doc; }) .collect(Collectors.toList()); } }10. 完整实战:智能客服系统
10.1 业务场景
一个电商平台的智能客服 RAG 服务,支持:
- 商品参数查询(规格、价格、库存)
- 售后政策解答(退换货条件、保修期限)
- 操作指南咨询(使用说明、常见问题)
10.2 核心服务实现
@Service public class EcommerceRagService { private final ChatClient chatClient; private final VectorStore vectorStore; private final KnowledgeRetriever retriever; public EcommerceRagService(ChatModel chatModel, VectorStore vectorStore, ChatClient.Builder chatClientBuilder) { this.vectorStore = vectorStore; this.retriever = new KnowledgeRetriever(vectorStore); // 构建高级 RAG Advisor Advisor ragAdvisor = RetrievalAugmentationAdvisor.builder() .queryTransformers( RewriteQueryTransformer.builder() .chatClientBuilder(chatClientBuilder) .build() ) .documentRetriever( VectorStoreDocumentRetriever.builder() .vectorStore(vectorStore) .similarityThreshold(0.65) .topK(6) .build() ) .documentPostProcessors( new DuplicateRemoverPostProcessor(), new FreshnessFilterPostProcessor(), new RelevanceRankerPostProcessor(0.7) ) .queryAugmenter( ContextualQueryAugmenter.builder() .allowEmptyContext(false) .build() ) .build(); this.chatClient = ChatClient.builder(chatModel) .defaultAdvisors(ragAdvisor) .build(); } /** * 通用客服咨询 */ public String answerGeneralQuestion(String question) { return chatClient.prompt() .user(question) .call() .content(); } /** * 按商品 ID 检索回答 */ public String answerProductQuestion(String question, String productId) { return chatClient.prompt() .user(question) .advisors(a -> a.param( VectorStoreDocumentRetriever.FILTER_EXPRESSION, "productId == '" + productId + "'" )) .call() .content(); } /** * 按部门 + 文档类型检索 */ public String answerFilteredQuestion(String question, String department, String docType) { return chatClient.prompt() .user(question) .advisors(a -> a.param( VectorStoreDocumentRetriever.FILTER_EXPRESSION, "department == '" + department + "' AND type == '" + docType + "'" )) .call() .content(); } /** * 流式回答(大段内容,边生成边返回) */ public Flux<String> streamAnswer(String question) { return chatClient.prompt() .user(question) .stream() .content(); } }10.3 REST API
@RestController @RequestMapping("/api/rag") public class RagController { private final EcommerceRagService ragService; public RagController(EcommerceRagService ragService) { this.ragService = ragService; } /** * 同步问答 */ @PostMapping("/ask") public ApiResponse<AnswerResult> ask(@RequestBody QuestionRequest request) { String answer; if (request.getProductId() != null) { answer = ragService.answerProductQuestion( request.getQuestion(), request.getProductId()); } else { answer = ragService.answerGeneralQuestion( request.getQuestion()); } return ApiResponse.success(new AnswerResult(answer)); } /** * 流式问答(SSE) */ @PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> streamAsk(@RequestBody QuestionRequest request) { return ragService.streamAnswer(request.getQuestion()); } } @Data class QuestionRequest { @NotBlank private String question; private String productId; // 可选:商品过滤条件 private String department; // 可选:部门过滤 } @Data @AllArgsConstructor class AnswerResult { private String answer; }10.4 测试验证
@SpringBootTest class EcommerceRagServiceTest { @Autowired private EcommerceRagService ragService; @Test void testGeneralQuestion() { String answer = ragService.answerGeneralQuestion("7天无理由退货的条件是什么?"); assertNotNull(answer); assertFalse(answer.contains("不知道")); System.out.println("回答: " + answer); } @Test void testProductSpecificQuestion() { String answer = ragService.answerProductQuestion( "电池续航多久?", "SMARTPHONE-X"); assertNotNull(answer); System.out.println("商品 X 续航回答: " + answer); } @Test void testUnanswerableQuestion() { String answer = ragService.answerGeneralQuestion( "公司CEO的狗叫什么名字?"); assertTrue(answer.contains("无法回答") || answer.contains("不知道")); } }10.5 预期效果
| 用户问题 | 检索逻辑 | 回答结果 |
|---|---|---|
| "手机 X 电池续航多久?" | 向量检索 + productId 过滤 | "智能手机 X 的待机时间为 72 小时,连续通话可达 15 小时。" |
| "PROD-123 退换货政策" | 元数据匹配 productId | "商品 PROD-123 支持 7 天无理由退换,需保持包装完好且未激活。" |
| "蓝牙耳机怎么连接?" | 纯向量语义检索 | "打开充电仓→手机开启蓝牙→搜索设备→点击配对" |
11. 性能优化
11.1 优化清单
| 优化方向 | 具体措施 | 预期效果 |
|---|---|---|
| 检索层 | 批量写入(1000 条/批次) | 减少 70% I/O 次数 |
| 检索层 | 常见问题 Redis 缓存 | 命中时延迟降至 5ms 以下 |
| 检索层 | HNSW 索引 + ef_construction=200 | 检索延迟 < 20ms |
| 生成层 | 流式输出 chatClient.stream() | 首字延迟减少 60% |
| 生成层 | 简单问题用 qwen-turbo,复杂问题用 qwen-plus | 降低 40% 推理成本 |
| 文档层 | ContentCompressorPostProcessor 压缩长文档 | Token 消耗降低 30% |
| 文档层 | 分块大小从 512 降至 300 | 提升精确度,减少无关内容 |
11.2 缓存策略实现
@Component public class RagCacheService { private final Cache<String, String> answerCache; public RagCacheService() { this.answerCache = Caffeine.newBuilder() .maximumSize(10000) .expireAfterWrite(Duration.ofHours(24)) .build(); } public Optional<String> getCachedAnswer(String question) { return Optional.ofNullable(answerCache.getIfPresent(normalize(question))); } public void cacheAnswer(String question, String answer) { answerCache.put(normalize(question), answer); } private String normalize(String question) { return question.trim().toLowerCase().replaceAll("\\s+", " "); } }11.3 性能基准参考
| 阶段 | 目标延迟 | 瓶颈 |
|---|---|---|
| 文档加载 | 1-3s / 文件 | 文件 I/O |
| 文本嵌入 | 50-100ms / 块 | Embedding API |
| 向量检索 | 10-30ms | 向量索引 |
| Prompt 增强 | < 5ms | 字符串拼接 |
| LLM 推理 | 500ms-3s | 模型推理 |
| 端到端 | 1-4s | LLM 推理 |
12. 常见问题排查
12.1 问题速查表
| 问题现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| 检索结果为空 | 向量库无数据 | 检查 vectorStore.addAll() 是否执行 | 确认 ETL pipeline 运行成功 |
| 检索结果为空 | 过滤条件过严 | 单独测试无 filter 的检索 | 放宽 FILTER_EXPRESSION |
| 检索结果为空 | 相似度阈值过高 | 逐步降低阈值测试 | 从 0.8 降至 0.6 再试 |
| 回答出现幻觉 | 上下文不足 | 检查检索返回的文档数 | 增大 topK 至 8-10 |
| 回答出现幻觉 | Prompt 无严格约束 | 检查 Prompt 模板 | 添加"严格基于上下文"指令 |
| 回答不相关 | 分块语义割裂 | 检查分块边界 | 增大 chunkOverlap 至 128 |
| 响应延迟高 | 检索未命中索引 | GET /_cat/indices | 重建 HNSW 索引 |
| 响应延迟高 | 文档块过大 | 检查平均 Token 数 | 减小 chunkSize 至 256 |
| 向量维数不匹配 | 换了 Embedding 模型 | 对比新旧模型维度 | 重建索引(不可在线改维度) |
12.2 调试工具类
@Component public class RagDebugger { private final VectorStore vectorStore; /** * 打印检索结果,用于排查检索质量问题 */ public void debugRetrieval(String query, int topK) { List<Document> results = vectorStore.similaritySearch( SearchRequest.builder() .query(query) .topK(topK) .build() ); System.out.println("=== 检索调试: " + query + " ==="); for (int i = 0; i < results.size(); i++) { Document doc = results.get(i); System.out.printf("[%d] 相似度: %.4f | 来源: %s%n", i + 1, doc.getScore(), doc.getMetadata().get("source")); System.out.printf(" 内容预览: %.100s...%n%n", doc.getText()); } System.out.println("=== 共检索到 " + results.size() + " 条 ==="); } /** * 检查向量库中的文档数量 */ public long countDocuments() { // 取决于具体 VectorStore 实现 return vectorStore.similaritySearch( SearchRequest.builder() .query("") .topK(10000) .build() ).size(); } }12.3 向量索引重建脚本
@Component public class IndexRebuildService { private final VectorStore vectorStore; private final ElasticsearchRestClient esClient; /** * 删除旧索引并重建 */ public void rebuildIndex(String indexName) { // 1. 删除旧索引 esClient.indices().delete(d -> d.index(indexName)); // 2. 创建新索引(通过 VectorStore 自动创建) // 重新运行 ETL pipeline System.out.println("索引 " + indexName + " 已重建"); } /** * 迁移数据到新索引 */ public void migrateData(String oldIndex, String newIndex) { // 1. 从旧索引读取所有文档 // 2. 重新 Embedding // 3. 写入新索引 System.out.println("数据迁移完成: " + oldIndex + " -> " + newIndex); } }总结:从零到一的 Checklist
- 搭建 Spring Boot 3.2+ 项目,引入 Spring AI Alibaba 依赖
- 配置 DashScope API Key 和 Elasticsearch 连接
- 实现 ETL Pipeline(加载→分块→嵌入→存储)
- 设计元数据结构(source、type、timestamp、业务字段)
- 配置 QuestionAnswerAdvisor(快速验证)
- 升级为 RetrievalAugmentationAdvisor(生产级)
- 添加查询变换(压缩 + 重写 + 扩展)
- 添加文档后处理(去重 + 排序 + 时效过滤)
- 添加缓存层(Caffeine/Redis)
- 性能测试 + 监控报警
- 建立知识库文档管理规范(添加/更新/删除流程)
