Spring AI 实战:RAG 问答助手引用溯源与幻觉治理
RAG 这个词在过去一年里被聊得很多但真正落到代码层面很多人卡在同一个地方检索回来的内容怎么塞给模型模型又怎么在回答里标出引用来源。我最近用 Spring AI 把一套带引用的问答助手跑通了从文档切分、向量入库到回答附上出处整条链路走下来踩了不少坑也攒了一些可以直接抄的配置。这篇就把我实际落地的过程完整拆一遍重点讲清楚引用这件事到底是怎么实现的以及为什么它能治住模型张口就来的毛病。1. 先搞清楚 RAG 到底在解决什么问题1.1 大模型睁眼说瞎话的根因大模型的知识全部来自训练时喂进去的数据训练一结束它的知识就冻结了。你问它一个训练截止之后发生的事情或者问它一份它从没见过的内部文档里的细节它不会说我不知道而是会基于语言概率编一个看起来很像那么回事的答案。这个现象业内叫幻觉。幻觉不是模型坏了而是它的工作机制决定的。它本质上是一个下一个词预测器目标是把话说通顺而不是把话说对。当它没有依据的时候它依然会沿着最可能的语言路径往下生成于是就编了。RAG 的思路很直接既然模型自己不知道那就在它回答之前先把相关资料找出来塞进它的上下文里让它看着材料答题。这样它的答案就有了依据幻觉自然大幅减少。1.2 RAG 和微调的区别在哪很多人第一反应是那我微调一下不就行了。微调确实能让模型记住新知识但它有几个绕不开的问题。第一成本高每次文档更新都要重新训练。第二微调后的模型依然可能编因为它学的是风格和模式不是精确事实。第三你没法知道它某句话是从哪份文档里学来的溯源基本做不到。RAG 恰好在这三点上都有优势文档更新只需要重新入库不用动模型答案基于检索到的原文事实性强检索到的片段可以直接作为引用展示出来来源清清楚楚。所以对于基于一堆文档做问答这个场景RAG 基本是首选方案。1.3 为什么选 Spring AI 来做市面上做 RAG 的框架不少Python 生态里 LangChain 最出名。但如果你的主业务是 Java 技术栈引入 Python 服务意味着多一套部署、多一套运维、多一套团队技能要求。Spring AI 的价值就在于它把这套能力搬到了 Spring 生态里用你熟悉的 Bean 注入、配置管理、依赖注入那一套就能搞定和现有 Spring Boot 项目无缝衔接。Spring AI 提供了几个关键抽象ChatClient负责和模型对话VectorStore负责向量存储和相似度检索DocumentReader负责读取文档TextSplitter负责切分。这几个东西组合起来一条 RAG 链路就成型了。下面我按实际搭建顺序来讲。2. 整体架构设计与关键选型2.1 一条完整的 RAG 链路长什么样先把全景图在脑子里过一遍后面每一步才知道自己在干嘛。一条标准的 RAG 问答链路分两个阶段。离线阶段也叫入库阶段读取原始文档把长文档切成小片段对每个片段做向量化embedding把向量和原文一起存进向量数据库。这个阶段是一次性的文档更新时重跑。在线阶段也叫查询阶段用户提问把问题也做向量化去向量数据库里找最相似的几个片段把这些片段拼进提示词连同问题一起发给大模型模型基于这些片段生成答案同时把用到的片段作为引用返回。关键点在于引用不是模型生成出来的而是我们在检索阶段就知道用了哪些片段回答完之后把这些片段的元信息附上去。这一点想通了引用功能就实现了一大半。2.2 向量数据库怎么选向量数据库的选择直接决定了部署复杂度和检索效果。我对比了几个常见选项。方案部署方式适合场景我的评价SimpleVectorStore内存本地开发、Demo零依赖重启数据就没了只适合调试Redis独立服务已有 Redis 的项目复用现有中间件运维成本低PostgreSQL pgvector独立服务数据量中等、要事务关系型数据库顺手SQL 能直接查专用向量库独立集群大规模、高并发功能强但运维重小项目没必要我这次用的是 PostgreSQL 加 pgvector 扩展。理由很实在项目里本来就有 Postgres加个扩展就能用不用再引入一个新的中间件而且向量和业务数据在同一个库里做联合查询、事务处理都方便。如果你项目里已经有 Redis用 Redis 也完全没问题Spring AI 对这些都有对应的实现。2.3 切分策略为什么这么重要文档切分是 RAG 里最容易被忽视、但影响最大的一环。切得太粗一个片段里混了好几个主题检索出来噪音大切得太细一个完整的语义被切断模型拿到半句话也答不好。我的经验是切分粒度要匹配文档的天然结构。技术文档按标题层级切合同按条款切FAQ 按问答对切。如果文档结构不明显就用固定长度加重叠的方式重叠部分是为了防止关键信息正好卡在切分边界上被切断。Spring AI 里的TokenTextSplitter就是干这个的它按 token 数切分比按字符数切更贴近模型的实际处理单位。我一般把 chunk size 设在 500 到 800 token 之间overlap 设在 100 到 200 token。这个范围是实测下来比较稳的太小检索不准太大噪音多。3. 核心环节的实操落地3.1 依赖引入与基础配置先把依赖加上。Spring AI 的版本迭代比较快建议用当前稳定版别用快照版快照版 API 说变就变。dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-pgvector-store-spring-boot-starter/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId /dependency配置文件里把模型和向量库的地址填上。这里有个细节要注意embedding 模型和 chat 模型可以是不同的embedding 负责把文本转向量chat 负责生成回答。embedding 模型一旦选定入库和查询必须用同一个否则向量空间对不上检索结果全是乱的。spring: ai: openai: api-key: ${API_KEY} chat: options: model: gpt-4o-mini temperature: 0.2 embedding: options: model: text-embedding-3-small datasource: url: jdbc:postgresql://localhost:5432/ragdb username: rag password: rag123temperature 我设成了 0.2比默认值低。原因很简单问答场景要的是准确和稳定不是创意。温度越低模型越倾向于选概率最高的词答案越保守、越贴近原文。如果你做的是创意写作那另说。3.2 文档读取与切分的代码实现读取文档这一步Spring AI 提供了多种 ReaderPDF、Markdown、纯文本都有对应的实现。我这次处理的是 Markdown 格式的内部文档。Component public class DocumentIngestor { private final VectorStore vectorStore; private final TokenTextSplitter splitter; public DocumentIngestor(VectorStore vectorStore) { this.vectorStore vectorStore; this.splitter new TokenTextSplitter(600, 150, 5, 10000, true); } public void ingest(Resource resource) { MarkdownDocumentReader reader new MarkdownDocumentReader(resource); ListDocument docs reader.get(); ListDocument chunks splitter.apply(docs); vectorStore.add(chunks); } }TokenTextSplitter的构造参数依次是目标 chunk 大小、重叠大小、最小 chunk 字符数、最大 chunk 字符数、是否保留分隔符。我设的 600 和 150 就是前面说的那个范围。最后一个参数true表示保留段落分隔符这样切出来的片段语义更完整。注意切分后的每个 Document 一定要带上元信息比如来源文件名、章节标题、页码。这些元信息就是后面引用的来源。很多人入库时图省事不带元信息等到要做引用的时候发现根本不知道片段来自哪只能推倒重来。3.3 给每个片段打上可追溯的元信息元信息是引用功能的命根子。我在读取阶段就给每个 Document 附加了来源信息。Document doc new Document(content, Map.of( source, fileName, section, sectionTitle, page, String.valueOf(pageNum) ));这样每个片段都知道自己从哪来。检索的时候Spring AI 会把元信息一起带回来我们就能在回答里标注这段来自《XX文档》第X节。元信息的字段设计也有讲究。source 用文件名或文档 IDsection 用章节标题page 用页码。如果文档有版本号也建议加上这样引用能精确到版本避免引用了旧版内容的尴尬。3.4 检索环节的参数调优检索是 RAG 的质量瓶颈。检索不准后面模型再强也白搭。核心参数有两个topK 和相似度阈值。topK 是返回最相似的几个片段。设太小可能漏掉关键信息设太大噪音多还会撑爆上下文窗口。我一般从 4 开始试根据效果调整。相似度阈值是过滤掉那些不太相关的片段避免把无关内容塞给模型。SearchRequest request SearchRequest.query(question) .withTopK(5) .withSimilarityThreshold(0.7); ListDocument docs vectorStore.similaritySearch(request);阈值 0.7 是个经验值。设太高可能一个片段都检索不到设太低什么垃圾都往里塞。这个值跟你的 embedding 模型有关不同模型的分值分布不一样建议先用一批测试问题跑一遍看看相关片段的实际分值落在哪个区间再定阈值。实操心得检索这块我强烈建议加一个重排序环节。先用向量检索召回一批候选比如 topK 设 20再用一个重排序模型对候选精排取前 5 个。向量检索快但粗重排序慢但准两者结合效果提升很明显。Spring AI 也支持接入重排序模型。3.5 把检索结果拼进提示词检索到片段之后要把它们组织成模型能理解的提示词。这里有个模板设计的技巧明确告诉模型只根据以下材料回答材料里没有的就说不知道。String template 你是一个严谨的问答助手。请严格根据下面提供的资料回答问题。 如果资料中没有相关信息直接回答根据现有资料无法回答不要编造。 回答时请在句末用 [来源: 文件名] 的格式标注依据。 资料 {context} 问题{question} ;这个模板有三个关键设计。第一明确约束只根据资料回答这是抑制幻觉的核心。第二给出无法回答的兜底话术让模型有退路不至于硬编。第三要求标注来源格式引导模型在生成时就带上引用标记。不过要注意模型标注的来源不一定百分百准确它可能标错。所以更稳妥的做法是引用信息由我们在代码层面根据实际检索到的片段生成不完全依赖模型自己标。模型标的是软引用代码生成的是硬引用两者结合最可靠。3.6 引用信息的组装与返回回答生成完之后把实际用到的片段元信息组装成引用列表返回给前端。public Answer ask(String question) { ListDocument docs retrieve(question); String context docs.stream() .map(Document::getContent) .collect(Collectors.joining(\n\n)); String answer chatClient.prompt() .user(u - u.text(template) .param(context, context) .param(question, question)) .call() .content(); ListCitation citations docs.stream() .map(d - new Citation( d.getMetadata().get(source).toString(), d.getMetadata().get(section).toString())) .distinct() .toList(); return new Answer(answer, citations); }Citation就是引用对象包含来源文件名和章节。前端拿到之后可以在答案下方列出参考资料或者做成可点击的角标点开跳到原文对应位置。这样用户就能自己验证答案对不对信任感一下就上来了。4. 常见问题与排查实录4.1 检索不到相关内容怎么办这是最常见的问题表现是用户问了一个文档里明明有的问题但检索结果为空或者全是无关内容。排查思路按顺序来。先确认文档真的入库了。直接查向量库的表看行数对不对。我遇到过好几次是入库任务没跑成功或者跑到了另一个库白折腾半天。再确认 embedding 模型一致。入库用的模型和查询用的模型必须是同一个这个前面强调过。如果中途换过模型旧数据全部作废必须重新入库。然后看切分粒度。如果文档切得太碎一个完整答案被切成好几段每段单独看都不够相关检索就召不回来。这时候把 chunk size 调大试试。最后看相似度阈值。阈值设太高会把相关片段也过滤掉。临时把阈值调到 0 或者很低看看能不能召回能召回就说明是阈值的问题。4.2 回答里引用标错或标漏引用不准通常有两个原因。一是模型自己标的引用不可靠它可能张冠李戴。解决办法是弱化模型标注强化代码生成以代码返回的引用列表为准。二是检索到的片段本身就不对模型基于错误材料回答引用自然跟着错。这还是要回到检索质量上加个重排序环节通常能改善。引用标漏的情况往往是提示词里没强调要标注或者模型偷懒。可以在提示词里加一句每个事实性陈述都必须标注来源并在后处理阶段检查答案里有没有引用标记没有的话补上。4.3 回答太慢怎么优化RAG 的延迟主要来自三块向量检索、模型生成、网络往返。向量检索通常很快几十毫秒级别除非数据量特别大。模型生成是大头尤其是答案长的时候。优化方向有几个。第一控制上下文长度检索片段别塞太多topK 从 5 降到 3 试试。第二用流式输出让用户先看到字感知上快很多。第三如果模型支持用更小的模型做初筛。第四把 embedding 结果缓存起来相同问题不用重复检索。4.4 常见问题速查表现象可能原因排查动作检索结果为空文档未入库 / 阈值过高查库行数临时降阈值检索结果不相关切分过碎 / 模型不一致调大 chunk核对 embedding 模型回答编造内容提示词约束不足强化只根据资料回答约束引用标错依赖模型自标改为代码生成引用回答慢上下文过长降 topK开流式输出重复内容多切分重叠过大减小 overlap4.5 几个容易踩的坑第一个坑是元信息丢失。前面反复强调这里再提一次因为真的太常见了。入库时不带元信息后面做引用只能干瞪眼。第二个坑是文档更新后没重新入库。文档改了向量库里还是旧的检索出来的是过时内容。建议做个文档版本管理文档一变就触发重新入库。第三个坑是忽略 token 限制。检索片段加上提示词模板加上问题总长度不能超过模型的上下文窗口。塞太多会被截断截断的位置很随机可能正好把关键信息切掉。入库前算一下留足余量。第四个坑是没做权限过滤。如果文档有权限分级检索时必须带上权限过滤条件否则低权限用户可能通过问答拿到高权限文档的内容。这个在元信息里加个权限字段检索时作为过滤条件就行。5. 效果验证与迭代方向5.1 怎么判断 RAG 做得好不好光靠肉眼看几个回答不够得有一套评估方法。我一般从两个维度看检索质量和生成质量。检索质量看召回率和准确率。准备一批测试问题每个问题标注好应该命中哪些片段然后跑检索看命中的比例。召回率低说明有该找的没找到准确率低说明找回来一堆没用的。生成质量看答案的事实性和引用准确性。事实性就是答案有没有编引用准确性就是标注的来源对不对。这两个都可以人工抽检量大的话可以借助另一个模型来打分。5.2 后续可以怎么扩展这套基础链路跑通之后有几个方向可以继续做深。一是多路召回。除了向量检索再加一路关键词检索两路结果融合。向量检索擅长语义相似关键词检索擅长精确匹配两者互补能覆盖更多场景。二是对话式追问。现在的实现是单轮问答用户追问那第二点呢的时候系统不知道第二点指什么。加上对话历史管理把上下文带进去就能支持多轮追问。三是引用高亮。前端拿到引用信息后不只是列出文件名还能定位到原文的具体段落并高亮用户体验会好很多。四是增量更新。现在文档更新是全量重新入库数据量大时很慢。可以做成增量只处理变化的文档配合文档指纹比如内容哈希来判断哪些需要更新。我在实际项目里跑下来这套方案对基于内部文档问答这个场景的改善是立竿见影的。以前用户问一个问题模型十有八九在编现在大部分回答都能给出准确出处用户自己点开引用一看就放心了。引用这个功能看着不起眼但它是建立信任的关键——用户不是要一个听起来对的答案而是要一个能验证的答案。把引用做扎实RAG 的价值才真正落地。

