技术群里经常有人问:团队已经把知识库搭好了,那 Java 程序员还能做什么?这个问题表面上是问技术栈,实际上是在问 RAG 的下一步。很多人以为知识库建好就等于大模型问答做好了,其实知识库只是把数据组织起来,真正的 RAG 是让模型在回答问题时先查资料再作答。这篇文章不讲 Python 那套,全程站在 Java + Spring AI 的立场,完整走一遍从文档入库到检索问答、再到多轮对话优化的实战链路。适合正在做企业知识库问答、又不想被 Python 生态绑架的后端团队,也适合那些已经把 Obsidian、Wiki、Confluence 建得漂漂亮亮、却卡在“下一步不知道干什么”的开发者。
1. 先想清楚:知识库≠RAG,RAG 到底在解决什么问题
1.1 知识库只是起点,不是终点
很多团队把知识库建设理解为“把文档整理好、打上标签、放进统一平台”,然后交给大模型,期待它变成无所不知的问答机器人。实际效果往往很尴尬:模型要么答得模棱两可,要么振振有词地编造内容。原因很简单——普通大模型的训练数据是有截止时间的,它根本没见过你们公司的内部文档。
RAG(Retrieval-Augmented Generation,检索增强生成)解决的就是这个问题。它的工作方式很像你给一个实习生配了一位资料员:实习生不背书,但每次回答前先让资料员去档案室查找相关资料,再根据这些资料组织回答。对应到技术上就是:先把文档切块、向量化、存入向量库,用户提问时先做相似度检索,把命中的文本片段拼进 Prompt,再让大模型基于这些片段生成答案。
我见过太多“知识库项目”停在第一步:文档全量导进去了,向量也算了,但没人关心召回效果、排序策略、Prompt 结构、答案可溯源这些事。这就像把书搬进图书馆却不做索引,读者找不到书,图书馆再大也没用。
1.2 为什么非要 RAG,而不是微调或者直接让模型硬答
有人会问:知识库有了,直接把文档灌给模型让它生成长文本,行不行?早期的“LLM Wiki 知识库”思路就是这样,把大段文档塞进上下文,但很快会遇到两个问题:一是 Token 成本极高,一份 100 页的文档塞进 Prompt 可能要几万 Token;二是大模型对长文本中细节的记忆能力并不靠谱,距离越远的内容越容易被忽略。
微调是另一条路,把领域知识“背”进模型参数里。但微调成本高、周期长,而且知识一更新就要重新训练,对企业内部高频变动的文档来说根本不现实。RAG 的关键优势在于知识即插即用:文档更新后只需要重新索引相关片段,模型的参数一行都不用动,回答还能标注“这个结论来自哪份文档”,方便溯源和审计。
1.3 先分清朴素 RAG、进阶 RAG 和 Agentic RAG
在动手写代码之前,我得先给 RAG 的形态泼一盆冷水。网上那些炫酷的 Agentic RAG、Graph RAG、Ontology RAG 听起来很高级,但大多数业务场景根本用不到。
- 朴素 RAG:用户提问 → 向量检索 → 拼接 Prompt → 生成答案。这是绝大多数场景的起步形态,也是本文的主角。
- 进阶 RAG:在朴素 RAG 基础上加入查询改写、多路召回、重排(Rerank)、结果过滤等优化,解决“问题表述不好检索”和“召回结果不准”的问题。
- Agentic RAG:让模型自己决定要不要检索、先做哪一步检索、检索完是否需要调用其他工具。适合多步骤推理的复杂任务,但也带来了更高的失败率和调试难度。
我的建议很直白:先用最朴素的方式把代码跑通,再逐步加复杂度。一上来就上 Agentic RAG,你连基础链路的问题都分不清是出在检索还是生成,排查起来会非常痛苦。
2. 技术选型:Java 程序员搭 RAG 的几个现实选项
2.1 为什么优先用 Spring AI,而不是 LangChain4j
Java 圈子里做 LLM 应用,目前绕不开两个框架:Spring AI 和 LangChain4j。LangChain4j 对标 Python 的 LangChain,功能很全,社区也很活跃,而且它的 API 设计在早期比 Spring AI 更成熟。但如果你本身就是 Spring Boot 深度用户,我更推荐 Spring AI。
原因有三点。第一,Spring AI 是 Spring 官方生态的一部分,依赖注入、自动配置、Starter 机制全部复用 Spring 的习惯,团队上手成本极低。第二,Spring AI 在 2025 年发布了 1.0 正式版,核心 API 趋于稳定,和 Spring Boot 3.x 的整合度非常高。第三,Spring AI 的生态里已经覆盖了 OpenAI 协议、智谱、通义、DeepSeek、Ollama 等主流模型接入,还支持 Tika 文档解析、向量数据库抽象、Prompt 模板、聊天记忆等组件,不需要你自己到处粘第三方库。
LangChain4j 也不是不能用,尤其是你需要一些 Spring AI 还没覆盖的生态组件时,它确实是个好备选。但 Java 团队做 RAG 的主线任务,用 Spring AI 就够了,没必要把两个框架混在一个项目里。
2.2 向量存储怎么选:从内存到生产级
向量数据库是 RAG 的根基,选型直接关系到后续的并发能力、运维成本和数据可靠性。Spring AI 通过VectorStore接口统一了各种实现,换存储基本不用改业务代码。我用一张表给大家一个直观认识:
| 方案 | 适合阶段 | 并发能力 | 运维成本 | 备注 |
|---|---|---|---|---|
| SimpleVectorStore(内存) | 本地原型验证 | 低 | 零 | 重启数据丢失,不能上生产 |
| Redis + RediSearch | 小中型系统 | 中 | 低 | 适合已有 Redis 的团队 |
| PostgreSQL + pgvector | 中小型系统 | 中 | 低 | 复用现有数据库,需要装插件 |
| Elasticsearch | 生产级规模 | 高 | 中 | 自带分词和全文检索,适合混合检索 |
| Milvus | 海量向量场景 | 高 | 高 | 独立集群,运维最重 |
如果你只是在本机跑 demo,SimpleVectorStore最省事,代码三行就能把文档存进去。但生产环境我强烈建议用 Elasticsearch 或 Redis。Elasticsearch 的额外好处是它天然支持关键词检索,当用户的提问里包含产品型号、订单编号这类精确词汇时,关键词召回往往比向量语义召回更准,二者可以混合作多路召回。
2.3 Embedding 模型和生成模型怎么搭
RAG 里有两个模型:一个是把文档和问题都转成向量的 Embedding 模型,一个是最后生成答案的大语言模型。这两个模型可以来自同一个厂商,也可以混搭。我在实际项目里常用的组合是:Embedding 用国内闭源 API 或者本地 BGE 系列,生成模型用智谱 GLM、通义千问或者 DeepSeek。
代码层面,Spring AI 的接入方式非常统一。比如接入智谱,只需要引入对应的 starter,然后在配置里填 key 和模型名。我的一个最小实践配置长这样:
spring: ai: zhipuai: api-key: ${ZHIPU_API_KEY} chat: options: model: glm-4-plus embedding: options: model: embedding-2如果在企业内部不想走公网 API,也可以用 Ollama 跑本地模型,Spring AI 有spring-ai-ollama的 starter。但要注意,本地小参数模型在复杂语义理解上会明显弱于大厂 API,Embedding 模型的选择对召回率的影响远比很多人想象的大。在同一个问题上,换一个更好的 Embedding 模型,比盲调切块参数带来的提升通常更显著。
2.4 Spring AI 版本踩坑提醒
Spring AI 的版本演进非常快,从 0.x 到 1.0 再到 2.0,API 变化挺多。很多人照着网上老教程写代码,发现ChatClient找不到、EmbeddingModel方法签名对不上,就是这个原因。我做项目时直接锁定 Spring AI 1.0.x,并在pom.xml里用官方 BOM 统一版本,避免一些 Starter 各自引入不同版本导致冲突:
<dependencyManagement> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencyManagement>如果你要接入阿里系,可以关注 Spring AI Alibaba 项目,它提供了spring-ai-alibaba-starter,对通义系模型和阿里云向量库的适配更完整。但不管选哪个,先确认官方文档对应的版本,再动手写代码,能省掉不少噪音。
3. 核心实操:Spring AI RAG 关键链路一步步落地
3.1 文档解析:从 PDF、Word、Markdown 到纯文本
RAG 的第一步是“读文档”。Spring AI 内置了一套 DocumentReader 体系,支持的格式包括 PDF、Word、Markdown、TXT 等。我们在项目中用最多的是 Tika 统一入口,省得为每种格式各自写一段解析逻辑:
import org.springframework.ai.reader.tika.TikaDocumentReader; import org.springframework.core.io.FileSystemResource; TikaDocumentReader reader = new TikaDocumentReader( new FileSystemResource("/data/kb/spring-ai-guide.pdf")); List<Document> documents = reader.get();这里有几个非常现实的坑。第一,扫描版 PDF 没有文本层,Tika 直接读取是空白,必须先走 OCR 流程,否则文档“成功入库”但检索结果全是垃圾。第二,Word 里头像、文本框、表格的解析顺序容易乱,落地时最好约定成文都要用 Markdown 或结构化 HTML 上传,而不是把 Word 直接扔进来。用 Java POI 处理 Word 虽然可行,但遇到复杂排版时你会想把文档源格式直接规范掉。第三,文档解析完记得保留原始来源信息,比如文件名、页码、章节路径,后续展示引用来源时全靠这些元数据。
3.2 切块:chunk size 与 overlap 是第一个调参点
切块是 RAG 里最容易理解、也最容易玩坏的一步。切得太小,语义被切断,比如一个完整结论从中间被劈开,检索时只能召回一半,答案自然残缺。切得太大,又会混入无关内容,向量表示的语义被稀释,而且直接推高 Token 成本。
Spring AI 自带TokenTextSplitter,可以按 Token 数切分,还支持重叠。我的起步参数通常是:
import org.springframework.ai.document.Document; import org.springframework.ai.transformer.splitter.TokenTextSplitter; TokenTextSplitter splitter = new TokenTextSplitter(500, 100, 5); List<Document> chunks = splitter.apply(documents);这三个参数分别是:目标 Token 数 500、重叠 Token 数 100、每次切分最多处理 5 个文档。500 这个数值对大部分技术文档比较友好,既不会太碎,也不会塞进太多无关内容。重叠部分的作用是为了缓解“切痕”问题——如果一个答案刚好横跨两个 chunk 的边界,重叠能保证下一块还保留上文的关键信息。
进阶一点的做法是按文档结构切:Markdown 按##标题切,Word 按章节标题切,而不是机械地按 Token 数硬切。我见过一个运维知识库,固定切块导致“故障排查步骤 1-3”在上一块,“步骤 4-6”在下一块,用户问完整流程时永远只命中一半。后来改成按章节切,效果好了一大截。切块策略一定要对齐你的文档结构和用户提问粒度,这是 RAG 调优里性价比最高的操作。
3.3 向量化与入库:从内存版到生产版
切完块之后,每个 chunk 就是一个独立的Document,接下来交给 Embedding 模型转成向量,然后存入向量库。Spring AI 的抽象层让这一步变得很透明,我先用最简单的SimpleVectorStore把流程跑通:
@Configuration public class RagConfig { @Bean VectorStore vectorStore(EmbeddingModel embeddingModel) { return new SimpleVectorStore(embeddingModel); } @Bean ApplicationRunner ingest(VectorStore vectorStore, TikaDocumentReader reader, TokenTextSplitter splitter) { return args -> { List<Document> chunks = splitter.apply(reader.get()); vectorStore.add(chunks); System.out.println("入库完成,共 " + chunks.size() + " 个片段"); }; } }如果你的数据量超过几万片段,或者需要多人同时查询,SimpleVectorStore 会很快撑不住。生产环境至少换成 Redis VectorStore 或 Elasticsearch。Spring AI 对存储的切换基本是“换依赖 + 换配置”的事,业务代码不用大改。比如用 Redis 时:
spring: ai: vectorstore: redis: index: knowledge-index prefix: rag:这里有个细节:Redis 作为向量库时,索引名knowledge-index一旦创建,修改向量维度就可能需要重建索引。所以你换 Embedding 模型时,最好同步换一个索引名,避免旧数据和新向量维度不一致导致检索报错。
3.4 检索与 Prompt 组装:决定问答质量的临门一脚
向量入库之后,用户提问时系统会先把问题转成向量,然后在库里做相似度检索。Spring AI 的检索方式非常直观:
List<Document> hits = vectorStore.similaritySearch( SearchRequest.builder() .query(question) .topK(4) .similarityThreshold(0.5) .build());topK=4表示取最相关的 4 个片段,similarityThreshold=0.5表示只保留相似度大于 0.5 的结果。这两个参数需要配合调:阈值太高,检索结果可能为空;阈值太低,不相关的片段混进 Prompt,大模型就会被带偏。
拿到检索片段之后,把它们和用户问题组装成一个结构清晰的 Prompt,再交给 ChatModel。我用的是 Spring AI 1.0 的ChatClient:
ChatClient chatClient = ChatClient.builder(chatModel).build(); String context = hits.stream() .map(d -> d.getFormattedContent()) .collect(Collectors.joining("\n---\n")); String answer = chatClient.prompt() .system("你是一名严谨的技术顾问。" + "请只根据下面的资料片段回答问题,不要编造。" + "如果资料中没有足够信息,直接说明“资料中未找到相关内容”。") .user("资料:\n" + context + "\n\n问题:" + question) .call() .content();Prompt 里那句“不要编造”不是摆设。大模型的默认倾向是讨好用户,你问什么它都想给个答案,给不出就编。明确约束“资料不足就直说”,能显著减少幻觉。另外,我强烈建议把命中的文档 ID、标题、页码拼到返回结果里,哪怕一开始只做简单展示,用户看到“这个答案来自《XX规范》第 3 章”时,信任度会完全不同。
3.5 一个可运行的完整链路示例
把前面几块串起来,一个最基础的 Spring AI RAG 应用的核心 Bean 就是下面这个样子:
@Bean public ChatClient chatClient(ChatModel chatModel) { return ChatClient.builder(chatModel).build(); } @Bean public QueryService queryService(VectorStore vectorStore, ChatClient chatClient) { return new QueryService(vectorStore, chatClient); }QueryService 里的核心方法就是先检索、再拼 Prompt、最后调用大模型。伪代码:
public String answer(String question) { List<Document> hits = vectorStore.similaritySearch( SearchRequest.builder().query(question).topK(4).similarityThreshold(0.5).build()); String context = hits.stream().map(Document::getFormattedContent).collect(Collectors.joining("\n---\n")); return chatClient.prompt() .system(SYSTEM_PROMPT) .user("资料:\n" + context + "\n\n问题:" + question) .call() .content(); }这段代码虽然简单,却已经是一套完整的朴素 RAG。先跑通它,再慢慢加查询改写、重排、记忆等进阶能力,比一上来就堆架构要靠谱得多。
4. 进阶玩法:多轮对话、RAG 与 MCP 的关系、Agentic RAG
4.1 多轮对话怎么设计:别让历史问题污染下一次检索
RAG 应用一旦接上对话,第一个翻车点就是多轮对话。用户先问“Spring AI 怎么配置智谱”,你答了,他又问“那 embedding 模型怎么选”,这里“那”指代的就是上一轮话题。如果直接用“the embedding model how to choose”去检索,根本查不到合理内容。
常见的解法有三种。第一种是查询改写:用大模型把当前问题结合历史对话改写成一句独立、完整的问题,再用改写后的语句去检索。我用一个简单的 Prompt 就能实现:
历史对话: 用户:Spring AI 怎么配置智谱? 助手:需要引入 spring-ai-starter-model-zhipuai,并在 application.yml 里配置 api-key。 当前问题:那 embedding 模型怎么选? 请把“当前问题”改写成一个不依赖历史也能理解的完整问题。改写结果大致是“在 Spring AI 中,智谱的 embedding 模型应该怎么选择?”。用这句话去做向量检索,命中率会高很多。
第二种是HyDE(Hypothetical Document Embeddings):先让大模型根据问题生成一段虚构的理想答案,再用这段“伪文档”去检索真实文档。说白了就是拿正确答案的长相去搜原答案,适合问题很短、检索空间很大的场景,但多一次模型调用,延迟也会增加。
第三种是对历史对话做摘要压缩,只保留和当前问题最相关的部分,避免整个对话历史塞进上下文造成 Token 浪费。Spring AI 里的 ChatMemory 和 Advisor 可以管理这部分,核心思路是“该记的记,不该记的别拖后腿”。
4.2 RAG 和 MCP 到底有什么区别
网络热词里总有人把 RAG 和 MCP 放一起对比,很多人被绕晕。我的理解很简单:RAG 解决的是知识不足,MCP 解决的是能力不足。
RAG 是给模型配参考书,让它学完再作答,属于信息供给。MCP(Model Context Protocol)是给模型配工具箱,让它能调用外部工具去完成某个动作——比如查天气、创建工单、操作数据库。一个是查资料,一个是干事情,完全不冲突。
在实际系统里,两者经常叠加使用。比如一个“工单助手”既能通过 RAG 检索企业内部运维手册,又能通过 MCP 调用工单系统接口创建工单。你在 Spring AI 里可以先用 RAG 走一遍检索问答,再通过工具调用(Function Calling)走 MCP 协议把下游动作接上。它们不是替代关系,而是“先查资料、再执行操作”的上下游关系。
4.3 Agentic RAG:是提效还是徒增复杂
Agentic RAG 的热度很高,但我不建议每个项目都上。所谓 Agentic RAG,核心变化是“让模型决定怎么检索”。比如用户问“我们公司最近的服务器故障有哪些”,模型不再直接拿这句话做一次向量检索,而是先判断:这是需要从知识库找文档,还是需要调监控系统的工具接口?接着规划步骤,一步步执行。
这种设计在“一个问题需要跨多个知识源、需要多步推理”的场景里确实有价值。但代价是:多一次决策就多一个失败点,模型可能选错工具、步骤顺序错乱、中间结果不达预期,而且这些问题在纯 Agent 架构里特别难排查。我的建议是:基础 RAG 连准确率都还没调明白之前,不要上 Agentic RAG。先把检索质量、Prompt 结构、评估闭环做好,再考虑用 Agent 编排那部分。
5. 常见问题与排查技巧实录
5.1 答非所问的排查清单
我在多个 RAG 项目里踩过不少坑,总结出一套排查流程。如果你的问答效果很离谱,不要急着调大模型,先按下面的顺序自查:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 检索结果跟问题无关 | 切块太大或太小、Embedding 模型语义能力弱 | 打印召回的原始片段,肉眼检查;换更强 Embedding 模型 |
| 有相关片段但回答乱编 | Prompt 约束不足、系统指令没强调“只依据资料” | 加上“资料中未找到就直说”的指令;降低温度 |
| 看起来有相关内容但答案残缺 | 关键信息被切块切散、overlap 太小 | 调整切块粒度;尝试按 Markdown 标题切 |
| 同一问题换几个说法就检索不到 | 用户表达和文档措辞差异大 | 加查询改写;尝试多路召回(关键词+向量) |
| 答案不稳定,时好时坏 | topK 太小,相关片段没召回到 | 适当增大 topK;加 Rerank 重排 |
| 检索到很多无关结果 | 相似度阈值太低 | 调高 similarityThreshold;观察分值分布 |
排查时最实用的工具就是“打印召回片段”。很多团队只盯着最终答案好不好,根本不看中间检索结果,这是最大的误区。RAG 的答案是由检索结果决定的,检索若偏了,后面怎么调 Prompt 都没用。
5.2 性能与并发:向量检索慢怎么办
Java 服务部署上线后,常见的问题就是并发上来了,检索耗时猛增。如果用的是内存版 SimpleVectorStore,重启丢数据只是其中一个问题,更大的问题是没有高效的索引结构,数据量过万之后检索会越来越慢。
我的建议是尽早迁到 Elasticsearch 或 Redis。ES 对中文分词和混合检索支持很好,之前踩过的坑包括:ik 分词器版本和 ES 版本不匹配导致插件加载失败;向量字段和普通字段混用时要建两个 index 或处理好 mapping。Redis 方案则要注意把Spring AI的向量前缀和业务 key 隔离,避免和缓存数据混在一起被误清理。
另一个隐形性能瓶颈是 Embedding 接口的调用。入库大量文档时,如果逐条调用外部 Embedding API,耗时和费用都会非常可观。此时可以批量提交(Spring AI 的 EmbeddingModel 支持 List 传入),也可以限制入库线程数,避免把外部 API 打爆。
5.3 知识更新:新增、删除、版本化
知识库不是一锤子买卖,文档一更新,RAG 系统就得跟着变。但很多人做知识库时只做了“全量入库”,文档改了之后旧 chunk 还在向量库里,检索时新旧内容混在一起,答案时对时错。
我的做法是给每个Document打上source和version元数据。入库时先按source删除该来源的所有旧片段,再写入新切块。如果是 MySQL 里的业务数据,可以记录一个增量时间戳,每次只处理变化部分。Obsidian 这类本地笔记库更方便,直接监听文件变化,只重建变更文件的 chunk 就行。
还有一点容易忽略:删除文档时,向量库里的旧向量不会自动清理。如果你用的是带过滤条件的 VectorStore,可以在检索时按source过滤,确保已经下线的文档不再参与召回。
5.4 自研体系 vs 开源工具:边界在哪里
热词里有很多开源知识库工具,Dify、RagFlow、MaxKB、AnythingLLM 都是当红选手。它们开箱即用,自带知识库流水线,有的还支持知识库 API 接入,甚至能和 Cursor 这类工具联动。那 Java 程序员为什么还要自己做?
我的判断是:如果你只是内部用,追求快速见效,直接部署 Dify 或 RagFlow,用它们的流水线灌文档、建知识库,比从零写 Spring AI 代码高效得多。如果你的系统是面向外部客户、需要深度定制问答逻辑、要和企业自己的权限体系/业务系统打通,那自研的价值就体现出来了。这时候 Spring AI 能让你在 Java 技术栈里保持完整的控制力,不依赖开源平台的前后端约束。
开源工具不等于零成本,Dify 的流水线配置、提示词调试、知识库切块策略同样需要花心思。自研和用工具不是对立关系,很多团队先是 Dify 跑通验证,再逐步把核心链路搬到自己的服务里。这正好回到本文开头那句话:知识库只是地基,真正决定效果的是检索、排序、生成这一整条链路的工程质量。
我自己跑 RAG 项目最大的体会是:百度一下到处都是概念,真正难的是把切块粒度、相似度阈值、Prompt 约束这些细节一点点磨到可用。别再问“知识库有了还能做什么”了,直接把文档切好、把检索结果打出来看一眼、把答案的来源标出来,你就已经比大多数团队走得更远。