spring-ai + ollama + milvus 实现本地知识库 RAG 向量增强检索
一、大模型 RAG
现有大语言模型的记忆局限与 RAG 技术
当前所有的大语言模型(LLM)实际上并不具备真正的记忆功能。例如,ChatGPT 所谓的“记忆”能力,仅仅是通过在接收用户输入时,将最近几轮的对话内容合并进提示词(prompt),然后再交给模型处理。然而,LLM 都受 Token 数量的约束,这意味着当输入文本超出一定范围,较早的对话内容就会被遗忘。
AI 依赖知识库回答问题的挑战
如果希望 AI 基于知识库进行回答,会遇到以下几个问题:
- 缺乏持久记忆:每次提问都需要携带完整的知识库内容,以便 AI 解析,导致处理大规模文本的过程极其低效。
- 高昂的成本:当前 AI 按 Token 计费,提交长文本会带来较高的费用。
- 隐私与安全问题:许多知识库属于内部资料,不适合直接暴露给公有 AI 模型。
RAG 技术:优化 AI 知识获取
为了解决上述问题,RAG(Retrieval-Augmented Generation,检索增强生成)技术应运而生,其主要流程如下:
- 数据向量化:先通过嵌入式模型对文档数据进行向量化处理,并存储到向量数据库中。
- 智能检索:用户提问时,AI 会先将问题向量化,并从向量数据库中检索出最相关的内容。
- 动态组合提示词:将检索到的相关文档与用户问题拼接成新的 Prompt,并提交给大语言模型处理。
- 优化响应效果:这种方式减少了对 LLM 内部记忆的依赖,提高了回答的准确性,同时减少 Token 消耗,从而降低使用成本。
RAG 技术的引入,使得 AI 在知识库驱动的应用场景中更加高效、精准,同时保障了数据的隐私性和可控性。
二、向量数据库
目前市场流行的向量数据库有很多,例如 milvus、elasticsearch、redistock、qdrant。
以 milvus 为例进行 docker 部署,milvus 官方单机部署文档
https://www.milvus-io.com/getstarted/standalone/install_standalone-docker
一键启动
wget https://github.com/milvus-io/milvus/releases/download/v{{var.milvus_release_tag}}/milvus-standalone-docker-compose.yml -O docker-compose.yml
三、本地部署大模型
近年来,许多组织开源了自研的大模型,使得本地部署这些大模型成为可能。本文介绍如何在本地部署大模型及相关工具。
本地部署方法
以下是几种常见的本地部署大模型的方法:
- VLLM
- Ollama
Ollama 是一个极具优势的选择。
Ollama 的优势
- 安装与运行极其简单,开箱即用
- 提供类似 OpenAI 的 API,便于集成
- 内置模型仓库,涵盖市面上主流的大模型
- 已集成至 SpringAI,开发者可以直接使用
📌 官方网站:Ollama
Ollama 安装指南
📖 安装参考:点击查看
macOS & Windows 下载安装
Ollama 常用命令
Ollama 的命令风格类似 Docker,主要命令如下:
sh
ollama serve # 启动 Ollama
ollama create # 从模型文件创建模型
ollama show # 显示模型信息
ollama run # 运行模型(若未下载,会自动拉取)
ollama pull # 从模型仓库拉取模型
ollama push # 将模型推送至仓库
ollama list # 列出已下载的模型
ollama ps # 显示当前运行的模型
ollama cp # 复制模型
ollama rm # 删除模型
Ollama模型库
- 模型库搜索 地址
运行模型
ollama run deepseek-r1:14b
四、SpringAI
SpringAI 旨在简化 AI 功能集成到应用程序中的过程,同时避免额外的复杂性,让开发更加高效。
功能支持
- 多种 AI 模型:支持 Chat、Embeddings、Image 等模型。
- 多种向量数据库:兼容 Qdrant、ElasticSearch、Redis、Milvus 等主流向量数据库。
- 高级功能:支持高级 Prompts 处理、数据管道等。
环境要求
- Java:要求 Java 17 及以上版本
- Spring Boot:需使用 Spring Boot 3.2+ 版本
SpringAI 集成
在项目中引入最新的 SpringAI 依赖。
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.ai</groupId>-->
<!-- <artifactId>spring-ai-qdrant-store-spring-boot-starter</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-milvus-store-spring-boot-starter</artifactId>
</dependency>
引入处理 PDF 文档相关依赖。
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
相关配置 application.yml
spring:
application:
name: springboot-rag
threads:
virtual:
enabled: false
http:
client:
connect-timeout: 30s
read-timeout: 600s
ai:
ollama:
base-url: 'http://192.168.1.200:11434'
embedding:
model: 'deepseek-r1:14b'
chat:
model: 'deepseek-r1:14b'
options:
temperature: 0.3
top-k: 3
top-p: 0.2
num-g-p-u: 1 # enable Metal gpu on MAC
vectorstore:
milvus:
collectionName: collection_02
client:
host: 192.168.1.204
port: 19530
connect-timeout-ms: 30000
initialize-schema: true
embeddingDimension: 20480
indexType: IVF_FLAT
databaseName: 'default'
metricType: COSINE
# qdrant:
# host: 192.168.1.205
# port: 6334
# collection-name: collection_02
依赖已经引入了 spring-ai-milvus-store-spring-boot-starter,并且配置的 vectorstore 指定了 milvus 向量数据库,starter 启动的时候会实例化 VectorStore
系统提示词 system.pmt 配置
以下是上下文信息:
---------------------
{documents}
---------------------
根据上下文信息而非已有知识回答问题。
答案内容只需要结果,不需要描述因果关系,答案无需重复描述问题,答案无需描述推理过程。
如果您的数据库仍无法回答该问题,请回复“不知道”。
请用简体中文作答。
问题:{question}
答案:
读取知识库文档
@Slf4j
@Service
public class DataLibServiceImpl implements DataLibService {
@Value("classpath:/data/data_01.pdf")
private Resource documentResource;
private final VectorStore vectorStore;
public DataLibServiceImpl(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
@Override
public void loadData() {
DocumentReader documentReader = null;
if (this.documentResource.getFilename() == null) {
log.info("知识库不存在");
return;
}
if (this.documentResource.getFilename().endsWith(".pdf")) {
documentReader = new PagePdfDocumentReader(this.documentResource, PdfDocumentReaderConfig.builder().withPageExtractedTextFormatter(ExtractedTextFormatter.builder().withNumberOfBottomTextLinesToDelete(3).withNumberOfTopPagesToSkipBeforeDelete(3).build()).withPagesPerDocument(3).build());
} else if (this.documentResource.getFilename().endsWith(".txt")) {
documentReader = new TextReader(this.documentResource);
} else if (this.documentResource.getFilename().endsWith(".json")) {
documentReader = new JsonReader(this.documentResource);
}
if (documentReader != null) {
var textSplitter = new TokenTextSplitter();
log.info("知识库写入向量数据库");
List<Document> documents = textSplitter.apply(documentReader.get());
List<Document> result = new ArrayList<>();
for (Document document : documents) {
Document item = new Document(IdUtil.fastUUID(), document.getText(), document.getMetadata());
result.add(item);
}
this.vectorStore.accept(result);
log.info("知识库写书向量数据库完成");
}
}
@Override
public long count() {
return this.vectorStore.similaritySearch("*").size();
}
}
通过 controller 执行加载动作
@PostMapping("/data/load")
public ResponseEntity<String> load() {
try {
dataLibService.loadData();
return ResponseEntity.ok("知识库索引成功!");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("知识库索引失败: " + e.getMessage());
}
}
通过 controller 问答
@GetMapping("/ask")
public Map findAnswer(@RequestParam(value = "question", defaultValue = "你是谁?") String question) {
String answer = this.ragService.findAnswer(question);
Map map = new LinkedHashMap();
map.put("question", question);
map.put("answer", answer);
return map;
}
向量数据检索过程
@Override
public String findAnswer(String query) {
logger.info("问题{}", query);
Message message = getRelevantDocs(query);
return aiClient.prompt(new Prompt(List.of(message, new UserMessage(query))))
.call()
.content();
}
private Message getRelevantDocs(String query) {
List<Document> similarDocuments = vectorStore.similaritySearch(query);
if (logger.isInfoEnabled()) {
logger.info("检索到知识库数据 {}.", similarDocuments.size());
}
String documents = similarDocuments.stream().map(Document::getText).collect(Collectors.joining("\n"));
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(this.systemPromptResource);
return systemPromptTemplate.createMessage(Map.of("documents", documents, "question", query));
}