☰
Spring AI + RAG:构建企业智能问答知识库实战指南
2026/10/17 11:13:53 网站建设 项目流程

先说说我最近在折腾的一个事。朋友的公司有一堆产品手册、技术文档和项目纪要,散落在各个共享盘和Wiki里。老板一句话:能不能做一个“智能问答机器人”,让员工直接问自然语言,机器人自己去找答案、给结论、附出处?我第一反应是:这不就是我们天天挂在嘴边的RAG吗。而这次我选的技术组合是Spring AI,纯Java生态,直接嵌进他们现有的Spring Boot后台里,不用单独再维护一套Python服务。

RAG全称Retrieval-Augmented Generation,检索增强生成。说人话就是:先把你的文档全部切成片段、转成向量存进向量数据库;用户提问时,先做一次语义检索,找到最相关的几段内容;然后把问题和这几段内容拼在同一个提示词里,交给大模型,让它“看着资料回答”。这样既不需要重新训练模型,又能保证答案是基于你最新、最全的私有文档,还能在回答末尾告诉用户“这段话来自哪篇文档”。Spring AI则是当前Java世界里接入大模型最省事的一套框架,它把Prompt、ChatModel、EmbeddingModel、VectorStore这些东西都抽象好了,Spring Boot项目里加几个依赖就能把整条链路跑起来。适合谁看?适合手里有Spring Boot项目、想把AI能力接进去的后端开发,也适合刚接触RAG但不想一上来就写一堆Python拼接逻辑的同学。下面把整个构建过程,踩过的坑,以及可以直接抄的配置和代码,一次性整理出来。

1. 为什么我在Java项目里选择Spring AI做RAG

1.1 RAG到底解决了什么问题

很多人一开始会问:为什么不直接把文档丢给大模型?一次丢不进去,就分次丢嘛。这个想法在做Demo时没问题,但一上生产就露馅:

第一,模型上下文窗口有限。现在主流大模型虽然能处理几十万甚至上百万token,但文档一多、一问一答之间来回塞内容,成本直接起飞。第二,模型知识有截止时间,你内部的SOP、售后记录、线上故障复盘,模型根本没学过。第三,也是最要命的——幻觉。模型不知道答案时会一本正经地编,做内部知识库这种东西绝不能接受。

RAG的核心思路不是让模型“记住”文档,而是让模型“在回答时查阅”文档。用户问什么,我们先去知识库检索相关片段,喂给模型当参考资料。模型的任务变成“读材料、总结要点、给出结论”,而不是“凭记忆瞎编”。这其实跟人干活一样:给你一摞会议纪要和合同模板,让你写一份备忘录,你肯定先翻材料再动笔,而不是凭脑子硬写。

从技术链路看,RAG由五段组成:文档接入、文本切分、向量化、向量检索、生成回答。Spring AI每个环节都给了对应抽象,这也是我愿意在Java项目里选它的核心原因——不用我手动去拼HTTP请求、维护embedding接口、管理向量库连接,框架把这些底层细节统一收口了。

1.2 Spring AI比“硬调HTTP”好在哪

我见过不少团队做AI功能是直接写RestTemplate去调大模型接口,代码倒也跑得通,但有两个很现实的问题。

一个是套路重复。Chat接口、Embedding接口、文档切分、向量检索、Prompt拼接,每个项目都要重写一遍,而且每换一家模型服务商,参数格式可能还不一样,全部要改。另一个是工程化缺失。没有统一的重试、超时、Token统计、异常处理,上线后出问题只能看日志骂人。

Spring AI解决的正是这两件事。它提供了风格类似Spring Data的Repository式开发体验。我在Service里只要注入ChatModel、EmbeddingModel、VectorStore,然后调用它们的统一方法,底层是什么模型API、什么向量数据库,配置切换即可。我实际测试下来,从OpenAI协议的HTTP接口切到本地大模型服务,只改依赖和配置,代码几乎不动。这好处在项目后期特别明显:今天用便宜的小模型验证效果,明天换更强的商业模型,后天想全部私有化部署,不会伤筋动骨。

