【AI全栈后端12-06】Spring Boot 用 RAG 搭企业私有知识库问答
本文是「Spring Boot AI 全栈后端」系列第 06 篇。前五篇我们解决了能不能对话“成本能不能降”“输出有没有形状”能不能查实时数据这些问题这一篇处理一个更普遍的痛点模型根本不知道你们公司自己写的那份手册。示例基于 Spring AI 2.0 / Boot 4.1全部已通过测试。一个让售后背锅的场景V哥 给一家做工业真空机组的客户做方案时他们的售后总监倒了一肚子苦水。厂里卖的是 VM-200 型真空机组客户买了设备遇到问题就打电话问“这台机器保修多久”“控制面板上显示 E-04 是什么意思”“过保了上门维修怎么收费”答案全都写在《VM-200 产品手册》《操作指南》《2025 版售后政策》这三份文档里加起来两百多页。但接电话的是人新人背不下老员工凭记忆偶尔记串。于是他们把希望寄托在 AI 上接了个通用大模型做客服助手。结果更糟。客户问VM-200 保修多久模型挺自信地回答通常这类工业设备保修一年——它是照着行业常识推测的根本没有读过他们家的保修条款。而真实的条款是整机 24 个月、真空泵主机 36 个月、密封圈等易耗件不在保修范围。客户拿着这个一年的答复去索赔售后只能背锅。问题到这里非常具体了不是模型不会说话是它的脑子里压根没有你公司这份私有资料。通用模型有海量常识但常识里没有你的产品型号、你的保修政策、你上个月刚改的价格表。为什么让它学一遍行不通遇到这个问题团队通常先想到三条路我在这里一并说清它们为什么都不合适一是把整本手册塞进提示词。两百页文档几十万字一次对话塞不下就算塞下了模型也会被无关内容稀释注意力命中率反而下降token 费用还照单全收。二是微调。让模型记住手册内容。问题是产品手册一个月改一次你打算每月微调一次成本和时间都不现实而且微调记进去的知识依然可能胡编因为它并没有真的查。三是给每个人发 PDF 让他自己搜。那还要 AI 干什么真正卡住的是三件事连在一起模型没有你的私有知识、加上它不敢说不知道、于是它只能用常识编一个像答案的答案。第三件才是最可怕的——错误答案听着比我不知道专业多了。解决思路先检索再让它照着资料说RAGRetrieval-Augmented Generation检索增强生成的思路朴素得不能再朴素入库把手册切成小段每段算出一个向量embedding存进向量库检索用户提问时把问题也做成向量去库里找最相似的几段增强生成把这几段原文塞进提示词告诉模型只准照着这些资料回答兜底一段都没命中就直接说没查到不许它自由发挥。第 3、4 步是整套方案的价值所在——模型从靠记忆答题变成开卷考试而且监考老师看着。第一步文档切块别整篇扔进去切块chunking这一步容易被新手跳过但它直接决定召回质量。整篇手册作为一个片段就意味着任何问题都会召回整篇等于没召回。Spring AI 提供了TokenTextSplitter按 token 数量切并且尽量在句号、分号这些地方断开ServicepublicclassKbIngestService{privatefinalVectorStorevectorStore;privatefinalTextSplittersplitter;publicKbIngestService(VectorStorevectorStore){this.vectorStorevectorStore;this.splitterTokenTextSplitter.builder().withChunkSize(120).withMinChunkSizeChars(20).withKeepSeparator(true).build();}publicintingest(ListKbDocdocs){ListDocumentchunksdocs.stream().flatMap(doc-splitter.split(newDocument(doc.content(),Map.of(source,doc.title()))).stream().peek(chunk-chunk.getMetadata().put(source,doc.title()))).toList();vectorStore.add(chunks);returnchunks.size();}}KbDoc只是标题和正文两个字段的记录publicrecordKbDoc(Stringtitle,Stringcontent){}注意那句peek每段我们都把来源标题写进 metadata。这是后面答案能给出处的前提——没有来源的片段等于没有引用价值的答案。第二步向量库选哪个都不影响业务代码Spring AI 把向量库抽象成VectorStore内存版、PGVector、Milvus、Redis、Elasticsearch 等等业务层看到的都是同一个接口。想离线把逻辑跑通用最简单的SimpleVectorStore就行ConfigurationpublicclassKbTestConfig{BeanPrimaryEmbeddingModelembeddingModel(){returnnewStubEmbeddingModel();}BeanVectorStorevectorStore(EmbeddingModelembeddingModel){returnSimpleVectorStore.builder(embeddingModel).build();}}上线的项目换成 PGVector也只是在同一个地方换掉这个 bean——引入spring-ai-starter-vector-store-pgvector依赖配置数据库连接即可KnowledgeBaseService一行都不用改。这就是抽象的价值存储选型改到最后也只是配置层的事。第三步问答——把检索到的资料塞进提示词这一步 Spring AI 给了现成的 advisorRetrievalAugmentationAdvisor。它在调用模型之前先执行一遍检索把命中的片段拼进提示词再发给你惯用的模型。ServicepublicclassKnowledgeBaseService{privatefinalVectorStorevectorStore;privatefinalChatClientchatClient;privatefinalinttopK;privatefinaldoublesimilarityThreshold;publicKnowledgeBaseService(ChatModelchatModel,VectorStorevectorStore,Value(${kb.top-k:3})inttopK,Value(${kb.similarity-threshold:0.35})doublesimilarityThreshold){this.vectorStorevectorStore;this.topKtopK;this.similarityThresholdsimilarityThreshold;this.chatClientChatClient.builder(chatModel).defaultSystem(SYSTEM).defaultAdvisors(RetrievalAugmentationAdvisor.builder().documentRetriever(VectorStoreDocumentRetriever.builder().vectorStore(vectorStore).topK(topK).similarityThreshold(similarityThreshold).build()).queryAugmenter(ContextualQueryAugmenter.builder().promptTemplate(newPromptTemplate(AUGMENT_TEMPLATE)).emptyContextPromptTemplate(newPromptTemplate(EMPTY_CONTEXT_TEMPLATE)).allowEmptyContext(false).build()).build()).build();}}RetrievalAugmentationAdvisor做三件事用VectorStoreDocumentRetriever取回片段topK控制取几条、similarityThreshold控制多相似才算数交给ContextualQueryAugmenter把资料拼进模板最后把增强后的 prompt 发给模型。模板必须换成中文而且兜底模板有个坑默认的模板是英文的用在中文客服系统里有两个问题一是风格不统一二是它会在没检索到时允许模型基于常识回答。所以两个模板都自己写publicstaticfinalStringAUGMENT_TEMPLATE 请依据下面的资料回答用户的问题。只使用资料中的内容资料里没有就说不知道。 资料 {context} 用户问题{query} ;publicstaticfinalStringEMPTY_CONTEXT_TEMPLATE 当前知识库里没有任何与用户问题相关的资料。 请直接回复“抱歉这个问题在我们的产品手册和售后政策里没查到依据我帮您转人工核实。” 不要尝试用常识作答也不要做任何推测。 ;这里有个 2.0.1 的实际坑V哥 踩过一次正常模板里的变量是{query}和{context}但兜底模板渲染时框架不传任何变量——默认的兜底模板本身连一个占位符都没有。你要是在EMPTY_CONTEXT_TEMPLATE里写{query}运行时就直接抛Not all variables were replaced。兜底话术要么写死成静态文案要么自己在服务层拼好后传进去。关键一步没查到就别让它编很多 RAG 教程到上一步就结束了但真正决定系统敢不敢上线的是这一段——前置一次检索自己判断是否命中publicKbAnsweranswer(Stringquestion){ListDocumenthitsvectorStore.similaritySearch(SearchRequest.builder().query(question).topK(topK).similarityThreshold(similarityThreshold).build());if(hits.isEmpty()){returnKbAnswer.miss(question);}StringanswerchatClient.prompt().user(question).call().content();returnnewKbAnswer(question,answer,sourcesOf(hits),true);}/** 出处去重答案里挨着的同一份手册不必重复列出。 */privatestaticListStringsourcesOf(ListDocumenthits){returnhits.stream().map(hit-String.valueOf(hit.getMetadata().getOrDefault(source,未标注来源))).distinct().toList();}为什么还要再检索一次因为它换来三样东西出处sourcesOf(hits)拿到了回答依据了哪几份手册前端可以显示本答复依据《VM-200 产品手册》客户查得到、法务认得出硬拦截一段都没命中时直接返回兜底话术连模型都不调——既省钱也从根上杜绝了编造可审计每一次回答是不是有据可依用grounded这个布尔值记下来方便后面做人工抽检。KbAnswer就是干这事的publicrecordKbAnswer(Stringquestion,Stringanswer,ListStringsources,booleangrounded){staticKbAnswermiss(Stringquestion){returnnewKbAnswer(question,MISS_MESSAGE,List.of(),false);}publicstaticfinalStringMISS_MESSAGE抱歉这个问题在我们的产品手册和售后政策里没查到依据我帮您转人工核实。;}于是你有两道防线服务层的前置拦截是第一道ContextualQueryAugmenter的EMPTY_CONTEXT_TEMPLATE是第二道——哪怕有人绕过服务层直接调 advisor模型收到的依然是请告诉用户没查到而不是裸的那个问题。对外接口就两个一个灌库一个问答RestControllerRequestMapping(/api/kb)publicclassKnowledgeBaseController{privatefinalKbIngestServiceingestService;privatefinalKnowledgeBaseServiceknowledgeBaseService;publicKnowledgeBaseController(KbIngestServiceingestService,KnowledgeBaseServiceknowledgeBaseService){this.ingestServiceingestService;this.knowledgeBaseServiceknowledgeBaseService;}PostMapping(/ingest)publicResponseEntityMapString,Integeringest(RequestBodyKbIngestRequestrequest){intchunksingestService.ingest(request.docs());returnResponseEntity.ok(Map.of(chunks,chunks));}PostMapping(/ask)publicResponseEntityKbAnswerask(ValidRequestBodyKbAskRequestrequest){returnResponseEntity.ok(knowledgeBaseService.answer(request.question()));}}怎么验证离线也能把这套链路跑通RAG 看着链路长但每一环都能在没有密钥、没有网络的环境里验证。重点验证这几件事切块与召回手册灌进去之后问VM-200 保修多久能不能召回包含24 个月的那一段片段真的进了提示词用一个会记录收到内容的桩模型断言它收到的用户消息里含手册原文——这一步证明增强是真的发生了而不是你以为发生了答案带出处同一个问题返回的sources里应当有《VM-200 产品手册》问E-04 是什么意思则应当来自《VM-200 操作指南》无关问题走兜底问公司团建去哪里玩应当返回grounded false、sources为空、答案是转人工且根本没有调用模型兜底模板生效单独用 advisor 问那个无关问题桩模型收到的应当是兜底话术而不是原问题入参校验空问题返回 400。向量化那一环不需要真实 embedding 模型把文本按词哈希成固定维度向量、再做归一化就能让共享词越多余弦相似度越高。它当然没有真实 embedding 模型的语义能力但验证逻辑够用了真上线换成 OpenAI / 国产 embedding业务代码一个字不动。落地要点阈值要自己调similarityThreshold不是拍脑袋定的。0.35 是个起步值具体要拿真实问题一批一批试——设高了答不知道设低了召回垃圾把这个数放到配置里kb.similarity-threshold上线后还能热改。切块大小跟着业务走手册段落清晰就用小块100-200 token法律条款这种前后关联强的用大块必要时让相邻片段重叠一部分避免一句话被切成两半。来源元数据别省售价表、售后政策、培训手册混在一个库里时少了source字段的片段既没法给前端展示出处也没法按文档做权限过滤。入库要幂等、要能增量更新文档改了就重新灌别让旧版本留在库里和客户说话生产上给每份文档一个版本号按版本覆盖。权限别忘知识库里可能有机密报价、内部政策按用户角色过滤 handler 后再检索比事后审核便宜得多。成本账要算两次一次是 embedding 入库的开销一次性一次是每次问答 embedding 的成本持续。文档总量大时增量入库很重要。最后 V哥 总结一句通用模型最大的问题从来不是不会说话而是没有读过你写的东西还能说得很自信。RAG 干的事就是先把你的手册找出来再把它的嘴按在资料上——答得出来给依据答不出来就老老实实说没查到。带上出处的答案才敢给客户看答不上来会喊停的助手才敢上线。下一篇07V哥 带你解决更难的一步单轮问答搞不定的多步运营任务——查数据、算出图、再发周报这种一串动作的任务怎么交给 Agent 自动跑完。最后一句别再把几百页手册塞进提示词赌模型记住——切成块存进向量库问一句召回一段让它在你的资料里开卷作答答得出给依据、答不出转人工这才叫能落地的企业知识库。

