Ollama RAG 知识库上传、解析和验证

本节需求

以大模型向量存储的方式,提交本地文件到知识库。并在 AI 对话中增强检索知识库符合 AI 对话内容的资料,合并提交问题。

RAG端到端流程

  1. 文件上传与解析:前端/接口上传文件 → 用 TikaDocumentReader 解析为文档 Document
  2. 文本拆分:TokenTextSplitter 拆分文本为片段(按 token/句子/段落等策略)。
  3. 文本标记:为每个 Document 片段写入 metadata,如 knowledge: "知识库名称",用于多库隔离。
  4. 向量化存储:
    • 使用 Ollama 的嵌入模型(nomic-embed-text)生成向量。
    • 将片段内容 + metadata + 向量写入 PgVectorStore(PostgreSQL)。
  5. 检索增强对话:
    • 问题来临时,基于向量相似度检索最相关的文档片段(TopK、Filter)。
    • 把检索到的片段拼入 System Prompt,再交给聊天模型生成更准确的回答。

功能实现

1. 工程结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ai-rag-knowledge/
├── xfg-dev-tech-api/ # API 接口层
│ └── IAiService.java # 定义 AI 服务接口,供应用层调用
├── xfg-dev-tech-app/ # 应用层
│ ├── Application.java # Spring Boot 启动类,应用入口
│ ├── config/ # 配置类目录
│ │ ├── OllamaConfig.java # 配置 Ollama 嵌入模型与向量存储
│ │ ├── RedisClientConfig.java # 配置 Redis 客户端连接
│ │ └── RedisClientConfigProperties.java # Redis 客户端属性配置
│ └── resources/ # 配置文件目录
│ ├── application.yml # 通用配置文件
│ ├── application-dev.yml # 开发环境配置文件
│ └── logback-spring.xml # 日志配置文件
├── xfg-dev-tech-trigger/ # 触发器层
│ └── OllamaController.java # HTTP 控制器,处理 Ollama 相关请求
└── docs/ # 文档目录
└── dev-ops/ # 运维相关
└── pgvector/sql/ # PostgreSQL 向量扩展脚本
└── init.sql # 初始化 pgvector 扩展

2. 依赖管理

app模块加入:

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
</dependency>

分别是用于解析用户上传的文件,提取纯文本内容,以及存储向量化后的文编片段及其元数据。

3. 配置管理

application.yml种,添加了新的数据库相关配置,以及AI嵌入(Embedding)相关的参数:

  1. model: nomic-embed-text
  • 指定使用的嵌入模型为 nomic-embed-text
  • 这个模型专门用于将文本转换为向量表示(嵌入向量)
  • 嵌入向量可以用于语义搜索、文本相似度计算、聚类等任务
  1. options: num-batch: 512
  • 设置批处理大小为 512
  • 表示每次处理文本时,会将文本分成批次,每批包含 512 个token或文本片段
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
spring:
datasource:
driver-class-name: org.postgresql.Driver
username: postgres
password: postgres
url: jdbc:postgresql://<部署了postgresql的公网IP>:15432/ai-rag-knowledge
type: com.zaxxer.hikari.HikariDataSource
# hikari连接池配置
hikari:
#连接池名
pool-name: HikariCP
#最小空闲连接数
minimum-idle: 5
# 空闲连接存活最大时间,默认10分钟
idle-timeout: 600000
# 连接池最大连接数,默认是10
maximum-pool-size: 10
# 此属性控制从池返回的连接的默认自动提交行为,默认值:true
auto-commit: true
# 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认30分钟
max-lifetime: 1800000
# 数据库连接超时时间,默认30秒
connection-timeout: 30000
# 连接测试query
connection-test-query: SELECT 1
ai:
ollama:
base-url: http://<部署了ollama的公网IP>:11434
embedding:
options:
num-batch: 512
model: nomic-embed-text

4. pgvector配置

以下操作均在云服务器实现:

1
2
3
4
5
6
7
8
# 进入 vector_db 容器
docker exec -it vector_db bash

# 连接到 ai-rag-knowledge 数据库
psql -U postgres -d ai-rag-knowledge

