Java 开发者零成本构建 RAG 知识库:Spring AI Alibaba + Ollama 搭建本地 RAG 知识库

# Java 开发者零成本构建 RAG 知识库:Spring AI Alibaba + Ollama 搭建本地 RAG 知识库

大模型再聪明,也不了解你的公司内部文档。RAG 就是让通用模型学会"你的知识"的最佳方案。


# 为什么需要 RAG?

假设你是一家公司的技术负责人,团队积累了大量内部文档——产品手册、运维手册、技术规范、FAQ。现在你想做一个智能问答系统,让员工可以用自然语言提问。

直接把这些文档丢给大模型?不现实。原因有三:

  1. 上下文窗口有限:即使是百万 Token 的模型,也无法一次性塞入整个公司的知识库
  2. 成本高昂:每次对话都发送大量文档,Token 费用难以承受
  3. 知识更新困难:文档变更时需要重新训练或重新输入

RAG(检索增强生成,Retrieval-Augmented Generation)完美解决了这个问题。它的核心思路很简单:

用户提问 → 从知识库中检索相关片段 → 把片段 + 问题一起交给大模型 → 大模型基于上下文回答
1

整个过程不需要训练模型,只需要在每次对话时"临时补充"相关知识。


# 一、技术架构

我们要搭建的系统由四个核心组件组成:

┌─────────────────────────────────────────────────────────┐
│                     用户请求                              │
│                        │                                 │
│                        ▼                                 │
│  ┌───────────────────────────────────────────────────┐   │
│  │         Spring AI Alibaba 应用 (Java)              │   │
│  │                                                    │   │
│  │  ① 接收问题                                         │   │
│  │  ② 将问题转为向量 (Embedding)                       │   │
│  │  ③ 在向量库中检索最相关的知识片段                     │   │
│  │  ④ 组装 Prompt (知识片段 + 用户问题)                 │   │
│  │  ⑤ 调用大模型生成回答                                │   │
│  │  ⑥ 流式返回结果                                     │   │
│  └───────┬───────────────────────┬───────────────────┘   │
│          │                       │                       │
│          ▼                       ▼                       │
│  ┌───────────────┐    ┌───────────────────┐              │
│  │  Ollama       │    │   向量数据库        │              │
│  │  (本地模型)    │    │   (Chroma/Milvus)  │              │
│  │               │    │                   │              │
│  │  · Embedding  │    │   存储文档向量       │              │
│  │  · Chat       │    │   支持语义检索       │              │
│  └───────────────┘    └───────────────────┘              │
└─────────────────────────────────────────────────────────┘
1
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
1
2
3
4
5

验证模型是否就绪:

ollama list
1

你应该能看到两个模型都出现在列表中。

# 2.2 启动向量数据库

这里选择 Chroma,因为它足够轻量——甚至不需要单独部署服务,可以通过嵌入式模式直接在 Java 进程中运行。但为了演示更通用的场景,我们用 Docker 启动一个独立服务:

docker run -d \
  --name chroma \
  -p 8000:8000 \
  -v chroma-data:/chroma/chroma \
  chromadb/chroma:latest
1
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>
1
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
1
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 的第一步是把你的文档"喂"给系统。这个过程分为四个阶段:

原始文档 → 文本提取 → 文本分块 → 向量化 → 存入向量库
1

在 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("知识库初始化完成");
    }
}
1
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,每页作为一个 Document
  • TokenTextSplitter:按 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();
    }
}
1
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。它的作用是在每次对话时自动执行以下操作:

  1. 将用户问题转为向量
  2. 在向量库中检索最相关的文本块
  3. 将检索到的文本块注入到 Prompt 中
  4. 调用大模型生成回答

你不需要手动拼接 Prompt——Spring AI 已经帮你处理好了。

# 3.5 自定义系统提示词

默认的 Prompt 模板可能不够贴合你的业务场景。可以通过自定义系统提示词来约束模型的行为:

src/main/resources/prompts/system-rag.st 中创建提示词模板:

你是一个专业的技术文档助手。请严格基于以下参考资料回答问题。

参考资料:
{question_answer_context}

要求:
1. 仅使用参考资料中的信息作答
2. 如果参考资料中没有相关信息,请明确告知
3. 回答要准确、简洁、有条理
4. 使用中文回答
1
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();
    }
}
1
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);
    }
}
1
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的保修期是多久?"
1

你会看到回答像打字机一样逐字输出,而不是等待全部内容生成后才返回。

# 对比效果

场景 直接问模型 RAG 增强后
问通用知识 ✅ 回答准确 ✅ 回答准确
问公司内部政策 ❌ 不知道/编造 ✅ 基于文档回答
问产品技术细节 ❌ 泛泛而谈 ✅ 引用具体参数

# 五、生产级优化

上面的实现已经能跑通完整的 RAG 流程。但如果要用于生产环境,还需要考虑以下优化方向:

# 5.1 混合检索(向量 + 全文)

纯向量检索擅长语义匹配,但对精确关键词(如产品型号、错误码)可能不够准确。可以结合全文检索:

SearchRequest searchRequest = SearchRequest.builder()
        .topK(5)
        .similarityThreshold(0.6)  // 设置相似度阈值,过滤低质量匹配
        .build();
1
2
3
4

如果使用的向量数据库支持(如 Elasticsearch、Milvus),可以配置混合检索策略。

# 5.2 重排序(Rerank)

检索到的文本块按相关性排序后,可以再用一个 Rerank 模型进行精排,确保最相关的内容被优先使用:

ChatClient runtimeChatClient = ChatClient.builder(chatModel)
        .defaultAdvisors(new RetrievalRerankAdvisor(
                vectorStore,
                rerankModel,      // 重排序模型
                searchRequest,
                promptTemplate,
                0.2               // 最低分数阈值
        ))
        .build();
1
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
1
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"
));
1
2
3
4
5
6

检索时按元数据过滤:

SearchRequest.builder()
        .topK(3)
        .filterExpression("source = 'product-manual' AND department = 'engineering'")
        .build();
1
2
3
4

这在多部门、多产品的场景中非常实用——可以确保检索结果只来自相关的文档范围。

# 5.5 增量更新

生产环境中知识库会持续更新。不需要每次都全量重建,可以按需增删:

// 添加新文档
vectorStore.add(newDocuments);

// 按 ID 删除旧文档
vectorStore.delete(List.of("doc-id-1", "doc-id-2"));
1
2
3
4
5

# 六、常见问题排查

Q:检索结果不相关怎么办?

A:排查顺序:

  1. 确认 Embedding 模型已正确加载(ollama list 检查)
  2. 检查分块大小——过大的块会引入噪音,过小的块会丢失上下文
  3. 尝试降低 similarityThreshold 阈值
  4. 考虑换用更强的 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 应用,本质上就是三件事:

  1. 把文档变成向量——读取、分块、Embedding、存储
  2. 把问题变成答案——检索相关片段,组装 Prompt,调用模型
  3. 把答案交给用户——流式输出,实时响应

Spring AI Alibaba 的价值在于,它把这套流程封装成了声明式的 API。你不需要手动处理向量计算、Prompt 拼接、HTTP 调用这些底层细节,只需要关注业务逻辑本身。

对于 Java 团队来说,这意味着:

  • 学习成本低:Spring Boot 开发者可以零门槛上手
  • 技术栈统一:不需要引入 Python 生态
  • 数据不出本机:Ollama + 本地向量库,完全离线运行
  • 可扩展性强:同一套代码可以无缝切换到云端模型或其他向量数据库