本地RAG增强检索实现

2025/03/21 源自  AI

spring-ai + ollama + milvus 实现本地知识库 RAG 向量增强检索


一、大模型 RAG

现有大语言模型的记忆局限与 RAG 技术

当前所有的大语言模型(LLM)实际上并不具备真正的记忆功能。例如,ChatGPT 所谓的“记忆”能力,仅仅是通过在接收用户输入时,将最近几轮的对话内容合并进提示词(prompt),然后再交给模型处理。然而,LLM 都受 Token 数量的约束,这意味着当输入文本超出一定范围,较早的对话内容就会被遗忘。

AI 依赖知识库回答问题的挑战

如果希望 AI 基于知识库进行回答,会遇到以下几个问题:

  • 缺乏持久记忆:每次提问都需要携带完整的知识库内容,以便 AI 解析,导致处理大规模文本的过程极其低效。
  • 高昂的成本:当前 AI 按 Token 计费,提交长文本会带来较高的费用。
  • 隐私与安全问题:许多知识库属于内部资料,不适合直接暴露给公有 AI 模型。

RAG 技术:优化 AI 知识获取

为了解决上述问题,RAG(Retrieval-Augmented Generation,检索增强生成)技术应运而生,其主要流程如下:

  1. 数据向量化:先通过嵌入式模型对文档数据进行向量化处理,并存储到向量数据库中。
  2. 智能检索:用户提问时,AI 会先将问题向量化,并从向量数据库中检索出最相关的内容。
  3. 动态组合提示词:将检索到的相关文档与用户问题拼接成新的 Prompt,并提交给大语言模型处理。
  4. 优化响应效果:这种方式减少了对 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));
}