Spring AI 本地 RAG 实战:用Redis、Chroma搭建离线知识问答系统

本文将用 Ollama + Qwen-7B 搭建离线知识问答系统(含 Redis/Chroma 向量库)



前言

在大模型应用持续火热的今天,“RAG”(Retrieval-Augmented Generation) 成为构建企业知识问答、私有文档智能检索的重要架构。然而很多开发者一开始就遇到这些问题:

  • API Token 限制(如 OpenAI 额度)
  • 网络限制(如内网环境、数据敏感)
  • 英文模型难以理解中文业务数据

于是,本地部署 LLM(如 Ollama + Qwen)成为热门选择。

Spring AI 1.0.0-M5 正式支持接入本地 LLM,使得 Java 社区开发者也能:

离线构建完整的 RAG 系统:私有数据存储 → 本地向量检索 → 本地模型回答

本教程将手把手演示如何使用:

  • Qwen-7B:通义千问开源中文大模型
  • Spring AI:统一的 LLM 接入框架
  • Redis 向量数据库 + Chroma 本地数据库
  • 构建可运行、可打包的离线问答系统

环境搭建

本文采用本地 Ollama + Qwen-7B 模型,参考之前的文章里,关于如何本地搭建的部分:Spring AI 基本组件详解 —— ChatClient、Prompt、Memory


项目结构设计

在这里插入图片描述


Maven 依赖pom.xml