相关新闻

GN构建系统接入armcc工具链完整配置指南

GN构建系统接入armcc工具链完整配置指南

最近在把一个裸机嵌入式项目从Makefile迁移到GN构建系统,整个迁移过程比预想中顺利,唯独卡在armcc工具链的接入上折腾了不少时间。GN这套元构建系统本身设计得很精巧,但它的工具链抽象几乎默认你是拿GCC或者Clang干活,突然换成arm…

2026/10/13 18:47:22 阅读更多 →
Hi3519A平台AAC音频播放全链路解析与实战避坑指南

Hi3519A平台AAC音频播放全链路解析与实战避坑指南

1. 从“播放ACC”这个说法说起:一个容易被忽略的音频链路盲区第一次看到“Hi3519A播放ACC”这个标题,我脑子里冒出来的第一个念头是:这里说的“ACC”到底是哪个东西?是音频编码格式AAC被手误打成了ACC,还是指某种自动增…

2026/10/13 18:47:27 阅读更多 →
ESP8266_RTOS_SDK V3.3 IDF Style环境搭建与hello_world烧录实战

ESP8266_RTOS_SDK V3.3 IDF Style环境搭建与hello_world烧录实战

最近翻出一块吃灰的ESP-01S,想把它接到本地MQTT服务上做几个简单的控制,结果第一步就卡在了编译环境上。网上一搜,大量资料教的还是ESP8266_RTOS_SDK老版本那套gen_misc.sh交互式配置,跟原厂目前主推的V3.3 IDF Style完全不是一回…

