使用Spring AI 、 Qdrant 和 Ollama 实现完全本私有化的RAG应用

news2025/1/11 4:08:23

使用Spring AI 、 Qdrant 和 Ollama 实现完全本私有化的RAG应用
迄今为止,Python 一直是实现检索增强生成(RAG)应用程序的首选语言,几乎成为开发大型语言模型(LLM)应用程序的默认选择。然而,对于 Java 的爱好者和倡导者来说,这一趋势并不意味着终结。恰恰相反,这是一种创新的机会。在这篇文章中,我们将探讨如何创建一个可扩展的、本地化的 RAG 应用程序,以处理复杂的文档。这将通过整合 #springboot 的稳健性、#qdrant 的高效性和 #ollama 的智能性来实现。

在这里插入图片描述

介绍

图片中所描绘的架构代表了一种处理和分析复杂文档(如调研报告、财务报告等)的复杂方法。用户首先通过一个称为 /load 的 API 上传文档,然后使用另一个称为 /ask 的 API 向系统提问。这表明这是一个交互式系统,初始动作是文档上传,随后是查询过程,使用户能够从上传的文档中提取有意义的信息。

此架构的核心是 “Spring AI”,当用户上传文档时,Spring AI 接收解析并分析文本。它将复杂文档的内容转化为一种结构化形式,使其适合于高级数据处理技术。Spring AI 的精髓在于其能够细致地理解和消化这些文档的内容,为处理过程的下一阶段做准备。

在 Spring AI 的初步处理之后,我们进入数据处理和存储的领域。在这里,处理后的数据被转换为向量,即捕捉文档语义本质的数值表示。这个转换至关重要,因为它使系统能够执行复杂的推理,这一任务由 Ollama 的组件管理。AI 驱动的 Llama3 模型随后使用这些嵌入来理解和解释内容,从而对用户查询做出智能响应。最后,这些嵌入被存储在专为文档设计的向量存储 Qdrant 中。通过将信息存储为向量,系统确保能够高效地检索和分析数据,从而快速、智能地响应用户查询。这种存储解决方案是系统能够处理重复交互的基础,每次查询都利用存储的嵌入来提供精确和上下文相关的见解。

通过这种三位一体的架构,系统提供了一种从文档上传到信息检索的无缝体验,由处理和理解复杂调研数据的高级 AI Llama3 所支撑。

实现

项目脚手架如下:

在这里插入图片描述

下面是代码详情

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>org.liugddx</groupId>
    <artifactId>springboot-rag</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-rag</name>
    <description>springboot-rag</description>
    <properties>
        <java.version>21</java.version>
        <spring-ai.version>0.8.1</spring-ai.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <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-pdf-document-reader</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </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>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <releases>
                <enabled>false</enabled>
            </releases>
        </repository>
    </repositories>

</project>

pom中是spring ai的主要依赖,然后就该创建配置文件application.yaml:这里最重要的是我们已经配置了从LLM到本地向量存储的所有内容,这为我们提供了最高的隐私和安全性,但这可能始终无法满足我们的要求。因此,只需更改此文件中的托管 URL,其余代码即可正常工作。

spring:
  application:
    name: springboot-rag
  threads:
    virtual:
      enabled: true
  ai:
    ollama:
      base-url: "http://localhost:11434"
      embedding:
        model: "mxbai-embed-large:latest"
      chat:
        model: "llama3:latest"
        options:
          temperature: 0.3
          top-k: 2
          top-p: 0.2
          num-g-p-u: 1 # enable Metal gpu on MAC
    vectorstore:
      qdrant:
        host: localhost
        port: 6334
        collection-name: infoq-report

我们将从 ravendb 下载一份关于 2023-2024 年 NoSql 趋势的复杂报告。

现在让我们将系统提示文件和数据分别保存在我们项目的特定文件夹prompts/system.pmtdata/InfoQ-Trend-Report.pdf中。


You're fielding inquiries related to a technology trends report provided by InfoQ.

The report is a compilation of the most sought-after InfoQ Trends Reports from 2023,
encompassing a range of topics like technological advancements, software development tendencies, and organizational best practices.
It delves into diverse themes such as the dynamics of hybrid work environments, comparative analysis of architectural patterns including monoliths, microservices, and moduliths, the role of platform engineering, the evolution of large language models (LLMs), and the latest developments in the Java domain.

Leverage the insights from the DOCUMENTS section to inform your responses, drawing on the information as if it were your own knowledge base. If the answer isn't clear, it's best to acknowledge the gap in information.

DOCUMENTS:
{documents}

让我们看一下两个主要接口及其实现。

DataIndexer.java

package org.liugddx.springbootrag.service;

