新闻详情

新闻详情

首页 / 资讯中心 / 详情

Spring AI Alibaba 实战:基于 RAG 构建企业知识库问答系统

发布时间:2026/10/2 3:37:46来源:尧图网络
Spring AI Alibaba 实战:基于 RAG 构建企业知识库问答系统
在系列前面的几篇里我们已经把 Spring AI Alibaba 的环境搭建、基础对话、提示词模板和 Function Calling 都过了一遍。走到这一步不少同学心里其实一直压着一个问题——光靠大模型聊天或者能调用几个函数距离“真正回答我们业务问题”还差得很远。模型记不住我们公司的内部规范也不会凭空知道我们的历史项目文档、产品手册和售后记录。这一篇就来解决这个最实际的痛点用 Spring AI Alibaba 把文档变成可检索的知识库再通过 RAGRetrieval-Augmented Generation检索增强生成的方式让模型基于真实资料回答问题。适合刚跑通基础接口、想往落地场景再往前走一步的读者也适合正在做内部知识库问答、客服辅助这类项目的同学参考。1. 为什么企业落地绕不开 RAG1.1 大模型的天花板和 RAG 的破局方式大模型在训练时见过的数据是固定的训练完成那一刻它的“知识”就冻结了。哪怕是用最新的模型它对你公司的产品 SKU、历史工单、售后话术也是一无所知。这不是模型不够聪明而是它的训练语料里根本没有这些东西。如果你直接去问它“我们公司 X 设备在 Y 场景下的故障率是多少”它大概率会一本正经地编一个听起来很合理的答案出来——这就是业内常说的“幻觉”。要解决这个问题思路其实很直白模型不知道那就给它看模型记不住那就帮它翻资料。人进新公司第一天怎么干活先翻文档、问同事、查系统而不是靠脑子硬记。RAG 做的就是这件事先把文档切碎、变成向量存起来用户提问的时候去这个知识库里把最相关的几段内容捞出来连同问题一起塞给大模型让模型“看着资料回答”。这样答案不是模型脑补的而是有出处的准确率和可信度完全是两个层级。这里有一个容易被忽略的好处RAG 让知识的更新成本变得极低。模型不需要重新训练你只需要往向量库里加一份新文档第二天它就能回答新内容。对于业务文档频繁变动的场景这个优势比任何微调方案都现实。我见过不少团队打算用微调解决知识更新问题结果训一次模型两三天发布一次还要走审批等文档上线业务早就又变了。1.2 Spring AI Alibaba 把 RAG 链路抽象成了什么RAG 整套流程拆开看是固定的四个动作加载文档、切分文本、向量化存储、检索并生成回答。Spring AI Alibaba 做的事情就是把这四个动作封装成了一套可以组合的组件让开发者不需要自己从零去拼接 PDF 解析、向量计算、相似度检索这些脏活累活。具体来说它给出了几个抽象层。文档加载对应 DocumentReader负责把 PDF、Word、Markdown 等不同来源读成统一的 Document 对象文本切分对应 TextSplitter负责把长文档切成符合模型上下文窗口的小块存储和检索对应 VectorStore负责把文本变成向量并支持相似度查询最后回答阶段有 QuestionAnswerAdvisor它把“检索 拼接上下文 限制模型只能基于上下文回答”这一整套逻辑打包好了。这套抽离的收益在切换组件时最明显。举个例子你今天先用内存版向量库把流程跑通明天线上要接 Redis 或者 PGVector只需要换一个 VectorStore 的 Bean 实现其余业务代码一行不用动。这就是框架存在的意义——它不替你写业务但它把你的实现成本和切换成本都压到了很低。我前几年自己用 Python 写过一套类似的流程光是把各种文档解析器的异常处理调明白就花了一周而在 Spring AI Alibaba 里这些边界情况基本都被框住了。2. 搭建前的准备依赖、模型和向量库怎么选2.1 最小依赖清单与版本对照先看依赖。核心只需要引入spring-ai-alibaba-starter它会把 ChatModel、EmbeddingModel 这些默认组件都自动装配好。我做 RAG 的时候会再加一个文档解析器的依赖如果是处理 PDF、Word 这类办公文档用spring-ai-tika-document-reader一个依赖就能覆盖绝大多数格式。dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-starter/artifactId version1.0.0-M5.1/version /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-tika-document-reader/artifactId /dependency版本这里要提醒一句Spring AI 的版本迭代非常快M5.1 是我当前在用的版本等你看这篇文章的时候正式版或者更高的 M 版应该已经发布了。建议直接去 Spring AI Alibaba 官方仓库看最新版本号并且让 Spring AI BOM 统一管理这些依赖的版本避免出现 reader 和 starter 版本不一致导致的兼容问题。我第一次踩过的坑就是手动指定了 reader 的版本结果 starter 实际用的 Spring AI Core 版本比我指定得高运行时报了一堆方法签名找不到的错误。2.2 模型搭配Chat 模型和 Embedding 模型的分工RAG 链路里有两个模型在协同工作很多人第一次做的时候会混淆它们。对话生成的模型就是 Chat 模型负责根据用户问题和检索到的上下文组织回答。这个没什么好说的你前面几篇用哪个继续用哪个就行。真正容易被忽视的是 Embedding 模型它负责把文本转成向量。这里有个关键要求写入向量库和查询向量库必须使用同一个 Embedding 模型。如果你写入的时候用的 A 模型查询的时候用的 B 模型两个向量的分布空间都不一样相似度计算出来的结果就是一堆乱码检索效果惨不忍睹。Spring AI Alibaba 默认会配置好 DashScope 的 Embedding 模型不用额外写代码。你在application.yml里只要配好 API Keyspring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY}框架会自动注入一个可用的 EmbeddingModel Bean。如果你有特殊需求比如内部部署了私有的 Embedding 模型只需要自己声明一个 EmbeddingModel 的 Bean 覆盖掉默认的即可。我个人建议前期不要折腾这一步先用默认模型的跑通整体链路再去考虑替换否则你在调通之前根本分不清问题是出在模型上还是自己的代码上。2.3 向量数据库先内存跑通再谈持久化Spring AI Alibaba 首选的默认向量库是内存版的 SimpleVectorStore对于入门和测试来说这个是体验最好的选择不需要额外部署任何服务项目启动的时候往里面塞数据就能用。但它有个硬伤数据只存在内存里服务重启就全丢了而且单机内存容量有限不适合生产。所以我的建议非常明确第一阶段不管最终目标是什么先用 SimpleVectorStore 把整套 RAG 链路跑通。原因很简单排除变量。等你确认了“文档解析没问题、切分没问题、检索没问题、回答没问题”再去切换正式的向量库。切换的成本很低因为 VectorStore 是一个接口你只需要换一个实现类。第二阶段再上 Redis 或者 PGVector 这类持久化方案只需要在 pom 里加对应的 starter然后配置一下连接地址。这里贴一下 SimpleVectorStore 的配置方式Bean public VectorStore vectorStore(EmbeddingModel embeddingModel) { return new SimpleVectorStore(embeddingModel); }判断一个向量库适不适合你的场景我一般看三个维度检索性能、数据持久化、运维成本。Redis 适合已经有 Redis 基础设施的团队PGVector 适合把 PostgreSQL 作为统一数据存储的团队各有各的取舍。入门阶段别纠结跑通了再去纠结不迟。3. 把 PDF 文档变成可检索的知识库解析、切分与向量化3.1 文档加载不同格式选不同的 Reader整个 RAG 流程里文档加载是最容易被低估的一步。很多人以为 PDF 解析就是把文字抽出来实际做一遍就明白了格式多样的文档会让异常处理变得非常麻烦。Spring AI Alibaba 给了一组统一的 DocumentReader 接口不同的格式对应不同的实现类。文档类型推荐方案说明PDFPdfDocumentReader 或 TikaDocumentReader文本型 PDF 直接解析扫描件需要 OCRWord/PPT/ExcelTikaDocumentReaderTika 统一处理覆盖格式广网页链接UrlDocumentReader直接抓取页面文本Markdown/纯文本直接用 Spring AI 的 TextReader简单场景够用在我实际项目里使用 PdfDocumentReader 加载一个产品手册代码是这样的public ListDocument loadPdf(String path) { PdfDocumentReader reader new PdfDocumentReader( new FileSystemResource(path)); return reader.read(); }读到的是统一的 Document 对象列表每个 Document 里面有文本内容、有元数据。元数据这个东西前期容易被忽视等你要在回答里标注“这段内容出自哪份文档哪一页”的时候就知道它有多重要了。所以我的习惯是加载完就打印一下每个 Document 的 id 和 metadata看看有没有 source 信息没有的话手动补上。3.2 切分策略chunk 大小和重叠怎么定文档加载完之后往往是几页甚至几十页的连续文本不可能整篇塞给模型必须切成小块也就是 chunk。这一步直接决定了检索质量的上限。切得太大每块包含太多无关信息向量之间的区分度下降检索出来的内容不够精准切得太小语义可能被截断一段完整的意思被切到两块里哪一块都检索不到关键信息。Spring AI 提供了几个切分器我常用的是 TokenTextSplitter按 Token 数量切分同时可以设置重叠:TextSplitter splitter new TokenTextSplitter(600, 150); ListDocument chunks splitter.apply(docs);第一个参数是每块的最大 Token 数第二个参数是相邻块之间的重叠 Token 数。600 这个数字不是拍脑袋定的普通技术文档的一段话大概占 200 到 300 个 Token600 能保证一个 chunk 里放得下一到三个完整段落150 的重叠保证跨段的语义不会因为硬切而断掉。用生活里的场景来类比切文本就像切菜刀不能太钝导致切不断也不能一刀切太大块导致炒不熟。如果你处理的文档结构比较规整比如强标题结构的帮助中心建议先按标题拆成章节再用 TokenTextSplitter 切一次如果是聊天记录这种短文本把 chunk 调小到 200 到 300反而效果更好。没有万能参数我把这组参数理解为“默认起点”每换一类文档都要手动验证调整。3.3 写入向量库先验证检索再谈问答所有的准备工作最终都是为了把切好的文本块向量化之后存进 VectorStore。这一步的代码短到让人怀疑是不是漏了什么public void buildKnowledgeBase(VectorStore vectorStore, ListDocument chunks) { vectorStore.add(chunks); }但真正重要的是写入之前和写入之后的验证。写入之前先打印一下 chunks 的数量和每块的字符长度确认不是所有内容都被切成了同一个空块这种情况我遇到过原因是文档解析出来本身就是空白文本。写入之后不要急着去写问答接口先单独跑一次相似度检索看看返回的 chunk 内容和预期的知识是否匹配。ListDocument results vectorStore.similaritySearch( SearchRequest.query(设备保修期是多久).withTopK(3));这一步做得好你的问答链路就成了一半。检索这一步如果捞上来的内容驴唇不对马嘴后面再怎么调 Prompt 也没用因为模型拿到的“参考资料”本身就是错的。我在每次接新领域的文档时都会先做这个验证发现检索不准就回头调切分参数而不是闷头写后面的代码。4. 组装问答链路从向量检索到大模型回答4.1 QuestionAnswerAdvisor 做了什么检索验证通过之后就到了整个 RAG 链路最后一步把检索到的文档片段和用户问题组装起来交给大模型生成回答。这里如果全手写你要做三件事拼接 Prompt 模板、控制检索结果的 Token 长度、在 Prompt 里反复强调“只能根据参考资料回答不要编造”。每次写都容易出错而且容易漏掉细节。Spring AI Alibaba 提供了一个现成的组件 QuestionAnswerAdvisor把这三件事都封装好了。它内部做的事情是先根据用户问题去 VectorStore 检索取出 TopK 个相关片段然后把这些片段作为系统上下文拼进 Prompt再调大模型回答。它内置的 Prompt 模板会自动告诉模型“只根据给定上下文回答如果上下文里没有相关内容就老实说不知道”。使用起来非常简洁QuestionAnswerAdvisor advisor QuestionAnswerAdvisor.builder(vectorStore) .similarityThreshold(0.6) .topK(4) .build();这里有两个参数值得展开说。similarityThreshold 是相似度阈值只有当相关度高于这个值的文档片段才会被采纳topK 是取前多少个片段。这两个参数是检索质量的双保险一个管下限一个管数量。它们的调优不是一劳永逸的具体值要看你的文档特征也需要结合实验结果去确认我后面会在问题排查那一节细讲。4.2 一个完整的 RAG 问答接口把前面的组件串起来就是一套完整的 RAG 问答代码。我通常会写一个 Service 类专门负责知识库构建和问答Service public class RagService { private final ChatModel chatModel; private final VectorStore vectorStore; public RagService(ChatModel chatModel, VectorStore vectorStore) { this.chatModel chatModel; this.vectorStore vectorStore; } public String answer(String question) { ChatClient chatClient ChatClient.builder(chatModel).build(); QuestionAnswerAdvisor advisor QuestionAnswerAdvisor.builder(vectorStore) .similarityThreshold(0.6) .topK(4) .build(); return chatClient.prompt() .advisors(advisor) .user(question) .call() .content(); } }对应的 Controller 就更简单了RestController public class QuestionController { private final RagService ragService; public QuestionController(RagService ragService) { this.ragService ragService; } PostMapping(/ask) public String ask(RequestParam String question) { return ragService.answer(question); } }整个代码量比大多数人想象中要少得多。但代码少不代表没东西要注意。我这里特别说明一下如果你的应用里有多个不同的知识库比如一个产品文档库、一个运维故障库不要把它们混在同一个 VectorStore 里建议按业务域拆成多个 VectorStore 实例或者在写入时给 Document 的 metadata 里打上分类标签检索的时候按标签过滤。混在一个库里检索出来的内容会很杂模型会把不相干的两类资料混在一起回答非常容易出错。4.3 关键参数相似度阈值、引用来源与流式返回答疑接口跑通之后你还想让它更贴近真实场景有几个进阶点要处理。第一个是相似度阈值。默认的 0.6 是我个人习惯的起始值具体调法是拿一批有标准答案的测试问题跑一遍观察检索返回的 TopK 内容里到底有多少是真正相关的。如果发现返回一堆不相关片段就把阈值往上调如果相关片段被过滤掉了导致模型说“不知道”就把阈值往下调。我见过有同事为了追求“回答率”把阈值设到 0.1结果模型答得天花乱坠但没几个字是对的这就是典型的指标选错了。第二个是回答里带上引用来源。Spring AI 的 Document 对象自带 metadata我在写入向量库之前会把 source 信息塞进去回答之后把 QuestionAnswerAdvisor 返回的上下文取出来提取 source 字段展示给用户。企业内部场景里用户看到“这段结论出自《XX设备运维手册》第 3 章”信任度完全不一样。第三个是流式返回。问答本身要联网调大模型接口如果同步返回动辄五六秒前端体验很差。用.stream()替代.call()就能拿到流式的 FluxFluxString answer chatClient.prompt() .advisors(advisor) .user(question) .stream() .content();前端配合 SSE 就能实现一个字一个字往外蹦的效果。这个看似只是体验优化但在客服辅助这类真实业务里几乎是必选项没人愿意盯着转圈等六秒。5. 实战避坑指南解析失败、检索偏差和幻觉残留5.1 文档解析最容易踩的三个坑文档解析是 RAG 链路里异常处理需求最密集的环节。我把自己反复踩过的坑集中整理一下。第一个坑扫描版 PDF 解析出来全是乱码或者干脆是空文本。很多公司内部手册是扫描件转的 PDF里面根本没有文字层PdfDocumentReader 只能解析出图片Tika 默认也做不了 OCR。解决办法是在 Tika 里配置 Tesseract OCR或者干脆换一个思路先用 OCR 工具把 PDF 转成带文字层的版本再喂给解析器。这个坑的可怕之处在于程序不报错只是解析出来的内容为空你会误以为向量库是好的直到用户提问时半天检索不到东西。第二个坑PDF 里的表格被解析成一大坨文字。表格本身就是视觉结构解析成文本之后行列关系全丢了。我处理产品参数表的时候一开始检索出来的是“型号 A 电压 220V 型号 B 电压 380V”这种粘连文本切块之后语义更乱。现在的处理方法是能用 Markdown 转存的就先转成结构化文本实在不行就把表格单独按行切小块让每个 chunk 尽量包含完整的一行数据反而检索效果更好。第三个坑加载外部 URL 的时候被反爬或者拿到一堆页面导航文本。UrlDocumentReader 抓到的页面里混杂了导航栏、页脚版权信息这些噪音直接向量化会严重污染检索结果。我现在的做法是抓取之后先做一轮清洗删掉.header、.footer这类标签里的内容再入库。问题现象主要原因解决方向解析结果乱码/空白扫描件无文字层OCR 预处理或换带文字层的文件表格信息混乱文本解析丢失行列结构结构化转存或按行切块抓取页面噪音太多导航/页脚混入正文清洗标签后再入库5.2 检索结果不理想的排查顺序用户问了一个问题结果答案没对上号大部分人第一反应是去改提示词这是方向性错误。检索不对提示词改出一朵花来也没用。我建议按照下面的顺序排查。先确认向量库里有没有数据。听起来像废话但我真的遇到过几次向量库写了一半就报错重启之后数据回滚服务看起来正常实际上库里空空如也。写一个启动自检打印向量库里的文档总数这个操作能省下大量排查时间。再确认切分参数是否合理。如果检索出来的 chunk 明显把一句话切成了两半导致永远搜不到关键词就调大重叠量或者换用按段落切分。如果发现检索结果和问题高度相关但 chunk 太多导致上下文超长被截断就调小 chunk 大小或者降低 topK。最后检查 Embedding 模型是否一致。如果你升级了依赖或者曾经切换过模型库里已有的旧向量是旧模型生成的和新模型不兼容。这个问题的表现非常隐蔽不是完全检索不到而是相关度都很低整体效果远不如之前。解决办法就是更新依赖后重新全量向量化。5.3 回答还有幻觉检查这三处如果你确信检索结果没问题上下文也正确拼进去了模型却还是给出了不在资料里的内容这个时候要查三处地方。第一处相似度阈值是不是设得太低。阈值低意味着很多不相关内容被塞进上下文模型面对一堆不相关文字更容易强行找逻辑从而编造答案。这是最常犯的错误用标准测试题目跑一遍对比往往立刻就能发现问题。第二处你的 Prompt 有没有被默认模板覆盖。QuestionAnswerAdvisor 内置的 Prompt 是“根据上下文回答”但如果你自己又叠了一层 system prompt两者可能打架。我见过的一种情况是自定义 system prompt 里写了“你是一个知识渊博的助手”于是模型即使在上下文里找不到答案也会因为系统人格设定而强行补充发挥。解决方法是把自定义 Prompt 和 Advisor 的指令对齐明确写上“如果上下文中没有对应信息直接回答不知道”。第三处你问的问题本身是不是不在知识库里。如果用户问“设备价格”而你的知识库里只放了设备参数没有任何价格信息模型被强制要求“根据上下文回答”它只好输出一句“上下文中未提供价格信息”。这时要区分清楚到底是没有检索到还是没有这个知识。前者是检索问题后者建议返回一个标准的“知识库中暂无相关内容”话术。这里提供一组我常用的兜底写法给 Prompt 加一个硬性约束请只根据上面提供的资料回答问题。 如果资料中没有直接答案请明确回答“当前资料中未找到相关信息”不要尝试猜测或补全。 内容请尽量引用原文保留关键数据和结论。最后再分享一个真香技巧把每次检索到的 chunk 内容连同最终回答一起打日志哪怕只是输出到控制台。这能在你搞不清到底是检索问题还是生成问题的时候直接撕开黑盒看清楚模型到底看到了什么。这个习惯让我少熬了无数次夜。RAG 这条路框架能帮你省掉大量重复劳动但最终的效果还是得靠你对自己文档的理解和持续的参数调优来打磨。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