相关新闻

Codex 与 WSL 集成指南:路径、Node 运行时与 Clawd 配对方案全解析

Codex 与 WSL 集成指南:路径、Node 运行时与 Clawd 配对方案全解析

桌面应用交互助手 【免费下载链接】clawd-on-desk A pixel desktop pet that watches Claude Code, Codex, Cursor & other AI coding agents — so you dont have to. 项目地址: https://gitcode.com/gh_mirrors/cl/clawd-on-desk 点击查看 免费下载 本文以 d…

2026/10/9 5:32:38 阅读更多 →
2027届毕设别硬蹭大模型:XGBoost算法 + 可视化分析才是大数据选题主线

2027届毕设别硬蹭大模型:XGBoost算法 + 可视化分析才是大数据选题主线

2027届毕设别硬蹭大模型:XGBoost算法 可视化分析才是大数据选题主线 又到了一年一度选毕设的时候。 每年这个时候,后台都会收到类似的留言:“学长,毕设能不能做LLM?”“我想做个大模型相关的是不是更好过?…

2026/10/9 5:32:38 阅读更多 →
谷歌 Chrome 浏览器关闭硬件加速的 3 种方法

谷歌 Chrome 浏览器关闭硬件加速的 3 种方法

Chrome 里的硬件加速有时候是在帮倒忙。这功能本意是靠**显卡(GPU)**分担渲染、让浏览器跑得更顺,可在部分机器上反而会带来崩溃、显示异常和兼容性问题。所以,如果你的 Chrome 正因为硬件加速闹脾气,下面这几招就是把它关掉的办法。 Chrome 的硬件加速到底是什么? 你可…