# 在 PostgreSQL 提示符下创建 vector 扩展
CREATE EXTENSION IF NOT EXISTS vector;

退出容器:

1
2
ai-rag-knowledge=# \q
root@6a239908062e:/# exit

5. 代码实现

Ollama 配置类OllamaConfig.java

新增三项配置:

  • TokenTextSplitter:文本分割器
  • SimpleVectorStore:基于内存的向量数据库
  • PgVectorStore:基于 PostgreSQL 的向量数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Bean
public TokenTextSplitter tokenTextSplitter() {
return new TokenTextSplitter();
}

@Bean
public SimpleVectorStore simpleVectorStore(OllamaApi ollamaApi) {
OllamaEmbeddingClient embeddingClient = new OllamaEmbeddingClient(ollamaApi);
embeddingClient.withDefaultOptions(OllamaOptions.create().withModel("nomic-embed-text"));
return new SimpleVectorStore(embeddingClient);
}

@Bean
public PgVectorStore pgVectorStore(OllamaApi ollamaApi, JdbcTemplate jdbcTemplate) {
OllamaEmbeddingClient embeddingClient = new OllamaEmbeddingClient(ollamaApi);
embeddingClient.withDefaultOptions(OllamaOptions.create().withModel("nomic-embed-text"));
return new PgVectorStore(jdbcTemplate, embeddingClient);
}

6. 功能测试

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package cn.bugstack.xfg.dev.tech.test;

import com.alibaba.fastjson.JSON;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.document.Document;
import org.springframework.ai.ollama.OllamaChatClient;
import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.ai.reader.tika.TikaDocumentReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.PgVectorStore;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
* @author 渣渣熊
* @description RAG功能测试
* @create 2025-10-18 13:32
*/
@Slf4j
@ActiveProfiles("dev")
@SpringBootTest
public class RAGTest {
@Resource
private OllamaChatClient ollamaChatClient;
@Resource
private TokenTextSplitter tokenTextSplitter;
@Resource
private PgVectorStore pgVectorStore;

@Test
public void upload() {
// 1. 文本上传并解析
TikaDocumentReader reader = new TikaDocumentReader("./data/file.text");

List<Document> documents = reader.get();
// 2. 文本拆分
List<Document> documentSplitterList = tokenTextSplitter.apply(documents);

// 3. 文本标记
documents.forEach(doc -> doc.getMetadata().put("knowledge", "知识库名称"));
documentSplitterList.forEach(doc -> doc.getMetadata().put("knowledge", "知识库名称"));

// 4. 向量化存储
pgVectorStore.accept(documentSplitterList);

log.info("上传完成");
}

@Test
public void chat() {
String message = "王大瓜,哪年出生";

String SYSTEM_PROMPT = """
Use the information from the DOCUMENTS section to provide accurate answers but act as if you knew this information innately.
If unsure, simply state that you don't know.
Another thing you need to note is that your reply must be in Chinese!
DOCUMENTS:
{documents}
""";

SearchRequest request = SearchRequest.query(message).withTopK(5).withFilterExpression("knowledge == '知识库名称'");

List<Document> documents = pgVectorStore.similaritySearch(request);
String documentsCollectors = documents.stream().map(Document::getContent).collect(Collectors.joining());

Message ragMessage = new SystemPromptTemplate(SYSTEM_PROMPT).createMessage(Map.of("documents", documentsCollectors));

ArrayList<Message> messages = new ArrayList<>();
messages.add(new UserMessage(message));
messages.add(ragMessage);

ChatResponse chatResponse = ollamaChatClient.call(new Prompt(messages, OllamaOptions.create().withModel("deepseek-r1:1.5b")));

log.info("测试结果:{}", JSON.toJSONString(chatResponse));
}
}

需要在resource目录下创建data/file.text将相关的资料存入:王大瓜 1990年出生

这时候运行测试,运行结果摘要:

1
2
3
"output": {
"content": "你好!根据你提供的信息,王大瓜是1990年出生的。如果你有其他问题或需要帮助,请随时告诉我!"
}

可以看到,rag知识库按照预期运行了!