我参与过的某内部知识库项目,原来评估过用Python写一套RAG服务,再由Java后台通过HTTP调用它。后来发现运维要维护两套技术栈、两套部署环境,出了问题两边还要来回对齐。Spring AI的方案直接让他们多了一个选择:继续用Java,一个服务解决问题。对团队的长期可维护性来说,这件事的价值被低估了。

2. 从零搭建Spring AI RAG知识库的准备工作

2.1 技术选型:模型、向量库、依赖版本

动手前先把下面几组关系理清楚。

第一组是ChatModel和EmbeddingModel。ChatModel负责“生成回答”,EmbeddingModel负责“把文本变成向量”。可以来自同一个服务商,也可以完全分开。比如国内某API服务商聊天模型很便宜,但另一个本地部署的模型做向量化效果也不错,Spring AI允许你分别配置。不过第一次做Demo,我建议先用同一个服务商的最简配置跑通,后面再针对性优化。

第二组是向量数据库。常见的有PGVector、Milvus、Elasticsearch的向量索引、Redis的VectorSet等。Spring AI都提供了对应的Store实现。我个人在小规模知识库场景下强烈推荐PGVector,原因很简单:大多数公司内部已经有PostgreSQL在生产跑,DBA熟悉,备份、权限、扩容方案齐全,不用新引入一套技术栈。你只需要在PostgreSQL里安装pgvector扩展,Spring AI会自动建表和写入。我当时就是把知识库向量表建在已有的PostgreSQL实例里,完全没有增加运维负担。

第三组是版本。Spring AI这框架现在迭代非常快,不同小版本之间API可能都会变。我这里以Spring Boot 3.2+、Spring AI 1.0.0-M5这个阶段性版本为例,你把版本对齐到相近即可。重点看代码里那些类的方法签名,如果升级了版本,根据IDE提示做相应调整。

2.2 初始化项目:引入Maven依赖和基础配置

新建一个Spring Boot Web项目,在pom.xml里加Spring AI的BOM,然后引入三个关键依赖:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0-M5</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-pgvector-store</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-tika-document-reader</artifactId> </dependency> </dependencies>

这里有一个坑要提前提醒:Spring AI的BOM不能像普通普通依赖那样直接写版本,要用<type>pom</type>和<scope>import</scope>依赖管理方式。很多新手把它当成普通依赖引入,结果Bean全扫不到,排查半天。如果你用的是Gradle,对应写法是implementation platform("org.springframework.ai:spring-ai-bom:1.0.0-M5")。

然后在application.yml里配置模型服务和向量库:

spring: main: allow-bean-definition-overriding: true ai: openai: api-key: ${AI_API_KEY:demo-key} chat: options: model: gpt-4o-mini-2024-07-18 temperature: 0.3 max-tokens: 1000 embedding: options: model: text-embedding-3-small datasource: url: jdbc:postgresql://localhost:5432/knowledge_db username: postgres password: postgres jpa: hibernate: ddl-auto: update

这里说一下为什么temperature要设低一点。做知识库问答时,我们希望答案稳定、忠于原文,而不是让模型发散。温度调的太高,模型会不自觉加戏;调到0.2到0.4之间,效果最舒服。我自己经历过温度设0.8时,模型频繁回答“根据资料,这个问题可能有多种看法”,毫无价值。

如果你没有OpenAI的密钥,但你有其他兼容OpenAI协议的服务地址,也可以把配置改成:

