在当今信息爆炸的时代,如何让机器精准理解并回答基于特定文档的问题,已成为企业智能化转型的关键。检索增强生成(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 (极致轻量)
EmbeddingBGE-M3 (维度 1024)BGE-Small-ZH (维度 512)
预期性能首字延迟 < 1s,推理流畅首字延迟 < 2s,勉强流畅
关键手段模型常驻内存、GPU 加速开启 Swap 虚拟内存、模型降级

二、基础设施准备:容器化部署核心服务

无论选择哪条路径,我们都推荐使用 Docker 来部署核心依赖服务,这能保证环境的一致性与可复现性,也是迈向 容器编排 的第一步。

1. 启动 Milvus 向量数据库
Milvus 是系统的“记忆中枢”。通过 Docker 运行是最佳实践。对于 Path A,直接使用官方镜像即可。对于 Path B 的低配环境,关键在于资源限制。你需要修改 Docker 运行参数,通过环境变量(如 mem_limit: 2g)来严格控制 Milvus 的内存使用,避免其拖垮整个服务器。

# 下载并启动 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

这是因为 bge-small-zh 并非 Ollama 官方库镜像。我们强烈推荐方案一:改用官方支持的 bge-m3。它是 BGE 系列的多语言旗舰模型,中文表现优异,且维护良好。

拉取模型:

ollama pull bge-m3

⚠️ 关键配置同步:更换模型后,必须在 Spring Boot 配置中同步修改向量维度。例如,bge-m3 的维度是 1024,配置如下:

spring:
ai:
ollama:
embedding:
model: bge-m3  # 修改这里
vectorstore:
milvus:
embedding-dimension: 1024 # ⚠️ 必须改为 1024

如果你的环境实在苛刻,必须使用极小模型,则需手动导入 GGUF 文件,具体步骤涉及下载文件和创建 Modelfile,详见方案二。但为了省心和高检索准确率,请直接采用方案一

三、后端工程:Spring Boot 应用构建详解

后端是系统的业务逻辑核心,我们将使用 Spring AI 提供的抽象,高效集成嵌入、检索和生成能力。

1. 依赖配置(避坑指南)
构建的第一步是确保 pom.xml 正确。我们使用了稳定版的 Spring Boot 3.4.3 与里程碑版的 Spring AI 1.0.0-M1。特别注意,需要指定 Lombok 版本(1.18.36)以兼容 JDK 21,并添加 Spring 里程碑仓库。

<?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. 核心配置与代码实现
application.yml 中,关键是指定嵌入模型和向量存储的连接信息,确保与 Ollama 和 Milvus 的设置一致。

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 # 最快索引

核心业务逻辑分为两部分:
- 文档入库:通过 IngestionService.java 接口,解析上传的 PDF/Word,分块、生成向量并存入 Milvus。
- RAG 问答:通过 RagService.java 接口,将用户问题向量化,在 Milvus 中检索相关文档片段,连同问题一起发送给 LLM 生成流式答案。

文档入库核心代码示意:

@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 配合 fetchReadableStream,可以轻松实现接收服务器端推送(SSE)的流式响应,并展示出流畅的打字机效果,极大提升用户体验。

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)。请务必执行以下操作:

  1. 开启 Swap(虚拟内存):用磁盘空间弥补物理内存的不足,防止进程被系统强制终止。
# 分配 8G 虚拟内存
sudo fallocate -l 8G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
# 验证
free -h
  1. JVM 参数调优:为 Java 进程设置明确的堆内存上下限,为 Ollama 和 Milvus 留出必要的运行空间。
java -Xms1g -Xmx2g -XX:+UseG1GC -jar app.jar
  1. Ollama 运行排查:如果看到 nvidia-smi command not found 警告,这通常意味着在用 CPU 运行模型,在低配环境下是预期行为。若需远程访问服务器上的 Ollama,需修改其配置。

五、总结与展望

通过本文的步骤,你已经能够构建一个适应不同硬件场景的 RAG 问答系统。本指南的完整性体现在它覆盖了从依赖冲突解决、模型选择、核心编码到容器化部署的全链路。对于高配 Path A,系统能提供接近生产环境的准确回答;对于低配 Path B,通过 Swap 和资源限制,也能保证流程跑通,响应延迟控制在可接受范围。

未来,你可以考虑将整个应用栈(Spring Boot App, Milvus, Ollama)通过 Docker Compose 编排,或进一步编写 Helm Chart 部署到 Kubernetes 集群中,实现更专业的容器编排、弹性伸缩和高可用,这将使你的智能问答系统具备更强的企业级服务能力。[AFFILIATE_SLOT_2]