2026/10/13 18:47:24 阅读更多 →

最新新闻

一张图微调YOLOv11:自然栖息地鸟类检测的流程与扩充

一张图微调YOLOv11:自然栖息地鸟类检测的流程与扩充

简介:面向 YOLOv11 模型验证需求,这份精简版鸟类目标检测数据集包含一张真实野外鸟类图像及配套标准标注,可用于快速原型验证、轻量级部署测试与小样本检测基线建立。压缩包共五个文件,包含三张 JPG 图像、一个数据配置文件与一个…

2026/10/14 3:39:38 阅读更多 →
ASP.NET + SQLServer葡萄酒商城源码:从数据库还原到下单事务全攻略

ASP.NET + SQLServer葡萄酒商城源码:从数据库还原到下单事务全攻略

简介:一套基于ASP.NET与SQL Server的葡萄酒网上商城销售系统,面向ASP.NET WebForm学习者、毕业设计学生以及需要搭建在线商城教学演示的开发者。系统围绕普通会员、商户会员和管理员三类角色展开,覆盖商品管理、购物车、订单处理、发货、商户…

2026/10/14 3:39:38 阅读更多 →
LangChain项目代码拆解:Chain、Agent、Memory与Retrieval四大核心组件实战指南

LangChain项目代码拆解:Chain、Agent、Memory与Retrieval四大核心组件实战指南