单北斗变形监测系统:原理、选型与实测要点全解析 2026/10/2 4:28:27

单北斗变形监测系统:原理、选型与实测要点全解析

1. "单北斗"为什么突然成了硬指标2024年下半年开始,几乎所有基建安全监测的招标文件里都出现了一个新词:单北斗。最开始我以为只是某个业主单位的特殊要求,后来发现水利、交通、矿山、自然资源这几个口子的项目都开始陆续提&#x…

阅读更多 →
C++宏的工程化实践:从预处理机制到X-Macros与条件编译 2026/10/2 4:28:27

C++宏的工程化实践:从预处理机制到X-Macros与条件编译

宏这东西,在很多现代C开发者眼里,几乎是“坏味道”的代名词。我见过不少人写代码时恨不得把#define从键盘上扣掉,觉得它破坏了类型安全、绕过了作用域规则、还难调试。但如果你真在一线维护过几套商业级代码库,会发现一个很现实的…

阅读更多 →
深入C++ STL set和map:红黑树原理、使用陷阱与容器选型 2026/10/2 4:28:27

深入C++ STL set和map:红黑树原理、使用陷阱与容器选型

1. 为什么set和map在STL里这么特殊:从"容器家族"看它们的定位刚开始学C STL的时候,我一度以为set和map就是"高级一点的数组",set用来去重,map用来存键值对,仅此而已。直到后来刷题、写业务代码、甚…

