Java 开发者零成本构建 RAG 知识库:Spring AI Alibaba + Ollama 搭建本地 RAG 知识库
# Java 开发者零成本构建 RAG 知识库:Spring AI Alibaba + Ollama 搭建本地 RAG 知识库
大模型再聪明,也不了解你的公司内部文档。RAG 就是让通用模型学会"你的知识"的最佳方案。
# 为什么需要 RAG?
假设你是一家公司的技术负责人,团队积累了大量内部文档——产品手册、运维手册、技术规范、FAQ。现在你想做一个智能问答系统,让员工可以用自然语言提问。
直接把这些文档丢给大模型?不现实。原因有三:
- 上下文窗口有限:即使是百万 Token 的模型,也无法一次性塞入整个公司的知识库
- 成本高昂:每次对话都发送大量文档,Token 费用难以承受
- 知识更新困难:文档变更时需要重新训练或重新输入
RAG(检索增强生成,Retrieval-Augmented Generation)完美解决了这个问题。它的核心思路很简单:
用户提问 → 从知识库中检索相关片段 → 把片段 + 问题一起交给大模型 → 大模型基于上下文回答
整个过程不需要训练模型,只需要在每次对话时"临时补充"相关知识。
# 一、技术架构
我们要搭建的系统由四个核心组件组成:
┌─────────────────────────────────────────────────────────┐
│ 用户请求 │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Spring AI Alibaba 应用 (Java) │ │
│ │ │ │
│ │ ① 接收问题 │ │
│ │ ② 将问题转为向量 (Embedding) │ │
│ │ ③ 在向量库中检索最相关的知识片段 │ │
│ │ ④ 组装 Prompt (知识片段 + 用户问题) │ │
│ │ ⑤ 调用大模型生成回答 │ │
│ │ ⑥ 流式返回结果 │ │
│ └───────┬───────────────────────┬───────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌───────────────┐ ┌───────────────────┐ │
│ │ Ollama │ │ 向量数据库 │ │
│ │ (本地模型) │ │ (Chroma/Milvus) │ │
│ │ │ │ │ │
│ │ · Embedding │ │ 存储文档向量 │ │
│ │ · Chat │ │ 支持语义检索 │ │
│ └───────────────┘ └───────────────────┘ │
└─────────────────────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 组件选型
| 组件 | 选择 | 理由 |
|---|---|---|
| 应用框架 | Spring AI Alibaba | Java 生态原生,API 统一,企业级能力丰富 |
| 模型运行时 | Ollama | 一条命令启动模型,封装所有复杂性 |
| Chat 模型 | deepseek-r1:8b | 中文能力强,8B 版本在消费级硬件可运行 |
| Embedding 模型 | nomic-embed-text | 轻量、效果好,专门用于文本向量化 |
| 向量数据库 | Chroma | 轻量级,零配置,适合快速原型验证 |
# 二、环境搭建
# 2.1 启动 Ollama
如果你还没有安装 Ollama,可以参考之前的本地部署教程。安装完成后,拉取我们需要的两个模型:
# 对话模型(负责生成最终回答)
ollama pull deepseek-r1:8b
# 向量模型(负责将文本转为向量)
ollama pull nomic-embed-text
2
3
4
5
验证模型是否就绪:
ollama list
你应该能看到两个模型都出现在列表中。
# 2.2 启动向量数据库
这里选择 Chroma,因为它足够轻量——甚至不需要单独部署服务,可以通过嵌入式模式直接在 Java 进程中运行。但为了演示更通用的场景,我们用 Docker 启动一个独立服务:
docker run -d \
--name chroma \
-p 8000:8000 \
-v chroma-data:/chroma/chroma \
chromadb/chroma:latest
2
3
4
5
启动后访问 http://localhost:8000/api/v1/heartbeat,如果返回心跳信息说明服务正常。
向量数据库的其他选择: 如果你的场景需要生产级部署,可以考虑 Milvus(支持分布式、亿级向量)、Elasticsearch(向量 + 全文混合检索)、或 PostgreSQL + pgvector(已有 PG 基础设施的团队)。Spring AI 对这些都有官方集成。
# 三、项目实现
# 3.1 创建 Spring Boot 项目
使用 Spring Initializr 创建一个 Spring Boot 3.x 项目,添加 Web 依赖后,引入以下依赖:
<dependencies>
<!-- Spring AI Ollama 集成 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Chroma 向量存储 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-chroma-store-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<!-- PDF 文档读取 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 3.2 配置文件
spring:
ai:
ollama:
base-url: http://localhost:11434
chat:
model: deepseek-r1:8b
options:
temperature: 0.7
num-predict: 4096
embedding:
model: nomic-embed-text
vectorstore:
chroma:
client:
host: http://localhost
port: 8000
collection-name: knowledge-base
initialize-schema: true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
配置解读:
chat.model:指定用于对话的模型embedding.model:指定用于文本向量化的模型vectorstore.chroma:向量数据库连接信息,initialize-schema: true表示自动创建集合
# 3.3 知识库初始化——将文档变为向量
RAG 的第一步是把你的文档"喂"给系统。这个过程分为四个阶段:
原始文档 → 文本提取 → 文本分块 → 向量化 → 存入向量库
在 Spring AI 中,这一切可以用很简洁的代码完成:
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.io.FileSystemResource;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class KnowledgeBaseInitializer implements ApplicationRunner {
private final VectorStore vectorStore;
public KnowledgeBaseInitializer(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
@Override
public void run(ApplicationArguments args) throws Exception {
// 1. 读取 PDF 文档
PagePdfDocumentReader reader = new PagePdfDocumentReader(
new FileSystemResource("knowledge/product-manual.pdf")
);
List<Document> documents = reader.get();
System.out.println("读取到 " + documents.size() + " 页文档");
// 2. 文本分块(按 Token 切分,避免单个块过大)
TokenTextSplitter splitter = new TokenTextSplitter();
List<Document> chunks = splitter.apply(documents);
System.out.println("切分为 " + chunks.size() + " 个文本块");
// 3. 向量化并存入向量库(Spring AI 自动完成 Embedding + 存储)
vectorStore.add(chunks);
System.out.println("知识库初始化完成");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
关键点说明:
PagePdfDocumentReader:按页读取 PDF,每页作为一个 DocumentTokenTextSplitter:按 Token 数量切分文本,默认每块约 800 Token,重叠 20%(避免关键信息被截断)vectorStore.add():这一步会自动调用 Embedding 模型将文本转为向量,然后存入 Chroma
关于分块策略: 分块大小直接影响检索效果。块太小→上下文不足;块太大→引入噪音。一般建议 500-1000 Token,重叠 10-20%。对于代码文档,建议按函数/类边界切分;对于问答文档,可以按问答对切分。
# 3.4 RAG 核心服务
知识库就绪后,接下来实现问答逻辑:
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
@Service
public class RagChatService {
private final ChatClient chatClient;
private final VectorStore vectorStore;
public RagChatService(ChatClient.Builder chatClientBuilder,
VectorStore vectorStore) {
this.vectorStore = vectorStore;
this.chatClient = chatClientBuilder.build();
}
/**
* RAG 问答(流式输出)
*/
public Flux<String> chat(String question) {
// 构建检索请求:取最相关的 3 个文本块
SearchRequest searchRequest = SearchRequest.builder()
.topK(3)
.build();
return chatClient.prompt()
.advisors(new QuestionAnswerAdvisor(vectorStore, searchRequest))
.user(question)
.stream()
.content();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
这里的核心是 QuestionAnswerAdvisor。它的作用是在每次对话时自动执行以下操作:
- 将用户问题转为向量
- 在向量库中检索最相关的文本块
- 将检索到的文本块注入到 Prompt 中
- 调用大模型生成回答
你不需要手动拼接 Prompt——Spring AI 已经帮你处理好了。
# 3.5 自定义系统提示词
默认的 Prompt 模板可能不够贴合你的业务场景。可以通过自定义系统提示词来约束模型的行为:
在 src/main/resources/prompts/system-rag.st 中创建提示词模板:
你是一个专业的技术文档助手。请严格基于以下参考资料回答问题。
参考资料:
{question_answer_context}
要求:
1. 仅使用参考资料中的信息作答
2. 如果参考资料中没有相关信息,请明确告知
3. 回答要准确、简洁、有条理
4. 使用中文回答
2
3
4
5
6
7
8
9
10
然后在代码中加载它:
import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor;
import org.springframework.core.io.Resource;
import org.springframework.beans.factory.annotation.Value;
@Service
public class RagChatService {
@Value("classpath:/prompts/system-rag.st")
private Resource promptTemplate;
// ...
public Flux<String> chat(String question) {
SearchRequest searchRequest = SearchRequest.builder()
.topK(3)
.build();
// 加载自定义提示词模板
String template = new String(promptTemplate.getInputStream().readAllBytes());
return chatClient.prompt()
.advisors(new QuestionAnswerAdvisor(
vectorStore,
searchRequest,
template))
.user(question)
.stream()
.content();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 3.6 对外暴露 REST 接口
最后,提供一个 HTTP 接口供前端调用:
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
@RestController
@RequestMapping("/api/rag")
public class RagChatController {
private final RagChatService ragChatService;
public RagChatController(RagChatService ragChatService) {
this.ragChatService = ragChatService;
}
@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chat(@RequestParam String question) {
if (question == null || question.isBlank()) {
return Flux.just("请输入问题");
}
return ragChatService.chat(question);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
使用 Server-Sent Events(SSE)格式 TEXT_EVENT_STREAM_VALUE,前端可以实时接收流式响应。
# 四、测试验证
启动应用后,用 curl 测试:
curl -N "http://localhost:8080/api/rag?question=产品A的保修期是多久?"
你会看到回答像打字机一样逐字输出,而不是等待全部内容生成后才返回。
# 对比效果
| 场景 | 直接问模型 | RAG 增强后 |
|---|---|---|
| 问通用知识 | ✅ 回答准确 | ✅ 回答准确 |
| 问公司内部政策 | ❌ 不知道/编造 | ✅ 基于文档回答 |
| 问产品技术细节 | ❌ 泛泛而谈 | ✅ 引用具体参数 |
# 五、生产级优化
上面的实现已经能跑通完整的 RAG 流程。但如果要用于生产环境,还需要考虑以下优化方向:
# 5.1 混合检索(向量 + 全文)
纯向量检索擅长语义匹配,但对精确关键词(如产品型号、错误码)可能不够准确。可以结合全文检索:
SearchRequest searchRequest = SearchRequest.builder()
.topK(5)
.similarityThreshold(0.6) // 设置相似度阈值,过滤低质量匹配
.build();
2
3
4
如果使用的向量数据库支持(如 Elasticsearch、Milvus),可以配置混合检索策略。
# 5.2 重排序(Rerank)
检索到的文本块按相关性排序后,可以再用一个 Rerank 模型进行精排,确保最相关的内容被优先使用:
ChatClient runtimeChatClient = ChatClient.builder(chatModel)
.defaultAdvisors(new RetrievalRerankAdvisor(
vectorStore,
rerankModel, // 重排序模型
searchRequest,
promptTemplate,
0.2 // 最低分数阈值
))
.build();
2
3
4
5
6
7
8
9
# 5.3 云端模型混合使用
本地模型推理速度受硬件限制。可以采取"本地 Embedding + 云端 Chat"的混合策略:
spring:
ai:
# 本地 Embedding(免费、快速)
ollama:
embedding:
model: nomic-embed-text
enabled: true
chat:
enabled: false # 关闭本地 Chat
# 云端 Chat 模型(高质量回答)
dashscope:
api-key: ${DASHSCOPE_API_KEY}
chat:
model: qwen-plus
2
3
4
5
6
7
8
9
10
11
12
13
14
15
向量检索是计算密集型操作,本地跑 Embedding 模型完全够用;而最终的回答生成交给云端更强的模型,兼顾成本和效果。
# 5.4 元数据过滤
为文档块添加元数据,可以实现更精细的检索控制:
Document doc = new Document(content, Map.of(
"source", "product-manual",
"version", "2.0",
"department", "engineering",
"language", "zh"
));
2
3
4
5
6
检索时按元数据过滤:
SearchRequest.builder()
.topK(3)
.filterExpression("source = 'product-manual' AND department = 'engineering'")
.build();
2
3
4
这在多部门、多产品的场景中非常实用——可以确保检索结果只来自相关的文档范围。
# 5.5 增量更新
生产环境中知识库会持续更新。不需要每次都全量重建,可以按需增删:
// 添加新文档
vectorStore.add(newDocuments);
// 按 ID 删除旧文档
vectorStore.delete(List.of("doc-id-1", "doc-id-2"));
2
3
4
5
# 六、常见问题排查
Q:检索结果不相关怎么办?
A:排查顺序:
- 确认 Embedding 模型已正确加载(
ollama list检查) - 检查分块大小——过大的块会引入噪音,过小的块会丢失上下文
- 尝试降低
similarityThreshold阈值 - 考虑换用更强的 Embedding 模型(如
bge-m3)
Q:回答速度慢?
A:8B 模型在 CPU 上推理确实较慢。优化方向:
- 换用更小的模型(1.5B/7B)
- 确保 Ollama 使用了 GPU 加速
- 减少
num-predict上限 - 或切换到云端模型
Q:模型回答中带有 <think> 思考过程?
A:DeepSeek-R1 是推理模型,会输出思考过程。处理方式:
- 代码层面用正则过滤
<think>.*?</think>之间的内容 - 或换用非推理版本的模型(如
qwen2.5)
Q:向量库连接失败?
A:确认 Chroma 服务已启动且端口正确。如果使用了 initialize-schema: true,Spring AI 会自动创建 Collection。首次启动时查看日志确认连接状态。
# 七、总结
搭建一个 RAG 应用,本质上就是三件事:
- 把文档变成向量——读取、分块、Embedding、存储
- 把问题变成答案——检索相关片段,组装 Prompt,调用模型
- 把答案交给用户——流式输出,实时响应
Spring AI Alibaba 的价值在于,它把这套流程封装成了声明式的 API。你不需要手动处理向量计算、Prompt 拼接、HTTP 调用这些底层细节,只需要关注业务逻辑本身。
对于 Java 团队来说,这意味着:
- 学习成本低:Spring Boot 开发者可以零门槛上手
- 技术栈统一:不需要引入 Python 生态
- 数据不出本机:Ollama + 本地向量库,完全离线运行
- 可扩展性强:同一套代码可以无缝切换到云端模型或其他向量数据库