public interface DataIndexer {
    void loadData();
    long count();
}

DataindexerServiceImpl.java:

package org.liugddx.springbootrag.service.impl;

import org.liugddx.springbootrag.service.DataIndexer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.document.DocumentReader;
import org.springframework.ai.reader.ExtractedTextFormatter;
import org.springframework.ai.reader.JsonReader;
import org.springframework.ai.reader.TextReader;
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;

@Service
public class DataIndexerServiceImpl implements DataIndexer {

    @Value("classpath:/data/InfoQ-NoSql-trend-Report.pdf")
    private Resource documentResource;
    private final VectorStore vectorStore;

    Logger logger = LoggerFactory.getLogger(DataIndexerServiceImpl.class);

    public DataIndexerServiceImpl(VectorStore vectorStore) {
        this.vectorStore = vectorStore;
    }

    @Override
    public void loadData() {
        DocumentReader documentReader = null;
        if(this.documentResource.getFilename() != null && this.documentResource.getFilename().endsWith(".pdf")){
            this.logger.info("Loading PDF document");
            documentReader = new PagePdfDocumentReader(this.documentResource,
                    PdfDocumentReaderConfig.builder()
                            .withPageExtractedTextFormatter(ExtractedTextFormatter.builder()
                                    .withNumberOfBottomTextLinesToDelete(3)
                                    .withNumberOfTopPagesToSkipBeforeDelete(3)
                                    .build())
                            .withPagesPerDocument(3)
                            .build());
        }else if (this.documentResource.getFilename() != null && this.documentResource.getFilename().endsWith(".txt")) {
            documentReader = new TextReader(this.documentResource);
        } else if (this.documentResource.getFilename() != null && this.documentResource.getFilename().endsWith(".json")) {
            documentReader = new JsonReader(this.documentResource);
        }
        if(documentReader != null){
            var textSplitter = new TokenTextSplitter();
            this.logger.info("Loading text document to qdrant vector database");
            this.vectorStore.accept(textSplitter.apply(documentReader.get()));
            this.logger.info("Loaded text document to qdrant vector database");
        }
    }

    @Override
    public long count() {
        return this.vectorStore.similaritySearch("*").size();
    }
}

RAGService.java:

package org.liugddx.springbootrag.service;

public interface RAGService {
    String findAnswer(String query);
}

RAGServiceImpl.java:

package org.liugddx.springbootrag.service.impl;

import org.liugddx.springbootrag.service.RAGService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
public class RAGServiceImpl implements RAGService {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    @Value("classpath:/prompts/system.pmt")
    private Resource systemPromptResource;

    private final VectorStore vectorStore;
    private final ChatClient aiClient;

    public RAGServiceImpl(VectorStore vectorStore, ChatClient aiClient) {
        this.vectorStore = vectorStore;
        this.aiClient = aiClient;
    }

    @Override
    public String findAnswer(String query) {
        // Combine system message retrieval and AI model call into a single operation
        ChatResponse aiResponse = aiClient.call(new Prompt(List.of(
                getRelevantDocs(query),
                new UserMessage(query))));

        // Log only necessary information, and use efficient string formatting
        logger.info("Asked AI model and received response.");
        return aiResponse.getResult().getOutput().getContent();
    }

    private Message getRelevantDocs(String query) {
        List<Document> similarDocuments = vectorStore.similaritySearch(query);

        // Log the document count efficiently
        if (logger.isInfoEnabled()) {
            logger.info("Found {} relevant documents.", similarDocuments.size());
        }

        // Streamline document content retrieval
        String documents = similarDocuments.stream()
                .map(Document::getContent)
                .collect(Collectors.joining("\n"));
        SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(this.systemPromptResource);
        return systemPromptTemplate.createMessage(Map.of("documents", documents));
    }

}

然后就是控制器,主要包将报告加载到向量存储到回答用户问题的特定操作。

DataIndexController.java:

package org.liugddx.springbootrag.controller;

import org.liugddx.springbootrag.service.DataIndexer;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1")
public class DataIndexController {

    private final DataIndexer dataIndexer;

    public DataIndexController(DataIndexer dataIndexer) {
        this.dataIndexer = dataIndexer;
    }

    @PostMapping("/data/load")
    public ResponseEntity<String> load() {
        try {
            this.dataIndexer.loadData();
            return ResponseEntity.ok("Data indexed successfully!");
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body("An error occurred while indexing data: " + e.getMessage());
        }
    }

    @GetMapping("/data/count")
    public long count() {
        return dataIndexer.count();
    }
}

RAGController.java:

package org.liugddx.springbootrag.controller;

import org.liugddx.springbootrag.service.RAGService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.LinkedHashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/v1")
public class RAGController {
    private final RAGService ragService;

