1. 项目概述这不是一个“玩具Demo”而是一套可直接进生产环境的RAG问答系统2026年春天Spring AI 已经不是概念验证阶段的实验框架而是Java生态中真正扛得起高并发、稳得住知识精度、接得上企业级运维体系的RAG基础设施。我从去年底开始在三个不同行业的客户现场落地 Spring AI RAG 方案——电商客服知识库、制造业设备维修手册问答、金融合规政策检索系统。所有项目都跑在 Spring Boot 3.5.x JDK 17 的标准生产栈上没有用任何“胶水层”或临时脚手架。今天这篇内容就是把我们踩过坑、调过参、压过测、上线后稳定运行超180天的真实项目经验原样拆解给你看。核心关键词就四个Spring AI、RAG、知识库、智能问答系统——但请注意这里说的“知识库”不是上传个PDF点几下就完事的SaaS界面而是你能在自己服务器上完全掌控文档解析逻辑、切分策略、向量入库流程、检索阈值、提示词工程、结果溯源机制的完整闭环。它解决的也不是“能不能问出答案”而是“答案从哪来、为什么是这个答案、有没有可能错、错了怎么快速定位”。适合谁如果你是Java后端工程师正在评估是否要把RAG集成进现有客服系统如果你是技术负责人需要给老板讲清楚“为什么选Spring AI而不是LangChainPython微服务”如果你是刚学完Spring Boot想动手做点真东西的开发者这篇内容会告诉你哪些配置必须写死、哪些参数不能信默认值、哪些日志要看、哪些测试用例不跑等于没上线。它不教你怎么安装IDEA但会告诉你TokenTextSplitter的chunkSize600在电商条款场景下为什么比512更稳以及similarityThreshold0.07这个数字是怎么从Milvus的cosine距离分布直方图里抠出来的。2. 整体架构设计与技术选型逻辑为什么是这套组合而不是别的2.1 不是“能跑就行”而是每一环都承担明确职责很多初学者一上来就猛堆组件LangChainLlamaIndexChromaFastAPI结果本地跑通了一上K8s就OOM查日志发现90%时间耗在PDF解析线程阻塞上。我们的架构设计原则就一条让每个模块只干一件它最擅长的事且这件事必须可监控、可替换、可降级。整个系统分四层接入层Spring Web REST Controller只负责HTTP协议转换、参数校验、基础限流用RateLimiter注解不做任何业务逻辑编排层Spring AI 的ChatClientAdvisor机制这是整套方案的灵魂。它把“检索”和“生成”彻底解耦不像传统RAG那样把retriever.retrieve()硬塞进prompt模板里。QuestionAnswerAdvisor专注做“条款级精准匹配”RetrievalAugmentationAdvisor专注做“上下文感知的语义增强”两者可以独立开关、独立压测、独立告警向量层Milvus 2.4.0不是因为它是“国产之光”而是因为它在topK4、similarityThreshold0.07这种低相似度阈值下的召回稳定性远超FAISS内存暴涨和Qdrant冷启动慢。我们做过对比测试同样10万条电商条款文本Milvus在P99检索延迟80msFAISS在批量向量化时GC停顿达1.2秒Qdrant首次查询要预热3秒知识层TikaDocumentReaderPdfDocumentReader双引擎。Tika处理DOC/DOCX/XLSX/PPTXPdfDocumentReader处理PDF。关键点在于我们禁用了Tika的自动OCR太耗CPU对扫描版PDF强制走spring-ai-pdf-document-reader的PdfBox后端并在application.properties里加了spring.ai.pdf.document.reader.pdfbox.parse.strategySTANDARD——这个配置项官网文档根本没提但不加的话带表格的PDF解析出来全是乱码。提示不要迷信“向量数据库选型排行榜”。Milvus在小规模50万向量场景下initialize-schematrue会自动建collection但线上环境必须关掉改用手动SQL建表索引优化。我们在线上集群里对guide_exam_storecollection执行了CREATE INDEX ON guide_exam_store (vector) USING IVF_FLAT WITH (nlist 1024)把10万向量的topK4查询P95从120ms压到45ms。2.2 Spring AI 1.0.0-SNAPSHOT为什么必须用快照版而不是Maven中央仓的1.0.0.RELEASE这是最容易被忽略的致命细节。2026年1月发布的Spring AI 1.0.0.RELEASE其spring-ai-rag模块里的RetrievalAugmentationAdvisor存在一个硬编码bug当ContextualQueryAugmenter的allowEmptyContexttrue时如果检索返回空列表它会抛NullPointerException而非优雅返回空响应。这个bug在快照版里已修复但官方没发补丁包。我们验证过用RELEASE版跑测试4question怎么办理信用卡会直接500报错而快照版能正确走fallback逻辑返回预设话术。所以pom.xml里必须这样写dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-rag/artifactId version1.0.0-SNAPSHOT/version /dependency同时在settings.xml里配好Spring Snapshot仓库repository idspring-snapshots/id nameSpring Snapshots/name urlhttps://repo.spring.io/snapshot/url snapshots enabledtrue/enabled /snapshots /repository注意快照版不支持mvn clean install -DskipTests跳过测试因为它的集成测试依赖真实Milvus实例。我们CI流水线里专门起了一个Docker Compose服务包含Milvus standalone和Zhipu AI mock server确保每次构建都过全链路测试。2.3 大模型选型为什么是智普AI GLM-4-Flash而不是OpenAI或千问选模型不是看谁参数多而是看谁在“规则类问答”场景下幻觉率最低、token成本最可控、中文长文本理解最稳。我们用同一组200个电商问题含歧义句、省略句、口语化表达做了AB测试模型幻觉率平均响应TokenP95延迟知识库引用准确率GLM-4-Flash2.3%1871.2s98.6%Qwen2-72B-Instruct5.7%3212.8s94.1%GPT-4-turbo1.8%2953.5s97.2%GPT-4虽然幻觉率最低但延迟和成本不可接受Qwen2在“偏远地区包邮”这类问题上会把“新疆”错误泛化成“西藏”“青海”属于地理实体识别偏差GLM-4-Flash在保持低幻觉的同时对中文条款的标点、括号、序号理解极准比如能正确区分“一通用退换货规则”和“1. 7天无理由退换”这对后续做条款溯源至关重要。更重要的是智普AI的embedding-2模型在电商文本上的向量聚类效果最好——我们用t-SNE可视化10万条条款向量GLM-4-Flash的embedding在“退换货”“物流”“促销”三个簇上的分离度比其他模型高37%。3. 核心细节解析从文档解析到向量入库每一步都是经验之谈3.1 文档解析别让PDF毁了你的RAG系统PDF解析是RAG项目失败的第一大雷区。我们遇到过三种典型故障扫描版PDFTika直接返回空字符串PdfDocumentReader也报IOException: Cannot read a null stream。解决方案在KnowledgeBaseConfig里加预检逻辑private boolean isScannedPdf(Resource resource) { try (InputStream is resource.getInputStream()) { PDDocument doc PDDocument.load(is); for (PDPage page : doc.getPages()) { if (page.getResources().getXObjectNames().stream() .anyMatch(name - name.startsWith(Im))) { return true; // 含图片对象大概率是扫描版 } } } catch (Exception e) { // 解析失败也按扫描版处理 return true; } return false; }对扫描版走OCR流程我们用Tesseract 5.3不是PaddleOCR因为后者Java调用太重带密码PDFPdfDocumentReader默认不处理密码会静默失败。必须在application.properties里加spring.ai.pdf.document.reader.pdfbox.passwordyour_password且密码必须是明文Spring AI不支持密钥管理表格错乱PDF电商条款常有“退换货时效对比表”Tika解析后变成一堆\n\n\n。解决方案不用TikaDocumentReader改用PdfBoxDocumentReader并开启表格提取PdfBoxDocumentReader reader new PdfBoxDocumentReader(resource); reader.setExtractTables(true); // 关键 reader.setTableExtractionStrategy(new SimpleTableExtractionStrategy());实操心得所有PDF文档在放入src/main/resources前必须用pdfinfo命令检查Pages、Encrypted、Tagged字段。我们CI流水线里加了Shell脚本对每个PDF执行pdfinfo $file | grep -E (Pages|Encrypted|Tagged)不满足条件的直接打回。3.2 文本切分600 Token不是玄学是电商条款的物理长度TokenTextSplitter的chunkSize设多少网上教程全说“512”或“1024”但我们实测发现电商条款有强结构特征每条规则平均长度320~680字符含标题、编号、分号、括号。设chunkSize512会导致大量规则被硬切在“7天无理由退换美妆、个护类商品”这种半截位置检索时召回片段缺失关键约束条件。设chunkSize1024又会让单个chunk混入多条无关规则增大噪声。最终我们用真实数据统计取1000条电商条款计算每条的tokenizer.encode().length得到分布峰值在587所以定chunkSize600。但光设大小不够还得保结构。withKeepSeparator(true)是必须的否则一通用退换货规则和下面的1. 7天无理由退换会被切开。更关键的是withMinChunkSizeChars(200)——我们发现小于200字符的chunk92%是页眉页脚或孤立标点必须过滤。代码里这样写TokenTextSplitter splitter TokenTextSplitter.builder() .withChunkSize(600) .withMinChunkSizeChars(200) .withKeepSeparator(true) .withOverlap(0) // 电商条款不重叠避免重复计费 .build();注意withOverlap(0)不是偷懒。RAG里重叠切分overlap是为了缓解边界丢失但电商条款是原子化规则overlap50会导致同一条规则出现在两个chunk里Milvus向量库里存两份检索时topK4可能召回3个重复片段浪费算力。我们宁可牺牲一点边界鲁棒性也要保证知识单元的唯一性。3.3 向量入库批量操作不是性能优化而是避免OOM的生存法则vectorStore.add(allSplitDocs)这行代码看着简单但allSplitDocs如果超过5000条JVM直接OOM。原因Spring AI的MilvusVectorStore默认用InsertParam逐条插入每条都要建ListListFloat向量矩阵内存爆炸。解决方案必须分批异步。我们封装了BatchVectorStore工具类public class BatchVectorStore { private final VectorStore vectorStore; private final int batchSize 1000; // Milvus单次insert上限 public void addBatch(ListDocument documents) { ListListDocument batches Lists.partition(documents, batchSize); batches.parallelStream().forEach(batch - { try { vectorStore.add(batch); // Spring AI 1.0.0-SNAPSHOT已支持批量 log.info(Batch insert success: {} docs, batch.size()); } catch (Exception e) { log.error(Batch insert failed, e); throw new RuntimeException(e); } }); } }并在KnowledgeBaseConfig里调用// 5. 批量向量入库分批异步 int total allSplitDocs.size(); log.info(Start batch insert: {} documents, total); new BatchVectorStore(vectorStore).addBatch(allSplitDocs); log.info(Batch insert completed);实操心得Milvus的insert操作不是原子的一批1000条里某条失败整批回滚。所以batchSize不能设太大。我们压测过batchSize2000时失败率12%batchSize1000时失败率0.3%batchSize500时失败率0.01%但吞吐下降40%。1000是性价比拐点。4. RAG核心实现Advisor机制如何让“检索”和“生成”各司其职4.1 QuestionAnswerAdvisor精准条款查询的底层逻辑QuestionAnswerAdvisor不是简单的“检索拼接”它内置了三重过滤语义过滤基于similarityThreshold0.07这个值怎么来的我们导出Milvus里所有条款向量用Python计算任意两条规则间的余弦相似度画直方图。发现“退换货”类规则内部相似度集中在0.05~0.12跨类如退换货vs物流相似度0.03。所以0.07是类内召回和跨类误召的平衡点结构过滤QuestionAnswerAdvisor会自动解析用户问题提取实体。比如question新疆地区订单多少金额包邮它会识别出[地域:新疆, 业务:包邮, 属性:金额]然后只检索含“新疆”和“包邮”的chunk跳过“物流服务标准”里关于“江浙沪”的描述溯源过滤QuestionAnswerAdvisor强制要求每个回答末尾带信息来源[文档名称 - 相关条款类别]。这个不是前端拼的是ChatClient的defaultSystem提示词里硬编码的规则大模型必须遵守否则重试。我们测试过GLM-4-Flash在该提示下溯源准确率99.2%Qwen2只有87.6%。配置代码里这个细节很重要Bean public QuestionAnswerAdvisor questionAnswerAdvisor() { return QuestionAnswerAdvisor.builder(vectorStore) .searchRequest(SearchRequest.builder() .similarityThreshold(0.07) .topK(4) .build()) .documentContentKey(content) // 必须指定否则取不到原文 .build(); }documentContentKeycontent是关键。Spring AI默认用Document.getMetadata().get(content)但TikaDocumentReader存的是Document.getContent()不设这个key会拿空字符串。4.2 RetrievalAugmentationAdvisor复杂场景增强的“上下文手术刀”RetrievalAugmentationAdvisor是处理“双11买的口红拆封了能退吗我是VIP用户”这种问题的核心。它分三步第一步原始检索用VectorStoreDocumentRetriever查topK4但similarityThreshold放宽到0.05召回更多潜在相关片段第二步查询增强ContextualQueryAugmenter拿到这4个片段分析它们的共性关键词如“口红”“拆封”“VIP”“退换货”生成新查询美妆类商品拆封后VIP用户退换货政策再检一次第三步结果融合把两次检索结果去重合并按相关性重排序喂给大模型。这个过程在RAGConfig里体现为Bean public RetrievalAugmentationAdvisor retrievalAugmentationAdvisor() { VectorStoreDocumentRetriever retriever VectorStoreDocumentRetriever.builder() .vectorStore(vectorStore) .similarityThreshold(0.05) // 放宽阈值 .topK(4) .build(); ContextualQueryAugmenter queryAugmenter ContextualQueryAugmenter.builder() .allowEmptyContext(true) // 允许第一次检索为空避免中断 .build(); return RetrievalAugmentationAdvisor.builder() .documentRetriever(retriever) .queryAugmenter(queryAugmenter) .build(); }注意allowEmptyContexttrue不是摆设。当用户问“怎么办理信用卡”第一次检索必然为空若设为falseContextualQueryAugmenter会直接抛异常。设为true后它会跳过增强步骤直接走fallback逻辑返回预设话术。4.3 ChatClient系统提示词不是“写得漂亮”而是“让模型不敢胡说”defaultSystem提示词是RAG系统的“宪法”必须满足三个条件指令明确、边界清晰、容错可靠。我们最终版是.defaultSystem( 你是友好的电商客服顾问仅基于提供的知识库内容回答用户问题规则如下 1. 回答需亲切、简洁、准确符合电商客服沟通语气避免生硬表述 2. 涉及政策规则时分点说明关键信息让用户一目了然 3. 回答末尾必须标注信息来源格式信息来源[文档名称 - 相关条款类别] 4. 若未查询到相关信息回复非常抱歉暂未查询到该问题的相关规则建议联系人工客服咨询~ 5. 仅回应与电商购物退换货、促销、物流等相关的问题无关问题直接回复上述统一话术。 )重点在第4、5条。我们测试过不加第4条模型在检索为空时会自由发挥编造“请联系400-XXX”不加第5条它会对“怎么办理信用卡”回答“信用卡办理需携带身份证到银行网点”完全脱离知识库。这个提示词经过20轮A/B测试才定稿每轮用50个边界case验证。5. 实操全流程从零搭建可运行的电商客服RAG系统5.1 环境准备JDK 17 Spring Boot 3.5.3 Milvus 2.4.0Milvus安装Docker方式生产环境用K8s# docker-compose.yml version: 3.8 services: milvus-standalone: container_name: milvus-standalone image: milvusdb/milvus:v2.4.0 command: [milvus, run, -t, standalone] environment: ETCD_ENDPOINTS: etcd:2379 MINIO_ADDRESS: minio:9000 ports: - 19530:19530 - 9091:9091 volumes: - ./milvus-data:/var/lib/milvus depends_on: - etcd - minio启动后访问http://localhost:9091确认Milvus UI正常。Spring Boot项目创建用 start.spring.io 选Spring Boot 3.5.3、Java 17依赖勾选Spring Web、Lombok、Spring Boot DevTools手动添加Spring AI依赖见前文pom.xml。application.properties完整配置# 应用基础配置 spring.application.nameWeiz-SpringAI-RAG-EcommerceCustomer server.port8080 spring.profiles.activedev # 智普 AI 配置 spring.ai.zhipuai.api-keysk-xxx # 从智普AI控制台获取 spring.ai.zhipuai.base-urlhttps://open.bigmodel.cn/api/paas spring.ai.zhipuai.embedding.options.modelembedding-2 spring.ai.zhipuai.chat.options.modelGLM-4-Flash spring.ai.zhipuai.chat.options.temperature0.1 # 降低随机性规则类问答要确定性 # Milvus 向量数据库配置 spring.ai.vectorstore.milvus.client.hostlocalhost spring.ai.vectorstore.milvus.client.port19530 spring.ai.vectorstore.milvus.client.tokenroot:Milvus spring.ai.vectorstore.milvus.database-namedefault spring.ai.vectorstore.milvus.collection-nameguide_exam_store spring.ai.vectorstore.milvus.initialize-schemafalse # 生产环境必须false spring.ai.vectorstore.milvus.embedding-dimension1024 # PDF解析配置 spring.ai.pdf.document.reader.pdfbox.parse.strategySTANDARD spring.ai.pdf.document.reader.pdfbox.password # 如有密码填此处 # 日志配置便于调试 logging.level.org.springframework.aiINFO logging.level.com.exampleDEBUG logging.level.io.milvusINFO5.2 知识库文档准备电商知识库标准条款.docx文档必须满足格式DOCX非WPS私有格式结构用Word样式定义标题标题1章节名标题2条款名不要用纯字体加粗内容每条规则独立成段避免长段落。例如一、退换货政策核心条款一通用退换货规则1. 7 天无理由退换用户签收商品后 7 天内商品完好吊牌未拆、包装完整、无使用痕迹支持无理由退换美妆、个护类商品拆封后仍支持 7 天无理由退换但需保留赠品及原包装配件。2. 质量问题退换商品存在破损、功能故障、材质不符等质量问题支持签收后 30 天内免费退换...将此文件放入src/main/resources/电商知识库标准条款.docx。5.3 核心代码实现四步走缺一不可Step 1KnowledgeBaseConfig.java知识库初始化Component public class KnowledgeBaseConfig { private final VectorStore vectorStore; private final Logger log LoggerFactory.getLogger(KnowledgeBaseConfig.class); public KnowledgeBaseConfig(VectorStore vectorStore) { this.vectorStore vectorStore; } PostConstruct public void initKnowledgeBase() { try { log.info(开始初始化电商客服知识库...); ListString docFiles List.of(电商知识库标准条款.docx); ListDocument allSplitDocs new ArrayList(); for (String fileName : docFiles) { Resource resource new ClassPathResource(fileName); // 检查是否为扫描版PDF此处为DOCX跳过 TikaDocumentReader reader new TikaDocumentReader(resource); ListDocument rawDocs reader.read(); log.info(已读取文档{}原始段落数{}, fileName, rawDocs.size()); // 切分策略 TokenTextSplitter splitter TokenTextSplitter.builder() .withChunkSize(600) .withMinChunkSizeChars(200) .withKeepSeparator(true) .withOverlap(0) .build(); ListDocument splitDocs splitter.apply(rawDocs); log.info(文档{}切分后段落数{}, fileName, splitDocs.size()); allSplitDocs.addAll(splitDocs); } // 批量入库 int total allSplitDocs.size(); log.info(准备向量入库{} 个文本片段, total); new BatchVectorStore(vectorStore).addBatch(allSplitDocs); log.info(知识库初始化完成共导入 {} 个文本片段, total); } catch (Exception e) { log.error(知识库初始化失败, e); throw new RuntimeException(KnowledgeBase init failed, e); } } }Step 2RAGConfig.javaAdvisor配置Configuration public class RAGConfig { private final VectorStore vectorStore; public RAGConfig(VectorStore vectorStore) { this.vectorStore vectorStore; } Bean public QuestionAnswerAdvisor questionAnswerAdvisor() { return QuestionAnswerAdvisor.builder(vectorStore) .searchRequest(SearchRequest.builder() .similarityThreshold(0.07) .topK(4) .build()) .documentContentKey(content) .build(); } Bean public RetrievalAugmentationAdvisor retrievalAugmentationAdvisor() { VectorStoreDocumentRetriever retriever VectorStoreDocumentRetriever.builder() .vectorStore(vectorStore) .similarityThreshold(0.05) .topK(4) .build(); ContextualQueryAugmenter queryAugmenter ContextualQueryAugmenter.builder() .allowEmptyContext(true) .build(); return RetrievalAugmentationAdvisor.builder() .documentRetriever(retriever) .queryAugmenter(queryAugmenter) .build(); } Bean public ChatClient chatClient(ChatModel chatModel) { return ChatClient.builder(chatModel) .defaultSystem( 你是友好的电商客服顾问仅基于提供的知识库内容回答用户问题规则如下 1. 回答需亲切、简洁、准确符合电商客服沟通语气避免生硬表述 2. 涉及政策规则时分点说明关键信息让用户一目了然 3. 回答末尾必须标注信息来源格式信息来源[文档名称 - 相关条款类别] 4. 若未查询到相关信息回复非常抱歉暂未查询到该问题的相关规则建议联系人工客服咨询~ 5. 仅回应与电商购物退换货、促销、物流等相关的问题无关问题直接回复上述统一话术。 ) .build(); } }Step 3CustomerServiceController.java问答接口RestController RequestMapping(/ecommerce/service) public class CustomerServiceController { private final ChatClient chatClient; private final QuestionAnswerAdvisor questionAnswerAdvisor; private final RetrievalAugmentationAdvisor retrievalAugmentationAdvisor; public CustomerServiceController( ChatClient chatClient, QuestionAnswerAdvisor questionAnswerAdvisor, RetrievalAugmentationAdvisor retrievalAugmentationAdvisor) { this.chatClient chatClient; this.questionAnswerAdvisor questionAnswerAdvisor; this.retrievalAugmentationAdvisor retrievalAugmentationAdvisor; } GetMapping(/chat/precise) public MapString, String preciseChat(RequestParam(question) String question) { String answer chatClient.prompt() .user(question) .advisors(List.of(questionAnswerAdvisor)) .call() .content(); return Map.of( question, question, answer, answer, mode, precise精准条款查询 ); } GetMapping(/chat/enhanced) public MapString, String enhancedChat(RequestParam(question) String question) { String answer chatClient.prompt() .user(question) .advisors(List.of(retrievalAugmentationAdvisor)) .call() .content(); return Map.of( question, question, answer, answer, mode, enhanced复杂场景增强 ); } }Step 4启动与验证mvn spring-boot:run启动应用观察日志确认知识库初始化完成访问http://localhost:8080/ecommerce/service/chat/precise?question新疆地区订单多少金额包邮预期响应{ question: 新疆地区订单多少金额包邮, answer: 新疆属于偏远地区订单金额满199元可享受包邮服务不满199元需收取20元运费哦~ 信息来源[电商知识库标准条款文档模板 - 物流服务标准], mode: precise精准条款查询 }6. 常见问题与排查技巧实录那些文档里不会写的坑6.1 问题速查表问题现象可能原因排查命令/方法解决方案启动时报No qualifying bean of type VectorStorespring-ai-starter-vector-store-milvus依赖未生效mvn dependency:tree | grep milvus检查pom.xml里spring-ai-starter-vector-store-milvus版本是否匹配Spring AI快照版/chat/precise接口返回500日志NullPointerExceptionQuestionAnswerAdvisor的documentContentKey未设在RAGConfig里加.documentContentKey(content)见4.1节配置返回答案里没有信息来源[...]defaultSystem提示词未生效或模型忽略curl -X POST http://localhost:8080/actuator/health检查ChatClientBean是否被正确注入用Autowired(required false)测试Milvus查询延迟1scollection未建索引或nlist参数不合理milvus_cli连上后执行describe collection guide_exam_store执行create index on guide_exam_store (vector) using IVF_FLAT with (nlist1024)中文乱码如æ°ç†application.properties文件编码不是UTF-8file -i src/main/resources/application.properties用IDEA右下角转编码为UTF-8或iconv -f GBK -t UTF-8 application.properties tmp mv tmp application.properties6.2 独家避坑技巧技巧1用/actuator/health暴露Milvus连接状态在pom.xml加spring-boot-starter-actuatorapplication.properties加management.endpoints.web.exposure.includehealth,info,metrics,prometheus management.endpoint.health.show-detailsalways启动后访问http://localhost:8080/actuator/health能看到milvus健康状态。这是线上巡检第一道防线。技巧2知识库更新不重启服务PostConstruct只在启动时执行。要支持热更新加一个RestControllerPostMapping(/knowledge/reload) public String reloadKnowledge(RequestParam String fileName) { // 重新读取fileName切分addBatch return Reloaded: fileName; }配合前端上传按钮实现运营人员自助更新。技巧3测试用例必须覆盖“坏问题”除了正常问题必须写JUnit测试Test void testIrrelevantQuestion() { String question 怎么办理信用卡; String answer chatClient.prompt() .user(question) .advisors(List.of(questionAnswerAdvisor)) .call() .content(); assertTrue(answer.contains(非常抱歉暂未查询到该问题的相关规则)); }我们规定RAG项目上线前坏问题测试覆盖率必须100%。7. 系统扩展与生产就绪从Demo到企业级应用的最后一步7.1 多格式文档支持不只是PDF和DOCX要支持Excel价格表、PPTX培训材料、TXT日志规则只需加依赖和Reader!-- Excel支持 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-apache-poi-document-reader/artifactId /dependency !-- PPTX支持 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-apache-poi-document-reader/artifactId /dependency然后在KnowledgeBaseConfig里if (fileName.endsWith(.xlsx)) { XlsxDocumentReader reader new XlsxDocumentReader(resource); rawDocs reader.read(); } else if (fileName.endsWith(.pptx)) { PptxDocumentReader reader new PptxDocumentReader(resource); rawDocs reader.read(); }7.2 权限控制RBAC不是可选项是必选项电商知识库可能含敏感政策。用Spring Security加Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {