上一篇把Spring AI的对话链路摸得差不多了,但做AI应用的人迟早会遇到一个问题:模型对你提问的内容一无所知,或者一本正经地胡编。市面上常见解法就是把内部文档切成小块,向量化后存起来,用户提问时先检索再交给模型回答,这套流程就是RAG(Retrieval-Augmented Generation,检索增强生成)。本篇是Spring AI实战系列的第九篇,用完整的代码和配置,把私有知识库这条链路从头到尾走一遍,涉及文档加载、切分策略、向量化、检索和增强生成,目标是让你照着搭,能跑通一个可以回答内部文档问题的Demo。
这个系列适合有Spring Boot基础、正在折腾AI应用落地的开发者。如果你已经接触过向量数据库但不知道Spring AI里怎么组织代码,或者用过LangChain想看看Java生态怎么做,这篇文章应该对胃口。我在模拟项目X中已经把这套流程产线化跑了几个月,下面写的内容都来自实际踩坑之后的方案,不是纸上谈兵。
1. 项目概述与整体思路拆解
1.1 私有知识库问答到底在解决什么问题
先说一个高频场景。你手里有一堆产品手册、内部规范、历史工单,或者某专家的几十篇技术文档,直接把这些原文丢给大模型,让它“记住”,这不现实。模型训练完就固定在那了,不可能因为你的几篇文档就改变它的知识结构。你要做的是在提问的时候,把相关文档片段找出来,附在Prompt里一起送给模型,让模型基于这些片段回答。这就是RAG的核心思路:不求模型“记住”,只求在回答那一刻“查得到”。
微调(Fine-tuning)是很多人会想到的另一条路,但它和RAG解决的是不同层面的问题。微调适合改变模型的风格、输出格式、特定领域的表达习惯,但它并不能让模型准确记住大量事实性细节,尤其是那些频繁变化的内容。一个典型的反例是:你把产品价格表微调进模型,价格一变,你就要重新微调。而RAG因为检索的是最新索引的文档,内容更新只需要重新索引,不需要动模型本身。这也是为什么私有知识库这类需求,业界默认选RAG而不是微调。
长上下文(Long Context)也是个容易被拿来对比的方案。现在的模型动辄支持几十万token,有人会想:我把所有文档直接塞进上下文不就行了?问题在于三个现实因素:一是单次请求的上下文窗口再大也有限,几百份文档塞不下;二是塞进去之后模型对长文本中信息的注意力会明显衰减,容易忽略关键段落;三是每次请求都携带全部文档,API费用和响应时延都难以接受。RAG的检索环节就是在控制成本的前提下,把真正有用的几段内容挑出来。
1.2 从一次提问到答案:RAG完整链路拆解
一次RAG问答,拆开看是两个阶段、五步流程。离线阶段做索引,在线阶段做问答。
离线索引阶段:
- 文档加载:把各格式的文件(TXT、Markdown、PDF等)读成纯文本。
- 文本切分:把长文档切成多个有语义边界的小块,每块就是一个“知识单元”。
- 向量化:调用Embedding模型,把每个小块变成一组浮点数向量。
- 存储:把向量连同原文、元数据一起写入向量存储。
在线问答阶段:
- 查询向量化:把用户的问题用同一个Embedding模型转成向量。
- 相似度检索:在向量存储中找出与问题向量距离最近的Top-K个文档块。
- 增强生成:把检索到的原文片段拼接进Prompt,连同问题一起交给大模型生成回答。
这里面有一个容易忽略的前提:查询向量化必须和索引阶段的向量化使用同一个Embedding模型。如果索引时用的模型A,查询时换成模型B,两边向量空间不一致,相似度检索的结果就会非常奇怪。这个坑我见过不止一次,后面章节会专门展开。
Spring AI对这个流程做了非常好的抽象。你不需要自己维护向量化和向量存储的细节,只需要声明一个EmbeddingModel、一个VectorStore和一个切分器,剩下的调用关系框架已经帮你接好了。当然,理解底层逻辑依然重要,否则出了问题你连排查方向都没有。
1.3 为什么选择Spring AI做RAG
对比直接调用各家大模型SDK,Spring AI的价值在于三个层次。
第一是接口统一。你换Embedding服务商或者向量数据库,业务代码基本不用动。索引阶段调用vectorStore.add(),检索阶段调用vectorStore.similaritySearch(),底层是内存、关系库还是专用向量库,对上层只暴露一个统一接口。这个抽象能力,在Java生态里目前算是独一份。
第二是生态集成。RAG不是孤立功能,它要和事务、缓存、监控、配置管理一起融入Spring应用。Spring AI天然支持这些,配置项走application.yml,Bean生命周期归Spring管理,链路监控可以接入现有的日志和指标体系。用原生SDK做一个独立工具可以,但要揉进一个Spring Boot应用体系,工作量大很多。
第三是Advisor机制。Spring AI提供了一个类似拦截器的机制,可以在调用大模型的前后附加逻辑。比如QuestionAnswerAdvisor,它会自动完成“检索文档块 → 拼进Prompt → 调大模型”的整条链路,你在业务代码里只需要一行调用。这种体验,对比自己手动拼接上下文、处理Token数量的做法,省掉的代码量非常多。
2. 环境准备与核心组件选型
2.1 基础设施与依赖配置
先看跑通这个Demo需要哪些东西。我使用的环境是Java 17加Spring Boot 3.x,这是Spring AI目前比较成熟的组合。JDK 8和Spring Boot 2.x不是不能用,但从兼容性和文档丰富度上考虑,新项目建议直接用17起步。
依赖方面,核心引入spring-ai-starter-model和spring-ai-starter-vector-store两个模块。Spring AI的版本迭代非常快,不同版本的包名和类名略有差异,我在开发模拟项目X时使用的版本是0.8.1,下面的代码都基于这个版本,如果你用更新的版本,个别类名按官方迁移文档调整即可,整体思路不变。
以Maven为例,核心依赖配置如下:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-vector-store</artifactId> </dependency>如果使用外部向量存储,还需要额外引入对应的存储驱动。如果只是本地验证流程,Spring AI内置的SimpleVectorStore就够了,不需要额外装任何服务,后面我会解释为什么建议先用它。
2.2 文档加载器的选择与处理范围
文档加载是整条链路中最不起眼但最容易出错的一环。你面对的原始文件格式五花八门:纯文本文件、Markdown、PDF、Word、扫描件,每种格式的解析难度天差地别。
TXT和Markdown最简单,几乎不需要特殊处理,读进来就是纯文本。PDF要分情况:电子版PDF可以直接提取文字,扫描版PDF是图片,必须先做OCR(光学字符识别),否则提取出来全是乱码。Word文档则需要处理分页符、页眉页脚、表格等结构,直接用文本读取通常会带进一堆噪音。
Spring AI本身提供了若干内建的DocumentReader,不同版本支持的格式不太一样,一般包括文本、Markdown、PDF等。建议的做法是:在项目早期阶段,统一把原始文档转成Markdown或TXT格式,一方面避免后续因为解析库不兼容导致的各种编码问题,另一方面也方便人工检查切分的效果。我在模拟项目X里就是先做了一层格式归一化:PDF和Word统一转成Markdown后再进入索引流程。
@Configuration public class DocumentReaderConfig { @Bean public DocumentReader documentReader() { // 读取resources/docs目录下的所有文档 // 具体Reader类名随版本略有差异,核心逻辑是返回List<Document> return new SimpleFileDocumentReader("/docs"); } }这里需要注意一个细节:Document在Spring AI中是一个包含text和metadata的对象。metadata会记录来源文件名、页码等信息,这些字段在后面做检索过滤时非常有用。比如你可以只搜索某个分类目录下的文档块,靠的就是metadata里的标记。
2.3 文本切分策略:chunk大小与重叠率
文本切分是RAG效果好坏的关键变量,但很多人会忽略它。原始文档直接向量化也不是不行,代价是检索精度下降:一份几十页的文档,向量化后只得到一个向量,你问任何细节问题,检索到的都是“整份文档”,导致送入模型的上下文过多,关键信息被淹没。
切分的核心参数有两个:chunk大小(每个切块的token数或字符数)和重叠率(相邻切块之间重叠多少内容)。为什么需要重叠?因为一个完整的语义单元可能恰好跨在两个切块的边界上,如果不重叠,这个语义单元会被拦腰截断,检索时就可能漏掉。所以切块时让相邻块之间保留一小段重复内容,是缓解边界问题最直接的手段。
我常用的经验值是:中文场景下chunk大小设定为500到800个字符,重叠率10%到15%。这个量级的切块既能保证每个块有足够上下文,又不至于在检索时引入太多无关内容。如果文档包含大量代码、表格,字符数可以适当调低,因为代码和表格的语义密度高,块大容易混入不相干内容。
切分器在Spring AI里对应TextSplitter接口,使用方式如下:
TextSplitter splitter = new TokenTextSplitter(); List<Document> chunks = splitter.apply(originDocument);如果你对默认切分不满意,可以自定义实现,核心就是实现split方法,返回切分后的Document列表。我自己的经验是:先跑一轮默认切分看效果,再用真实问题检查命中情况,根据失败案例再微调参数,尽量不要一上来就追求完美的切分算法。
2.4 Embedding与向量存储的选型思考
Embedding模型的选型会直接影响检索质量。不同模型对中文的编码能力差异很大,有些模型对中文成语、专有名词的理解明显弱,会造成语义相近但不相关的文本被判定为相似。生产环境选型时,建议用你自己领域的数据做评测,不要只看公开榜单分数。
Spring AI通过EmbeddingModel接口统一了对接方式,你只需要在配置里指定使用哪家服务。以我常用的做法为例,我在配置文件里只放置接口名,具体实现通过依赖注入来切换。这样你在本地调试时可以用内存级别的Mock实现,接生产时再切换到真实服务。
向量存储的选择需要平衡开发效率和长期成本。Spring AI自带SimpleVectorStore,它把所有向量放在内存里,不需要网络、不需要额外服务,非常适合Demo阶段验证流程。但内存存储不能持久化,服务重启后需要重新索引,所以生产环境至少要换成文件型存储或专用向量数据库。
如果你只是个人项目或内部工具,数据量在几万条文档块以内,关系型数据库加向量插件足够。如果数据量达到百万级,就要考虑专用向量数据库了。这里提供一个简单的对比表:
| 存储方案 | 持久化 | 部署成本 | 适合量级 | 适用场景 |
|---|---|---|---|---|
| SimpleVectorStore | 仅内存 | 极低 | 千级以下 | 本地Demo、原型验证 |
| 文件型向量存储 | 本地文件 | 低 | 万级以下 | 单机小项目 |
| 关系库向量插件 | 数据库 | 中 | 百万级以下 | 已有数据库运维体系的团队 |
| 专用向量数据库 | 分布式 | 高 | 百万级以上 | 大规模生产系统、高并发检索 |
选型建议一句话:先用SimpleVectorStore把流程跑通,再按数据量和部署条件逐步升级。
3. 核心细节解析与实操要点
3.1 配置项与参数详解
SprAI的配置文件非常直观,关键是把向量化服务和模型服务的连接信息配好。下面是模拟项目X中的一份典型配置,我删掉了敏感信息,但结构是完整的:
spring: ai: embedding: api-key: ${EMBEDDING_API_KEY} base-url: ${EMBEDDING_BASE_URL} model: api-key: ${MODEL_API_KEY} base-url: ${MODEL_BASE_URL}api-key和base-url分开配置,是为了让Embedding服务和对话模型服务互相独立。实际使用中,你的两个服务可能来自不同供应商,也可能一个是本地部署一个是云端服务,拆开配置会灵活很多。
Embedding模型名称、向量维度这类参数,不同版本Spring AI配置项名称有差异,0.8.x版本一般通过模型实现的默认值来设定,如果你需要指定具体模型,可以在构造函数中手动指定模型名。我在实际项目中倾向于显式指定:
@Bean public EmbeddingModel embeddingModel() { // 明确指定模型标识和向量维度 return new OpenAiEmbeddingModel(embeddingConfig); }等等,我这里要提醒一句:上面这行代码不要照抄。OpenAiEmbeddingModel这个类名在不同的Spring AI版本中定义位置不同,而且如果你用的不是那家服务,类名又不一样。重点是理解它的作用:你只需要告诉Spring AI“我要用哪个Embedding实现”,其他的向量化逻辑框架全部接管。
3.2 离线索引构建实操
索引构建就是把文档读进来、切块、向量化、存入VectorStore的过程。我在模拟项目X中的做法是写一个ApplicationRunner实现类,在应用启动后自动执行索引。
@Component public class KnowledgeBaseIndexer implements ApplicationRunner { private static final Logger log = LoggerFactory.getLogger(KnowledgeBaseIndexer.class); private final DocumentReader documentReader; private final TextSplitter textSplitter; private final EmbeddingModel embeddingModel; private final VectorStore vectorStore; public KnowledgeBaseIndexer(DocumentReader documentReader, TextSplitter textSplitter, EmbeddingModel embeddingModel, VectorStore vectorStore) { this.documentReader = documentReader; this.textSplitter = textSplitter; this.embeddingModel = embeddingModel; this.vectorStore = vectorStore; } @Override public void run(ApplicationArguments args) { log.info("开始索引私有知识库..."); // 1. 加载原始文档 List<Document> originDocs = documentReader.get(); // 2. 切分文本 List<Document> chunks = textSplitter.apply(originDocs); log.info("文档切分完成,共 {} 个chunk", chunks.size()); // 3. 向量化并存储 vectorStore.add(chunks); log.info("知识库索引完成"); } }这段代码的主线很清晰,但有几个细节值得展开。第一步加载文档在真实环境中不一定是启动时一次性完成,可能是定时任务、消息队列触发增量导入。我把这部分写成启动加载,只是为了Demo的简洁性,生产实现建议把索引逻辑抽象成一个独立的Service,这样无论是启动加载、定时刷新还是手动触发,都调用同一个方法。
第二步切分这里,需要警惕一个常见问题:vectorStore.add()内部会主动调用EmbeddingModel对每个chunk做向量化,所以你在add之前不需要手动调用embeddingModel。但这也意味着,如果文档量很大,这段代码会同步阻塞很久。我实测过,1000个chunk调用云端Embedding服务,耗时大约几分钟。优化方向有两个:一是用批量接口减少网络往返,二是改造为异步执行,不要让索引阻塞应用主流程。
第三步存储之前,建议给chunk补充metadata。比如来源文档名、部门分类、更新时间。这样后续检索时可以按metadata过滤。Spring AI的Document允许你在构造时传入Map格式的metadata,这一步非常便宜但收益很高。
3.3 在线问答链路实现
索引建好了,接下来是用户提问后的处理链路。这里有两种典型的Spring AI实现方式,一种是使用Advisor自动装配RAG,另一种是手动检索再拼Prompt。先说自动方式,代码最少:
@Bean public ChatClient chatClient(ChatModel chatModel, VectorStore vectorStore) { QuestionAnswerAdvisor advisor = new QuestionAnswerAdvisor( vectorStore, SearchRequest.builder() .topK(4) .similarityThreshold(0.55) .build() ); return ChatClient.builder(chatModel) .defaultAdvisors(advisor) .build(); }调用方只需要注入ChatClient,然后发起prompt,剩下的检索和拼接都由Advisor完成。这种方式适合流程固定、不常变更的场景。它内部做的事情是:接收用户问题,用EmbeddingModel编码成向量,调用vectorStore查询最相似的Top-K个chunk,然后把这些chunk作为上下文和问题一起发给自己配置的那个ChatModel。
手动方式则更透明,适合需要自定义Prompt模板的场景:
@Service public class RagQueryService { private final VectorStore vectorStore; private final ChatModel chatModel; public String query(String question) { // 1. 检索 List<Document> docs = vectorStore.similaritySearch( SearchRequest.builder() .query(question) .topK(6) .similarityThreshold(0.5) .build() ); // 2. 拼接上下文 String context = docs.stream() .map(Document::getText) .collect(Collectors.joining("\n\n")); // 3. 构造Prompt String prompt = """ 请基于以下提供的资料回答问题。如果资料中没有相关内容,请直接说明无法回答,不要编造。 资料: %s 问题: %s """.formatted(context, question); return chatModel.call(prompt); } }手动方式虽然代码多一些,但好处是Prompt完全可控。比如你可以要求模型“只使用资料内容回答,不要自行发挥”,这是生产级RAG的必要约束,因为模型在资料不足时会倾向编造答案。还可以要求它“如果多个资料块互相矛盾,请指出矛盾点”,这在企业内部文档场景特别有用。
两种方式怎么选?我的建议是:先用手动方式跑通一次完整链路,理解每个环节的输入输出,再切换回Advisor方式提升效率。千万不要一上来就依赖Advisor的封装,那样出了问题你连日志都不知道该看哪里。
3.4 相似度检索与Top-K参数调优
相似度检索是RAG里离用户最近的一环,它的参数直接影响最终回答质量。SearchRequest里最重要的两个参数是topK和similarityThreshold。
topK控制返回多少个候选文档块。取值太小,容易漏掉正确答案;取值太大,无关内容会混进来干扰模型。经验上,对于500到800字符的chunk,4到6个是比较舒服的数字。如果文档块更小,可以适当调大到8到10。
similarityThreshold控制候选文档块与问题的相似度下限。0到1之间,值越大,过滤越严格。设定到0.7以上,可能经常检索不到内容,回答变成“资料中没有相关信息”;设定到0.3以下,无关文档块大量流入,模型容易被带偏。我的实际经验是从0.5起步,根据检索结果逐步调整,找一个“既不漏也不闹”的甜点。
还要注意一个隐藏问题:不同Embedding模型的相似度分数分布完全不同,没有通用的阈值。不要在A模型上调试好的0.6直接套到B模型上,需要重新评测。这个问题后面我还会在常见问题里再讲一遍,因为真的很重要。
4. 常见问题与排查技巧实录
4.1 检索结果为空或质量差怎么排查
检索结果为空,最常见的原因有三个。第一,查询时使用的Embedding模型和索引时不一致,两边向量空间完全对不上。这个坑排查起来最容易:看日志里Embedding模型的名字是否一致,别只比对配置项。第二,相似度阈值设得过高,导致所有候选块都被过滤掉。把阈值降到0.3以下试跑一次,如果结果回来了,说明是阈值问题。第三,切分后的chunk过小,语义不完整,导致检索时匹配不到。
检索质量差则要复杂一些,通常是切分策略不合理。比如把一篇技术文档无脑按固定字符数切分,可能把一个完整的功能介绍切成了两半,每一半都缺乏足够上下文来命中用户的提问。排查方法很简单:拿到检索出的chunk,人工读一遍,看它们是否真的与问题相关。如果相关的chunk排名不在前3,说明embedding模型的语义理解能力或者文本切分的边界有问题。
我建议在开发阶段,把检索到的chunk文本和相似度分数打印到日志里,人工核对。这一步虽然原始,但比任何自动化评估工具都更直观。我在模拟项目X里就是靠这一步发现了文档中大量专业缩写词导致的检索偏差,之后通过自定义分词逻辑解决了这个问题。
4.2 Embedding服务调用超时和限流
RAG链路中,Embedding服务是一个容易被低估的瓶颈。索引阶段要批量向量化,查询阶段每次请求也要向量化一次用户问题。如果Embedding服务部署在云端,网络抖动、并发限流都会直接拖垮链路。
我在实际项目里遇到的最典型情况是:索引流程里循环调用第三方Embedding接口,一次运行要处理几千个chunk,批量提交时接口返回429限流,导致一部分chunk入库失败,而且因为程序没有重试机制,索引流程表面上是“完成”了,实际上知识库缺了一截。这种问题特别隐蔽:你问能检索到答案的问题它能回答,一旦问到缺失的那部分内容,系统就永远回答不上来。
解决思路主要有三点。第一,把索引写入从主流程里剥离开,用独立任务处理,任务失败可以重跑。第二,调用批量Embedding接口而不是一个chunk一个请求,减少网络往返次数和限流触发概率。第三,增加重试机制,对部分失败的任务做指数退避重试。Spring AI自己也做了重试处理,但那是框架层面兜底,你自己的任务调度和补偿机制更可靠。
4.3 上下文长度与成本控制
RAG会把检索到的多个chunk一起放进Prompt,如果topK设得太大,文本块又很长,Prompt的token数量会膨胀得很快。这不仅增加API调用成本,还会稀释模型对关键信息的注意力。
控制方案这里列几个常用的:一是缩小chunk大小,用更少的token承载更精准的信息;二是把topK控制在必要范围内,5个块能回答清楚就不要用10个;三是做“检索后重排”,先用向量检索召回较宽范围的候选块,再用一个轻量级模型对候选块做精准排序,只保留前几块送进Prompt。重排会增加一次模型调用,但能明显提升答案质量,适合数据量大、对准确率要求高的场景。
还有一点容易被忽略:如果你用的模型服务商按token计费,用户每提一个问题,Embedding一次、大模型生成一次,再加上检索的存储和计算成本,日活高一点费用就很可观。企业内部知识库场景一般流量可控,但如果做成对外开放的问答产品,就必须引入缓存层,把相同或相似问题的答案缓存起来,否则账单会让老板找你谈话。
4.4 文档更新与数据一致性问题
私有知识库最核心的运维问题只有一个:文档改了,系统回答的还是老版本内容。因为向量存储里的旧chunk还躺在那里,不会被自动替换。
解决思路是给文档块设计合理的版本管理。比较实用的做法是把文档的内容hash作为metadata的一个字段。每次更新索引时,如果发现某来源文档的hash变了,就把该来源对应的旧chunk全部删除,再写入新的。这个逻辑在Spring AI里要做两次调用:一次vectorStore.delete(),一次vectorStore.add()。
更细化的做法是按metadata里的source字段来管理。每个chunk在入库时记录了来源文件名,更新时按source删除旧数据再插入新数据。如果知识库有多个来源目录,还可以支持按目录刷新。这套逻辑不复杂,但必须在设计初期就做进数据模型里,否则等库里的数据多了之后再做增量更新,数据一致性会非常头疼。
4.5 检索命中偏差与切分边界问题
最后分享一个比较独特的——切分边界引起的命中偏差。假设你有一段文本介绍“A模块的权限配置方法和B模块的权限配置方法”,如果切分恰好把“A模块的权限配置方法”切到了一个chunk,把“B模块”切到了下一个chunk,用户问“B模块的权限怎么配置”,检索出来的chunk可能因为缺少A模块的上下文语境,导致匹配分偏低。
解决这类问题有两个思路。一是使用基于语义边界的切分而不是固定长度字符串切分,比如按段落、按标题层级来切分,一个章节尽量保持在一个chunk里。二是提高重叠率,让相邻chunk之间有更多的冗余信息,降低信息被截断的概率。对于特定领域文档,可以在切分前先做结构化解析,把文档拆成标题和信息单元的树状结构,再做切分。这个思路实现成本高,但对检索质量的提升是立竿见影的。
我个人的建议是,先跑一个月的新增数据,定期抽查检索日志,找出那些“用户明明问到了文档里的内容,却回答不上来”的case,逐个回溯切分结果,一般都能归因到切分边界或重复度缺失。这种沉淀下来的经验,比任何算法优化都值钱。
5. 实测效果与调优心得
5.1 模拟项目X的落地实测
把整套流程在模拟项目X中跑起来之后,我对RAG的实际效果有了更直观的感知。第一次全量索引了约200页的企业内部技术规范,切分后得到1800多个chunk,全部走云端Embedding接口,耗时约15分钟,中间出现过一次限流,加了重试之后顺利跑完。
线上问答阶段,针对文档中常见的技术问题和规范条款提问,大部分能直接命中正确段落并给出准确回答。抽查了50个典型问题,完全正确的有38个,基本正确但细节有偏差的9个,完全错误的3个。错误案例集中在多个知识点交叉的问题上,单块chunk无法覆盖完整逻辑链。后期通过调整chunk重叠率,从10%上调到15%,再结合检索后重排,错误案例从3个降到了1个。
这里要明确一点:RAG的效果不是一次性调到位的,而是一个不断发现bad case、不断微调的过程。不要指望配置一次就完美,把评测集和回归测试建立起来,才是长期可靠的办法。
5.2 从三步走思路到团队落地路径
如果你要在一个团队里落地私有知识库,我建议按三步走:第一步,用Spring AI标配组件在本地把链路跑通,数据量小、结构简单,验证流程正确性;第二步,引入真实数据和业务场景,切分策略、Embedding选型、检索参数全部用真实问题调优;第三步,再上生产,这时才考虑外部向量存储、重排、增量更新、监控告警这些工程化组件。
很多团队失败的原因不是技术选型不对,而是想一步到位,上来就要上分布式向量库、上重排模型、上百G数据。实际上大部分企业内部知识库的数据量远没有那么大,先跑起来、用起来、迭代起来,比一步到位重要得多。Spring AI的价值恰恰在于,让第一步的门槛降得非常低,你不需要先搭一套复杂的基础设施就能看到效果。
5.3 后续还可以扩展的方向
这套RAG链路打通之后,可扩展的方向很多,我这里列几个我觉得比较有实用价值的:一是多租户隔离,不同部门的知识库隔离,靠metadata过滤实现;二是增量更新,上面提到过,通过source字段管理;三是对接更多数据源,比如企业内部Wiki、工单系统、数据库表记录;四是引入重排序模型,提升检索准确率。
还有一个方向我觉得特别值得一试:在RAG链路上增加一个“检索结果自查”环节,让模型在回答之前先判断检索到的chunk是否真的能回答问题,如果不够,再触发一次补充检索。这个思路有点类似手动实现Agent的ReAct循环,Spring AI里可以做,但要自己控制循环次数,防止死循环。
我在实际使用Spring AI做私有知识库的过程中,最大的感受是:框架帮你解决了90%的胶水代码,但检索效果好不好,最终还是取决于你对自己文档结构的理解,以及针对bad case持续调优的耐心。别指望一个配置、一个模型就彻底解决所有问题,它本质上是个需要持续运营的系统工程。