Spring Boot集成Spring AI:打造带引用来源的RAG知识库问答助手
做 AI 应用开发这一年多我最怕的是模型“睁眼说瞎话”——明明不知道答案还要编一个头头是道的回答。企业内部文档问答里这种幻觉最致命用户拿着 AI 给的答案去办事结果来源根本不存在。最近我在一个 Spring Boot 项目里用 Spring AI 搭了一套 RAG 问答助手核心只干一件事让回答带上引用来源。AI 说出的关键结论都能在原始文档里找到出处幻觉问题被压到了可接受范围。这个项目本身代码量不算大但坑不少。从需求拆解到落地实测我把完整过程整理出来涉及 RAG 的核心原理、Spring AI 的常用组件、文档切分参数、向量检索配置以及引用格式这类容易翻车的细节。如果你正打算给 Spring Boot 服务接大模型问答能力或者纠结怎么让 AI 的回答“有据可查”这篇实战记录应该能帮你省不少时间。1. 项目概览与 RAG 核心思路1.1 先搞清楚大模型的幻觉是怎么来的大模型生成回答时本质是在做概率预测根据你给的上下文一个字一个字地推断下一个最可能的词。它没有一个“查数据库”的动作也没法真正知道自己不知道什么。训练数据中没见过的细节、内部专有名词、时效性很强的配置信息它都会按“最像那么回事”的方式编出来。这就是幻觉的来源。做产品的人不能把“幻觉”当段子听因为幻觉会带来实打实的后果。比如我问内部知识库“生产环境数据库连接池上限是多少”模型凭印象回了一个数字而这个数字跟实际配置不符运维照着去调参事故就来了。要缓解幻觉业界最成熟的方案不是一味换更大的模型而是 RAG——检索增强生成。思路很直白模型不知道没关系你先把相关资料检索出来“喂”给它让它基于资料作答。手里有了上下文模型自由发挥的空间就被挤掉了。1.2 本项目要解决什么问题这个项目具体解决两件事。第一回答有据可依。模型不能只凭训练时的“记忆”作答必须基于从知识库检索回来的文档片段生成回答。第二关键结论带引用。回答里的事实性内容都能在源文档里找到对应位置。用户点一下引用就能跳过去核对原文而不是把 AI 的回答当“最终结论”直接用。这两件事单拆开都不难但组合在一起放进 Spring Boot 生态就需要考虑技术栈整合成本。这也是我选 Spring AI 的核心原因。Spring AI 在 Spring 生态里把“文档加载、切分、向量化、存储、检索、对话”这条 RAG 链路基本串好了我只需要专注业务层的组装而不是从零对接各家 SDK。1.3 项目适用范围与非目标这个方案适合的场景有这几类企业内部文档、制度、产品手册的智能问答需要答案可核查、可追溯的客服支撑系统已经以 Spring Boot 为主技术栈、不想额外引入 Python 服务的团队不适合的场景我也说清楚。如果知识库内容是强动态的比如生产监控数据实时查询RAG 的离线索引方式有明显滞后性你得额外处理增量同步。如果要求答案极度精确而检索结果经常不够理想那问题可能不在模型而在文档切分策略和 Embedding 选型——这个后面会展开。2. 技术选型与整体架构2.1 为什么选 Spring AI 而不是自己拼在这套项目之前我做过一个类似的问答系统当时是手搓自己写 Embedding 调用、自己连向量库、自己拼 Prompt、自己接大模型 API。做完最大的感受是胶水代码太多每个环节都要单独对接 SDK数据结构还得自己定义维护成本很高。Spring AI 给我最直接的帮助是统一抽象层。它定义了Document、VectorStore、ChatModel、EmbeddingModel这些核心接口并且提供了从文档加载到检索、对话的一整套实现。使用这套抽象你从一开始就在写业务逻辑而不是写适配代码。不过 Spring AI 也不是银弹。版本迭代很快不同大版本间的 API 差异明显网上资料经常对不上。我项目里用的是相对稳定的版本后面的代码写法以我实际用的版本为准。如果你用的版本不同接口可能有微调但核心链路不会变。2.2 整体架构数据怎么流转整个系统数据流可以拆成离线索引和在线问答两条线。离线索引线负责把文档变成可检索的向量读取文档Markdown、PDF、Word、纯文本按语义把文档切成小块比如每块几百个 token对每块调用 Embedding 模型转成向量把向量、原文、来源元数据一并存入向量数据库在线问答线负责响应用户提问用户输入问题问题同样做 Embedding转成向量在向量数据库中做相似度检索取回最相关的 Top-N 个文档片段把问题和这些片段拼进 Prompt发给大模型大模型基于片段生成回答并在关键句后标注引用编号后端把引用编号替换成可跳转的原文链接返回前端两条线合起来就是一条完整的 RAG 链路。Spring AI 对链路每个环节都有组件覆盖下面逐一实现。2.3 项目的模块划分按代码组织我分了几个包controllerREST 接口层接收提问service问答主流程编排indexer文档索引构建跑离线任务retriever检索逻辑封装向量库查询promptPrompt 模板管理source引用来源解析与格式化这个划分看似简单但实际排查问题时候很有用。RAG 链路长出了问题必须先定位“是检索不行还是生成不行”把索引、检索、生成拆成独立模块后每个环节都能单独验证。我之前没有分包的时候一个地方报错排查范围就是全链路效率很低。3. 从零搭建 RAG 问答助手核心实现3.1 第一步引入依赖与基础配置项目基于 Spring Boot 3.x。Spring AI 的起步依赖以 starter 形式提供按需引入。核心依赖大概是这样的dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-vector-store-pgvector/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-pdf-document-reader/artifactId /dependency需要说明一点spring-ai-starter-model要配合具体模型的适配器使用。因为不同大模型服务商的接口有差异Spring AI 针对主流服务商分别提供 starterartifactId 形如spring-ai-starter-model-xxxxxx 是服务商标识。如果你的服务商提供了标准兼容接口只要在配置里填好 base-url 和 api-key 就行。application.yml 的基础配置spring: ai: model: chat: api-key: ${LLM_API_KEY} base-url: ${LLM_BASE_URL} options: model: ${LLM_MODEL_NAME} temperature: 0.2 embedding: api-key: ${EMBEDDING_API_KEY} base-url: ${EMBEDDING_BASE_URL} options: model: ${EMBEDDING_MODEL_NAME} datasource: url: jdbc:postgresql://localhost:5432/rag_demo username: postgres password: postgres两个细节单独说。第一temperature调到 0.2目的是让回答更收敛、更忠实于检索到的文档减少自由发挥。做知识库问答温度不宜高我见过有人开默认值结果模型动不动就“创造”。第二Embedding 模型和对话模型可以来自不同服务商Spring AI 允许分开配置。实际项目里很常见——向量化用便宜的专用模型对话用更强的模型。还有个容易忽略的点。PGVector 的 starter 在启动时会去 PostgreSQL 里建向量扩展但数据库用户必须有权限执行CREATE EXTENSION。我本地是先手动执行了扩展创建再去启动应用的。如果你是第一次用 PGVector这条坑基本必踩。3.2 第二步文档加载与切分决定检索质量的关键RAG 效果好不好一半以上责任在文档切分。这里我踩过不少坑先说结论。文档加载用 Spring AI 的DocumentReader系列。我整理了三种常见格式的写法Markdown 或纯文本用TextReader直接读PDF用PagePdfDocumentReader按页加载Word先转成纯文本再交给TextReader加载完成得到一批Document对象每个包含content正文内容和metadata元数据如文件名、页码、标题。这两种信息后面都要用。切分是重头戏。Spring AI 提供了按 token 切分的实现比按字符切分更贴合模型的实际上下文窗口。我的初始配置是这样的TextSplitter splitter TokenTextSplitter.builder() .withChunkSize(800) .withOverlap(150) .build(); ListDocument chunks splitter.apply(sourceDocuments);chunkSize是每块的目标 token 数overlap是相邻块之间的重叠 token 数。为什么要重叠因为句子上下文是连续的如果正好在段落中间切断后半块就丢了前半块的信息检索时就可能召回不到。重叠 150 左右能尽量保住跨块的上下文。但光切分还不够来源信息必须留下来。我给每个切分块都补上了原始文件和页码chunks.forEach(chunk - { chunk.getMetadata().put(source, sourcePath); chunk.getMetadata().put(page, pageNumber); });这一步千万别偷懒。如果切分时没记来源后面就算检索对了也不知道这句话来自哪份文档引用也就无从谈起。再往后还有一个非常影响检索质量的细节Markdown 标题层级。标题是强语义信息但直接切分时经常被忽略。我在实验中发现切分正文片段时把所属的一级、二级标题拼到片段最前面检索召回率有肉眼可见的提升。Spring AI 没内置这个能力我是自己遍历元数据里的标题字段拼上去的。别小看这个操作知识库文档一旦结构复杂效果差距很明显。3.3 第三步Embedding 与向量存储建模服务商提供向量化接口Spring AI 里对应EmbeddingModel。用法很简单Autowired private EmbeddingModel embeddingModel; float[] vector embeddingModel.embed(数据库连接池配置);实际工程里不需要手动为每条文本调用 EmbeddingSpring AI 的VectorStore会在add()时自动调用。把切分好的 Document 列表直接交给 VectorStore 就行Autowired private VectorStore vectorStore; public void indexDocuments(ListDocument documents) { vectorStore.add(documents); log.info(入库完成共 {} 个文档块, documents.size()); }我用的向量库是 PGVector也就是 PostgreSQL 的向量扩展。选它主要因为三点。第一团队本来就熟 PostgreSQL不用额外部署一套独立向量数据库。第二PGVector 支持 HNSW 索引百万级文档块规模内检索性能完全够用。第三Spring AI 对它的集成是官方维护的初始化时自动建表。向量表核心字段大致这样字段说明id主键UUIDcontent原始文档块文本metadataJSON 格式存来源、页码等embeddingvector 类型存储向量索引配置也值得注意。我建的是 HNSW 索引距离函数用 cosineCREATE INDEX ON vector_store USING hnsw (embedding vector_cosine_ops);选 cosine 还是 L2取决于 Embedding 模型和检索语义。大多数 Embedding 模型训练时都用 cosine 相似度所以vector_cosine_ops通常是对的。如果你用的模型官方明确推荐其他度量再相应调整。有一个必须提醒的点Embedding 模型的输出维度和表结构必须一致。某模型输出 1024 维表里的 embedding 字段就要定义成vector(1024)。Spring AI 的 PGVector 实现会自动建表但如果表已存在而维度对不上启动时会直接报错查起来还挺费劲。3.4 第四步问答链路封装离线索引建好后在线问答核心代码反而不多。我用 Spring AI 的ChatClient构建问答方法Service public class RagAssistantService { private final VectorStore vectorStore; private final ChatClient chatClient; public RagAssistantService(VectorStore vectorStore, ChatClient.Builder chatClientBuilder) { this.vectorStore vectorStore; this.chatClient chatClientBuilder.build(); } public String ask(String question) { // 1. 检索相关文档块 ListDocument similarDocs vectorStore.similaritySearch( SearchRequest.builder().query(question).topK(5).build() ); // 2. 拼装上下文 StringBuilder context new StringBuilder(); for (int i 0; i similarDocs.size(); i) { Document doc similarDocs.get(i); context.append(String.format( [%d] %s (来源:%s), i 1, doc.getContent(), doc.getMetadata().get(source) )).append(\n\n); } // 3. 组装 Prompt 并调用对话模型 String systemPrompt PROMPT_TEMPLATE.replace({context}, context.toString()); return chatClient.prompt() .system(systemPrompt) .user(question) .call() .content(); } }这里有两个参数值得展开。Top-K设为 5指每次从向量库取 5 个最相关文档块。这个值不是越大越好。取回越多上下文越全面但噪音也越多模型可能被不相关信息带偏。我实测下来文档问答场景 5 到 8 是常见合理区间具体要在你自己的数据上跑几轮看看。PROMPT_TEMPLATE是决定“回答是否忠实于资料”的关键。我用的模板大概这样你是一个知识库问答助手。请严格根据下面的参考资料回答。 要求 1. 只使用参考资料中的信息不要使用你自己的知识。 2. 如果参考资料不足以回答直接回答根据现有资料无法确认。 3. 涉及事实性结论时在句尾标注引用编号编号对应参考资料前的 [编号]。 4. 参考资料里没有的信息绝对不要编造。 参考资料 {context} 用户问题{question}这个 Prompt 里“严格根据资料”“不确定就不要答”是核心约束。我做过对照实验只给上下文但不在 Prompt 里做约束模型照样会在信息不足时自由发挥。约束写不写效果差很多。生产环境我建议把接口改造成流式返回。Spring AI 的ChatClient支持.stream()前端按 token 逐步展示。用户对大模型问答的等待耐心通常只有几秒流式输出能显著缩短感知时间是体验优化的关键手段。4. 回答带引用从“看起来是对的”到“能核实”4.1 引用标注的核心设计回答带引用这件事本质上分两步。第一步让模型在生成时标注引用编号。第二步把编号映射回真实文档链接。这两步如果混在一起写代码会非常乱。我在 Prompt 里给模型规定了引用编号规则参考资料每条都有[编号]回答中引用哪条资料就在对应句尾标哪个编号。比如根据项目文档生产环境连接池上限为 200 [1]测试环境为 50 [2]。模型输出的是这种带编号文本。第二步在后端解析编号替换成真实的前端链接。为什么要模型输出编号而不是直接给它完整链接因为模型即使知道链接构造超长 URL 也很容易出错URL 里的特殊字符模型经常写错。让模型只负责“逻辑位置”把“链接拼接”放到代码层做可靠得多。4.2 引用格式化的实现后端解析引用的代码大致是这样public String renderAnswerWithSources(String rawAnswer, ListDocument sources) { Pattern pattern Pattern.compile(\\[(\\d)\\]); Matcher matcher pattern.matcher(rawAnswer); MapInteger, Document sourceMap new HashMap(); for (int i 0; i sources.size(); i) { sourceMap.put(i 1, sources.get(i)); } StringBuffer result new StringBuffer(); while (matcher.find()) { int idx Integer.parseInt(matcher.group(1)); Document doc sourceMap.get(idx); if (doc null) { matcher.appendReplacement(result, matcher.group(0)); continue; } String sourcePath (String) doc.getMetadata().getOrDefault(source, 未知来源); String page String.valueOf(doc.getMetadata().getOrDefault(page, )); matcher.appendReplacement(result, String.format(sup class\citation\>