简介:这是一份LangChain入门与实践的项目代码包,面向正在学习大模型应用开发的Python开发者,旨在帮助读者快速理解LangChain的模块化设计与核心用法。包内共3个文件,包含一个HTML格式的入门讲解页、一个项目运行配置文件以及一个版…

2026/10/14 3:39:38 阅读更多 →
C++学习日记 Day21:C++11(下)lambda表达式和function包装器

C++学习日记 Day21:C++11(下)lambda表达式和function包装器

## 今天学了什么今天学习C11的lambda表达式和function包装器。## 可调用对象概念:在C中有一类对象可像函数一样调用;分类:①函数指针using prt void(*)(int, string); prt f1 fun; f1(1, "函数指针");②仿函数 --有 operator&am…

2026/10/14 3:39:38 阅读更多 →
一人公司AI工作流:6步实现内容生产去体力化

一人公司AI工作流:6步实现内容生产去体力化

1. 一人公司的内容困局与破局思路一个人做内容,最怕的不是没灵感,而是灵感来了却卡在“执行”上。写一篇长文要三小时,做一张配图要半小时,剪一条短视频要一整个下午,排版发布又要来回切换四五个平台。一天下来&#x…

2026/10/14 3:39:38 阅读更多 →
8765平台前后摄DVDD电压不一致排查与修复指南

8765平台前后摄DVDD电压不一致排查与修复指南

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

2026/10/14 3:38:38 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 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/13 22:01:27 阅读更多 →
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/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练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 阅读更多 →