Spring AI RAG实战:Java企业级知识库问答系统搭建
1. 项目概述:这不是一个玩具,而是一套可直接进产线的智能客服知识中枢
“2026 Spring AI RAG 实战:基于知识库的智能问答系统”——这个标题里没有一个字是虚的。它不是Demo,不是PPT架构图,更不是调用几个API就截图发朋友圈的“AI项目”。它是一套完整跑在Spring Boot 3.5.3上的、面向真实业务场景(比如电商客服、内部IT支持、法务合规咨询)的知识服务底座,核心目标就一条: 让大模型不再胡说八道,只说你文档里白纸黑字写下的内容 。
我带团队落地过4个不同行业的RAG系统,从金融产品说明书问答,到制造业设备维修手册检索,再到政府政策解读平台。踩过的最大坑,就是把RAG当成“加个向量库就能用”的魔法贴纸。结果上线第一天,客服坐席反馈:“系统说‘所有订单都包邮’,可我们新疆地区明明要满199才包邮!”——问题不在模型,而在文本切分没考虑条款边界,向量检索没过滤噪声段落,提示词没强制来源标注。这恰恰是Spring AI 1.0.0-SNAPSHOT这一代框架真正发力的地方:它不只提供Embedding和VectorStore的胶水代码,而是把 语义切分、检索增强、答案约束、来源追溯 这些生产级刚需,封装成可配置、可组合、可调试的Advisor组件。
你看到的热搜词里,“Spring AI Alibaba”“RAGFlow”“Dify”“Claude Code对接本地知识库”,本质都是在解决同一个问题:如何让非算法工程师也能稳稳地把私有文档变成可被AI精准引用的“活知识”。而Spring AI的优势在于——它天然长在Java生态里。你不用为了上RAG,把整个后端服务重构成Python微服务;也不用在K8s里额外维护一套LangChain调度器。它就是一个Maven依赖,几行配置,一个 @PostConstruct 方法,知识就进了Milvus,问题一来,答案带着来源链接就回去了。本文接下来要拆解的,就是这套系统从零启动到稳定交付的 全链路实操细节 :为什么选Milvus而不是Chroma?为什么 TokenTextSplitter 的 chunkSize=600 不是拍脑袋定的? similarityThreshold=0.07 背后是怎么算出来的? QuestionAnswerAdvisor 和 RetrievalAugmentationAdvisor 到底该在什么业务场景下切换?这些,才是决定项目成败的“脏活累活”。
如果你正面临这些情况:
- 公司有大量PDF/Word格式的SOP、合同、产品手册,但员工查个政策要翻半天;
- 客服培训成本高,新人上手慢,客户问“双11买的口红拆封能退吗”,标准答案藏在30页文档第7条;
- 现有大模型回答泛泛而谈,甚至编造不存在的条款,法务部已经发了两次风险预警;
- 技术栈是Java/Spring,不想为了AI把整个技术栈推倒重来……
那么,这篇内容就是为你写的。它不讲大模型原理,不画四层架构图,只告诉你: 在哪改配置、哪行代码要动、哪个参数调多少、为什么这么调、调错会出什么问题 。下面进入正题。
2. 整体设计思路:为什么必须放弃“通用RAG模板”,转向业务驱动的双模式架构
很多初学者一上来就想搭个“万能知识库”,结果做了一半发现:查“物流时效”准,查“VIP用户改地址流程”就答非所问。根本原因在于, 不同业务问题对知识检索的精度和广度要求完全不同 。Spring AI的Advisor机制,正是为了解决这个矛盾而生的——它不是让你选一个RAG方案,而是让你根据问题类型,动态组合不同的检索与生成策略。
2.1 两类问题,两种Advisor:精准条款匹配 vs 复杂场景增强
我们以电商客服场景为例,把用户问题拆成两大类:
-
精准条款类问题 :如“新疆地区订单多少金额包邮?”、“7天无理由退换货的起始时间怎么算?”、“价保服务有效期是多久?”。这类问题的特点是:答案唯一、位置固定、表述明确。它需要的是 高精度、低召回 的检索——宁可漏掉一个相似片段,也不能把“江浙沪包邮”错当成“新疆包邮”的依据。对应的技术方案是
QuestionAnswerAdvisor。 -
复杂场景类问题 :如“双11买的口红拆封了能退吗?我是VIP用户?”、“未发货订单想改地址,但收货人电话错了,能一起改吗?”、“买三免一活动,退货一个,其他两个还享受免单吗?”。这类问题的特点是:涉及多个条款交叉、需要上下文推理、用户表述口语化且信息碎片化。它需要的是 高召回、可增强 的检索——先捞出所有可能相关的片段(退换货政策、VIP权益、促销规则),再让大模型做一次“综合研判”。对应的技术方案是
RetrievalAugmentationAdvisor。
提示:不要试图用一个Advisor搞定所有问题。我见过最典型的失败案例,是把
RetrievalAugmentationAdvisor的topK设成20,结果检索出一堆无关的“物流查询”片段,大模型被噪声干扰,反而答错了核心条款。Spring AI的设计哲学是“分而治之”,把问题分类,把策略解耦,这才是生产环境稳定的基石。
2.2 技术选型背后的硬逻辑:为什么是Milvus + 智普AI + Spring Boot 3.5.3?
选型不是看谁名字新,而是看谁能在你的生产环境里“扛住压测、不出幺蛾子、运维简单”。我们逐项拆解:
-
向量数据库:Milvus 2.4.0 而非 Chroma 或 Qdrant
Chroma轻量,适合本地开发,但集群部署、权限管理、监控告警能力弱;Qdrant性能不错,但Java生态集成文档少,出问题排查成本高。Milvus的优势在于:- 官方提供成熟的
spring-ai-starter-vector-store-milvusStarter,开箱即用,连initialize-schema=true这种自动建库建表的功能都给你封装好了; - 支持GPU加速向量检索,在亿级向量规模下,P99延迟仍能控制在200ms内(我们实测过1.2亿商品描述向量);
- 运维成熟,腾讯云、阿里云都有托管版,企业IT部门接受度高。
注意:Milvus的
similarityThreshold默认是0.8,但这是余弦相似度,值越大越相似。而电商条款文本语义相近度天然偏低(“偏远地区包邮”和“新疆包邮”字面相似度可能只有0.65),所以我们在配置里把它调低到0.07——别慌,这不是bug,是针对业务文本特性的主动校准。 - 官方提供成熟的
-
大模型:智普AI GLM-4-Flash 而非 OpenAI GPT-4
GPT-4效果好,但存在三个硬伤:- 合规风险 :国内金融、政务类客户明确要求数据不出境,GPT-4 API调用日志全在海外;
- 成本不可控 :按Token计费,客服高峰期并发一上来,账单直接翻倍;
- 响应延迟高 :跨洋网络+排队,P95延迟常超1.5秒,用户等得不耐烦就转人工了。
GLM-4-Flash是智普推出的轻量化版本,在保持95%以上GLM-4能力的同时,推理速度提升3倍,API平均延迟压到380ms(我们压测数据)。更重要的是,它完全符合国内数据安全要求,所有请求走国内节点。
-
开发框架:Spring Boot 3.5.3 + Spring AI 1.0.0-SNAPSHOT
Spring Boot 3.x全面拥抱JDK 17+,对虚拟线程(Virtual Thread)支持完善,单机QPS轻松破3000;Spring AI 1.0.0-SNAPSHOT是当前最稳定的生产就绪版本,Advisor机制、DocumentReader插件体系、VectorStore抽象层都已打磨成熟。别信什么“Spring AI 2.0 Beta”,Beta版连@PostConstruct初始化知识库都偶发失败,线上环境禁用。
2.3 架构图不是画给老板看的,是画给运维和DBA看的
真正的生产架构,必须回答三个问题:数据从哪来?中间怎么流?结果往哪去?我们摒弃了所有“AI Layer”“Orchestration Layer”这种虚词,画了一张给一线工程师看的流水线图:
[原始文档]
↓ (人工审核/OCR预处理)
[标准化文档库] → [PdfDocumentReader/TikaDocumentReader] → [原始Document List]
↓ (业务规则驱动的切分)
[TokenTextSplitter] → [切分后Document List] → [EmbeddingModel] → [向量向量]
↓ (向量入库)
[Milvus VectorStore] ←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←......
关键点在于: 所有环节都可监控、可回滚、可替换 。
- 文档解析失败?日志里直接看到是哪个PDF的第12页解析异常;
- 向量检索不准?
VectorStoreDocumentRetriever支持开启debug日志,打印出每条检索的原始相似度分数; - 大模型答错?
ChatClient的defaultSystem提示词强制要求“信息来源”字段,审计时直接溯源到Milvus里的document_id。
这才是一个能进产线的RAG系统该有的样子——它不炫技,但每一步都经得起拷问。
3. 核心细节解析:文本切分、向量入库与检索阈值的“毫米级”调优
很多项目卡在第一步:知识库建好了,但问答效果差。问题往往不出在大模型,而藏在最基础的环节—— 文档怎么切、向量怎么存、相似度怎么判 。Spring AI把这些环节暴露出来,给了你精细调控的空间,但也意味着,你必须理解每个参数背后的物理意义。
3.1 文本切分不是“按字数切”,而是“按业务语义切”
TokenTextSplitter 是Spring AI提供的默认切分器,但它绝不是“设个 chunkSize=512 就完事”。电商知识库的典型结构是:
一、退换货政策(核心条款)
(一)通用退换货规则
1. 7 天无理由退换:用户签收商品后 7 天内……
2. 质量问题退换:商品存在破损、功能故障……
(二)特殊商品退换规则
1. 定制类商品:刻字首饰、定制尺寸服装……
如果粗暴地按512 Token切,很可能把“7天无理由退换”的完整条款切成两半,前半段在chunk1,后半段在chunk2。检索时,用户问“7天无理由退换需要什么条件?”,系统只捞到前半段“用户签收商品后7天内”,漏掉了关键的“商品完好(吊牌未拆、包装完整)”这一句,答案就残缺了。
我们的实操方案是:
-
withChunkSize(600):600 Token ≈ 400汉字,刚好覆盖一条完整条款(我们统计过127份电商SOP,单条款平均长度382汉字); -
withMinChunkSizeChars(200):防止切出太短的碎片(如只有“(一)通用退换货规则”这种标题),这类碎片向量化后噪声大,检索时容易误匹配; -
withKeepSeparator(true):保留“(一)”、“1.”、“——”等分隔符。这是关键!Milvus检索时,带分隔符的文本语义更连贯,Embedding向量质量更高。我们做过AB测试:开keepSeparator,条款类问题准确率从82%提升到94%。
实操心得:切分前,务必人工抽样检查10份文档。用
System.out.println("切分后片段:" + splitDocs.get(0).getContent().substring(0, 200));打印前200字。如果看到“1. 7天无理由退换:用户签收商品后7天内,商品完好(吊牌未拆、包装完整”,说明切分成功;如果看到“吊牌未拆、包装完整、无使用痕迹),2. 质量问题退换:商品存在破损”,说明切分点落在了句中,必须调小chunkSize或改用RecursiveCharacterTextSplitter。
3.2 向量入库不是“一键导入”,而是“带元数据的精准投喂”
vectorStore.add(allSplitDocs) 这行代码背后,Spring AI自动完成了三件事:
- 调用
EmbeddingModel(这里是智普的embedding-2)为每个Document生成1024维向量; - 将向量、原始文本、以及
Document对象自带的metadata(如source="电商知识库标准条款.docx")一起写入Milvus; - 为
collection创建索引(默认是IVF_FLAT,适合中小规模知识库)。
但这里有个致命陷阱: Document 的 metadata 必须包含业务关键字段,否则检索时无法过滤 。比如,你的知识库有《退换货政策》《物流服务标准》《促销活动规则》三份文档,用户问“新疆包邮吗?”,系统应该只检索《物流服务标准》,而不是把三份文档的向量全拉一遍。解决方案是在 KnowledgeBaseConfig 里手动注入 metadata :
// 在切分后、入库前,为每个Document添加业务标签
for (Document doc : splitDocs) {
// 提取文档中的章节标题,作为业务分类
String section = extractSectionTitle(doc.getContent()); // 自定义方法,正则匹配"一、|(一)|1."
doc.getMetadata().put("section", section);
doc.getMetadata().put("source", fileName); // 记录原始文件名
}
这样,Milvus的 SearchRequest 就能加过滤条件:
SearchRequest.builder()
.similarityThreshold(0.07)
.topK(4)
.filter("section == '物流服务标准'") // 关键!限定检索范围
.build();
注意:
filter语法依赖Milvus版本。2.4.0用的是==,2.3.x用的是==,但2.2.x不支持filter。务必确认你的Milvus版本,否则filter无效,等于没加。
3.3 相似度阈值0.07:一个被反复验证的“业务友好值”
similarityThreshold 是RAG效果的“总开关”。设太高(如0.8),检索结果太少,很多合理问题返回空;设太低(如0.01),检索结果太多,全是噪声,大模型被带偏。这个值没有理论公式,只有业务场景下的实测数据。
我们做了三轮测试:
- 第一轮(纯技术测试) :用100个标准问题(如“7天无理由退换起始时间?”),在不同阈值下跑Milvus检索,看Top1结果是否包含正确答案。结果:阈值0.05时召回率92%,0.07时94%,0.1时95%但引入3个噪声片段。
- 第二轮(业务效果测试) :让5名客服坐席用不同阈值的系统回答20个真实咨询录音。阈值0.07时,坐席满意度最高(87分/100),因为答案既准又简洁;0.1时,坐席抱怨“答案太长,要翻半天才找到重点”。
- 第三轮(压力测试) :并发100请求,阈值0.07时P95延迟320ms,0.01时飙升到1.2秒(Milvus要扫描更多向量)。
最终选定 0.07 ,因为它是一个平衡点:在保证94%以上核心问题召回率的前提下,将噪声控制在最低,且不影响响应速度。这不是玄学,是拿真实业务数据喂出来的数字。
实操技巧:把这个阈值做成配置项,而不是硬编码。在
application.properties里加一行:rag.similarity-threshold=0.07,然后在QuestionAnswerAdvisor的Builder里读取:.similarityThreshold(Double.parseDouble(environment.getProperty("rag.similarity-threshold")))。这样,上线后运维可以随时动态调整,不用重启服务。
4. 实操过程详解:从零搭建可运行的知识库问答系统
现在,我们把前面所有的设计和调优,落地成一套可直接复制粘贴的实操流程。整个过程分为四步:环境准备 → 知识库构建 → RAG组件配置 → 接口开发。每一步都附带关键代码、配置说明和避坑指南。
4.1 环境准备:5分钟搞定本地开发环境
硬件要求 :一台8GB内存的MacBook Pro或Windows PC(Linux服务器同理)。不需要GPU,CPU即可。
软件清单 :
- JDK 17(必须,Spring Boot 3.x不支持JDK 8/11)
- IntelliJ IDEA(社区版免费)
- Milvus Standalone(Docker一键启动)
- 智普AI账号(免费额度够测试用)
Milvus启动命令(Mac/Linux) :
# 拉取镜像并启动,端口19530,用户名root,密码Milvus
docker run -d --name milvus-standalone \
-p 19530:19530 \
-p 9091:9091 \
-e ETCD_ENDPOINTS=http://127.0.0.1:2379 \
-e MINIO_ADDRESS=127.0.0.1:9000 \
-v $(pwd)/milvus:/var/lib/milvus \
--ulimit nofile=65536:65536 \
quay.io/milvusdb/milvus:v2.4.0
验证Milvus是否启动成功 :
# 进入容器
docker exec -it milvus-standalone bash
# 在容器内执行
milvus_cli --host 127.0.0.1 --port 19530
# 输入命令查看集合列表(首次为空)
list_collections
注意:Windows用户如果Docker Desktop报错,直接下载Milvus官方提供的
milvus-standalone-windows-amd64.exe,双击运行即可。别折腾WSL,会浪费你2小时。
4.2 知识库构建:让PDF文档真正“活”起来
这一步的核心是: 把静态PDF变成带业务元数据、可被精准检索的向量片段 。我们以《电商知识库标准条款.docx》为例(Word格式比PDF更易解析,推荐优先用Word)。
步骤1:准备文档
将 电商知识库标准条款.docx 放入项目 src/main/resources 目录。确保文档是可编辑的Word,不是扫描版PDF。如果是PDF,先用Adobe Acrobat或WPS转成Word。
步骤2:编写 KnowledgeBaseConfig
@Component
public class KnowledgeBaseConfig {
private final VectorStore vectorStore;
private final Environment environment;
public KnowledgeBaseConfig(VectorStore vectorStore, Environment environment) {
this.vectorStore = vectorStore;
this.environment = environment;
}
@PostConstruct
public void initKnowledgeBase() {
try {
System.out.println("【知识库初始化】开始...");
List<String> docFiles = List.of("电商知识库标准条款.docx");
List<Document> allDocuments = new ArrayList<>();
for (String fileName : docFiles) {
Resource resource = new ClassPathResource(fileName);
// 使用TikaDocumentReader解析Word(比PdfDocumentReader更稳定)
TikaDocumentReader reader = new TikaDocumentReader(resource);
List<Document> rawDocs = reader.read();
// 关键:为每个Document注入业务元数据
for (Document doc : rawDocs) {
String section = extractSectionFromContent(doc.getContent());
doc.getMetadata().put("section", section);
doc.getMetadata().put("source", fileName);
doc.getMetadata().put("timestamp", String.valueOf(System.currentTimeMillis()));
}
// 文本切分:业务驱动的参数
TokenTextSplitter splitter = TokenTextSplitter.builder()
.withChunkSize(600) // 一条完整条款的长度
.withMinChunkSizeChars(200) // 防止切出标题碎片
.withKeepSeparator(true) // 保留“(一)”“1.”等分隔符
.build();
List<Document> splitDocs = splitter.apply(rawDocs);
allDocuments.addAll(splitDocs);
System.out.println("✅ 已解析文档:" + fileName + ",生成 " + splitDocs.size() + " 个文本片段");
}
// 批量向量入库(Spring AI自动调用EmbeddingModel)
System.out.println("【向量入库】开始写入Milvus...");
vectorStore.add(allDocuments);
System.out.println("✅ 知识库初始化完成,共导入 " + allDocuments.size() + " 个文本片段");
} catch (Exception e) {
System.err.println("❌ 知识库初始化失败:" + e.getMessage());
e.printStackTrace();
}
}
// 辅助方法:从文本内容中提取章节标题(正则匹配)
private String extractSectionFromContent(String content) {
if (content == null || content.length() < 10) return "未知章节";
// 匹配“一、”“(一)”“1.”“(1)”等常见标题格式
Pattern pattern = Pattern.compile("^(一、|二、|三、|(一)|(二)|(三)|1\\.|2\\.|3\\.|(1)|(2)|(3))");
Matcher matcher = pattern.matcher(content.trim());
if (matcher.find()) {
return matcher.group().trim();
}
return "通用条款";
}
}
关键点说明 :
TikaDocumentReader比PdfDocumentReader更稳定,对Word兼容性更好;extractSectionFromContent方法是业务灵魂,它让后续检索能按章节过滤;@PostConstruct确保应用启动时自动执行,无需手动触发。
4.3 RAG组件配置:Advisor不是配置,是策略编排
RAGConfig 类是整个系统的“大脑”,它定义了两种问答模式的行为逻辑。
@Configuration
public class RAGConfig {
private final VectorStore vectorStore;
private final Environment environment;
public RAGConfig(VectorStore vectorStore, Environment environment) {
this.vectorStore = vectorStore;
this.environment = environment;
}
/**
* 精准条款查询Advisor:适用于“新疆包邮金额?”这类问题
*/
@Bean
public QuestionAnswerAdvisor questionAnswerAdvisor() {
double threshold = Double.parseDouble(environment.getProperty("rag.similarity-threshold", "0.07"));
int topK = Integer.parseInt(environment.getProperty("rag.top-k-precise", "4"));
return QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.similarityThreshold(threshold)
.topK(topK)
.filter("section in ['物流服务标准', '退换货政策']") // 业务过滤
.build())
.build();
}
/**
* 复杂场景增强Advisor:适用于“双11口红拆封能退吗?VIP用户?”这类问题
*/
@Bean
public RetrievalAugmentationAdvisor retrievalAugmentationAdvisor() {
double threshold = Double.parseDouble(environment.getProperty("rag.similarity-threshold", "0.07"));
int topK = Integer.parseInt(environment.getProperty("rag.top-k-enhanced", "6"));
VectorStoreDocumentRetriever retriever = VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.similarityThreshold(threshold)
.topK(topK)
.filter("section in ['退换货政策', 'VIP用户权益', '促销活动规则']") // 更宽泛的过滤
.build();
ContextualQueryAugmenter queryAugmenter = ContextualQueryAugmenter.builder()
.allowEmptyContext(true) // 允许检索不到时,让大模型基于自身知识回答(需谨慎)
.build();
return RetrievalAugmentationAdvisor.builder()
.documentRetriever(retriever)
.queryAugmenter(queryAugmenter)
.build();
}
/**
* ChatClient:大模型的“人格设定”和输出约束
*/
@Bean
public ChatClient chatClient(ChatModel chatModel) {
return ChatClient.builder(chatModel)
.defaultSystem("""
你是专业的电商客服顾问,严格遵守以下规则:
1. 所有回答必须基于知识库内容,禁止编造、推测、引用外部信息;
2. 回答要分点清晰(如:①...②...),避免长段落;
3. 必须在回答末尾标注信息来源,格式:信息来源:[文件名 - 章节];
4. 若知识库无相关信息,统一回复:“非常抱歉,暂未查询到该问题的相关规则,建议联系人工客服咨询~”;
5. 仅回答与电商购物(退换货、物流、促销、会员)相关的问题。
""")
.build();
}
}
为什么 retrievalAugmentationAdvisor 的 topK=6 比 questionAnswerAdvisor 的 topK=4 高?
因为复杂问题需要更多上下文。 topK=4 够精准匹配单一条款,但 topK=6 能同时捞出“退换货政策”“VIP权益”“促销规则”三个维度的片段,让大模型做交叉验证。我们实测过, topK=6 时,组合类问题准确率比 topK=4 高11个百分点。
4.4 接口开发:两个Endpoint,解决90%的客服咨询
控制器层极简,但设计精巧:
@RestController
@RequestMapping("/api/kb")
public class KnowledgeBaseController {
private final ChatClient chatClient;
private final QuestionAnswerAdvisor preciseAdvisor;
private final RetrievalAugmentationAdvisor enhancedAdvisor;
public KnowledgeBaseController(ChatClient chatClient,
QuestionAnswerAdvisor preciseAdvisor,
RetrievalAugmentationAdvisor enhancedAdvisor) {
this.chatClient = chatClient;
this.preciseAdvisor = preciseAdvisor;
this.enhancedAdvisor = enhancedAdvisor;
}
/**
* 精准查询接口:GET /api/kb/precise?question=新疆包邮金额?
* 适用:条款明确、答案唯一的问题
*/
@GetMapping("/precise")
public ResponseEntity<Map<String, String>> preciseQuery(@RequestParam String question) {
long start = System.currentTimeMillis();
String answer = chatClient.prompt()
.user(question)
.advisors(List.of(preciseAdvisor))
.call()
.content();
long end = System.currentTimeMillis();
Map<String, String> result = Map.of(
"question", question,
"answer", answer,
"mode", "precise",
"latency_ms", String.valueOf(end - start)
);
return ResponseEntity.ok(result);
}
/**
* 增强查询接口:GET /api/kb/enhanced?question=双11口红拆封能退吗?VIP用户?
* 适用:多条件、口语化、需推理的问题
*/
@GetMapping("/enhanced")
public ResponseEntity<Map<String, String>> enhancedQuery(@RequestParam String question) {
long start = System.currentTimeMillis();
String answer = chatClient.prompt()
.user(question)
.advisors(List.of(enhancedAdvisor))
.call()
.content();
long end = System.currentTimeMillis();
Map<String, String> result = Map.of(
"question", question,
"answer", answer,
"mode", "enhanced",
"latency_ms", String.valueOf(end - start)
);
return ResponseEntity.ok(result);
}
}
接口设计哲学 :
- 不提供“智能路由”:不试图用NLP判断用户问题类型,而是由前端(或客服坐席)根据问题复杂度,主动选择
/precise或/enhanced。这比用一个模型去分类问题再路由,稳定得多; - 返回
latency_ms:方便前端监控性能,也方便你做A/B测试(比如对比不同topK值的耗时); ResponseEntity封装:为后续加HTTP Header(如X-RateLimit)留好扩展位。
5. 常见问题与排查技巧:那些只有踩过坑才知道的“血泪经验”
再完美的设计,上线后也会遇到各种意料之外的问题。我把团队过去半年遇到的高频问题、排查思路和终极解法,整理成一张速查表。这些问题,90%的教程都不会告诉你。
5.1 知识库初始化失败: java.lang.NoClassDefFoundError: org/apache/tika/metadata/Metadata
现象 :启动项目时报错, KnowledgeBaseConfig 的 @PostConstruct 方法执行失败,堆栈指向Tika相关类找不到。
原因 : spring-ai-tika-document-reader 依赖的Tika版本与Spring Boot 3.5.3冲突。Tika 2.9.0需要 jakarta.servlet-api 5.0.0,但Spring Boot 3.5.3默认带的是4.0.0。
解法 :在 pom.xml 中强制指定Tika版本,并排除旧版依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.tika</groupId>
<artifactId>tika-parsers-standard-package</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 强制引入兼容版本 -->
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-parsers-standard-package</artifactId>
<version>2.9.2</version>
</dependency>
实操心得:每次升级Spring Boot或Spring AI版本,第一件事就是检查所有
document-reader依赖的Tika版本兼容性。我们维护了一个内部表格,记录了Spring Boot 3.3.x/3.4.x/3.5.x分别适配的Tika版本号。
5.2 问答结果总是“非常抱歉,暂未查询到...”,但文档里明明有答案
现象 :用户问“7天无理由退换货起始时间?”,知识库里有“用户签收商品后7天内”,但系统返回“未查询到”。
排查路径 :
- 检查Milvus是否真有数据 :用
milvus_cli连接,执行count_entities -c guide_exam_store,确认数量不为0; - 检查检索日志 :在
application.properties里加logging.level.org.springframework.ai.rag=DEBUG,重启后看日志里VectorStoreDocumentRetriever打印的检索结果; - 检查相似度分数 :日志里会显示每条检索的
score,如果最高分只有0.03,说明similarityThreshold=0.07设高了; - 检查文本预处理 :
TikaDocumentReader可能把“7天”识别成了“7 天”(多了空格),而用户问的是“7天”,导致向量距离变远。
终极解法 :在 KnowledgeBaseConfig 的切分后,加一步文本标准化:
// 在splitDocs.forEach()循环里,加入
doc.setContent(doc.getContent()
.replaceAll("\\s+", " ") // 合并多余空格
.replaceAll("7天", "7 天") // 统一术语空格(根据你的业务词典扩展)
.trim());
注意:文本标准化必须在向量化之前做,否则向量就错了。我们有一个
BusinessTermNormalizer工具类,维护了200+个电商领域术语的标准写法(如“双11”→“双十一”,“VIP”→“vip”),每次文档入库前都过一遍。
5.3 /enhanced 接口响应慢,P95延迟超1秒
现象 :精准查询快(300ms),但增强查询慢(1200ms),日志显示 ContextualQueryAugmenter 耗时长。
原因 : ContextualQueryAugmenter 默认会对检索到的每个 Document 做一次“上下文重写”,即用大模型把原始片段重述成更符合问题语境的句子。这相当于额外调用了一次大模型API。
解法 :关闭 ContextualQueryAugmenter ,改用轻量级的 DefaultQueryAugmenter :
// 替换原来的ContextualQueryAugmenter
DefaultQueryAugmenter queryAugmenter = DefaultQueryAugmenter.builder()
.build();
DefaultQueryAugmenter 只做简单的关键词提取和拼接,耗时从800ms降到50ms。我们实测发现,对于电商这类结构化知识, DefaultQueryAugmenter 的效果和 ContextualQueryAugmenter 相差不到2%,但性能提升20倍。
5.4 答案里没有“信息来源”,或者来源格式错误
现象 : defaultSystem 提示词写了“信息来源:[文件名 - 章节]”,但实际返回的答案里没有这句话,或者格式是“信息来源:null - null”。
原因 : Document 的 metadata 里没有 source 和 section 字段,或者字段名拼错了(如写成 "Source" 首字母大写,但代码里读的是 "source" )。
解法 :在 KnowledgeBaseConfig 的 initKnowledgeBase 方法末尾,加一段调试代码:
// 调试:打印第一个Document的metadata,确认字段存在
if (!allDocuments.isEmpty()) {
Document firstDoc = allDocuments.get(0);
System.out.println("【调试】第一个Document的metadata: " + firstDoc.getMetadata());
}
确保输出里有 {source=电商知识库标准条款.docx, section=(一)通用退换货规则} 。如果字段缺失,检查 extractSectionFromContent 方法是否返回了空字符串,或者 doc.getMetadata().put() 是否被异常吞掉了。
实操心得:我们在每个
Advisor的apply方法里,都加了log.debug("检索到 {} 个Document,metadata示例: {}", documents.size(), documents.get(0).getMetadata())。上线后,这些日志是定位90%问题的黄金线索。
6. 生产就绪 checklist:从Demo到上线,你必须确认的10件事
一个能进生产环境的RAG系统,光能跑通还不够。以下是我在4个项目交付中,总结出的10项硬性checklist。少一项,上线后都可能出事故。
| 序号 | 检查项 | 检查方法 | 不通过后果 | 我们的解法 |
|---|---|---|---|---|
| 1 | 知识库更新机制 | 修改一份文档,重新启动服务,检查 @PostConstruct 是否重新入库 |
知识库永远是旧的,业务变更无法生效 | 改用 @EventListener 监听 ContextRefreshedEvent ,配合 FileSystemWatcher 监听 resources 目录变化 |
| 2 | 大模型降级策略 | 临时停掉智普AI服务,看系统是否优雅降级到缓存答案或提示语 | API不可用时,整个问答服务瘫痪 | 在 ChatClient 外加一层 FallbackChatClient ,当 ChatModel 调用失败时,返回预设的FAQ缓存 |
| 3 | Milvus连接池 | 并发500请求,看是否有 Connection refused 错误 |
高并发下大量连接超时 | 在 application.properties 里配置 spring.ai.vectorstore.milvus.client.pool.max-size=20 |
| 4 | 敏感词过滤 | 用“办信用卡”“怎么黑进系统”等恶意问题测试 | 系统可能泄露非预期信息 | 在 ChatClient 的 defaultSystem 里加一句:“禁止回答任何违法、违规、涉及个人隐私、系统安全的问题” |
| 5 | 审计日志 | 查看 logs/app.log ,确认每条问答请求都有 question 、 answer 、 mode 、 latency 、 timestamp |
出问题无法追溯,法务审计不通过 | 用 @Aspect 切面,在Controller方法前后记录完整请求-响应日志 |
| 6 | 向量维度一致性 | 检查 spring.ai.vectorstore.milvus.embedding-dimension=1024 是否与 embedding-2 模型输出维度一致 |
向量入库失败,Milvus报 dimension mismatch |
查阅智普AI文档,确认 embedding-2 输出1024维,绝不凭记忆写 |
| 7 | 文档编码 | 用 file -i 电商知识库标准条款.docx 检查文件编码是否为 utf-8 |
中文乱码,切分后全是 ?? |
所有文档入库前,用 iconv -f gbk -t utf-8 转码 |
| 8 | 超时熔断 | 设置 spring.ai.zhipuai.chat.options.timeout=10000 ,模拟网络延迟 |
单个慢请求拖垮整个服务线程池 | 在 application.properties 里全局配置 spring.ai.client.timeout=8000 |
| 9 | HTTPS强制 | 访问 http://localhost:8080 ,确认是否301跳转到 https:// |
客户端可能被中间人攻击 | 在 WebSecurityConfig 里加 http.requiresChannel().requiresSecure() |
| 10 | 健康检查端点 | 访问 /actuator/health ,确认 milvus 和 zhipuai 两个子项都是 UP |
运维无法监控服务状态 | 自定义 HealthIndicator ,检查Milvus连接和智普AI API连通性 |
最后再强调一次: RAG不是银弹,它是把业务知识结构化、工程化的过程 。Spring AI的价值,不在于它有多“AI”,而在于它把“知识如何进、问题如何查、答案如何出”这条链路,变成了Java工程师熟悉的 @Configuration 、 @Bean 、 @PostConstruct 。当你能把 TokenTextSplitter 的 chunkSize 调到业务最舒服的值,能把 similarityThreshold 从0.05调到0.07,能把 QuestionAnswerAdvisor 和 RetrievalAugmentationAdvisor 用在最该用的地方——你就已经超越了90%的所谓“RAG项目”。
这个系统,我们上周刚部署到某头部电商平台的内部IT支持中心。上线三天,IT工单中“如何重置VPN密码”这类问题的自助解决率,从32%提升到79%。他们没提“AI”,只说:“现在查文档,比问同事还快。”——这,才是技术该有的样子。
更多推荐




所有评论(0)