    public RAGController(RAGService ragService) {
        this.ragService = ragService;
    }

    @GetMapping("/ask")
    public Map findAnswer(@RequestParam(value = "question", defaultValue = "give me the general summary on the trend report") String question) {
        String answer = this.ragService.findAnswer(question);
        Map map = new LinkedHashMap();
        map.put("question", question);
        map.put("answer", answer);
        return map;
    }
}

就这样,现在是我们运行代码库的时候了。一旦一切正常,我们应该看到服务器输出以及服务已启动的端口,如下所示。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

总结

总之,借助 Spring AI 及其与 Ollama 和 Qdrant 的集成,我们实现了一个简单的全本地的文档问答RAG系统。Spring AI 作为我们检索回答生成器(RAG)的智能基石,提供了一种细腻的方法来消化和解释复杂的调研报告。它与 Ollama 的无缝协作,不仅能检索答案,还能通过上下文感知的响应来生成理解,使 RAG 成为一个对话助手,而不仅仅是一个简单的查询工具。Qdrant 通过提供优化的向量存储,进一步提升了这一过程,支持 RAG 以快速和准确的方式获取最相关的文档部分。

代码地址:git@github.com:liugddx/springbot-rag.git

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1987485.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

WPF 依赖属性 IsHitTestVisible

IsHitlTestVisible 仅影响本身的元素&#xff08;含内部包含的子元素&#xff09;&#xff0c;不影响父元素效果&#xff0c;且事件会传递到父元素。 Eg&#xff1a; 如父元素有click事件&#xff0c; 子元素设置了IsHitTestVisiblefalse&#xff0c; 当鼠标单击这个子元素时&…

openssl 制作 信用库与服务证书

文章目录 前言openssl 制作 信用库与服务证书1. CA 证书2. 服务器证书/秘钥库3. 创建信用库4. 注意事项 前言 如果您觉得有用的话&#xff0c;记得给博主点个赞&#xff0c;评论&#xff0c;收藏一键三连啊&#xff0c;写作不易啊^ _ ^。   而且听说点赞的人每天的运气都不会…

【JS】一篇BOM详解笔记 | b站李立超

文章目录 一、前言&#x1f680;&#x1f680;&#x1f680;二、BOM简介&#xff1a;☀️☀️☀️2.1 BOM是什么及有什么用2.2 BOM对象有哪些 三、BOM各类对象讲解&#xff1a;☀️☀️☀️3.1 Navigator3.2 Location3.3 History 三、补充知识&#x1f680;&#x1f680;&#…

尚硅谷谷粒商城项目笔记——六、使用navciat连接docker中的mysql容器【电脑CPU:AMD】

六、使用navciat连接docker中的mysql容器 注意&#xff1a; 因为电脑是AMD芯片&#xff0c;自己知识储备不够&#xff0c;无法保证和课程中用到的环境一样&#xff0c;所以环境都是自己根据适应硬件软件环境重新配置的&#xff0c;这里的虚拟机使用的是VMware。 1navicat免费…

浴室柜哪个牌子质量好性价比高 | 提亮空间,点缀生活!

摘要&#xff1a;作为浴室的标配&#xff0c;浴室柜不仅是重要的收纳家具&#xff0c;也是最能体现卫浴空间和美感的存在。浴室柜看似平凡&#xff0c;却在无形之中散发出自身的魅力&#xff0c;为浴室颜值加分。浴室柜哪个品牌好&#xff1f;无论是注重外观的你&#xff0c;还…

《计算机组成原理》(第3版)第4章 存储器 复习笔记

第4章 存储器 一、概述 &#xff08;一&#xff09;存储器分类 1&#xff0e;按存储介质分类 &#xff08;1&#xff09;半导体存储器&#xff1b; &#xff08;2&#xff09;磁表面存储器&#xff1b; &#xff08;3&#xff09;磁芯存储器&#xff1b; &#xff08;4&…

面试笔记8.6

缓存 1.如何保证redis与数据库一致性 redis面试&#xff1a;如何保证缓存和数据库数据的一致性&#xff1f;_使用update更新数据,json缓存不更新-CSDN博客 如果先删除缓存&#xff0c;再删除数据库&#xff0c;数据不一致&#xff0c; 解决 删 1.先操作缓存但不删除缓存&…

lambda语法,java8Stream流,maven