spring: ai: openai: base-url: ${AI_BASE_URL:https://api.your-service.example} api-key: ${AI_API_KEY:demo-key}

只要协议兼容,代码不需要改。这是我用Spring AI觉得最值的一点,模型服务商切换成本低到可以忽略。

3. 知识库核心链路:从文档到向量的完整落地

3.1 文档读取:用Tika把PDF、Word、Markdown统一处理

知识库不会只有一种格式。产品手册可能是PDF,内部SOP是Word,还有些人习惯写Markdown。Spring AI提供了一整套DocumentReader接口,其中TikaDocumentReader基于Apache Tika,可以自动识别几乎所有常见格式,并把内容抽取成纯文本。

import org.springframework.ai.document.Document; import org.springframework.ai.reader.tika.TikaDocumentReader; import org.springframework.core.io.FileSystemResource; import java.util.List; public class DocumentLoader { public List<Document> loadFromFile(String filePath) { TikaDocumentReader reader = new TikaDocumentReader(new FileSystemResource(filePath)); return reader.get(); } public List<Document> loadFromDirectory(String directoryPath) { List<Document> allDocs = new ArrayList<>(); File dir = new File(directoryPath); File[] files = dir.listFiles((d, name) -> name.endsWith(".pdf") || name.endsWith(".docx") || name.endsWith(".md")); if (files == null) { return allDocs; } for (File file : files) { allDocs.addAll(loadFromFile(file.getAbsolutePath())); } return allDocs; } }

读取出来的Document对象里包含两个关键部分:content(文本内容)和metadata(文件名、文件路径、页码等)。metadata特别重要,后面做引用溯源和权限隔离全靠它。我建议在读取阶段就把额外的业务标签塞进metadata里,比如所属部门、更新时间、可见级别。这一步做得越早,后面检索时过滤越简单。

Tika有个小毛病,对扫描版PDF它会返回空白,因为里面没有文字层。公司里那种纯图片扫描合同,需要先做OCR。Spring AI官方文档里没有强推这件事,但你如果遇到“文档入库了但永远检索不到”的情况,先检查是不是扫描件。我当时用了一段开源OCR流程先行处理,才把这类文档接进来。

3.2 文本切分:块大小、重叠和切分策略的选择

原始文档动辄几十页,不可能整篇塞进向量库,也没必要。我们需要把长文本切成一个个有独立语义的小段落。切分参数非常影响检索效果,这步是整个RAG里的“玄学高发区”。

Spring AI里默认的切分器是TokenTextSplitter,按token数来切,而不是按字符数。因为大模型API计费、上下文限制都是按token算的,按token切可以精确控制生成时的输入长度。

import org.springframework.ai.document.Document; import org.springframework.ai.transformer.splitter.TokenTextSplitter; public class TextSplitterConfig { public List<Document> splitDocuments(List<Document> documents) { TokenTextSplitter splitter = new TokenTextSplitter(500, 150, 5); return splitter.apply(documents).toList(); } }

上面这个构造参数的含义:每个块最多500个token,相邻块之间有150个token的重叠,同时保留最少5个完整句子。重叠的意义是防止句子被拦腰切断,导致语义丢失。比如一段关于“退款流程”的文章,前半段在说明适用场景,后半段才开始说步骤,如果中间被切断了,检索时可能只找到步骤却不知道前提条件。重叠部分让上下文信息在两个块里都存在,检索召回率能明显提升。

不过默认的按空白和标点切分对中文支持一般。英文按空格切分天然合理,中文词和词之间没有空格,它很容易把一句话切得乱七八糟。我处理中文文档时,优先用SpringAIClientSideSentenceSplitter先把文本按句号、问号、感叹号切成句子,再用TokenTextSplitter合并成块。本质上就是“先断句,再合并”,这样能最大程度保住语义完整。

import org.springframework.ai.transformer.splitter.SentenceTokenTextSplitter; public class ChineseFriendlySplitter { public List<Document> split(List<Document> docs) { // 内部先按中文标点断句,再按token合并 SentenceTokenTextSplitter splitter = new SentenceTokenTextSplitter.Builder() .withChunkSize(500) .withOverlap(50) .build(); return splitter.apply(docs).toList(); } }

这里我踩过一个真实的大坑。最开始用默认TokenTextSplitter切一份中文产品手册,检索“故障代码10086怎么处理”时,召回的片段里经常只有半句话,模型回答得驴唇不对马嘴。后来改成“先分句再合并”,回答质量肉眼可见地变好。所以做中文知识库,切分器一定要调研好。

3.3 向量化与入库:EmbeddingModel + VectorStore

文本一旦切好,下一步就是给每个Document生成向量,然后写入向量库。Spring AI把这两步也抽象得很干净。你只需要定义好EmbeddingModel和VectorStore两个Bean,直接调add()方法,它会自动做embedding并完成写入。

import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.pgvector.PgVectorStore; import org.springframework.ai.vectorstore.pgvector.PgVectorStoreConfig; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; @Configuration public class RagBeansConfig { @Bean public PgVectorStore vectorStore(DataSource dataSource, EmbeddingModel embeddingModel) { return PgVectorStore.builder() .dataSource(dataSource) .embeddingModel(embeddingModel) .schemaName("public") .tableName("knowledge_docs") .dimensions(1536) .build(); } }

这里有个参数必须注意:dimensions,也就是向量维度。它必须跟你使用的EmbeddingModel输出维度一致。常见的模型一般是1536维或者1024维,你配置文件里如果用text-embedding-3-small,那它是1536维;如果换成了其他模型,维度不同,启动时候能建表但检索时会报维度不匹配。最好在配置里就从模型信息里动态取维度,别写死,但我图省事写死了,也顺便提醒你。

入库的Service代码:

import org.springframework.ai.document.Document; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.stereotype.Service; import java.util.List; @Service public class KnowledgeIngestionService { private final VectorStore vectorStore; public KnowledgeIngestionService(VectorStore vectorStore) { this.vectorStore = vectorStore; } public void ingest(List<Document> documents) { List<Document> splitDocs = new ChineseFriendlySplitter().split(documents); vectorStore.add(splitDocs); System.out.println("成功入库文档片段数: " + splitDocs.size()); } }

真正跑起来时你会发现向量化是个耗时操作,几百份文档可能要跑好几分钟。建议在管理后台做一个异步导入任务,解析、切分、向量化、入库全部扔进线程池或消息队列。第一次上线时,我直接在主线程同步跑,结果接口超时,被质疑“系统是不是坏了”。这一步的工程化一定要提前设计,别图简单用同步方法。

另外,同一个文档如果重复执行入库,向量库里会出现多份重复数据。这会导致检索时同一篇文档霸占多个TopK位置,其他有效文档被挤掉。我的方案是:入库前先根据metadata里的source字段查出是否已存在,存在就delete再add,类似“覆盖更新”的语义。你可以在metadata里塞一个唯一业务ID,然后在VectorStore提供的delete(filter)方法里按它删除,非常方便。

4. 智能问答:检索、提示词组装与大模型生成

4.1 检索接口:相似度搜索和结果过滤

知识库建好了,接下来是问答环节。当用户输入一个问题,我们的第一件事不是调大模型,而是先去向量库检索。

import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.document.Document; import org.springframework.stereotype.Service; import java.util.List; @Service public class RetrievalService { private final VectorStore vectorStore; public RetrievalService(VectorStore vectorStore) { this.vectorStore = vectorStore; } public List<Document> retrieve(String question) { SearchRequest request = SearchRequest.builder() .query(question) .topK(5) .similarityThreshold(0.25) .build(); List<Document> docs = vectorStore.similaritySearch(request); return docs; } }

这里有两个概念需要解释。

topK是返回多少个相关片段。太少可能漏掉关键信息,太多可能超出模型上下文长度,也会引入噪声。我实践下来内部知识库topK取4到6比较合适。根据你输入上下文上限,比如模型支持8K token,检索回来的片段总量就要控制在2K到3K token以内,留足空间给回答生成。

similarityThreshold是相似度阈值。向量检索返回的是一个相似度分数,通常用余弦距离,范围在0到1之间,越接近1表示越相关。设阈值可以过滤掉那些“勉强相关”的片段。这个值跟你的切分方式、embedding模型、文档领域都有关系,我给不出一个万金油数值。我的做法是先把这个值设为0,在调试接口里把所有召回片段的相似度打印出来,人工看一眼:相关内容的分数底线在哪,然后设低一点阈值。别设太高,否则检索经常为空。我见过不少团队把阈值设成0.7,结果大多数问题查不到资料,体验极差。

4.2 组装Prompt:让模型“读资料,做摘编”

检索到相关文档之后,把它们拼进提示词。这里也是很多人的翻车现场。最笨的做法是把所有文本一股脑拼在System Prompt后面,模型根本分不清哪些是资料、哪些是用户问题,回答起来容易混。

我建议用Spring AI的PromptTemplate,显式定义角色、资料、问题,并且明确告诉模型“资料不足时不要编造”:

import org.springframework.ai.chat.prompt.PromptTemplate; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.document.Document; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @Service public class ChatWithDocsService { private final ChatModel chatModel; private final RetrievalService retrievalService; public ChatWithDocsService(ChatModel chatModel, RetrievalService retrievalService) { this.chatModel = chatModel; this.retrievalService = retrievalService; } public String answer(String question) { List<Document> docs = retrievalService.retrieve(question); if (docs.isEmpty()) { return "根据现有知识库,我没有找到与您问题相关的资料。"; } String context = docs.stream() .map(doc -> "【来源: " + doc.getMetadata().getOrDefault("source", "未知") + "】\n" + doc.getContent()) .collect(Collectors.joining("\n\n")); String template = """ 你是一个严谨的企业知识库助手。请基于下面提供的资料内容回答用户问题。 要求如下: 1. 如果资料中没有明确答案,请直接说“知识库中没有相关答案”,不要自行脑补。 2. 回答时尽量引用资料原文中的关键信息。 3. 回答结束时,列出处资料的来源标记。 ------------ 资料内容: {context} ------------ 用户问题: {question} """; Map<String, Object> params = new HashMap<>(); params.put("context", context); params.put("question", question); PromptTemplate promptTemplate = new PromptTemplate(template, params); Prompt prompt = Prompt.of(promptTemplate.createMessage()); ChatResponse response = chatModel.call(prompt); return response.getResult().getOutput().getContent(); } }

有个细节:我在资料内容里给每个Document加了【来源: xxx】前缀,并要求模型在回答里列出来源标记。这样用户能追溯答案出处,极大增加可信度。这个设计在内部知识库场景几乎是刚需,没有出处的AI回答,没人敢全信。

组装Prompt时还有一个顺序问题。我习惯把资料放在用户问题之前,并且用分隔符隔开。因为指令遵循能力强的模型会优先关注Prompt末尾的内容,把问题放后面等于提醒模型“真正要回答的是这个”,而不是让它迷失在长资料里。

4.3 Controller层:一个简单的文生文接口

最后写接口对外暴露:

import org.springframework.web.bind.annotation.*; import java.util.Map; @RestController @RequestMapping("/api/chat") public class ChatController { private final ChatWithDocsService chatWithDocsService; public ChatController(ChatWithDocsService chatWithDocsService) { this.chatWithDocsService = chatWithDocsService; } @PostMapping("/ask") public Map<String, String> ask(@RequestBody Map<String, String> payload) { String question = payload.get("question"); String answer = chatWithDocsService.answer(question); return Map.of("question", question, "answer", answer); } }

到这里,一个最简易的RAG知识库已经能跑了。你启动项目,在PostgreSQL里建好库,配置好API密钥,跑一遍文档导入,然后调/api/chat/ask接口,输入“那个新的退款政策是什么”,如果能等到一份带来源标记的回答,恭喜你,核心链路通了。接下来更重要的,是解决那些“本地没问题、一上就翻车”的细节问题。

5. 实战中高频踩坑与排查经验

5.1 中英文切分导致的检索失败

很多国内团队用Spring AI跑RAG,第一个崩溃点往往不是模型问题,而是中文没切好。默认的TokenTextSplitter是基于空格和标点的,中文一句话没有空格,切出来经常是半句话甚至更碎。检索时模型那边问的是“异地备份策略”,库里存的是“同步策略”“灾备措施”这种碎片化文本,向量相似度自然低。我的处理办法前面已经提了:用SentenceTokenTextSplitter,先按句号、问号、感叹号断句,再按token合并。此外,检索时如果发现效果仍然不理想,可以考虑把问句也做一次近义词扩展,或者直接改用混合检索(BM25+向量检索),但那是进阶玩法了,后面再说。

5.2 重复导入导致的知识库膨胀

你第一次导入50份文档很顺利,第二次再导入同一个目录,发现检索结果被同一篇文档霸屏了。原因就是没有做幂等。很多时候文档导入是手动触发的,同一个文件被反复处理,向量表里就会有重复片段。轻则占用存储,重则破坏检索排序。我鼓励在导入接口里增加一个deleteByMetadata的操作,比如:

public void refreshDocument(String sourcePath) { vectorStore.delete(List.of("source", sourcePath)); List<Document> docs = new DocumentLoader().loadFromFile(sourcePath); ingest(docs); }

VectorStore的delete支持传metadata键值对过滤器,删除后再写入,就能实现覆盖更新。如果你有定时任务自动扫描目录,这个“删旧写新”的逻辑必须加上。

5.3 上下文超限或被模型忽略

假设你设置topK=6,每段500token,那一次问答需要送入模型的资料是3000token左右。如果用户的原始文档里有超大表格或很长的代码段,切分器并不能每次都准到500token,实际可能会超过。此时模型调用可能会报错,或者虽然不报错,但生成答案时会丢掉后半段资料。我建议在组装Prompt前对context字符串做一个长度截断,按字符数或简单按token估算截断。估算token可以粗略用字符数除以3或4,中文除以1.5到2。更稳妥的做法是接入Spring AI的Tokenizer接口,它内部会按模型分词器精确计算。如果你用的模型对总上下文有硬限制,一定要在发送前就控制变量,不要全丢给模型去处理。

5.4 不同版本API变化太快

写Spring AI的时候,我特意去看过若干版本源码。1.0.0-M系列和之前0.8版本差别非常大。比如VectorStore接口从search(String)改成了similaritySearch(SearchRequest),PromptTemplate的构造方法也变过。如果你照着网上的老文章写代码,可能会遇到IDE爆红。解决办法是:不要死记API,依赖版本锁定,然后多看当前版本的JavaDoc和测试类。Spring AI官方每次发版都会同步更新示例项目,遇到编译问题先查示例,比反复搜其他博客更高效。

5.5 相似度阈值策略:别让它变成下一个“薛定谔的Bug”

我见过不少团队在检索后面加一句“如果相似度低于0.5就返回兜底答案”,看起来很智能,实际很危险。不同Embedding模型产生的相似度分布差异极大,有的模型相似度常年0.7以上,有的模型在0.4附近波动。你设死一个阈值,很容易把部分低分但实际有答案的文档过滤掉。更合理的做法是先用一段测试集统计Top1到Top5的分数分布,再定阈值。或者干脆只在结果非常明显不合理时才加过滤,不要让阈值成为主要召回策略。我在知识库项目里就是把阈值放在配置中心,方便随时调,而不是写死在代码里。

5.6 检索、重排、生成三层分离思维

很多人把RAG理解成“检索后直接生成”,但在生产环境,中间其实还可以再加一层rerank。简单说,向量检索先把候选文档从几十万片段里捞回来,TopK取20个;再用一个rerank模型(比如专门做文本排序的模型)对这20个候选重新打分,最终只把分数最高的5个送给大模型。这样能把向量检索的粗召回和模型的精排序结合起来。如果你不想额外引入服务,也可以退而求其次,用大模型自己判断哪个文档更相关,但那样成本高。我的经验是,初期阶段先把基础链路跑通,等知识库扩大到几千份文档以上,再考虑引入rerank。过早优化会让你陷入无休止的调参循环。

6. 工程化增强:从Demo到能用、好用

6.1 流式输出:给用户更快的反馈

把chatModel.call()换成chatModel.stream(),响应就变成流式的了。用户的体验会立刻变好,不用等好几秒才能看到第一个字,而是像使用ChatGPT一样一个字一个字蹦出来。Spring AI对流式封装得不错,在Web应用里可以用text/event-stream返回,Controller返回Flux<String>即可。如果你通过SSE推送给前端,注意设置响应头Content-Type: text/event-stream;charset=UTF-8。我做过一个接入前端的版本,体验和直接调官方Chat差距不大。

6.2 多租户与权限过滤:让不同部门看到不同资料

企业内部知识库必然牵扯权限。同一条操作手册,普通员工和管理员能看到的内容不一样。千万不能把所有文档灌进同一个向量库,让所有人检索同一套数据。Spring AI的metadata过滤在这里非常有用。你在写入文档时,把departmentId、permissionLevel写进metadata;检索时构造过滤条件,只检索当前用户有权限的文档。比如:

SearchRequest request = SearchRequest.builder() .query(question) .topK(5) .filterExpression("departmentId == 'finance' && permissionLevel <= 2") .build();

注意filterExpression语法不同版本有差异,不一定叫这个名字,但核心思路一样:检索前先按权限缩小范围。不要让模型回答出本来没权限看的资料,这个教训我从第一版就牢牢记住。

6.3 定时增量更新与来源追踪

文档不是一成不变的,知识库需要持续更新。你可以写一个定时任务扫描文件目录,把有变化的文件识别出来,重新切分、向量化、覆盖旧文档。同时,在回答时保留来源信息。我返回接口的数据结构一般是这样:

{ "answer": "根据资料,退款申请需要在7个工作日内完成审核。", "sources": [ { "source": "docs/refund-policy.pdf", "chunk": "退款审核周期为7个工作日..." }, { "source": "docs/faq.md", "chunk": "如果超过7个工作日,请联系财务..." } ] }

这样做的好处是,即便某个回答是错的,用户也能顺着来源去校验。在内部知识库场景里,“可追溯”比“答得漂亮”重要得多。我把这个思想写进了项目文档,团队大家一致觉得值。

6.4 私有化部署:换成本地模型和免费向量库

最后说一下私有化。有些公司数据不能出内网,这种情况你要把ChatModel和EmbeddingModel全切成本地部署。Spring AI同样支持,比如接入Ollama本地模型服务的spring-ai-ollama,或者接入HuggingFace推理服务。只需要新增依赖、修改base-url,代码逻辑依然复用。向量库也可以用本地的Chroma或LanceDB,甚至直接还是在PGVector上,不迁移。我帮朋友部署过一个纯内网版本,聊天模型和向量模型都跑在一台GPU机器上,问答效果虽然比商业模型稍弱,但数据安全性完全掌控在自己手里。对于合规比较严格的公司,这是最后的底线方案。

最后再分享一点体会。RAG这件事,单看每一步都不难,但组合起来细节极多。很多人在网上抄一段代码能跑通第一个Demo,一上真实数据就发现效果不行,然后怀疑模型、怀疑向量库。根据我的实际经验,大部分问题出在文档切分、重复数据、检索阈值这三个容易被忽略的地方。越是基础的部分,越值得认真打磨。你把这几个步骤理顺了,再复杂的知识库需求,本质上都是在这条链路上做扩展。

对我个人来说,Spring AI最有价值的地方不是某个类封装得多优雅,而是让Java后端团队能低门槛地踏入大模型应用开发。你不需要从零去学Python框架,也不用维护两套服务,只靠熟悉的Spring生态,就能把一套过得去的RAG知识库从想法变成产品。后续我会继续把rerank、混合检索、反馈闭环这些模块一点点补进这个项目。如果你也在做类似的事,建议先跑通今天这条最小链路,再回头看哪些环节最痛,然后针对性优化。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询