相关新闻

VisionMaster标定方法:像素坐标到物理坐标的高精度转换指南

VisionMaster标定方法:像素坐标到物理坐标的高精度转换指南

简介:面向机器视觉开发者的VisionMaster标定方法可运行示例,围绕十二点与九点标定中的参数提取环节展开。压缩包共3个文件,包括HTML演示页、inscode运行配置与gitignore设置,体积仅8KB,却提供了一套可直接学习的轻量示…

2026/10/13 1:41:12 阅读更多 →
地图匹配原理与Python实现:用HMM将偏移GPS轨迹拉回路网

地图匹配原理与Python实现:用HMM将偏移GPS轨迹拉回路网

简介:这套基于Python实现的地图匹配项目资源,专注于将GPS轨迹数据与路网进行匹配,把偏移道路的漂移点拉回正确道路上,适用于智慧交通、车辆监控、路径规划、共享出行轨迹分析等多个场景,能有效解决GPS漂移带来的轨迹与…

2026/10/13 1:41:12 阅读更多 →
Matplotlib中文乱码解决方案:字体配置与坐标轴调整全攻略

Matplotlib中文乱码解决方案:字体配置与坐标轴调整全攻略

第一次在matplotlib里碰到中文变方块,应该很多人都有印象:数据算得好好的,标签也写得规规矩矩,结果图片一出来,横轴纵轴上的中文全变成一个个小方框。这个现象在数据可视化里太常见了,尤其是用matplotlib画…