阅读更多 →
Prometheus监控MySQL重启误报:三大根因排查与告警规则优化 2026/10/2 4:28:27

Prometheus监控MySQL重启误报:三大根因排查与告警规则优化

凌晨2点17分,告警群弹出一条消息:“[FIRING] MySQLRestart,instance10.0.8.101:9104”。如果你维护过一套靠 Prometheus 做数据库监控的系统,这个时刻的血压一定不低。我经历过好几次这样的误报:值班人员登录数据库一看…

阅读更多 →
SEED-VII情感脑电数据集实战:从预处理到跨被试评估 2026/10/2 4:28:26

SEED-VII情感脑电数据集实战:从预处理到跨被试评估

去年有个研究生学弟跑来问我,情感脑电方向入门到底该用哪个数据集。他试过DEAP,觉得刺激材料太短太弱,情绪唤醒度不够;又嫌SEED系列的分类粒度太粗,四分类五分类做完总觉得离真实情绪场景差点意思。我给他推荐了SEED-V…

阅读更多 →
Vibe Coding 实战:从提示词工程到 Agent 模式的工程化落地 2026/10/2 4:28:05

Vibe Coding 实战:从提示词工程到 Agent 模式的工程化落地

1. 从“会写代码”到“会描述意图”:Vibe Coding 到底改了什么“Vibe Coding”这个词最近在开发圈子里出现的频率越来越高,很多人第一次听到会以为是某种新的编程语言或者框架,其实它描述的是一种工作方式的转变:开发者不再逐行敲…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