本节需求
以大模型向量存储的方式,提交本地文件到知识库。并在 AI 对话中增强检索知识库符合 AI 对话内容的资料,合并提交问题。
RAG端到端流程
文件上传与解析:前端/接口上传文件 → 用 TikaDocumentReader 解析为文档 Document。
文本拆分:TokenTextSplitter 拆分文本为片段(按 token/句子/段落等策略)。
文本标记:为每个 Document 片段写入 metadata,如 knowledge: "知识库名称",用于多库隔离。
向量化存储:
使用 Ollama 的嵌入模型(nomic-embed-text)生成向量。
将片段内容 + metadata + 向量写入 PgVectorStore(PostgreSQL)。
检索增强对话:
问题来临时,基于向量相似度检索最相关的文档片段(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)相关的参数:
model: nomic-embed-text
指定使用的嵌入模型为 nomic-embed-text
这个模型专门用于将文本转换为向量表示(嵌入向量)
嵌入向量可以用于语义搜索、文本相似度计算、聚类等任务
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: pool-name: HikariCP minimum-idle: 5 idle-timeout: 600000 maximum-pool-size: 10 auto-commit: true max-lifetime: 1800000 connection-timeout: 30000 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 docker exec -it vector_db bash psql -U postgres -d ai-rag-knowledge 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;@Slf4j @ActiveProfiles("dev") @SpringBootTest public class RAGTest { @Resource private OllamaChatClient ollamaChatClient; @Resource private TokenTextSplitter tokenTextSplitter; @Resource private PgVectorStore pgVectorStore; @Test public void upload () { TikaDocumentReader reader = new TikaDocumentReader ("./data/file.text" ); List<Document> documents = reader.get(); List<Document> documentSplitterList = tokenTextSplitter.apply(documents); documents.forEach(doc -> doc.getMetadata().put("knowledge" , "知识库名称" )); documentSplitterList.forEach(doc -> doc.getMetadata().put("knowledge" , "知识库名称" )); 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知识库按照预期运行了!