2026/10/13 1:41:12 阅读更多 →

最新新闻

Reclilisib:从PI3K激动剂到选择性辐射保护剂的双重角色

Reclilisib:从PI3K激动剂到选择性辐射保护剂的双重角色

做肿瘤放疗研究的朋友应该都有过这种纠结:治疗窗口太窄,射线过去,肿瘤细胞和正常组织一起遭殃。我们这些年一直在找"选择性辐射保护剂"——最好能保护正常细胞,却不给肿瘤细胞留活路。我第一次注意到Reclilisib&#xf…

2026/10/13 2:18:27 阅读更多 →
MFC连接MySQL实战:ODBC封装、CRUD与避坑手册

MFC连接MySQL实战:ODBC封装、CRUD与避坑手册

简介:这是一套面向MFC桌面应用开发者的MySQL数据库访问示例工程,核心演示如何借助ODBC标准数据库接口,在C程序中完成连接建立、SQL查询、结果集遍历与数据更新等常用操作。压缩包内共含117个文件,压缩后约181KB;其中49…

2026/10/13 2:18:27 阅读更多 →
从Java转运营第5天:用技术思维做抖音新号,先学会拆解对标账号

从Java转运营第5天:用技术思维做抖音新号,先学会拆解对标账号

今天是我从 Java 转运营的第 5 天,也是正式开始折腾抖音新号起号的日子。说实话,从写完第一版账号简介到研究“到底该发什么”,我踩的坑比写的需求还多。如果把起号比作一次系统上线,那我 Day05 最大的收获就是:先别急…