lambda语法 Arraylist排序 ArrayList<Integer> a new ArrayList();a.add(1);a.add(2);a.add(3);a.sort(new Comparator<Integer>() {Overridepublic int compare(Integer o1, Integer o2) {return o1-o2;//对集合a中数据进行升序排序}}); 但是这样的书写还是有…

JavaEE: 进程和线程

文章目录 进程线程的概念和区别总结如何创建线程1.继承Thread重写run2.实现Runnable重写run3.继承Thread重写run,通过匿名内部类来实现4. 实现Runnable重写run,通过匿名内部类来实现5.基于lambda表达式来创建 虚拟线程 并发编程: 通过写特殊的代码&#xff0c;把多个CPU核心都利…

Leetcode力扣刷题——182.查找重复的电子邮箱

题目 编写解决方案来报告所有重复的电子邮件。 请注意&#xff0c;可以保证电子邮件字段不为 NULL。 以 任意顺序 返回结果表。 结果格式如下例。 结果 # Write your MySQL query statement below select Email from Person group by email having count(*)>1; 知识点 c…

Java 抽象知识笔记总结(油管)

Java系列文章目录 Java Optional 容器笔记总结 文章目录 Java系列文章目录一、前言二、学习内容&#xff1a;三、问题描述四、解决方案&#xff1a;4.1 抽象类的使用4.2 抽象类与接口的区别4.2.1 接口复习4.2.2 具体区别4.2.3 使用场景4.2.3.1 抽象类使用场景4.2.3.2 接口使用…

读零信任网络:在不可信网络中构建安全系统11用户组的认证和授权

1. 用户组的认证和授权 1.1. 几乎在每个系统中都有一小部分操作需要被密切关注 1.1.1. 每个应用对这部分操作的风险容忍度各有不同&#xff0c;且没有任何下限 1.1.2. 一部分风险是由用户个人的可信度决定的 1.1.2.1. 单个用户的可信度可能很低 1.1.2.2. 多个用户组合的可信…

OpenAI人事变动:联合创始人John Schulman离职加入Anthropic

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

【JVM基础14】——垃圾回收-强引用、软引用、弱引用、虚引用的区别

目录 1- 引言&#xff1a;为什么分多种引用类型2- ⭐核心&#xff1a;2-1 强引用2-2 软引用2-3 弱引用2-4 虚引用 3- 小结&#xff1a;3-1 强引用、软引用、弱引用、虚引用的区别&#xff1f; 1- 引言&#xff1a;为什么分多种引用类型 在 Java 中&#xff0c;引用类型被分为强…

ICM-20948芯片详解(8)

接前一篇文章&#xff1a;ICM-20948芯片详解&#xff08;7&#xff09; 六、寄存器详解 1. 总述 ICM-20948共有user bank 0~3共4组寄存器。 USER BANK 0寄存器图 USER BANK 0所包含的寄存器如下图所示&#xff1a; USER BANK 1寄存器图 USER BANK 1所包含的寄存器如下图所…

校园外卖微信小程序的设计与实现

校园外卖微信小程序的设计与实现 校园外卖微信小程序的设计与实现 [摘要] 随着社会的进步和生活质量的提高&#xff0c;人们对用餐体验有了更高的要求&#xff0c;导致电话和网上订餐服务日益发展。这一趋势也推动了以大学生为主要服务对象的校园外卖的发展。此次设计主要为大…

AI大模型巡游记第二回探索幻梦

unsetunset第二回&#xff1a;探索幻梦unsetunset **随着夕阳西下&#xff0c;李逸结束了今天的探险之旅…… 李逸回到了现实世界&#xff0c;心中满是对“幻境大模型”体验的好奇与惊叹。他迫不及待地想要了解更多关于这项技术背后的秘密。于是&#xff0c;他决定访问“幻梦”…

Python写UI自动化--playwright(元素焦点的控制)

在UI测试中会遇到一种情况&#xff0c;在输入框输入内容&#xff0c;鼠标焦点离开输入框后&#xff0c;前端会对输入框内容进行判断是否合法&#xff0c;比如输入不正确格式的账号&#xff0c;会有相应的提示。该篇文章就讲一讲playwright关于元素焦点的控制&#xff1a; Play…

Makefile基础语法 看这一篇就够了

Makefile 基础语法 Makefile 是一种用于自动化构建过程的脚本文件&#xff0c;它通过定义目标文件和依赖关系来告诉 make 工具如何构建项目。Makefile 的语法和结构可以非常灵活&#xff0c;适用于各种复杂的构建场景。本教程将详细介绍 Makefile 的基本概念、语法以及如何编写…

python3 pyside6图形库学习笔记及实践(五)

目录 前言选项卡(QTabWidget)创建选项卡常用属性和方法常用信号 堆叠页面(QStackedWidget)创建堆叠容器切换页面过渡动画 前言 本系列文章为b站PySide6教程以及官方文档的学习笔记 原视频传送门&#xff1a;【已完结】PySide6百炼成真&#xff0c;带你系统性入门Qt 官方文档…