这份指南不是从概念开始一层层剥洋葱,而是直接告诉你:我把Spring AI的RAG跑通后,哪些步骤和参数最坑、哪些环节决定成败。RAG(Retrieval-Augmented Generation,检索增强生成)这个组合词听起来很唬人,但只要拆开看,它的核心其实就是一句话:先查资料,再写答案。Spring AI则把这套流程标准化成了几个Java接口,让你不用自己管理一堆底层SDK。如果你已经在用Spring Boot,又想让大模型回答你私域知识库里的内容,这篇文章可以直接照着做。
这篇文章适合两类人:一是后端Java开发,想避开Python那套生态,直接在Spring项目里接入AI能力;二是刚接触RAG、手里有文档但没有落地路径的初学者。我会按“为什么需要RAG、Spring AI怎么帮你实现、完整可运行的代码、调参踩坑经验、生产环境注意点”的顺序来写,最后再说说这个方向还能往哪里延伸。
1. 先想清楚一件事:RAG到底解决了什么问题
1.1 大模型的三个硬伤
裸的大模型接口,直接放进业务里,通常会有三个让人头疼的问题。
第一个是知识截止。模型训练完成那一刻,它的知识就等于被定格了。产品上个月刚更新的新版本功能、公司内部刚发布的流程规范,裸模型一概不知。你问“最新版价格方案是什么”,它只能拿训练数据里的旧内容糊弄你。
第二个是幻觉。这是最坑人的。模型不知道答案的时候,并不会诚实地说“我不清楚”,而是会基于语言概率编造一段看起来很有逻辑、但完全错误的内容。这在闲聊场景问题不大,放在客服、合同审查、医疗咨询这些场景里,一次错误就可能引起严重的信任危机。
第三个是私域数据隔离。企业真正的核心知识,几乎都在内部文档里:产品规格书、技术方案、合同模板、FAQ。这些数据不能拿去训练公有大模型,也没必要。模型需要的不是“记住”你的数据,而是能在问答时“查”你的数据。
这三个硬伤叠加在一起,导致大模型在严肃业务场景里很难直接上岗。RAG就是冲着解决这三个问题去的。
1.2 和微调怎么选:不是二选一,而是分工不同
很多刚接触AI应用的人都会纠结:到底该做微调,还是做RAG?这里我说下自己的判断。
微调的本质是改变模型权重,让模型“记住”某些领域的知识或表达风格。RAG的本质是给模型提供可检索的外部知识,让它生成答案时参考这些材料。
打个比方:微调像是送员工去脱产进修,改变他的知识结构;RAG像是给员工配一台内网电脑,让他随时查公司档案。进修成本高、周期长、不能频繁做;查档案随时可以更新,成本低、见效快。
我最早也动过微调的念头,结果光是梳理训练集、设计评估集就让我放弃了。后来先用RAG,当天就把知识库问答做出来了。后来需求复杂了,才考虑用微调去调整模型的回答风格。所以我的建议很简单:知识类问答优先RAG,RAG跑不通再考虑微调,两者并不是必须二选一的对立关系。
2. Spring AI在RAG里扮演什么角色
2.1 统一抽象,把AI链路做成Spring风格
在Spring AI出现之前,Java开发者想搭一个RAG应用,要对接的东西非常零散:模型厂商的SDK、向量数据库的客户端、文档解析库、文本切分工具,甚至还要自己拼提示词模板。组件之间接口不一致,版本动不动就冲突。
Spring AI做的事情,就是把RAG链路里的每一个环节都抽象成Spring风格的接口,然后让Boot的自动装配把它们串起来。我体会最深的是:数据源是DataSource,向量库是VectorStore,模型调用是ChatClient,全都走IoC容器管理。这让熟悉Spring的后端开发几乎零成本切入AI应用开发。
2.2 RAG管线里的核心组件
一个标准RAG流程,从文档进来到答案出去,要经过六个环节:
- 读文档(DocumentReader):把PDF、Markdown、TXT等格式读成统一文档对象
- 切分(TextSplitter):把长文档切成大小合适的块,防止超长上下文
- 向量化(EmbeddingModel):把文本块转成高维向量
- 存向量(VectorStore):把向量和原文存进向量数据库
- 检索(DocumentRetriever):用户提问时,把问题向量化后在库里找最相近的块
- 生成(ChatClient):把检索到的上下文拼接进提示词,调用模型生成答案
Spring AI为每个环节都提供了现成实现,我整理了一张表:
| 组件 | 接口/类 | 职责 | 我实际用过的实现 |
|---|---|---|---|
| 文档读取 | DocumentReader | 解析各种格式文件 | PagePdfDocumentReader、MarkdownDocumentReader |
| 文档切分 | TextSplitter | 长文分段,带重叠区间 | TokenTextSplitter、ParagraphTextSplitter |
| 向量化 | EmbeddingModel | 文本转高维向量 | OpenAiEmbeddingModel、OllamaEmbeddingModel |
| 向量存储 | VectorStore | 存储+相似度检索 | PgVectorStore、ChromaVectorStore |
| 检索增强 | RetrievalAugmentationAdvisor | 检索、注入上下文、组装提示词 | 内置Advisor |
| 对话生成 | ChatClient | 统一的聊天补全入口 | DefaultChatClient |
2.3 一个容易被忽略的关键点:Document对象
Spring AI里所有知识都以Document对象为最小单元。这个对象有三个部分:content(正文)、metadata(元数据)、id(唯一标识)。
很多人前期不重视metadata,后面就会吃亏。因为RAG进入生产后,向量库里往往不是只有一类文档。比如同一个库里既有产品手册,又有技术方案,还有销售话术。查询时如果不过滤,检索出来的内容会非常混杂。metadata就是用来支持这种过滤的,比如给每类文档打上type=MANUAL、type=TECH的标签,查询时加过滤条件,才能精准命中用户需要的范围。
3. 从零到一:搭一个可运行的RAG服务
3.1 初始化工程与依赖
我这边用的是Spring Boot 3.3以上、Java 17、Spring AI 1.0.0版本。先建一个空项目,然后在pom.xml里加依赖:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-vector-store-pgvector</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> </dependencies>如果你不想用OpenAI,想出不出网环境用本地模型,可以把spring-ai-starter-model-openai替换成spring-ai-starter-model-ollama,原理一样。
3.2 配置文件:几个参数必须对齐
在application.yml里,最重要的是三组配置:模型、向量库、数据源。
spring: application: name: spring-ai-rag-demo ai: openai: api-key: ${OPENAI_API_KEY} chat: options: model: gpt-4o-mini temperature: 0.3 embedding: options: model: text-embedding-3-small vectorstore: pgvector: index-type: hnsw distance-type: cosine dimensions: 1536 datasource: url: jdbc:postgresql://localhost:5432/ragdb username: postgres password: postgres几处容易出错的地方我单独说下。dimensions必须和前面选择的Embedding模型输出维度一致,text-embedding-3-small输出1536维,text-embedding-3-large输出3072维。如果你换成BGE系列,维度也要跟着改成1024。可以设置temperature: 0.3甚至更低,RAG问答希望答案稳定、少发散,温度低了不容易乱编。
还需要在PostgreSQL里启动pgvector扩展:
CREATE EXTENSION IF NOT EXISTS vector;3.3 文档入库:从读取到向量化
RAG的第一步是“喂”知识库。我写了一个启动时自动加载的类,读取classpath下的PDF手册,切分之后写入向量库:
@Component public class DataInitializer implements ApplicationRunner { private final VectorStore vectorStore; public DataInitializer(VectorStore vectorStore) { this.vectorStore = vectorStore; } @Override public void run(ApplicationArguments args) { if (vectorStore.count() > 0) { return; } PagePdfDocumentReader reader = new PagePdfDocumentReader("classpath:/docs/manual.pdf"); List<Document> docs = reader.read(); TokenTextSplitter splitter = new TokenTextSplitter.Builder() .withChunkSize(500) .withOverlap(80) .build(); List<Document> chunks = splitter.apply(docs); vectorStore.add(chunks); } }这里有个细节:先通过vectorStore.count() > 0判断,避免每次重启应用都把知识库重新灌一遍。这个问题看起来很基础,实际上很多人漏了这一步,测试多次后向量库重复内容堆积,检索结果越来越乱。
如果你不想启动时自动导入,可以暴露一个导入接口:
@RestController @RequestMapping("/api/knowledge") public class KnowledgeController { private final VectorStore vectorStore; public KnowledgeController(VectorStore vectorStore) { this.vectorStore = vectorStore; } @PostMapping("/import") public String importDocs(@RequestParam String path) { PagePdfDocumentReader reader = new PagePdfDocumentReader("file:" + path); List<Document> docs = reader.read(); TokenTextSplitter splitter = new TokenTextSplitter.Builder() .withChunkSize(500) .withOverlap(80) .build(); List<Document> chunks = splitter.apply(docs); vectorStore.add(chunks); return "导入完成,共 " + chunks.size() + " 个分块"; } }3.4 问答链路:用ChatClient完成RAG
接下来是核心环节。用户问一个问题,系统要做两件事:从向量库里检索相关文档,再把文档内容拼进提示词让模型回答。
Spring AI里最直接的方式是用内置的Advisor。我在配置类里组装一个全局ChatClient:
@Configuration public class RagConfig { @Bean public ChatClient chatClient(ChatClient.Builder builder, VectorStore vectorStore) { DocumentRetriever retriever = VectorStoreDocumentRetriever.builder() .vectorStore(vectorStore) .topK(4) .similarityThreshold(0.5) .build(); RetrievalAugmentationAdvisor advisor = RetrievalAugmentationAdvisor.builder() .documentRetriever(retriever) .build(); return builder .defaultAdvisors(advisor) .defaultSystem("你是一个企业知识库助手,请基于参考资料回答问题。如果参考资料中没有相关内容,请明确告知用户。") .build(); } }然后就可以在Controller里调用了:
@RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient chatClient) { this.chatClient = chatClient; } @GetMapping("/ask") public String ask(@RequestParam String question) { return chatClient.prompt().user(question).call().content(); } }这段配置里的topK和similarityThreshold,是调整检索质量最核心的两个参数,我会在下一章详细讲怎么调。
3.5 进阶:流式输出与结构化返回
上面这种同步返回值的方式,简单是简单,但模型生成几百字可能要等十几秒,前端体验很差。更合理的方式是流式输出:
@GetMapping(value = "/ask/stream", produces = "text/event-stream") public Flux<String> askStream(@RequestParam String question) { return chatClient.prompt().user(question).stream().content(); }另外,企业场景里我们往往不希望只返回一段文本,还想顺带拿到引用来源、要点列表之类的结构化信息。Spring AI也支持直接把模型输出绑定为Java对象:
public record AnswerWithSources(String answer, List<String> sources) {} @GetMapping("/ask/structured") public AnswerWithSources askStructured(@RequestParam String question) { return chatClient.prompt() .user(question) .call() .entity(AnswerWithSources.class); }这个功能我实测下来非常实用。以前要自己用字符串截取去解析JSON,现在模型输出直接映射成对象,省了一大堆模板代码。
4. 调参实录:检索质量由这四个关键点决定
4.1 分块参数:块大块小都是事
文本切分是整个RAG链路上最不起眼、但影响最大的环节。块切得太小,语义不完整,一句话被拦腰截断;块切得太大,一个块里混了好几个主题,检索时噪音多、上下文也长。
我自己用下来,技术文档、合同这种偏正式的文件,chunkSize在500附近最稳;问答型知识库,200到300更合适。overlap(重叠区间)建议设置在50到100之间,目的是把切分边缘可能被切断的语义“救回来”。
| chunkSize | 常见效果 |
|---|---|
| 200 | 问答类知识库好使,但长文档容易语义碎片化 |
| 500 | 大部分场景的安全值,信息完整、检索稳定 |
| 1000以上 | 噪音多,多个主题混在一起,检索命中后上下文干扰严重 |
4.2 topK和similarityThreshold怎么配合
这两个参数是检索环节的“闸门”。topK决定每次最多召回多少个候选块;similarityThreshold决定相似度低于多少的块直接丢弃。
如果topK设太大,比如8或者10,召回来的块里总有两三个不相关的,这些噪音会被一起拼进提示词,模型很容易回答偏。如果similarityThreshold设太高,比如0.7以上,那检索出来的经常是零散碎片,很多有效文档被过滤掉,模型只能频频回答“资料中没有”。
我推荐的起步组合是topK=4、similarityThreshold=0.5,然后根据实际效果微调。注意一点:similarityThreshold不是越高越好,它是一个“保住下限”的参数,防止浑水摸鱼,而不是用来追求精准的。真正追求精准要靠metadata过滤和重排。
4.3 metadata过滤:知识库一定要分层
前面我提到过给文档打标签,这一步在真实项目里几乎必做。假设向量库里同时存了产品手册、项目日报、合同模板,用户问“上个月的合同数据”,如果不过滤,检索结果可能一半是日报。
入库时给每个Document的metadata设置分类:
doc.getMetadata().put("category", "contract"); doc.getMetadata().put("batchId", "20250415");查询时带上过滤条件:
VectorStoreDocumentRetriever retriever = VectorStoreDocumentRetriever.builder() .vectorStore(vectorStore) .filterExpression("category == 'contract'") .build();这样就能做到“业务线隔离”,每一个查询都只在其所属的知识范围内检索。注意不同向量数据库的过滤表达式语法有差异,从PGVector迁移到其他库时,这块一定要做适配。
4.4 中文场景下Embedding模型怎么选
Embedding模型直接决定“召回率”,这是RAG效果的地基。地基没打牢,后面调什么都白搭。
如果你的项目用的是OpenAI接口,text-embedding-3-small对中文的语义理解已经不错,且价格便宜。text-embedding-3-large精度更高,但成本明显增加,维度也从1536翻到了3072,存储和计算开销都会变大。
如果因为数据敏感需要私有化部署,可以考虑本地的BGE系列模型,比如bge-large-zh或bge-m3。这类模型对中文支持很好,维度通常1024,通过Ollama就能跑起来,Spring AI接入也很平滑。
我个人的选择策略是:先按数据量估算,如果只是几万条分块的内部知识库,text-embedding-3-small完全够用;如果召回率确实不够再升级大模型。不要一上来就上大模型,成本差距会明显体现在账单上。
5. 生产环境才真正看得见的几个细节
5.1 向量查询慢?先建索引
PGVector刚灌完数据时,查一次可能是全表扫描,数据过万之后慢得让人怀疑。建一个HNSW索引能明显改善:
CREATE INDEX ON vector_store USING hnsw (embedding vector_cosine_ops);也可以在配置里直接指定index-type: hnsw。数据量在十万级以内,IVFFlat也能用;百万级我建议HNSW,召回率更稳定。
5.2 知识库更新与增量导入
业务文档随时会变,不可能每次更新都删库重建。比较实用的做法是“批次管理”:给同一批导入的文档打上相同的batchId,更新时先删旧批次再灌新批次。
vectorStore.delete(Filter.expression("batchId == '20250415'")); vectorStore.add(newChunks);这样做的好处是原子性好,更新不会影响其他批次的数据。我经历过一次没有批次管理、直接全量重灌的项目,中间出了异常,向量库里新旧数据混杂,检索效果一塌糊涂,从那之后批次管理再也不敢省。
5.3 Embedding成本控制:避免重复向量化
很多团队第一天就把Embedding接口的配额打爆,原因只有一个:每次应用重启、每次测试都重新把全库向量化一遍。正规做法是记录文件指纹,只在文件变化时才重新向量化导入。
可以简单做一张表:文件路径、文件哈希、导入批次。每次启动时对比哈希,没变化的文件直接跳过。这个思路实现成本不高,但能省下大量时间和费用。
5.4 日志要有“考古价值”
RAG出了问题,大多数情况不是模型变了,而是检索结果不对。所以日志里一定要记录检索到了哪些文档、相似度分数是多少。
List<Document> retrieved = retriever.retrieve(question); retrieved.forEach(doc -> log.info("Retrieved doc: {}, score: {}", doc.getMetadata().get("source"), doc.getMetadata().get("distance")) );排查的时候先看检索结果是否精准,再看拼进提示词的上下文是否正确,最后才怀疑模型。这个顺序能帮你省掉大量无谓的排查时间。
6. 这块还能往哪里再走一步
到这里,你已经能搭出一个可以跑通的Spring AI RAG服务。但我实际做完一个项目后发现,真正的挑战在后面这几件事上。
检索不到想要的内容时,可以考虑做查询改写,把问题拆成子查询,或多轮对话下先从历史里提取上下文。检索结果太杂时,可以引入重排,对召回的候选块做个“精排”,把最相关的排到最前面。文档类型多了之后,可以做多路混合检索,把全文检索和向量检索的结果融合起来再用模型统一打分。再往后,还可以把RAG叠加到Agent里面,让模型决定什么时候检索、什么时候调用工具。
我在做第二个RAG项目时,直接把查询改写和重排都加上了,线上准确率比第一版明显提升。但这些都是在基础RAG跑通之后才值得做的事。先把基础链路打通、参数调顺,再逐步加复杂度,是最稳妥的路径。
实测下来最大的体会是:RAG的工程细节远比想象中多,但Spring AI把链路的骨架已经搭得足够好用,剩下的就是要你在参数调优和知识库治理上多花心思。如果你也想把一个会学习的业务问答系统落地,这份指南基本上可以把从零到一的路走通。