Spring AI Java程序员的AI之Spring AI(三)RAG实战

Spring AI之RAG实战与原理分析

  • 前言
  • RAG
    • Document
    • DocumentReader
    • DocumentTransformer
    • DocumentWriter
  • VectorStore
    • SimpleVectorStore
    • RedisVectorStore
    • 元数据搜索
    • 组装提示词

前言

检索增强生成(RAG)是一种结合信息检索和生成模型的技术,用于将相关数据嵌入到Prompts中,以提高AI模型的响应准确性。该方法包括一个批处理风格的编程模型,读取未结构化的数据,将其转换,然后写入向量数据库。总体上,这是一个ETL(Extract, Transform, Load)管道。向量数据库在RAG技术中用于检索部分。

简单解释就是:

搭建自己的知识库除了文档嵌入到向量数据库之外,就是RAG了。当用户提问的时候先从想来数据库搜索相关的资料,再把相关的资料拼接到用户的提问中,再让模型生成答案。

RAG

Document

Spring AI提供了:

  1. DocumentReader:用来读取TXT、PDF等文件内容
  2. DocumentTransformer:用来解析文件内容
  3. DocumentWriter:用来写入文件内容到向量数据库

DocumentReader

实现类有:
JsonReader:读取JSON格式的文件
TextReader:读取txt文件
PagePdfDocumentReader:使用Apache PdfBox读取PDF文件
TikaDocumentReader:使用Apache Tika来读取PDF, DOC/DOCX, PPT/PPTX, and HTML等文件

比如使用TextReader来读取meituan.txt文件内容:

package com.qjc.demo.service;import org.springframework.ai.document.Document;
import org.springframework.ai.reader.TextReader;
import org.springframework.ai.transformer.splitter.TextSplitter;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;import java.util.List;/**** @projectName spring-ai-demo* @packageName com.qjc.demo.service* @author qjc* @description TODO* @Email qjc1024@aliyun.com* @date 2024-10-17 10:15**/
@Component
public class DocumentService {@Value("classpath:meituan-qa.txt") // This is the text document to loadprivate Resource resource;public List<Document> loadText() {TextReader textReader = new TextReader(resource);textReader.getCustomMetadata().put("filename", "meituan-qa.txt");List<Document> documents = textReader.get();return documents;}
}
@GetMapping("/document")
public List<Document> document() {return documentService.loadText();
}

得到的结果为:
在这里插入图片描述

所以DocumentReader只负责将文件转换为Document对象,如果要对文件进行切分,则需要使用DocumentTransformer。

DocumentTransformer

Spring AI默认提供了一个TokenTextSplitter,我们可以基于Document来进行切分:

public List<Document> loadText() {TextReader textReader = new TextReader(resource);textReader.getCustomMetadata().put("filename", "meituan.txt");List<Document> documents = textReader.get();TokenTextSplitter tokenTextSplitter = new TokenTextSplitter();List<Document> list = tokenTextSplitter.apply(documents);return list;
}

得到的结果如下:
在这里插入图片描述

发现它并不是按问题-答案对来进行切分的,TokenTextSplitter的工作原理:

  1. 先将文本encode为tokens
  2. 按指定的chunkSize(默认为800)对tokens进行切分,得到一个chunk
  3. 将chunk进行decode,得到原始文本
  4. 获取原始文本中最后一个’.‘、’?‘、’!‘、’\n’的位置,该位置表示一段话的结束。
  5. 如果结束位置超过了minChunkSizeChars,那么则进行切分得到一段话的chunk,否则不切分
  6. 将切分后的chunk记录到一个List中
  7. 然后跳转到第二步,处理剩余的tokens

如果要按问题-答案对来进行切分,需要自定义一个TextSplitter:

package com.qjc.demo.utils;import org.springframework.ai.transformer.splitter.TextSplitter;import java.util.List;/**** @projectName spring-ai-demo* @packageName com.qjc.demo.utils* @author qjc* @description TODO* @Email qjc1024@aliyun.com* @date 2024-10-17 10:18**/
public class QjcTextSplitter extends TextSplitter {@Overrideprotected List<String> splitText(String text) {return List.of(split(text));}public String[] split(String text) {return text.split("\\s*\\R\\s*\\R\\s*");}
}

然后直接调用就可以了:

