最近做企业系统的朋友来找我,说公司内部已经用 Dify 把售后知识库、产品文档都搭好了,网页端的 AI 问答也跑通了。他真正的问题是这样的:「我知道知识库已经有了,但我是 Java 程序员,下一步到底还要做什么?我能在自己的 Spring Boot 业务系统里直接调用这个能力吗?」
这个问题问得特别好,因为它戳中了很多后端团队的误区:以为知识库搭完,RAG 就结束了。事实恰恰相反,知识库只是原料仓库,Java 程序员真正要做的,是把这份原料接到自己的应用里,让它变成可编程、可权限控制、可被业务逻辑调用的 AI 能力。而这件事,Spring AI 的 RAG 支持刚好能帮上大忙。这篇文章我打算从「知识库现状盘点」开始,把 Spring AI 里和 RAG 相关的核心抽象讲透,再给出一套能落地的接线方案,最后聊聊进阶的 Agentic RAG 和那些文档里不会写清楚的坑。
1. 「知识库已经有了」这句话里,藏着两个你没细想的缺口
1.1 你手里的知识库,到底是个什么东西
先别急着写代码。你得先搞清楚自己手上的「知识库」是哪种形态,因为不同形态决定了 Java 侧接入方式完全不一样。
我见到的团队,八成会落在这三类里面:
| 知识库形态 | 常见载体 | 对外能力 | Java 接入代价 |
|---|---|---|---|
| 平台托管型 | Dify、Coze、豆包等搭建的知识库 | 网页聊天窗、开放 HTTP API | 低,但灵活性也低 |
| 文件/资料型 | Obsidian、本地 Markdown、Wiki 导出 | 静态文件,无检索能力 | 中,需要自己解析和向量化 |
| 业务数据型 | MySQL、PostgreSQL 里的业务表 | 结构化数据,可 SQL 查询 | 需要 NL2SQL 或混合检索设计 |
如果你是拿 Dify 搭的知识库,最简单粗暴的做法就是用 Dify 发布的 API 接口,直接在 Java 里发 HTTP 请求。很多团队也确实这么干了,但我后面会解释为什么这条路只能算「应急方案」。
如果你用的是 Obsidian 或者一堆 Markdown 文档,那你其实连知识库都还没有——你只有知识原料。你需要自己写管线把文档读进来、切分、向量化、存储,然后才能谈检索。
如果是业务数据,那就更复杂了,结构化查询和非结构化检索是两套完全不同的技术栈。我见过用向量库硬存 SQL 表数据的,效果惨不忍睹,后面讲 Agentic RAG 时会聊到正确姿势。
1.2 知识库有了,但应用侧缺的是这三件套
不管上面的形态是哪种,只要你想在 Java 业务系统里实现 RAG,就必须补齐三个核心组件:
第一,向量索引。知识库里的文档是离散的,你要先通过 Embedding 模型把每一段文本转换成向量,存到向量数据库里。没有这一步,检索就无从谈起。
第二,检索器。用户问「合同审批流程是什么」,你不能把整个知识库都塞给大模型。你需要实时把用户问题和知识库里的文本片段做相似度匹配,召回 top-k 个最相关的块。
第三,增强生成。召回到的片段要拼装成上下文,和用户问题一起发给大模型,让它基于给定的资料回答,而不是凭空瞎编。
一句话总结就是:知识库负责「存」,RAG 管线负责「找」和「用」。存的问题你已经解决了,找和用的能力,就是 Java 程序员要补的功课。Spring AI 恰好把这三件套做成了嵌入式组件,不需要你从零写余弦相似度算法,也不用自己管理大模型 HTTP 会话。
2. Spring AI 的 RAG 能力拆解:不是造轮子,是把轮子拧到对的位置
2.1 为什么说 Spring AI 之于 RAG,就像 Spring Framework 之于数据库
很多 Java 老炮第一次看 Spring AI 的文档会觉得抽象,但其实你把它类比成 Spring JDBC 就好理解了。
回忆一下:在没有 Spring 的年代,操作数据库要自己管 Connection、自己处理异常、自己拼接 SQL。Spring 做的事情是提供 JdbcTemplate、提供事务抽象、把「获取连接→执行语句→处理结果」这套流程标准化。RAG 也一样,它的固定流程是「读文档→切分→向量化→存库→检索→拼上下文→调模型」。这个流程完全可以被抽象成一套可复用的骨架,Spring AI 做的就是这个事。
具体来说,Spring AI 里有一套完整的抽象组件:
DocumentReader:负责把不同格式的文件读成统一文档对象,PDF、Word、Markdown 都行;TextSplitter:把长文档切成合适大小的块,避免超出模型上下文窗口;EmbeddingModel:封装了各种向量化模型,可以是 OpenAI、通义、Ollama 本地模型;VectorStore:统一了向量数据库的读写接口,PGVector、Redis、Milvus 可以随时换;ChatClient/Advisor:负责把检索结果和提示词组装成最终请求。
这套抽象的好处是:你写业务代码时不需要关心底层是哪个向量库、哪个模型。今天用本地 Ollama 做测试,明天换 OpenAI,配置一改就切过去。作为 Java 程序员,你最熟悉的「面向接口编程」体验,Spring AI 原样搬到了 AI 领域。
2.2 必须吃透的四个抽象概念
在写代码之前,建议先把这四个概念吃透,不然你还是会觉得 Spring AI 像黑盒。
Document 是贯穿始终的数据结构。不管你读的是 PDF 还是 Markdown,最终都会被转成Document对象。它有两个关键属性:content(正文文本)和metadata(元数据,比如文件名、页码、章节标题)。这个元数据非常重要,后面做检索过滤、显示引用来源全靠它。
EmbeddingModel 决定了你的检索质量上限。向量化就是把文本映射到高维空间里的坐标。坐标选得好不好,直接影响「相似度计算」靠不靠谱。你用text-embedding-3-small还是用bge-m3,检索效果差距是巨大的,而且这不是模型好坏的问题,是模型和你的语料是否匹配的问题。
VectorStore 是你的记忆仓库。它负责把向量存下来,并提供相似度查询。Spring AI 的VectorStore接口把这个过程简化成了add()和similaritySearch()。但注意,不同向量数据库的索引参数差异很大,HNSW 的M和efConstruction这些参数在 Spring AI 默认配置里是不会替你做最优选择的。
Advisor 是 RAG 管线里的「组装车间」。Spring AI 1.0 之后提供了Advisors.retrievalAugmentation()这样的方法,你只需要声明「我要用向量库做检索增强」,框架会自动在每次调用模型之前完成检索和上下文拼装。这是 Spring AI 最省心的地方——它把 RAG 的胶水代码藏起来了。
3. 实战:把「已有知识库」变成 Spring Boot 可调用的 AI 接口
3.1 接入思路选择:直接调 API、复用文件、还是自己搭检索
这里我先回答开头那个朋友的问题:已经在 Dify 里搭好的知识库,要不要直接在 Java 里调 Dify 的 API?
我的建议是:如果只是临时用,可以调 API;如果想要做成产品级能力,建议在自己的工程里重建检索链路。原因有三个。
第一,权限控制。业务系统里不同的角色能看的知识范围不一样。Dify 的知识库 API 只支持按知识库维度隔离,你很难做到「文档级」或「片段级」的细粒度权限。而在自己的管线里,你可以在检索前先根据用户身份过滤 metadata,这个能力是产品落地时几乎必考的。
第二,响应延迟和成本。Dify 的 API 是黑盒,你没法控制它的检索参数。你可能想先做问题改写、再做混合检索,这些精调在外部平台 API 上都做不了。自己做管线,命中率和成本是可以肉眼优化的。
第三,离线复用。文件型知识库(Obsidian、Markdown)本来就不依赖 Dify,你用 Java 自己搭检索反而更轻——不需要多一个外部平台依赖。
所以我的结论是:知识库可以继续用 Dify 管理,但 Java 侧要自己搭一套 RAG 执行链路。你只需要把 Dify 里的文档导出,或者直接用 Obsidian 的本地文件夹作为知识源。
3.2 最小可运行实现:pom、配置和核心代码
我直接给你一套能跑起来的最小实现。先加依赖:
<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>然后是配置文件:
spring: ai: openai: api-key: ${OPENAI_API_KEY} chat: options: model: gpt-4o-mini embedding: options: model: text-embedding-3-small vectorstore: pgvector: index-type: HNSW distance-type: COSINE_DISTANCE接着是知识库初始化逻辑。这里我以 Obsidian 文件夹为例,用文件系统遍历 + Tika 解析所有 Markdown:
@Configuration public class RagConfig { @Bean public VectorStore vectorStore(EmbeddingModel embeddingModel) { return new PgVectorStore(embeddingModel); } @Bean public ApplicationRunner knowledgeBaseInitializer( VectorStore vectorStore, EmbeddingModel embeddingModel) { return args -> { // 检查是否已经初始化过,避免重复灌库 if (vectorStore.similaritySearch("初始化检查", 1).isEmpty()) { Path root = Path.of("/data/obsidian-vault"); List<Document> docs = Files.walk(root) .filter(p -> p.toString().endsWith(".md")) .flatMap(p -> { try { return new TikaDocumentReader(p.toUri().toString()).get().stream(); } catch (Exception e) { return Stream.empty(); } }) .toList(); // 切分:兼顾语义完整和召回粒度 TextSplitter splitter = new TokenTextSplitter(500, 50); List<Document> chunks = splitter.apply(docs); vectorStore.add(chunks); } }; } }然后是业务侧调用。用 Spring AI 的ChatClient,一行就能启用 RAG:
@Service public class KnowledgeAssistantService { private final ChatClient chatClient; public KnowledgeAssistantService(ChatClient.Builder builder) { this.chatClient = builder .defaultAdvisors(Advisors.retrievalAugmentation()) .build(); } public String ask(String userQuestion) { return chatClient.prompt() .user(userQuestion) .call() .content(); } }看到没?你不写任何检索逻辑,retrievalAugmentation()这个 Advisor 会自动在调用模型前完成向量检索并把命中片段塞进上下文。这个最小闭环跑通之后,你已经有了一台「知识库问答引擎」,而且它是跑在你自己的 Spring Boot 进程里的。
3.3 命中率和检索质量怎么度量、怎么调
跑到这一步,很多人就以为大功告成了。但我建议你花时间做一次检索质量评估,因为这才是 RAG 项目真正拉开差距的地方。
业界最常用的两个指标:Hit Rate(命中率)和MRR(平均倒数排名)。
- Hit Rate:对于一批测试问题,答案是否出现在被召回的 top-k 片段里。它是「有没有找到」;
- MRR:正确答案排在召回列表第几位。它是「找得多靠前」。
评估方法很朴素:准备 50~100 个业务问题,每个问题标注出它应该对应知识库里的哪一段。然后写个脚本调用similaritySearch,看命中情况。手动维护这个测试集确实麻烦,但这是后续所有调优动作的基准线,没有它你就是在盲调。
影响命中率最直接的因素有三个:
- 分块大小。块太大会混入太多噪声,块太小又会让语义不完整。我自己常用 400~600 token 一档,重叠 10%~20%。但这必须结合你的文档类型试错,没有一个万能值;
- Embedding 模型。领域专有名词多的场景,通用 embedding 模型常会检索不准。这个要靠实验数据说话,别迷信某一家的模型;
- 查询改写。用户口语提问往往和文档里的话术不一致,比如用户问「报销咋弄」,文档里写的是「费用报销流程」。这种情况需要在检索前先让大模型改写一下问题,Spring AI 里可以用
QueryTransformer或者自定义 Advisor 实现。
我自己踩过最大的坑是:测试集用文档原文改写成问题,导致命中率虚高。因为 embedding 模型天生偏向「文本越像越能命中」。真实用户提问和原文措辞差别很大,所以测试问题一定要用口语化表达重新写一遍,不能偷懒。
4. 从单次检索到 Agentic RAG:Java 侧的进阶编排
4.1 什么时候才需要一步步思考的 RAG
基础 RAG 的流程是一条直线:问题 → 检索 → 生成。这对「知识库问答」这种场景够用。但真实业务里很快会遇到三个让它失灵的情况。
第一个是多跳问题。比如「去年 Q3 华东区销售额最高的产品是什么,这个产品的售后文档里提到过哪些常见故障?」这条问题需要先后查业务数据库、再查售后文档库,一次检索根本搞不定。
第二个是路由需求。用户问的可能是政策问题,也可能是技术问题。不同的知识库要各自检索,而不是把所有库全扫一遍。基础 RAG 没有自主选择知识源的能力。
第三个是检索结果不充分。有时候第一次检索回来的片段质量很差,你需要让模型根据初步结果决定「再补检索一次」还是「换个关键词再搜」。基础 RAG 不会自己反思。
这时候就要上Agentic RAG——不再是一次检索生成完事,而是让大模型充当调度者,它可以决定调用哪些检索工具、按什么顺序、什么时候结束。在 Java 侧实现这件事并没有你想象的那么玄乎,Spring AI 的函数调用能力就是地基。
4.2 用 Spring AI 的函数调用与 Advisor 组合做多步查询
实现 Agentic RAG 的核心是把不同检索能力暴露成工具,然后让模型按需调用。Spring AI 里最直接的方式就是@Tool注解。
下面这段代码演示了两个检索工具,一个查知识库,一个查业务数据库:
@Component public class RetrievalTools { private final VectorStore vectorStore; private final JdbcTemplate jdbcTemplate; public RetrievalTools(VectorStore vectorStore, JdbcTemplate jdbcTemplate) { this.vectorStore = vectorStore; this.jdbcTemplate = jdbcTemplate; } @Tool(description = "在内部知识库中检索与问题相关的文档片段,参数为用户的原始问题") public List<String> searchKnowledgeBase(String question) { return vectorStore.similaritySearch(question, 4) .stream() .map(Document::getContent) .toList(); } @Tool(description = "查询业务数据库中的销售数据,支持按年份和地区过滤") public List<Map<String, Object>> querySalesData(String year, String region) { return jdbcTemplate.queryForList( "SELECT product, total_amount FROM sales WHERE year = ? AND region = ?", year, region); } }然后在构建ChatClient时把这些工具加进去:
@Bean ChatClient agenticChatClient(ChatClient.Builder builder, RetrievalTools tools) { return builder .defaultTools(tools) .defaultAdvisors( Advisors.retrievalAugmentation(), new MessageChatMemoryAdvisor(new InMemoryChatMemory())) .build(); }这样模型接收用户问题时,就会自己判断:这问题需要查知识库还是查数据库?甚至先查数据库、再把结果和问题一起作为检索条件去查知识库。这就是从 RAG 到 Agentic RAG 的跃迁——从「固定流水线」变成「大模型自主编排」。
不过要提醒一句:Agentic RAG 不是默认选项。它的代价是更长的响应时间、更大的 token 消耗,以及工具调用失败时的错误处理复杂度。我的建议是,先把基础 RAG 跑稳,拿命中率数据说话,确实遇到多跳和路由需求再升级。
5. 我用 Spring AI 做 RAG 踩过的坑和参数调优笔记
5.1 分块大小、Embedding 选择和向量库参数
分块策略是所有 RAG 项目里最「百家争鸣」的地方。我直接说说实践结论。
用TokenTextSplitter时,中文场景建议块大小 300~500 token。中文的信息密度比英文高,同样 token 数能表达的意思更多。块太大,一段里杂糅了多个主题,召回时相关度会被稀释;块太小,语义不完整,模型看起来像是猜谜。
重叠(overlap)建议设成 10%~20%。重叠的作用是保证被切在两块边缘的句子不会丢失语义上下文。我用得比较多的是new TokenTextSplitter(400, 80),块大小 400、重叠 80,命中率相对稳定。
Embedding 模型的选择,我的经验是:中文业务场景优先试国产模型或者本地部署模型,不要无脑 OpenAI。倒不是效果一定更好,而是中文语料场景下,通义text-embedding-v3或 BGE 系列对中文长文本的维度编码通常更稳定,而且数据不出域这个合规优势对很多企业是硬需求。在 Spring AI 里换个 embedding 模型只需要改配置,建议你有条件把两三个模型都跑一遍测试集,看 hit rate 说话。
还有一个很多人忽略的坑:PGVector 的 HNSW 参数。Spring AI 的默认配置能跑,但不一定是最优。M参数控制每个节点的最大连接数,efConstruction控制建索引时的搜索范围。这两个值越大,检索越准,但索引构建越慢、内存越高。对于千万级以下的文档片段,M=32, efConstruction=64是个性价比不错的起点。如果你只有几万个片段,甚至不用刻意调,默认值足够。
5.2 延迟、Token 成本和稳定性的权衡
RAG 上线后你大概率会面临两个来自业务方的质问:「为什么这么慢?」「为什么这么贵?」
先说延迟。RAG 比普通大模型对话多了一轮向量检索,PGVector 本地检索一般 50ms 内搞定,这不是瓶颈。真正的瓶颈在 embedding 和回答这两次模型调用。如果你的 LLM 放在云端,网络往返加首 token 延迟,整体响应在 3~8 秒都算正常。想降延迟,可以考虑三步:把 embedding 模型换成本地部署的轻量模型,比如 bge-small;回答模型选择首 token 更快的型号;对高频问题加一层缓存,命中缓存直接返回,根本不走管线。
再说成本。最容易失控的地方是把所有历史对话都塞进上下文。Spring AI 的MessageChatMemoryAdvisor会让模型记住整段聊天记录,你以为上下文才 5 轮,实际 token 已经爆炸。我实际使用时的做法是:开启记忆但限制轮数,只保留最近 5 到 8 轮;另外在retrievalAugmentation()时把召回的 top-k 调整为 3 到 4 个片段,不要贪多。
最后是稳定性。RAG 服务线上最容易出的幺蛾子就是向量库和模型版本不一致导致的「检索结果漂移」。比如你换了 embedding 模型,但没重新灌库,那么旧文档的向量是旧模型生成的,查询向量是新模型生成的,相似度全是废的。所以记住一条铁律:换 embedding 模型 = 必须全量重建知识库向量。这个坑我踩过一次,排查了两天才发现是两套向量空间不兼容。
5.3 个人实践中最值得保留的一段配置
聊到这儿,我分享一个我自己保留到现在的小做法:在application.yml里把 Embedding 模型和向量库的关联做成可切换的 profile。
spring: profiles: active: dev --- spring: config: activate: on-profile: dev ai: openai: embedding: options: model: text-embedding-3-small vectorstore: pgvector: index-type: HNSW --- spring: config: activate: on-profile: prod ai: ollama: embedding: options: model: bge-m3 vectorstore: pgvector: index-type: HNSW distance-type: COSINE_DISTANCE开发环境用云 API 快速验证,生产环境切到本地嵌入模型,数据不出内网。每次切环境别忘了把向量库重建,这个东西我在代码里加了个启动检查,similaritySearch测试向量数量,数量异常直接抛异常阻止启动。宁可启动慢,也不能让线上检索静默失效。
最后再补充一个所有 RAG 项目最终都会遇到的问题:知识库会持续更新,文档增删改怎么同步?我的做法是给文档加 metadata 版本号,初始化时扫描全量,之后靠定时任务查文件的lastModified,变了的文件重新切分并覆盖旧向量。增量同步不做的话,上线三周后你就可以对一个早已删除的旧流程文档问答自如——这体验谁用谁知道。