2026/10/13 2:18:27 阅读更多 →
安卓手机跑 Windows 应用完整指南:Winlator 安装、配置与排障实操

安卓手机跑 Windows 应用完整指南:Winlator 安装、配置与排障实操

安卓手机跑 Windows 应用完整指南:Winlator 安装、配置与排障实操 【免费下载链接】winlator Android application for running Windows applications with Wine and Box86/Box64 项目地址: https://gitcode.com/GitHub_Trending/wi/winlator 手机里下了个 .…

2026/10/13 2:18:27 阅读更多 →
eksctl v0.149.0 发布解析:AWS 区域环境变量修复与 eksctl-io 组织迁移

eksctl v0.149.0 发布解析:AWS 区域环境变量修复与 eksctl-io 组织迁移

云原生容器编排DevOps 【免费下载链接】eksctl The official CLI for Amazon EKS 项目地址: https://gitcode.com/gh_mirrors/ek/eksctl 点击查看 免费下载 本指南围绕 eksctl 官方 CLI 的 v0.149.0 发布说明展开,逐项解析该版本引入的功能更新、两个关…

2026/10/13 2:18:27 阅读更多 →
Win11升级后应用打不开?从升级机制到修复方案全解析

Win11升级后应用打不开?从升级机制到修复方案全解析

最近一段时间,我这边处理最多的求助不是软件本身崩溃,而是同一句话:升完 Win11 之后,XX 应用打不开了。有的是设备厂商的配置工具,点开就没反应;有的是行业软件启动后直接闪退;还有的是虚拟机一…

2026/10/13 2:17:27 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 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/12 21:05:09 阅读更多 →
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/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练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/12 18:11:17 阅读更多 →