package com.qjc.demo.service;import org.springframework.ai.document.Document;
import org.springframework.ai.reader.TextReader;
import org.springframework.ai.transformer.splitter.TextSplitter;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;import java.util.List;/**** @projectName spring-ai-demo* @packageName com.qjc.demo.service* @author qjc* @description TODO* @Email qjc1024@aliyun.com* @date 2024-10-17 10:15**/
@Component
public class DocumentService {@Value("classpath:meituan-qa.txt") // This is the text document to loadprivate Resource resource;public List<Document> loadText() {TextReader textReader = new TextReader(resource);textReader.getCustomMetadata().put("filename", "meituan-qa.txt");List<Document> documents = textReader.get();QjcTextSplitter qjcTextSplitter = new QjcTextSplitter ();List<Document> list = qjcTextSplitter .apply(documents);return list;}
}

得到的结果为:

在这里插入图片描述

DocumentWriter

得到按指定逻辑切分后的Document之后,就需要把它们做向量化并存入向量数据库了。DocumentWriter有一个子接口VectorStore,就表示向量数据库,而VectorStore有一个默认实现类SimpleVectorStore,可以先尝试使用它来进行Document的向量化和存储。

VectorStore

SimpleVectorStore

SimpleVectorStore只提供了一个构造方法:

public SimpleVectorStore(EmbeddingClient embeddingClient) {Objects.requireNonNull(embeddingClient, "EmbeddingClient must not be null");this.embeddingClient = embeddingClient;
}

因此可以直接定义一个SimpleVectorStore的Bean,利用构造注入得到EmbeddingClient:

@Bean
public SimpleVectorStore vectorStore(EmbeddingClient embeddingClient) {return new SimpleVectorStore(embeddingClient);
}

然后直接使用VectorStore就可以了:

package com.qjc.demo.service;import org.springframework.ai.document.Document;
import org.springframework.ai.document.DocumentWriter;
import org.springframework.ai.embedding.EmbeddingClient;
import org.springframework.ai.reader.TextReader;
import org.springframework.ai.transformer.splitter.TextSplitter;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;import java.util.List;/**** @projectName spring-ai-demo* @packageName com.qjc.demo.service* @author qjc* @description TODO* @Email qjc1024@aliyun.com* @date 2024-10-17 10:15**/
@Component
public class DocumentService {@Value("classpath:meituan-qa.txt") // This is the text document to loadprivate Resource resource;@Autowiredprivate VectorStore vectorStore;public List<Document> loadText() {TextReader textReader = new TextReader(resource);textReader.getCustomMetadata().put("filename", "meituan-qa.txt");List<Document> documents = textReader.get();QjcTextSplitter qjcTextSplitter = new QjcTextSplitter ();List<Document> list = qjcTextSplitter .apply(documents);// 向量存储vectorStore.add(list);return list;}}

我们再提供一个Controller用来进行向量查找:

@GetMapping("/documentSearch")
public List<Document> documentSearch(@RequestParam String message) {return documentService.search(message);
}
public List<Document> search(String message){List<Document> documents = vectorStore.similaritySearch(message);return documents;
}

先进行向量存储,从控制台可以发现利用EmbeddingClient进行了多次文本向量化,因为我们把文本拆分成了多个问答对:
在这里插入图片描述

然后进行查询:
在这里插入图片描述
可以看出确实进行了相似搜索。

RedisVectorStore

SimpleVectorStore是利用了ConcurrentHashMap来进行存储,如果我们想换成Redis,只需要引入相关依赖和定义相关的Bean就可以了。

引入依赖:

<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-redis</artifactId>
</dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>5.1.0</version>
</dependency>

定义Bean:

@Bean
public RedisVectorStore vectorStore(EmbeddingClient embeddingClient) {RedisVectorStore.RedisVectorStoreConfig config = RedisVectorStore.RedisVectorStoreConfig.builder().withURI("redis://localhost:6379").withMetadataFields(RedisVectorStore.MetadataField.text("filename")).build();return new RedisVectorStore(config, embeddingClient);
}

在测试前,请先进入redis-cli执行redis-cli FT.DROPINDEX spring-ai-index DD对现有redis中的数据进行清空,不然会受到之前数据的影响。
在这里插入图片描述
测试发现,使用Redis时,meatdata中多了一个vector_score,表示相似度分数,这是RedisVectorStore帮我们设置的,和LangChain4j不一样的时,RedisVectorStore的vector_score是越低表示越相似。

元数据搜索

我们希望把每个问答对的问题存到元数据中,这样就可以使用问题的精准搜索了。我们先定义Redis中的元数据字段:

@Bean
public RedisVectorStore vectorStore(EmbeddingClient embeddingClient) {RedisVectorStore.RedisVectorStoreConfig config = RedisVectorStore.RedisVectorStoreConfig.builder().withURI("redis://localhost:6379").withMetadataFields(RedisVectorStore.MetadataField.text("filename"),RedisVectorStore.MetadataField.text("question")).build();return new RedisVectorStore(config, embeddingClient);
}

先删除Redis中的Index:

redis-cli FT.DROPINDEX spring-ai-index DD

然后设置每个Document的元数据:

public List<Document> loadText() {TextReader textReader = new TextReader(resource);textReader.getCustomMetadata().put("filename", "meituan-qa.txt");List<Document> documents = textReader.get();ZhouyuTextSplitter zhouyuTextSplitter = new ZhouyuTextSplitter();List<Document> list = zhouyuTextSplitter.apply(documents);// 把问题存到元数据中list.forEach(document -> document.getMetadata().put("question", document.getContent().split("\\n")[0]));// 向量存储vectorStore.add(list);return list;
}

重新进行向量化以及存储:
在这里插入图片描述
重新进行相似度搜索:
在这里插入图片描述
定义元数据搜索:

@GetMapping("/documentMetadataSearch")
public List<Document> documentMetadataSearch(@RequestParam String message, @RequestParam String question) {return documentService.metadataSearch(message, question);
}
public List<Document> metadataSearch(String message, String question) {return vectorStore.similaritySearch(SearchRequest.query(message).withTopK(5).withSimilarityThreshold(0.1).withFilterExpression(String.format("question in ['%s']", question)));
}

搜索结果
在这里插入图片描述

组装提示词

通过以上两个步骤,我们可以将自己的知识库进行向量化存储和搜索了,那么接下来,我们只需要将搜索结果和用户问题进行提示词组装送给大模型就可以得到答案了。

@GetMapping("/customerService")
public String customerService(@RequestParam String question) {// 向量搜索List<Document> documentList = documentService.search(question);// 提示词模板PromptTemplate promptTemplate = new PromptTemplate("{userMessage}\n\n 用以下信息回答问题:\n {contents}");// 组装提示词Prompt prompt = promptTemplate.create(Map.of("userMessage", question, "contents", documentList));// 调用大模型return chatClient.call(prompt).getResult().getOutput().getContent();
}

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

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

相关文章

C语言——数组

1.数组的概念 数组是一组相同类型元素的集合&#xff1b; 数组中可以存放1个或多个元素&#xff0c;但数组元素个数不能为0。 同时数组可以分为一维数组和多维数组&#xff0c;多维数组一般常见 是二维数组。 2.一维数组的创建和初始化 一维数组的创建的基本语法&#xff1a; …

大数据-168 Elasticsearch 单机云服务器部署运行 详细流程

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

【C++】类和对象(类的默认成员函数)

目录 一.构造函数 二.析构函数 三.拷贝构造函数 四.赋值运算符重载 五.取地址运算符重载 默认成员函数就是用户没有显式实现&#xff0c;编译器会自动生成的成员函数称为默认成员函数。一个类&#xff0c;我们不写的情况下编译器会默认生成以下6个默认成员函数。 一.构造…

目标检测中的损失函数

损失函数是用来衡量模型与数据的匹配程度的&#xff0c;也是模型权重更新的基础。计算损失产生模型权重的梯度&#xff0c;随后通过反向传播算法&#xff0c;模型权重得以更新进而更好地适应数据。一般情况下&#xff0c;目标损失函数包含两部分损失&#xff0c;一个是目标框分…

Vue3万能初始化

项目创建后第一步 项目创建&#xff1a;Vue3创建-CSDN博客 创建后执行下面 删除默认提供的HelloWorld.vue组件和src/APP.vue中的默认样式和内容。 App.vue&#xff0c;代码&#xff1a; <template></template><script setup></script><style&g…

数控机械制造工厂ERP适用范围有哪些

在当今制造业高速发展的背景下&#xff0c;企业资源计划(ERP)系统已成为提升工厂管理效率、实现生产自动化与信息化的关键工具。特别是对于数控机械制造工厂而言&#xff0c;一个合适的ERP系统能够帮助其优化生产流程、提高产品质量、降低生产成本并增强市场竞争力。 1. 生产计…

IP数据包格式、ICMP封装步骤

IP数据包格式 版本号&#xff1a;占4位&#xff0c;表示IP协议的版本&#xff0c;目前广泛使用的是IPv4&#xff0c;其版本号为4。 首部长度&#xff1a;占4位&#xff0c;表示IP首部的长度&#xff0c;单位为32位字节。首部长度最小为20字节&#xff0c;最大为60字节。 服务…

dayjs日期格式化,开发uniapp或unicloud前后端进行时间格式转换

一、 为什么要用日期格式化 因为在开发项目过程中&#xff0c;会遇到各种各样的日期格式&#xff0c;有的显示完整的年-月-日 时:分:秒&#xff0c;而有的场景就只显示月-日等格式&#xff0c;还有就是显示当前时间和注册时间的间隔时长等&#xff0c;场景非常多&#xff0c;如…

idea2024年版本

最简单安装2024.2版本idea 内带安装教程 ** 下载链接&#xff1a;https://pan.quark.cn/s/ab24afbaa43f 提取码&#xff1a;KHrq

jmeter发送post请求

在jmeter中&#xff0c;有两种常用的请求方式&#xff0c;get和post.它们两者的区别在于get请求的参数一般是放在路径中&#xff0c;可以使用用户自定义变量和函数助手等方式进行参数化&#xff0c;而post请求的参数不能随url发送&#xff0c;而是作为请求体提交给服务器。而在…

FreeRTOS——中断管理

中断理论剖析 中断简介 中断是一种机制&#xff0c;用于处理高优先级的事件或故障。当一个中断事件发生时&#xff0c;单片机可以立即中断正在执行的程序&#xff0c;转而处理中断事件。这种机制可以提高系统的响应速度和实时性。 中断优先级分组设置 ARM Cortex-M使用了8位宽…

Wed前端入门——HTML、CSS

Wed前端入门——HTML、CSS 一般的页面有HTML、CSS以及JavaScript组成 HTML定义了页面的结构和内容&#xff0c;包括文本、图像、链接等等CSS用于定义页面的布局和样式JS用于添加交互性和动态功能作用 一、HTML 基本格式&#xff1a; <!-- 文档类型为HTML --> <!D…

【C++笔试强训】如何成为算法糕手Day9

学习编程就得循环渐进&#xff0c;扎实基础&#xff0c;勿在浮沙筑高台 循环渐进Forward-CSDN博客 目录 循环渐进Forward-CSDN博客 添加逗号 思路&#xff1a; 代码实现&#xff1a; 跳台阶 思路&#xff1a; 代码实现&#xff1a; 扑克牌顺子 思路&#xf…

收藏!时间序列特征提取最全总结

时间序列数据在很多领域都是重要的结构化数据形式&#xff0c;例如金融、经济、生态学、神经科学和物理学。在多个时间点观测或测量的数据形成了时间序列。许多时间序列是固定频率的&#xff0c;也就是说数据是根据相同的规则定期出现的&#xff0c;例如每15秒、每5分钟或每月1…

高级IO——五种IO模型

一般我们在写一些简单的小项目的时候&#xff0c;免不了会用到IO接口&#xff0c;比如C语言中的scanf/printf又或者是 C中的cout/cin&#xff0c;或者是在Linux操作系统中的文件IO接口read/write。这些接口默认都是阻塞的&#xff0c; 这又引出了阻塞/非阻塞IO的概念&#xff0…

Lobby——网络游戏大厅设计与参考建议!!!

随着网络游戏越来越多&#xff0c;游戏的主界面也是做的越来越花哨&#xff0c;各种界面层出不穷&#xff01;恨不得&#xff0c;一个主界面直接把所有的业务塞满&#xff01;&#xff01; 看着这十年不换的界面&#xff0c;经久不换&#xff0c;如同嚼蜡&#xff01;你会发现x…

GPU编程(1)GPU架构

总体 显卡结构 风扇在下面&#xff0c;采用热管方式&#xff0c;用气体液体的转化来带走热量。包裹热管的是铜制散热板&#xff0c;外围是铝制格扇&#xff0c;更快排除热量。 视频接口个pcie都是直接连接GPU。 所有的供电模块公用一个PWM芯片。 显存的型号就称之为显存颗粒…

保护企业终端安全,天锐DLP帮助企业智能管控终端资产

为有效预防员工非法调包公司的软硬件终端资产&#xff0c;企业管理员必须建立高效的企业终端安全管控机制&#xff0c;确保能够即时洞察并确认公司所有软硬件资产的状态变化。这要求企业要有一套能够全面管理终端资产的管理系统&#xff0c;确保任何未经授权的资产变动都能被迅…

Git推送被拒

今天开发完成一个新的需求&#xff0c;将自己的分支合并到test分支后&#xff0c;推送到远程仓库&#xff0c;结果显示推送被拒&#xff1a; 原因是因为有人更新了test分支的代码&#xff0c;我在合并之前没有拉取最新的test分支代码&#xff0c;所以他提示我“推送前需要合并…

企业级业务架构和IT架构规划方案(120页PPT下载)

方案内容综述 方案涵盖了从战略分析到具体实施路径的内容。提出了IT架构规划的工作思路&#xff0c;包括项目启动、部门访谈、资料收集、内部数据库搜索与先进实践研究等步骤&#xff0c;旨在通过这些步骤完成现状及差距分析&#xff0c;并基于此设计未来的应用架构、数据架构…