在当今信息爆炸的时代,如何让机器精准理解并回答基于特定文档的问题,已成为企业智能化转型的关键。检索增强生成(RAG)技术结合了信息检索与大型语言模型的优势,为解决这一难题提供了优雅方案。本文将手把手带你使用 Spring AI 和 Milvus 向量数据库,构建一个支持多格式文档解析、高效向量检索与流式响应的智能问答系统,并针对不同硬件环境提供两套完整的容器化部署方案。
一、架构概览与场景化部署路线选择
我们的系统核心目标是构建一个端到端的智能问答应用。技术栈以 Spring Boot 3.4 作为后端框架,集成 Spring AI 1.0.0-M1 来统一调用嵌入模型和 LLM,使用 Milvus 作为高性能向量数据库存储文档片段嵌入,并以 Vue3 构建交互式前端。整个系统设计充分考虑了容器化部署的便捷性,无论是使用 Docker 单机运行还是未来扩展到 K8s 集群,都能平滑过渡。
硬件资源往往是项目落地的第一道门槛。为此,我们精心设计了两条实施路径,你可以根据手头的服务器资源灵活选择:
- Path A(标准企业级):适用于拥有充足计算资源(如 8核16G 及以上)的环境,追求最佳的问答效果和响应速度。
- Path B(低配实验级):专为资源有限的个人开发者或测试环境(如 2核4G 云服务器)设计,核心目标是“跑通流程”,在有限资源下实现功能。
选择你的战斗路线:
| 特性 | Path A:标准企业级方案 (推荐) | Path B:低配/个人实验方案 (无 GPU) |
|---|---|---|
| 适用场景 | 生产环境、演示汇报、高性能要求 | 个人学习、阿里云低配 ECS、无显卡环境 |
| 硬件要求 | 至少 16GB 内存,建议有 NVIDIA 显卡 | 2核4G / 4核8G,纯 CPU |
| LLM 模型 | Qwen-7B (4-bit 量化) | Qwen2.5-1.5B (极致轻量) |
| Embedding | BGE-M3 (维度 1024) | BGE-Small-ZH (维度 512) |
| 预期性能 | 首字延迟 < 1s,推理流畅 | 首字延迟 < 2s,勉强流畅 |
| 关键手段 | 模型常驻内存、GPU 加速 | 开启 Swap 虚拟内存、模型降级 |
二、基础设施准备:容器化部署核心服务
无论选择哪条路径,我们都推荐使用 Docker 来部署核心依赖服务,这能保证环境的一致性与可复现性,也是迈向 容器编排 的第一步。
1. 启动 Milvus 向量数据库
Milvus 是系统的“记忆中枢”。通过 Docker 运行是最佳实践。对于 Path A,直接使用官方镜像即可。对于 Path B 的低配环境,关键在于资源限制。你需要修改 Docker 运行参数,通过环境变量(如 )来严格控制 Milvus 的内存使用,避免其拖垮整个服务器。mem_limit: 2g
# 下载并启动 Milvus
wget https://github.com/milvus-io/milvus/releases/download/v2.3.0/milvus-standalone-docker-compose.yml -O docker-compose.yml
sudo docker compose up -d
2. 配置 Ollama 与嵌入模型
Ollama 简化了本地大模型的运行。模型选择直接影响系统效果与资源消耗。
- Path A (效果优先): 使用能力更强的 7B 参数模型,以获得更准确的理解和生成能力。
# 对话模型
ollama pull qwen:7b
# 向量模型 (多语言支持好,维度 1024)
ollama pull bge-m3
- Path B (能跑优先): 最初可能尝试更小的模型,但可能会遇到模型不存在的问题:
# 对话模型 (1.5B 参数量,CPU 也能跑飞快)
ollama pull qwen2.5:1.5b
# 向量模型 (小巧,维度 512,注意修改 application.yml)
ollama pull bge-small-zh
错误提示:
root@iZ2zeh0qmg2jplpdlcz131Z:~# ollama pull bge-small-zh
pulling manifest
Error: pull model manifest: file does not exist
这是因为 并非 Ollama 官方库镜像。我们强烈推荐方案一:改用官方支持的 bge-small-zh。它是 BGE 系列的多语言旗舰模型,中文表现优异,且维护良好。bge-m3
拉取模型:
ollama pull bge-m3⚠️ 关键配置同步:更换模型后,必须在 Spring Boot 配置中同步修改向量维度。例如, 的维度是 1024,配置如下:bge-m3
spring:
ai:
ollama:
embedding:
model: bge-m3 # 修改这里
vectorstore:
milvus:
embedding-dimension: 1024 # ⚠️ 必须改为 1024如果你的环境实在苛刻,必须使用极小模型,则需手动导入 GGUF 文件,具体步骤涉及下载文件和创建 Modelfile,详见方案二。但为了省心和高检索准确率,请直接采用方案一。
三、后端工程:Spring Boot 应用构建详解
后端是系统的业务逻辑核心,我们将使用 Spring AI 提供的抽象,高效集成嵌入、检索和生成能力。
1. 依赖配置(避坑指南)
构建的第一步是确保 正确。我们使用了稳定版的 Spring Boot pom.xml 与里程碑版的 Spring AI 3.4.3。特别注意,需要指定 Lombok 版本(1.0.0-M1)以兼容 JDK 21,并添加 Spring 里程碑仓库。1.18.36
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.3</version>
</parent>
<properties>
<java.version>21</java.version>
<spring-ai.version>1.0.0-M1</spring-ai.version>
<!-- ⚠️ 必加:解决 JDK 21 兼容性问题 -->
<lombok.version>1.18.36</lombok.version>
</properties>
<dependencies>
<!-- Web & 流式响应 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- Spring AI 核心组件 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-milvus-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- ⚠️ 必加:Spring 里程碑仓库 -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots><enabled>false</enabled></snapshots>
</repository>
</repositories>
</project>
2. 核心配置与代码实现
在 中,关键是指定嵌入模型和向量存储的连接信息,确保与 Ollama 和 Milvus 的设置一致。application.yml
spring:
application:
name: policy-rag-system
ai:
ollama:
base-url: http://localhost:11434
chat:
# Path A 用 qwen:7b, Path B 用 qwen2.5:1.5b
model: qwen:7b
options:
temperature: 0.3 # 政策问答需严谨
embedding:
# Path A 用 bge-m3, Path B 用 bge-small-zh
model: bge-m3
vectorstore:
milvus:
client:
host: localhost
port: 19530
collection-name: policy_docs
# ⚠️ 关键点:bge-m3=1024, bge-small-zh=512
embedding-dimension: 1024
index-type: HNSW # 最快索引
核心业务逻辑分为两部分:
- 文档入库:通过 接口,解析上传的 PDF/Word,分块、生成向量并存入 Milvus。IngestionService.java
- RAG 问答:通过 接口,将用户问题向量化,在 Milvus 中检索相关文档片段,连同问题一起发送给 LLM 生成流式答案。RagService.java
文档入库核心代码示意:
@Service
@RequiredArgsConstructor
public class IngestionService {
private final VectorStore vectorStore;
public void processDocument(MultipartFile file) throws IOException {
// Tika 解析
TikaDocumentReader loader = new TikaDocumentReader(new InputStreamResource(file.getInputStream()));
// 切片 (TokenSplitter 防止语义切断)
TextSplitter splitter = new TokenTextSplitter(500, 100, 10, 10000, true);
List<Document> splitDocuments = splitter.apply(loader.get());
// 元数据注入
splitDocuments.forEach(doc -> doc.getMetadata().put("filename", file.getOriginalFilename()));
// 入库
vectorStore.add(splitDocuments);
}
}RAG流式问答核心代码示意:
@Service
@RequiredArgsConstructor
public class RagService {
private final ChatClient.Builder chatClientBuilder;
private final VectorStore vectorStore;
public Flux<String> streamAnswer(String query) {
ChatClient client = chatClientBuilder.build();
// 1. 检索
List<Document> docs = vectorStore.similaritySearch(
SearchRequest.query(query).withTopK(3).withSimilarityThreshold(0.6));
// 2. 组装上下文
String context = docs.stream().map(Document::getContent).collect(Collectors.joining("\n\n"));
String refs = docs.stream().map(d -> (String) d.getMetadata().get("filename")).distinct().collect(Collectors.joining(", "));
// 3. Prompt
String prompt = "基于以下政策上下文回答问题:\n" + context + "\n\n问题:" + query;
// 4. 流式返回 + 引用
return client.prompt(prompt).stream().content()
.concatWith(Flux.just("\n\n---\n 来源: " + refs));
}
}
[AFFILIATE_SLOT_1]
四、前端交互与低配环境终极调优
前端实现
使用 Vue 3 配合 和 fetch,可以轻松实现接收服务器端推送(SSE)的流式响应,并展示出流畅的打字机效果,极大提升用户体验。ReadableStream
const sendMessage = async () => {
// ... 前置逻辑 ...
const response = await fetch(`http://localhost:8080/api/chat?query=${encodeURIComponent(query)}`);
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 实时追加到 UI
assistantMsg.content += decoder.decode(value, { stream: true });
}
};
低配环境(Path B)避坑指南
在阿里云 2核4G 这类低配 ECS 上运行,若不进行调优,极易发生内存溢出(OOM)。请务必执行以下操作:
- 开启 Swap(虚拟内存):用磁盘空间弥补物理内存的不足,防止进程被系统强制终止。
# 分配 8G 虚拟内存
sudo fallocate -l 8G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
# 验证
free -h
- JVM 参数调优:为 Java 进程设置明确的堆内存上下限,为 Ollama 和 Milvus 留出必要的运行空间。
java -Xms1g -Xmx2g -XX:+UseG1GC -jar app.jar
- Ollama 运行排查:如果看到
警告,这通常意味着在用 CPU 运行模型,在低配环境下是预期行为。若需远程访问服务器上的 Ollama,需修改其配置。nvidia-smi command not found
五、总结与展望
通过本文的步骤,你已经能够构建一个适应不同硬件场景的 RAG 问答系统。本指南的完整性体现在它覆盖了从依赖冲突解决、模型选择、核心编码到容器化部署的全链路。对于高配 Path A,系统能提供接近生产环境的准确回答;对于低配 Path B,通过 Swap 和资源限制,也能保证流程跑通,响应延迟控制在可接受范围。
未来,你可以考虑将整个应用栈(Spring Boot App, Milvus, Ollama)通过 Docker Compose 编排,或进一步编写 Helm Chart 部署到 Kubernetes 集群中,实现更专业的容器编排、弹性伸缩和高可用,这将使你的智能问答系统具备更强的企业级服务能力。[AFFILIATE_SLOT_2]
浙公网安备 33010602011771号