2026/10/9 5:32:38 阅读更多 →

最新新闻

日期处理陷阱:从1月25日看时区与历法边界

日期处理陷阱:从1月25日看时区与历法边界

我很少拿一个日期当文章标题,但1月25日这个数字,我记了快一整年。不是因为它特殊——公历里它既不是节日也不算节气,每年对应的星期几、农历日子完全不一样。正因为它"每天都在变、又好像什么都没变",才在交付前一周把我…

2026/10/9 7:01:47 阅读更多 →
别急着定标题:把零散素材盘成完整内容的方法论

别急着定标题:把零散素材盘成完整内容的方法论

手头堆积了一大捧碎料子,没想好叫什么题目,也没想清楚要从哪儿下刀的时候,我就干过最蠢的一件事:硬着头皮挑一个看起来“最像样”的碎片开始写,指望写着写着思路自己就通了。结果写了两千字,发现方向偏了&a…

2026/10/9 7:01:47 阅读更多 →
强化学习训练看板:从指标监控到产线决策中枢

强化学习训练看板:从指标监控到产线决策中枢

1. 这不是“监控页面”,而是一张RL训练的作战地图你打开浏览器,输入地址,看到一个带折线图、柱状图和实时刷新数字的网页——它叫“MiMo-v2.6 RL 训练看板”。但如果你只把它当成一个“看看loss降没降”的仪表盘,那等于拿着战术平…