本例适配 Spring Boot 2.5.3 + Spring AI 1.0.0-M5:

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>spring-ai-rag-local</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>Spring AI Local RAG</name>

  <properties>
    <java.version>17</java.version>
    <spring.boot.version>2.5.3</spring.boot.version>
    <spring-ai.version>1.0.0-M5</spring-ai.version>
  </properties>

  <dependencies>
    <!-- Spring Boot 核心依赖 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring AI 核心 -->
    <dependency>
      <groupId>org.springframework.ai</groupId>
      <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
      <version>${spring-ai.version}</version>
    </dependency>

    <!-- 嵌入模型与 VectorStore -->
    <dependency>
      <groupId>org.springframework.ai</groupId>
      <artifactId>spring-ai-redis-vector-store</artifactId>
      <version>${spring-ai.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.ai</groupId>
      <artifactId>spring-ai-chroma-vector-store</artifactId>
      <version>${spring-ai.version}</version>
    </dependency>

    <!-- Redis -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <!-- 日志与测试 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-logging</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <!-- Spring Boot 插件 -->
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

application.yml 配置(Redis + Ollama)

server:
  port: 8080

spring:
  ai:
    ollama:
      base-url: http://localhost:11434
      model: qwen:7b
  data:
    redis:
      host: localhost
      port: 6379

Ollama 默认监听端口为 11434,Qwen 模型会按需唤起。


Redis 向量库实战

我们将实现以下几个模块:

  • 配置类:初始化 Ollama 客户端与 VectorStore(Redis)

  • 服务类:实现文档添加 + 问答检索逻辑

  • 控制器:暴露 REST 接口

  • 示例文档:测试 RAG 问答效果


OllamaConfig.java

package com.example.rag.config;

import org.springframework.ai.embedding.EmbeddingClient;
import org.springframework.ai.embedding.ollama.OllamaEmbeddingClient;
import org.springframework.ai.ollama.OllamaChatClient;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.redis.RedisVectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;

@Configuration
public class OllamaConfig {

    @Bean
    public OllamaChatClient chatClient() {
        return new OllamaChatClient("http://localhost:11434", "qwen:7b");
    }

    @Bean
    public EmbeddingClient embeddingClient() {
        return new OllamaEmbeddingClient("http://localhost:11434");
    }

    @Bean
    public VectorStore redisVectorStore(EmbeddingClient embeddingClient, StringRedisTemplate redisTemplate) {
        return new RedisVectorStore(embeddingClient, redisTemplate);
    }
}

💡 如果遇到 OllamaEmbeddingClient 无法解析,说明 Spring AI 版本不兼容,建议先使用 OpenAiEmbeddingClient 作为 placeholder。


RagService.java

package com.example.rag.service;

import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.messages.ChatMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.Document;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class RagService {

    private final VectorStore vectorStore;
    private final ChatClient chatClient;

    public RagService(VectorStore vectorStore, ChatClient chatClient) {
        this.vectorStore = vectorStore;
        this.chatClient = chatClient;
    }

    // 添加文档并向量化
    public void addDocument(String text) {
        Document doc = new Document(text);
        vectorStore.add(List.of(doc));
    }

    // 提问并返回回答
    public String ask(String question) {
        List<Document> docs = vectorStore.similaritySearch(SearchRequest.query(question).withTopK(3));
        String context = docs.stream().map(Document::getContent).reduce("", (a, b) -> a + "\n" + b);

        Prompt prompt = new Prompt(List.of(
                new UserMessage("请根据以下知识回答问题:\n" + context + "\n问题是:" + question)
        ));

        return chatClient.call(prompt).getResult().getOutput().getContent();
    }
}

RagController.java

package com.example.rag.controller;

import com.example.rag.service.RagService;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/rag")
public class RagController {

    private final RagService ragService;

    public RagController(RagService ragService) {
        this.ragService = ragService;
    }

    @PostMapping("/add")
    public String addDocument(@RequestBody String content) {
        ragService.addDocument(content);
        return "文档已添加";
    }

    @GetMapping("/ask")
    public String ask(@RequestParam String question) {
        return ragService.ask(question);
    }
}

RagApplication.java

package com.example.rag;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class RagApplication {
    public static void main(String[] args) {
        SpringApplication.run(RagApplication.class, args);
    }
}

测试样例

# 添加文档
curl -X POST http://localhost:8080/api/rag/add \
  -H "Content-Type: text/plain" \
  -d "Spring AI 是一个可以统一封装各类大语言模型调用的 Java 框架,支持 OpenAI、Ollama、HuggingFace 等。"

# 发起问答
curl "http://localhost:8080/api/rag/ask?question=Spring AI 支持哪些模型?"

RAG 增强

要求:

  1. 上传 .txt / .md 文件或多个文档
  2. 自动读取文件内容
  3. 切片(Chunk)处理
  4. 批量向量化并写入向量数据库(Redis/Chroma)

Maven 依赖补充(文档处理 + Multipart)

pom.xml 中加入:

<!-- 文件上传 & 文本处理 -->
<dependency>
  <groupId>commons-io</groupId>
  <artifactId>commons-io</artifactId>
  <version>2.11.0</version>
</dependency>

RagService 增强(addDocumentsFromFiles)

public void addDocumentsFromFiles(List<MultipartFile> files) {
    List<Document> docs = new ArrayList<>();

    for (MultipartFile file : files) {
        try {
            String content = new String(file.getBytes(), StandardCharsets.UTF_8);
            List<String> chunks = splitIntoChunks(content, 300); // 每段 300 字
            for (String chunk : chunks) {
                docs.add(new Document(chunk));
            }
        } catch (IOException e) {
            throw new RuntimeException("无法读取文件: " + file.getOriginalFilename(), e);
        }
    }

    vectorStore.add(docs);
}

// 简单文本切片方法
private List<String> splitIntoChunks(String text, int size) {
    List<String> chunks = new ArrayList<>();
    for (int start = 0; start < text.length(); start += size) {
        int end = Math.min(start + size, text.length());
        chunks.add(text.substring(start, end));
    }
    return chunks;
}

新增接口 RagController 上传文档接口

@PostMapping("/upload")
public String uploadFiles(@RequestParam("files") List<MultipartFile> files) {
    ragService.addDocumentsFromFiles(files);
    return "文件已成功处理并存入向量库";
}

使用 Postman / Swagger 测试

请求:

  • POST /api/rag/upload

  • 请求体类型 form-data

    • 参数名:files
    • 类型:file
    • 支持上传多个 .txt / .md / .log 等文本文件

示例返回:

文件已成功处理并存入向量库

问答结果增强

为了更透明的返回,我们调整一下 RagService 返回结构:

public Map<String, Object> askWithSources(String question) {
    List<Document> docs = vectorStore.similaritySearch(SearchRequest.query(question).withTopK(3));
    String context = docs.stream().map(Document::getContent).collect(Collectors.joining("\n"));

    Prompt prompt = new Prompt(List.of(
        new UserMessage("根据以下知识回答问题:\n" + context + "\n用户问题:" + question)
    ));

    String answer = chatClient.call(prompt).getResult().getOutput().getContent();

    Map<String, Object> result = new LinkedHashMap<>();
    result.put("answer", answer);
    result.put("sources", docs.stream().map(Document::getContent).collect(Collectors.toList()));
    return result;
}

对应接口:

@GetMapping("/ask-v2")
public Map<String, Object> askWithSources(@RequestParam String question) {
    return ragService.askWithSources(question);
}

总结

功能模块说明
本地模型使用 Ollama + Qwen-7B,适配中文
多向量数据库Redis、Chroma 双向支持
文本输入接口支持纯文本输入、批量切片
文件上传接口支持多文件上传(txt/md/log)
问答接口支持用户提问 + LLM 回答
源内容返回可返回匹配文档内容
RESTful 风格接口标准、易集成前端
离线部署零外网依赖,适合内网环境
可扩展结构支持未来加入模型切换、API Gateway、消息队列

参考

《Java驱动AI革命:Spring AI八篇进阶指南——从架构基础到企业级智能系统实战》
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

勤奋的知更鸟

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值