摘要:本文通过一个生动的"学霸带小抄"比喻,深入浅出地解释了 RAG(检索增强生成)的核心原理。文章详细演示了如何使用 Spring AI 框架,仅用几十行 Java 代码快速搭建一个可运行的 RAG 问答系统,涵盖 Maven 依赖配置、文档注入、向量检索、Prompt 设计等关键步骤。针对 Java 开发者面临的 Python 生态迁移难题,提供了从理论到实践的完整解决方案,并分享了生产环境中的常见坑点与面试标准回答,帮助读者快速掌握 RAG 的落地能力。
上篇文章结尾我留了个钩子:下一篇用几十行 Java 代码,搭一个能用的 RAG demo。
有人私信我说:老哥你别是画饼吧,几十行能干啥?
今天就把饼烙出来给你看。
先说说为什么我坚持用 Java 来搭这玩意儿。你去搜 RAG 教程,十个有九个用 Python。什么 LangChain、LlamaIndex,环境配置就能折腾一整天。装好之后才发现,你公司的后端全是 Java,你一个人用 Python 写了个 Demo,没人能接。
我就问你,你写了个没人能接的 Demo,图什么呢?
Spring AI 不一样。Maven 加依赖——注入——调 API,全是 Java 后端玩顺手的东西。搭好之后直接塞进你现有的 Spring Boot 项目里,Git 一提交,同事就能用。
什么叫落地能力?这就是。
先说清楚 RAG 是干嘛的
别着急上代码。先说个例子你听着。
你家楼下有个学霸,什么都会。你问他"1953年冬奥会在哪办的",他秒回——奥斯陆。这是大模型。厉害吧?
但你问他"咱们公司上个月的退货率是多少",他傻了。他不知道你们公司的数据。他还不知道你们公司叫啥。这是大模型真正的短板——训练数据截止了,私域数据更是一点都没学到。
RAG 的做法是什么?你不是问我公司数据吗?好,我先去数据库里翻一翻,找到上个月所有退货订单,统计一下——哦,退货率 3.2%。然后我拿着这个数据,告诉学霸:这是你要的背景信息,现在你帮用户回答,要参考这些数据,别瞎编。
RAG = 带小抄的学霸。先翻资料再答题。
流程图走一遍:
1. 你上传一堆文档(Word、PDF、TXT)→ 系统读出来
2. 大段文档切成小块(太长了检索不准)
3. 每块转成向量(数学上的"语义编号")
4. 存到向量数据库里备用
5. 用户提问 → 把问题也转成向量 → 到库里找最像的几个块
6. 拿出来组装成 Prompt → 扔给大模型 → 大模型看完再回答
一共三步:存进去 → 搜出来 → 回答。
第一步:Maven 依赖
咱们不用几百个依赖。Spring AI 的核心就这几个:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <version>1.0.0-M6</version> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId> <version>1.0.0-M6</version> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-pdf-document-reader</artifactId> <version>1.0.0-M6</version> </dependency>看出来了吗?没有 Python、没有 PyTorch。全是 Spring Boot 那一套:starter = 开箱即用。
OpenAI starter → 帮你配好了 ChatClient,你直接@Autowired就能调 GPT。
pgvector starter → 帮你连上了 PostgreSQL 的向量插件,不需要自己去写 JDBC 查询向量距离。
PDF reader → 文件解析工具,把 PDF 内容读成字符串。
pgvector 是什么?就是 PostgreSQL 的一个插件,给普通数据库加了"向量检索"的能力。不需要额外部署一个 Milvus——当然,大流量场景 Milvus 更强,这个咱们第4篇聊。
第二步:文档注入——把知识存进去
假设你有一堆项目文档,PDF 和 TXT 混着。你想做成一个内部问答系统。第一步就是把它们倒进数据库:
@Service public class DocumentIngestionService { @Autowired private VectorStore vectorStore; public void loadDocuments(String pdfPath, String txtPath) throws Exception { // 1. 读 PDF PdfDocumentReader pdfReader = new PdfDocumentReader( new FileSystemResource(pdfPath)); List<Document> pdfDocs = pdfReader.read(); // 2. 读 TXT TextDocumentReader txtReader = new TextDocumentReader( new FileSystemResource(txtPath)); List<Document> txtDocs = txtReader.read(); // 3. 合并 List<Document> allDocs = new ArrayList<>(); allDocs.addAll(pdfDocs); allDocs.addAll(txtDocs); // 4. 自动切块(默认每块 800 tokens,重叠 200) TokenTextSplitter splitter = new TokenTextSplitter(800, 200); List<Document> chunks = splitter.apply(allDocs); // 5. 写入向量数据库 // 这一步会自动调 OpenAI 的 embedding API 转成向量 vectorStore.accept(chunks); System.out.println("成功注入 " + chunks.size() + " 个文档块"); } }来,一句一句解释:
PdfDocumentReader—— Spring AI 内置的 PDF 解析器。你把文件路径给它,它把 PDF 一页一页读成Document对象。每个 Document 里有两个字段:content存文字内容,metadata存页码、文件名这些信息。
TokenTextSplitter(800, 200)—— 切块器。800 是每块最大的 token 数(不是字数)。200 是块与块之间的重叠 token 数。为什么要有重叠?因为一段话可能在上一块结尾和下一块开头之间被切断了。重叠保证关键信息不会刚好被切丢。
vectorStore.accept(chunks)—— 神奇的一行。你传入文档列表,它自动:调用 embedding 模型把每个块转成向量 → 把向量和原文一起存到 pgvector。一行代码做了三件事。
第三步:检索+问答——问它
文档存好了,现在有人来问问题了:
@Service public class RAGService { @Autowired private VectorStore vectorStore; @Autowired private ChatClient chatClient; public String ask(String question) { // 1. 把问题转向量,在库里搜最像的 3 个块 List<Document> similarDocs = vectorStore .similaritySearch(SearchRequest.query(question) .withTopK(3)); // 2. 把搜到的内容拼成上下文 String context = similarDocs.stream() .map(Document::getContent) .collect(Collectors.joining("\n---\n")); // 3. 组装 Prompt String prompt = """ 你是公司内部知识库助手。 请只根据以下资料回答问题。 如果资料里找不到答案,直接说「我不知道」,不要编。 资料: %s 问题:%s """.formatted(context, question); // 4. 调大模型 return chatClient.prompt() .user(prompt) .call() .content(); } }再一句一句拆:
vectorStore.similaritySearch(...)—— 这就是 RAG 的核心操作。把你问的问题转成向量之后,去库里做余弦相似度搜索,找到最相关的 topK 个文档块。withTopK(3)意思是要 3 条。太少了信息不够,太多了会引入噪音,3-5 是个经验值。
Document::getContent—— 取出每个文档块的原文字段。注意这里取的是content不是metadata。metadata 只存辅助信息(页码、文件名),content 才是你当初写进去的那段话。
prompt—— 模板字符串。关键设计:加了"不要编"的指令。很多人用 RAG 效果不好,不是因为向量搜得不准,是因为大模型自以为是——搜到了 A 却觉得 B 更合理,自己脑补了一段。你必须告诉它:有资料就用资料,没资料就说不知道。
chatClient.prompt().user(prompt).call().content()—— 经典的 Spring AI 调用链。prompt()创建一个 Prompt 对象 →user()设置用户消息 →call()发出去等结果 →content()拿到模型返回的文字。
跑起来需要什么配置?
别忘了在application.yml里配一下:
spring: ai: openai: api-key: ${OPENAI_API_KEY} chat: options: model: gpt-4o-mini temperature: 0.1 vectorstore: pgvector: index-type: HNSW distance-type: COSINE_DISTANCE index-list-size: 100 datasource: url: jdbc:postgresql://localhost:5432/rag_db username: postgres password: postgrestemperature: 0.1—— 温度。越低回答越"死板"越精确,RAG 场景就该低,你不需要它创意发挥。0.9以上适合写诗写段子,不适合回答技术问题。
index-type: HNSW—— 向量索引类型。HNSW 是一种多层图的检索算法,搜索快,适合生产。你可以简单理解为:给数据库里的向量建了个"网状寻路图",找东西不用逐个比,顺着图跳着走几步就到了。
COSINE_DISTANCE—— 向量距离算法。就是把两个向量比一下方向是不是接近。方向一致就是相似(cos=1),方向相反就是不相关(cos=-1)。
整个流程串起来是什么样?
你打开 IDE。跑Application.java。控制台打一行日志。
然后你在 Postman 发个请求:
GET /api/ask?question=咱们项目的线上监控系统是怎么设计的?等了大概 3 秒(调大模型 + 查向量数据库的时间),返回:
我们项目使用 Prometheus + Grafana 进行线上监控。 关键指标包括:JVM堆内存使用率、接口响应时间P99、GC暂停频率和时间。 告警通过钉钉机器人推送到值班群,级别分为WARNING和CRITICAL。 详细监控配置文档见:http://wiki.internal/metrics-setup你什么都没干,就是上传了几份 PDF,写了二十多行代码。
但是对外可以说:我用 Spring AI 搭建了内部知识库问答系统,实现了基于 RAG 的文档智能检索。
你猜面试官听到这个会不会眼睛一亮?
踩坑提示
我说几个我遇到的坑,你留着看:
坑一:OpenAI 的 embedding 模型有输入长度限制。
text-embedding-3-small最长 8192 tokens。超过的文档块会被静默截断。Spring AI 的 TokenTextSplitter 默认 800 一块,基本不会超。你要自己切的话注意控制块大小。
坑二:首次启动会创建向量表,自动做。但如果你改了 schema,得手动 DROP 重建。
Spring AI 的 VectorStore 实现会在启动时CREATE TABLE IF NOT EXISTS。索引也是自动创建的。这很方便,但你要改索引类型(比如从 IVFFlat 换 HNSW),它不会自动重建。
坑三:大模型可能还是会在你不想要的时候"创意发挥"。
上面的代码加了"不知道就说不知道"的约束,但大模型不是 100% 听话的。生产环境一定要加校验层:让大模型给出引用的文档块 ID,前端可以点进去看原文验证。我见过一个 RAG 系统,员工问薪资政策,大模型自己编了一套——法务部门差点炸了。
🎯 面试官视角的标准回答
如果面试官问:"你用过 RAG 吗?具体怎么实现的?"
用 Spring AI 搭的。就是标准的三阶段 RAG 流程——注入、检索、生成。注入阶段:用 Spring AI 内置的 PdfDocumentReader 和 TokenTextSplitter 处理文档。切块策略我设的 800 token 加 200 token 重叠,存进 PostgreSQL + pgvector。Spring AI 的 VectorStore 接口封装了 embedding + 存储,一行代码搞定。检索阶段:用 similaritySearch 做向量检索,topK 设 3。问题是用户输入的,问题本身也会被 embedding 转成向量,然后去库里做余弦相似度匹配。生成阶段:搜到的文档块拼成 context,塞进一个预设好的 Prompt 模板里。Prompt 里加了指令约束——有资料用资料,没资料说不知道,不让模型自由发挥。调通用 chatClient.prompt().user(prompt).call().content()。整体效果还可以。几十行代码搞定了一个能在内部用的文档问答系统。当然生产化还需要做超时处理、多轮对话的上下文管理、以及大模型输出的校验(必须带引用原文)。
好,这一篇先把 RAG 的骨架搭起来了。下一篇我们聊一个更'虚'但更重要的东西——Prompt 怎么写,大模型才有你想要的答案。
别觉得 Prompt 就是"随便说句话"。同一道题,好 Prompt 和烂 Prompt 的效果能差 40%。
私信回复「666」,一次性领走:
面试宝典:Java 高频考点速查表、HashMap/ConcurrentHashMap 源码笔记、JVM 调优案例、Spring Boot 面试 50 问
AI 编程工具箱:Cursor/Copilot/Codex 六工具对比表、10 个 Prompt 模板、Debug 万能公式、Cursor 速查手册、AI 图片生成入门、30+ 效率工具包
一份资料包,两个专栏都能用。「唠点键盘之外的」,只讲干货。