2026/10/9 7:01:47 阅读更多 →
降AI率工具横评:8款AI改写与检测工具的实战避坑指南

降AI率工具横评:8款AI改写与检测工具的实战避坑指南

前两天有个专科大三的学弟给我发来一张截图:期末课程论文用AI起稿,写完还挺顺手,结果拿去检测平台一测,AI疑似率35%。他当场懵了,“老师一眼就能看出来这不是我写的”。这种“AI写得爽,检测全露馅”的情况&…

2026/10/9 7:01:47 阅读更多 →
基于Spring Boot+MyBatis的汽车租赁管理系统设计与实现

基于Spring Boot+MyBatis的汽车租赁管理系统设计与实现

做毕设辅导这些年,看到汽车租赁管理系统这个题目几乎是“常青树”一般的存在。每年都有学生选它,原因不难理解:车辆、用户、订单、租金这几样核心对象,正好把增删改查练透,又比图书管理多了一层业务状态流转&#xff0…

2026/10/9 7:01:47 阅读更多 →
浏览器扩展端侧AI推理:WebGPU+ONNX Runtime实战架构

浏览器扩展端侧AI推理:WebGPU+ONNX Runtime实战架构

1. 这不是“把模型塞进浏览器”那么简单:端侧AI在扩展环境里的真实战场“现代浏览器扩展环境下的端侧 AI 推理系统架构与工程实现规范”——这个标题里没有一个词是虚的,每个字都踩在当下前端工程最硬的几块石头上。我从去年开始带团队落地三个真实商用级…

2026/10/9 7:00:47 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 10:10:36 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 6:17:20 阅读更多 →