目录
[一、场景:为什么需要 RAG](#一、场景:为什么需要 RAG)
[二、RAG 四步工作流](#二、RAG 四步工作流)
[三、本地版:ETL 链路代码](#三、本地版:ETL 链路代码)
[3.1 准备知识文档](#3.1 准备知识文档)
[3.2 文档加载器(Extract)](#3.2 文档加载器(Extract))
[3.3 向量库装配(Transform + Load + Query)](#3.3 向量库装配(Transform + Load + Query))
[3.4 第一次初始化 vs 后续加载(本篇重点)](#3.4 第一次初始化 vs 后续加载(本篇重点))
[3.5 检索增强接入(doChatWithRag)](#3.5 检索增强接入(doChatWithRag))
[4.1 云控制台准备](#4.1 云控制台准备)
[4.2 接入代码](#4.2 接入代码)
摘要:模型答错业务政策 = 事故。用真实客服场景从 0 实现 RAG:文档加载 → 切分 → Embedding → 入库,再到检索增强问答,本地 SimpleVectorStore 与云端知识库两套方案对照,附"首次初始化 vs 后续加载"的启动机制拆解与检索日志排查技巧。 标签:Spring AI、RAG、向量数据库、知识库、大模型 分类:AI 应用开发 / Spring AI
一、场景:为什么需要 RAG
上篇文章给客服接上了记忆,它能记住对话。但业务问题照样答不了:
问题在哪:模型没有你公司的售后政策数据,只能靠"常识"编。这就是大模型的三大原罪:
|-------------|-----------------|
| 原罪 | 表现 |
| 不知道私有数据 | 你的公司政策它根本没学过 |
| 知识有截止时间 | 2024 年后改的政策它不知道 |
| 幻觉 | 不知道也硬答,还答得理直气壮 |
RAG(Retrieval-Augmented Generation,检索增强生成) 就是解法:模型回答前,先去知识库检索相关资料,把资料拼进提示词,再生成答案。相当于"开卷考试带资料"------不背进脑子,而是现查。
二、RAG 四步工作流
① 文档收集切割 :把售后政策、FAQ 等写成 Markdown/PDF,按语义切成小块(500~1000 字/块) ② 向量转换存储 :用 Embedding 模型把每块文本变成高维向量,连原文一起存进向量数据库 ③ 检索 :用户提问也转成向量,在库中算相似度,召回最相关的 topK 块 ④ 增强:把召回片段拼进 prompt:"参考以下资料回答...",模型只依据资料生成
三、本地版:ETL 链路代码
3.1 准备知识文档
我建了一个 document/ 目录放客服知识,纯 Markdown、标题清晰,方便切分后语义完整:
写作心法:一条政策写成一句完整的话(不依赖上下文代词),这样无论怎么切都不会"腰斩"关键信息。
3.2 文档加载器(Extract)
withAdditionalMetadata("filename", filename)很关键------每个 chunk 都会带上"来自哪个文件"的元数据,后面过滤检索和排查问题都靠它。
3.3 向量库装配(Transform + Load + Query)
3.4 第一次初始化 vs 后续加载(本篇重点)
上面这段代码藏着 RAG 落地的一个关键机制------向量化是有"一次性成本"的,不该每次启动都付:
为什么要区分这两步?
|--------|---------------------------|----------------|
| 维度 | 首次初始化 | 后续加载 |
| 做了什么 | 读文档 + Embedding + 入库 + 落盘 | 从本地文件反序列化读回 |
| 耗时 | 30~40 秒(网络请求逐片向量化) | 2~5 秒(纯本地 IO) |
| API 调用 | 每个 chunk 一次 embedding(花钱) | 0 次 |
| 什么时候跑 | 知识库首次建好 / 文档更新后删缓存 | 每次应用启动 |
底层原理 :SimpleVectorStore 序列化时把向量也一起写进了 JSON (不只是文本)。所以 load() 时向量是现成的,根本不需要重新调 Embedding------向量库文件 vector-store.json 里存的就是"文本 + 高维向量 + 元数据"的完整数据。
日常使用的心智模型:。
3.5 检索增强接入(doChatWithRag)
改动就一处 :多挂一个 QuestionAnswerAdvisor,检索 + 拼 prompt + 基于资料回答全自动完成。
验证:
------不再瞎编,答案有据可依了。
四、云端版:托管知识库接入
本地版有个痛点:SimpleVectorStore 存内存/本地文件 ,不适合多实例部署。生产上更省事的做法是用云厂商的托管知识库(阿里云百炼 / AWS Bedrock 等),文档上传由控制台管理,你只负责接 API。
4.1 云控制台准备
- 登录百炼控制台,创建知识库
- 把
document/的文档上传到知识库(云端自动完成切分 + 向量化)
- 记下知识库名称(索引名,代码里要用,必须一字不差)
4.2 接入代码
然后 .advisors(cloudRagAdvisor) 挂上即可,调用方代码和本地版完全一样 ------QuestionAnswerAdvisor 屏蔽了底层差异,这是接口抽象的威力。
💡 本地 vs 云端在"初始化"上的差别 :本地版需要你自己管理"首次向量化 + 缓存复用";云端版把这个成本转移到了控制台------文档上传时云端就完成切分向量化,应用每次启动只是连接远程检索,天然没有"重复向量化"问题。
五、测试:检索命中日志怎么看
RAG 最怕"以为在检索,其实没命中"。建议在自定义 Advisor 里打日志看每次检索命中什么:
日志里该看到的样子:
看到 filename 来源、命中条数、内容预览 → 检索链路就是通的。
六、易错点速查表
|-------------------------------|---------------------------------------------------|---------------------------------------------|
| 坑 | 原因 | 排查/修复 |
| 召回为 0 | 入库和检索用的 Embedding 模型不一致(两个不同的 embedding 向量空间) | 全程用同一个 EmbeddingModel Bean |
| 检索到但答案仍瞎编 | topK 太小没召回对 / system prompt 没约束"必须基于资料" | 调大 topK(5~10),prompt 写死"资料没有就说不知道" |
| 云端报 Index not found / 404 | 知识库名称与配置不一致 | 代码里的索引名 = 控制台知识库名,一字不差 |
| 改了文档但检索结果没变 | 缓存文件没删,走的是 load 分支 | 文档更新后删除 tmp/vector-store.json 重新初始化 |
| 重启后检索全空 | 本地 SimpleVectorStore 没落盘 | store.save(File) 落盘,启动时 load(File) |
| 切分后语义断裂 | chunk 太小或太大 | 5001000 字/块,保留 50200 字重叠 |
| 命中一堆无关片段 | 没做召回后的相关性过滤 | 设 similarityThreshold(如 0.6),低分块不进 prompt |