Spring AI应用:利用DeepSeek+嵌入模型+Milvus向量数据库实现检索增强生成- ...

打印 上一主题 下一主题

主题 1718|帖子 1718|积分 5158

目录
一、文件内容剖析
1、Apache Tika先容
2、Apache Tika实践
二、文天职片(切割)
1. 使用分词库
2. 基于呆板学习的方法
3. 基于LangChain的文天职割工具
3、集成HanLP和LangChain4J
三、上传文档到向量数据库
四、Advisors API
1、Advisors 的核心功能与设计目标
2、内建 Advisors 的分类与用途
3、自定义 Advisors 的实现
五、实现检索增强生成
1、Spring AI的向量存储依靠
2、实现Spring AI向量存储本领
3、测试大模型
六、总结

继上篇我们相识了向量数据库Milvus的基本知识(上篇地址:Spring AI应用:利用DeepSeek+嵌入模型+Milvus向量数据库实现检索增强生成--RAG应用(一)(超详细)-CSDN博客),本期将继续学习基于向量数据库如何实现检索增强生成。
那么在有了向量数据库后,我们如今须要做的就是将知识库相关文档内容剖析,然后使用相关工具将文档内容分割,并转换成向量表示存储在Milvus中。
一、文件内容剖析

在Java中,文件内容剖析可以通过多种方式实现,具体取决于文件的格式息争析需求。如果我们要搭建知识库,文档的类型肯定是多样化的。那么有没有什么工具可以不考虑文档类型,又可以实现我们的需求呢?答案就是Apache Tika。
1、Apache Tika先容

简介:


  • 开发语言:Java
  • 官方网站:Apache Tika
  • 特点:Tika 是一个强大的内容分析工具包,支持从超过1000种文件类型中提取文本、元数据和其他布局化数据。它可以或许处置惩罚多种格式的文档,包括但不限于PDF、Word、Excel、PPT等。
  • 使用场景:实用于须要处置惩罚多种不同格式文档的大规模信息抽取任务。
主要特性:


  • 支持广泛的文档格式剖析。
  • 提供了丰富的元数据提取本领。
  • 可以通过下令行或编程接口(如Java API)调用。
  • 内置天然语言检测功能。
  • 支持OCR(通过集成外部服务如Tesseract)来处置惩罚图像中的文字。
优点:


  • 强大的多格式支持,几乎可以处置惩罚任何类型的文档。
  • 易于与大数据生态体系集成(比方Hadoop)。
  • 活跃的社区支持和连续更新。
缺点:


  • 对于某些特定格式(如PDF),可能不如专用工具精确。
  • 性能上可能不是最优,特别是在处置惩罚大量小文件时。
2、Apache Tika实践

接下来我们将学习如何在Spring Boot里使用Apache Tika剖析文件内容,首先在pom.xml里引入相关依靠
  1. <!-- Tika依赖 文件内容提取 -->
  2. <dependency>
  3.    <groupId>org.apache.tika</groupId>
  4.    <artifactId>tika-core</artifactId>
  5.    <version>3.0.0</version>
  6. </dependency>
  7. <dependency>
  8.    <groupId>org.apache.tika</groupId>
  9.    <artifactId>tika-parsers-standard-package</artifactId>
  10.    <version>3.0.0</version>
  11. </dependency>
复制代码
然后新建一个TikaUtil的服务类,编写一个文件剖析的方法,代码如下:
  1. package com.wanganui.tika;
  2. import com.alibaba.fastjson.JSON;
  3. import dev.langchain4j.data.document.Document;
  4. import dev.langchain4j.data.document.DocumentSplitter;
  5. import dev.langchain4j.data.document.splitter.DocumentSplitters;
  6. import dev.langchain4j.data.segment.TextSegment;
  7. import dev.langchain4j.model.openai.OpenAiTokenizer;
  8. import lombok.RequiredArgsConstructor;
  9. import org.apache.tika.metadata.Metadata;
  10. import org.apache.tika.parser.AutoDetectParser;
  11. import org.apache.tika.parser.ParseContext;
  12. import org.apache.tika.parser.Parser;
  13. import org.apache.tika.sax.BodyContentHandler;
  14. import org.springframework.stereotype.Component;
  15. import org.springframework.web.multipart.MultipartFile;
  16. import java.util.Arrays;
  17. import java.util.HashMap;
  18. import java.util.List;
  19. import java.util.Map;
  20. import java.util.stream.Collectors;
  21. /**
  22. * @author xtwang
  23. * @des 文件内容提取工具
  24. * @date 2025/2/25 下午4:57
  25. */
  26. @Component
  27. @RequiredArgsConstructor
  28. public class TikaUtil {
  29.     public String extractTextString(MultipartFile file) {
  30.         try {
  31.             // 创建解析器--在不确定文档类型时候可以选择使用AutoDetectParser可以自动检测一个最合适的解析器
  32.             Parser parser = new AutoDetectParser();
  33.             // 用于捕获文档提取的文本内容。-1 参数表示使用无限缓冲区,解析到的内容通过此hander获取
  34.             BodyContentHandler bodyContentHandler = new BodyContentHandler(-1);
  35.             // 元数据对象,它在解析器中传递元数据属性---可以获取文档属性
  36.             Metadata metadata = new Metadata();
  37.             // 带有上下文相关信息的ParseContext实例,用于自定义解析过程。
  38.             ParseContext parseContext = new ParseContext();
  39.             parser.parse(file.getInputStream(), bodyContentHandler, metadata, parseContext);
  40.             // 获取文本
  41.             return bodyContentHandler.toString();
  42.         } catch (Exception e) {
  43.             e.printStackTrace();
  44.             return null;
  45.         }
  46.     }
  47. }
复制代码
完成后在MilvusController里添加一个控制器,用于文件上传剖析
  1. @Operation(summary = "解析文件内容")
  2. @PostMapping("/extractFileString")
  3. public ResponseEntity<String> extractFileString(MultipartFile file) {
  4.     return ResponseEntity.ok(tikaUtil.extractTextString(file));
  5. }
复制代码
运行步调,访问swagger,选择一个文档测试剖析功能

可以看到步调已成功剖析出了word文档里的内容,接下来再试试其他文件格式
 如上图我们的步调日志文件也可以剖析。
通过以上步骤可以基本实现不同文档类型的内容剖析。但如今的问题是,一般的企业里相关文档内容都是比较多的。如果我们将剖析出来的文本一股脑全往模型里塞,可能会导致上下文过长,达不到我们想要的效果。
为相识决这种情况,我们须要将文天职片,把内容切割成一个一个的文本块,再将文本块转换为向量表示存入向量数据库,如许大模型在检索的时间只须要将相关的文本块添加到上下文中,既能保证回答的准确性,也解决了上下文过长的问题。
二、文天职片(切割)

那么如何将过长的文本内容分割成文本块,又要保证每块内容的语义完整,这就须要用到天然语言处置惩罚(NLP)技能,可以考虑使用以下几种方法和技能:
1. 使用分词库



  • Stanford NLP: 提供了丰富的工具集用于处置惩罚各种天然语言处置惩罚任务,包括中文分词、命名实体辨认等。可以利用其提供的Java API进行文天职割。
  • Jieba分词: 虽然Jieba最初是为Python设计的,但也有Java版本可用(如jieba-analysis)。它支持精确模式、全模式等多种分词模式,并且可以或许通过自定义词典来进步分词准确性。
  • HanLP: HanLP是一个高效的中文处置惩罚工具包,支持多种语言和功能,包括分词、词性标注、命名实体辨认等。HanLP提供了Java接口,非常适合须要对中文文本进行处置惩罚的场景。
2. 基于呆板学习的方法



  • CRF (条件随机场): CRF是一种序列标注模型,广泛应用于分词、词性标注等领域。可以通过练习特定领域的数据集来进步分词准确性。
  • 深度学习框架: 如TensorFlow、PyTorch等虽然主要以Python为主,但也可以通过调用它们的Java API大概通过RESTful服务的方式与Java步调集成,使用预练习的语言模型(比方BERT)来进行更复杂的文天职析任务,包括基于上下文感知的分词。
3. 基于LangChain的文天职割工具



  • LangChain4J 是一个用于开发语言模型应用的框架,Spring AI的直接竞争对手,它提供了多种工具和组件来处置惩罚文本数据,包括文档分割(Document Splitters)。文档分割器(DocumentSplitters)的主要目的是将大型文档分割成更小的块或片断,以便更有效地处置惩罚或分析。这些分割策略对于确保文本块适合于特定的语言模型输入限制(如最大token长度)非常重要。
那么在Java开发中,对于像Stanford NLP、HanLP如许的库,可以直接添加依靠项到你的pom.xml中,如许我们开发起来可以避开Python调用的繁杂过程。
3、集成HanLP和LangChain4J

接下来我们将使用HanLP分词库和LangChain4J两种方式来做一个文天职割的对比。
HanLP分词库还提供了语义分析、文本择要、文天职类、情绪分析等多种功能,感爱好的同学可以查看官方文档:HanLP | 在线演示
在pom.xml里添加相关依靠
  1. <!-- LangChain4J依赖 -->
  2. <dependency>
  3.     <groupId>dev.langchain4j</groupId>
  4.     <artifactId>langchain4j-open-ai</artifactId>
  5.     <version>0.31.0</version>
  6. </dependency>
  7. <dependency>
  8.     <groupId>dev.langchain4j</groupId>
  9.     <artifactId>langchain4j</artifactId>
  10.     <version>0.31.0</version>
  11. </dependency>
  12. <!-- HanLP中文分词 -->
  13. <dependency>
  14.     <groupId>com.hankcs</groupId>
  15.     <artifactId>hanlp</artifactId>
  16.     <version>portable-1.8.6</version>
  17. </dependency>
复制代码
在TikaUtil里增长对应的分词方法,代码如下
  1. // 目标段落长度(汉字字符数)
  2.     private static final int TARGET_LENGTH = 200;
  3.     // 允许的段落长度浮动范围(±20字)
  4.     private static final int LENGTH_TOLERANCE = 20;
  5. /**
  6.      * 使用langchain4j的分段工具
  7.      *
  8.      * @param content 输入的大文本
  9.      * @return 段落列表,每个段落至少包含minLength个字符
  10.      */
  11.     public List<String> splitParagraphsLangChain(String content) {
  12.         DocumentSplitter splitter = DocumentSplitters.recursive(TARGET_LENGTH, LENGTH_TOLERANCE, new OpenAiTokenizer());
  13.         List<TextSegment> split = splitter.split(Document.document(content));
  14.         return split.stream().map(TextSegment::text).toList();
  15.     }
  16.    /**
  17.      * 使用HanLP进行句子分割
  18.      *
  19.      * @param text 输入的大文本
  20.      * @return 段落列表,每个段落至少包含minLength个字符
  21.      */
  22.     public static List<String> splitParagraphsHanLP(String text) {
  23.         List<String> paragraphs = new ArrayList<>();
  24.         if (text == null || text.isEmpty()) {
  25.             return paragraphs;
  26.         }
  27.         // 1. 使用 HanLP 分词并分句
  28.         List<String> sentences = splitSentences(text);
  29.         // 2. 动态合并句子到段落
  30.         paragraphs = mergeSentencesIntoParagraphs(sentences);
  31.         return paragraphs;
  32.     }
  33.     // 使用 HanLP 分词实现分句
  34.     private static List<String> splitSentences(String text) {
  35.         List<String> sentences = new ArrayList<>();
  36.         StringBuilder currentSentence = new StringBuilder();
  37.         List<Term> terms = HanLP.segment(text);
  38.         for (Term term : terms) {
  39.             currentSentence.append(term.word);
  40.             // 使用正则表达式匹配句子结束标点(支持中英文标点)
  41.             if (term.word.matches("[。!?;.!?;]+")) {
  42.                 sentences.add(currentSentence.toString());
  43.                 currentSentence.setLength(0);
  44.             }
  45.         }
  46.         // 添加最后一个句子(如果没有标点结尾)
  47.         if (!currentSentence.isEmpty()) {
  48.             sentences.add(currentSentence.toString());
  49.         }
  50.         return sentences;
  51.     }
  52.     // 动态合并句子到段落
  53.     private static List<String> mergeSentencesIntoParagraphs(List<String> sentences) {
  54.         List<String> paragraphs = new ArrayList<>();
  55.         StringBuilder currentParagraph = new StringBuilder();
  56.         int currentLength = 0;
  57.         for (String sentence : sentences) {
  58.             int sentenceLength = countChineseChars(sentence);
  59.             // 处理超长句子(强制分割)
  60.             if (sentenceLength > TARGET_LENGTH + LENGTH_TOLERANCE) {
  61.                 if (currentLength > 0) {
  62.                     paragraphs.add(currentParagraph.toString());
  63.                     currentParagraph.setLength(0);
  64.                     currentLength = 0;
  65.                 }
  66.                 // 按标点二次分割超长句
  67.                 List<String> subSentences = splitLongSentence(sentence);
  68.                 paragraphs.addAll(subSentences);
  69.                 continue;
  70.             }
  71.             // 合并到当前段落的条件
  72.             if (currentLength + sentenceLength <= TARGET_LENGTH + LENGTH_TOLERANCE) {
  73.                 currentParagraph.append(sentence);
  74.                 currentLength += sentenceLength;
  75.             } else {
  76.                 // 当前段落达到长度,保存并重置
  77.                 paragraphs.add(currentParagraph.toString());
  78.                 currentParagraph.setLength(0);
  79.                 currentParagraph.append(sentence);
  80.                 currentLength = sentenceLength;
  81.             }
  82.         }
  83.         // 添加最后一个段落
  84.         if (currentLength > 0) {
  85.             paragraphs.add(currentParagraph.toString());
  86.         }
  87.         return paragraphs;
  88.     }
  89.     // 处理超长句子:按逗号、分号等二次分割
  90.     private static List<String> splitLongSentence(String sentence) {
  91.         List<String> validParts = new ArrayList<>();
  92.         StringBuilder current = new StringBuilder();
  93.         int currentLength = 0;
  94.         // 按标点分割句子
  95.         String[] parts = sentence.split("[,;;,]");
  96.         for (String part : parts) {
  97.             int partLength = countChineseChars(part);
  98.             if (currentLength + partLength > TARGET_LENGTH + LENGTH_TOLERANCE) {
  99.                 // 当前部分过长,保存并重置
  100.                 validParts.add(current.toString());
  101.                 current.setLength(0);
  102.                 currentLength = 0;
  103.             }
  104.             // 补回分割符号
  105.             current.append(part).append(",");
  106.             currentLength += partLength;
  107.         }
  108.         // 添加最后一个部分
  109.         if (!current.isEmpty()) {
  110.             validParts.add(current.toString());
  111.         }
  112.         return validParts;
  113.     }
  114.     // 统计中文字符数(忽略标点、英文)
  115.     private static int countChineseChars(String text) {
  116.         return (int) text.chars()
  117.                 .filter(c -> Character.UnicodeScript.of(c) == Character.UnicodeScript.HAN)
  118.                 .count();
  119.     }
复制代码
接下来在MilvusController中,分别添加对应的控制器方法,代码如下
  1. @Operation(summary = "解析文件内容-LangChina分片")
  2. @PostMapping("/splitParagraphsLangChain")
  3. public ResponseEntity<List<String>> splitParagraphsLangChain(MultipartFile file) {
  4.     return ResponseEntity.ok(tikaUtil.splitParagraphsLangChain(tikaUtil.extractTextString(file)));
  5. }
  6. @Operation(summary = "解析文件内容-HanLP分片")
  7. @PostMapping("/splitParagraphsHanLP")
  8. public ResponseEntity<List<String>> splitParagraphsHanLP(MultipartFile file) {
  9.     return ResponseEntity.ok(tikaUtil.splitParagraphsHanLP(tikaUtil.extractTextString(file)));
  10. }
复制代码
运行步调,访问swagger测试对应接口,首先我们用段落分明、符号清晰的文档来测试结果


从两个工具文天职割的结果可以看出以下几点 

  • 段落分割

    • 两种方式都将原始文天职割为了5个主要部分,每个部分都以林夏的经历为线索推进故事。
    • 分割点的选择基本相同,均依据故事叛逆的发展,如信号接收、意识投射、遇到艾露、文明交流与冲突、了局展现等关键情节节点进行划分。

  • 内容完整性

    • 在两种分割方式中,每一个段落都包含了相对完整的情节单位,使得读者可以在每一段中获取到一个情节发展的大致脉络。
    • 文本中的细节描述和对话都被保存下来,保证了故事叙述的连贯性和完整性。

  • 细节处置惩罚

    • 虽然团体布局相似,但在具体句子或短语的断句上可能存在细微差异,不外基于提供的信息,我们无法直接观察到这些细节上的不同,因为两者的文本输出几乎完全相同。

 然后我们再使用标点符号以及段落不明显的文档分别测试


从对比结果来看,LangChain和HanLP两种工具在处置惩罚文天职割时产生了不同的效果。以下是对这两种工具分割结果的分析:
LangChain分割结果特点:

  • 项目阶段划分清晰:LangChain可以或许很好地辨认并分割出“项目正式启动前”、“项目正式启动”、“项目完工后”以及“项目回访”这几个主要阶段,并且每个阶段都有明确的开始和结束标志。
  • 细节内容保存完整:对于每一个工作职责和负责人的描述,LangChain都能准确地进行分段,保持了原文档中信息的完整性。
  • 格式同等性好:在整个分割过程中,LangChain维持了原始文档的布局化格式,使得阅读和理解变得轻易。
HanLP分割结果特点:

  • 部分阶段混合:HanLP在某些情况下未能完全将不同阶段的内容区分开来,比方“项目正式启动”部分内容与前面或背面的内容有所稠浊。
  • 细节处置惩罚较为风雅:虽然在大段落分割上不如LangChain清晰,但HanLP在处置惩罚具体的工作职责细分时体现出了较好的细节捕获本领,好比对“概算、招标控制价、成天职析”的进一步细分处置惩罚得比较细致。
  • 格式略有不同等:相较于LangChain,HanLP生成的部分段落开头可能缺少明确的阶段标识,导致在快速欣赏时不轻易立即分辨出当前段落属于哪个阶段。
总结:


  • 如果须要一个清晰、布局化的输出,尤其是当文档包含多个明显不同的章节或阶段时,LangChain可能是更好的选择。
  • 对于那些更关注于文本内部细节而非团体布局的应用场景,HanLP可能会提供更加精致的分割结果。
根据实际需求的不同,可以选择更适合的工具来进行文天职割处置惩罚。如果目的是为了后续的信息提取或自动化处置惩罚,考虑两者的特点选择合适的工具会更加有效。本人对分词库的相关技能相识有限,文中代码或描述如有错误或遗漏,欢迎各人指正,谢谢!
三、上传文档到向量数据库

上述步骤中,我们学习了如何从文件中提取文件内容,以及如何将长内容进行文天职割。接下来我们将学习如何将分割后的文本转换为向量表示后存入向量数据库中。
1、在MilvusService接口类中新增一个批量插入接口,并在实现类中实现具体逻辑
  1.     /**
  2.      * 批量插入数据
  3.      *
  4.      * @param vectorParam 向量参数
  5.      * @param text        文本
  6.      * @param metadata    元数据
  7.      * @param fileName    文件名
  8.      */
  9.     InsertResp batchInsert(List<float[]> vectorParam, List<String> text, List<String> metadata, List<String> fileName);
复制代码
  1. /**
  2.      * 批量插入数据
  3.      *
  4.      * @param vectorParam 向量参数
  5.      * @param text        文本
  6.      * @param metadata    元数据
  7.      * @param fileName    文件名
  8.      */
  9.     @Override
  10.     public InsertResp batchInsert(List<float[]> vectorParam, List<String> text, List<String> metadata, List<String> fileName) {
  11.         if (vectorParam.size() == text.size() && vectorParam.size() == metadata.size() && vectorParam.size() == fileName.size()) {
  12.             List<JsonObject> jsonObjects = new ArrayList<>();
  13.             for (int i = 0; i < vectorParam.size(); i++) {
  14.                 JsonObject jsonObject = new JsonObject();
  15.                 // 数组转换成JsonElement
  16.                 jsonObject.add(MilvusArchive.Field.FEATURE, new Gson().toJsonTree(vectorParam.get(i)));
  17.                 jsonObject.add(MilvusArchive.Field.TEXT, new Gson().toJsonTree(text.get(i)));
  18.                 jsonObject.add(MilvusArchive.Field.METADATA, new Gson().toJsonTree(metadata.get(i)));
  19.                 jsonObject.add(MilvusArchive.Field.FILE_NAME, new Gson().toJsonTree(fileName.get(i)));
  20.                 jsonObjects.add(jsonObject);
  21.             }
  22.             InsertReq insertReq = InsertReq.builder()
  23.                     // 集合名称
  24.                     .collectionName(MilvusArchive.COLLECTION_NAME)
  25.                     .data(jsonObjects)
  26.                     .build();
  27.             return milvusClientV2.insert(insertReq);
  28.         }
  29.         return null;
  30.     }
复制代码
2、新建一个TikaVo类,用作传输文档剖析分片后的结果
  1. package com.wanganui.tika;
  2. import io.swagger.v3.oas.annotations.media.Schema;
  3. import lombok.Data;
  4. import lombok.experimental.Accessors;
  5. import java.io.Serializable;
  6. import java.util.List;
  7. /**
  8. * @author xtwang
  9. * @des tika返回对象
  10. * @date 2025/2/26 上午10:30
  11. */
  12. @Accessors(chain = true)
  13. @Schema(description = "tika返回对象")
  14. @Data
  15. public class TikaVo implements Serializable {
  16.     @Schema(description = "文本内容")
  17.     private List<String> text;
  18.     @Schema(description = "元数据")
  19.     private List<String> metadata;
  20. }
复制代码
3、在TikaUtil服务中新建一个用于剖析并分片的方法
  1. /**
  2.      * 文件内容提取
  3.      *
  4.      * @param file 上传的文件
  5.      * @return 文件内容
  6.      */
  7.     public TikaVo extractText(MultipartFile file) {
  8.         try {
  9.             // 创建解析器--在不确定文档类型时候可以选择使用AutoDetectParser可以自动检测一个最合适的解析器
  10.             Parser parser = new AutoDetectParser();
  11.             // 用于捕获文档提取的文本内容。-1 参数表示使用无限缓冲区,解析到的内容通过此hander获取
  12.             BodyContentHandler bodyContentHandler = new BodyContentHandler(-1);
  13.             // 元数据对象,它在解析器中传递元数据属性---可以获取文档属性
  14.             Metadata metadata = new Metadata();
  15.             // 带有上下文相关信息的ParseContext实例,用于自定义解析过程。
  16.             ParseContext parseContext = new ParseContext();
  17.             parser.parse(file.getInputStream(), bodyContentHandler, metadata, parseContext);
  18.             // 获取文本
  19.             String text = bodyContentHandler.toString();
  20.             // 元数据信息
  21.             String[] names = metadata.names();
  22.             // 将元数据转换成JSON字符串
  23.             Map<String, String> map = new HashMap<>();
  24.             for (String name : names) {
  25.                 map.put(name, metadata.get(name));
  26.             }
  27.             return splitParagraphs(text);
  28.         } catch (Exception e) {
  29.             e.printStackTrace();
  30.             return null;
  31.         }
  32.     }
  33.     /**
  34.      * 使用langchain4j的分段工具
  35.      *
  36.      * @param content 文本内容
  37.      */
  38.     private TikaVo splitParagraphs(String content) {
  39.         DocumentSplitter splitter = DocumentSplitters.recursive(TARGET_LENGTH, LENGTH_TOLERANCE, new OpenAiTokenizer());
  40.         List<TextSegment> split = splitter.split(Document.document(content));
  41.         return new TikaVo().setText(split.stream().map(TextSegment::text).toList()).setMetadata(split.stream().map(textSegment -> JSON.toJSONString(textSegment.metadata())).toList());
  42.     }
复制代码
4、在MilvusController类中新增一个上传知识库的控制器,将剖析分片后的文本内容逐一转换成向量表示,再调用批量插入方法生存到向量数据库。
  1. @Operation(summary = "上传知识库")
  2. @PostMapping("/uploadFile")
  3. public ResponseEntity<InsertResp> uploadFile(MultipartFile file) {
  4.         // 获取文件内容
  5.         TikaVo tikaVo = tikaUtil.extractText(file);
  6.         if (tikaVo != null && Objects.nonNull(tikaVo.getText())) {
  7.             List<float[]> embedList = new ArrayList<>();
  8.             List<String> textList = tikaVo.getText();
  9.             List<String> metadataList = tikaVo.getMetadata();
  10.             List<String> fileNameList = new ArrayList<>();
  11.             for (String s : tikaVo.getText()) {
  12.                 embedList.add(ollamaEmbeddingModel.embed(s));
  13.                 fileNameList.add(file.getOriginalFilename());
  14.             }
  15.             return ResponseEntity.ok(milvusService.batchInsert(embedList, textList, metadataList, fileNameList));
  16.         }
  17.         return ResponseEntity.ok(null);
  18. }
复制代码
5、完成后运行步调,访问swagger页面,测试功能是否正常

可以看到文档被分成了多个文本块,然后存入了向量数据库,接下来我们调用一下搜刮接口,测试一下相关文本能不能搜刮出来
 从上图可以看出,相关的文本块已按照相似度排序返回。
四、Advisors API

我们前面学习了这么多知识,铺垫了这么多东西,如今终于要进入正题了。那么怎么来实现检索增强生成呢?通俗来说就是将用户的问题先转换为向量表示,再从向量数据库中搜刮相关文本块,然后将搜刮结果组装到用户会话的上下文中传递给大语言模型,大模型再根据这些内容做出准确的回答。
这个逻辑的前两步我们已经实现了,那么如何将文本块添加到谈天上下文呢,这就须要使用到Spring AI的Advisors API,我们来学习一下这门技能。
在Spring AI 的官网中是如许先容的:Spring AI 的Advisors API 提供了一种灵活而强大的方法来拦截、修改和增强 Spring 应用步调中 AI 驱动的交互。通过利用Advisors API,开发人员可以创建更复杂、可重用和可维护的 AI 组件。主要优势包括封装重复出现的生成式 AI 模式、转换发送到和来自语言模型 (LLM) 的数据,以及提供跨各种模型和用例的可移植性。
说的通俗一点你可以理解为它就是AOP,只不外是专注于 AI 模型交互的上下文处置惩罚。
1、Advisors 的核心功能与设计目标


  • 请求/响应拦截与修改
    Advisors 通过链式布局拦截并动态修改发送给 AI 模型的请求(AdvisedRequest)和模型返回的响应(AdvisedResponse)。比方,可以增强输入数据的上下文或过滤敏感内容。
  • 功能模块化与复用

    • 封装重复逻辑:如日志记录、汗青会话管理(MessageChatMemoryAdvisor)或敏感词校验(SafeGuardAdvisor)等通勤奋能可封装为可复用的 Advisor。
    • 数据转换:优化输入数据格式(如提示词模板化)或后处置惩罚输出结果(如 JSON 布局化)。

  • 跨模型兼容性
    通过抽象接口(如 CallAroundAdvisor 和 StreamAroundAdvisor),Advisors 可适配不同 AI 模型(如 OpenAI、HuggingFace),提升代码可移植性。
2、内建 Advisors 的分类与用途

Spring AI 提供多种预置 Advisors,覆盖常见 AI 交互场景:

  • 上下文记忆管理

    • MessageChatMemoryAdvisor:将用户与模型的对话汗青添加到请求的 messages 参数中,需模型支持多轮对话(在我另一篇博客中有实践:Spring AI进阶:AI谈天呆板人之ChatMemory长期化(二)_springai chatclient 向量数据库-CSDN博客)。
    • PromptChatMemoryAdvisor:将汗青记录封装到体系提示词(systemPrompt),实用于不直接支持 messages 参数的模型。

  • 检索增强生成(RAG)
    QuestionAnswerAdvisor:在用户提问时,从向量数据库检索相关文档片断并附加到输入中,提升回答准确性(本次将要用到的技能)。
  • 安全与日志

    • SafeGuardAdvisor:拦截包含敏感词的请求,阻止调用 AI 模型。
    • SimpleLoggerAdvisor:记录请求与响应的日志,便于调试。

  • 恒久记忆存储
    VectorStoreChatMemoryAdvisor:将对话汗青存储到向量数据库,支持通过 chat_memory_conversation_id 关联会话,需注意制止因 ID 管理不妥导致数据冗余。
3、自定义 Advisors 的实现

用户可通过实现 CallAroundAdvisor 或 StreamAroundAdvisor 接口创建自定义逻辑。以下是一个 Re-Reading(Re2)Advisor 的示例,用于在请求前重复用户问题以提升模型理解:
  1. public class ReReadingAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {
  2.     @Override
  3.     public AdvisedResponse aroundCall(AdvisedRequest request, CallAroundAdvisorChain chain) {
  4.         // 修改请求:将原始问题追加到提示词模板
  5.         String modifiedText = String.format("%s\n请再仔细阅读问题: %s", request.userText(), request.userText());
  6.         AdvisedRequest modifiedRequest = AdvisedRequest.from(request)
  7.             .withUserText(modifiedText)
  8.             .build();
  9.         return chain.nextAroundCall(modifiedRequest);
  10.     }
  11. }
复制代码
通过 before 方法预处置惩罚请求,或通过 aroundCall 控制链式调用流程。
五、实现检索增强生成

学习了Spring AI的Advisors API,接下来我们把它运用到实际开发中,本次我们就不自定义Advisors的实现了,直接使用Spring AI提供的QuestionAnswerAdvisor来实现我们的需求。
1、Spring AI的向量存储依靠

在使用QuestionAnswerAdvisor时须要明确向量存储库以及相关本领接口,Spring AI提供了两种依靠方式来引入对向量数据库Milvus的支持
自动装配的依靠
  1. <dependency>
  2.         <groupId>org.springframework.ai</groupId>
  3.         <artifactId>spring-ai-milvus-store-spring-boot-starter</artifactId>
  4. </dependency>
复制代码
手动设置的依靠
  1. <dependency>
  2.         <groupId>org.springframework.ai</groupId>
  3.         <artifactId>spring-ai-milvus-store</artifactId>
  4. </dependency>
复制代码
本次演示我们使用的是手动设置的依靠项。
2、实现Spring AI向量存储本领

新建一个MilvusVectorStore类实现VectorStore接口,在实现类中注入MilvusService和OllamaEmbeddingModel,通过我们自己写的向量数据库操纵接口来实现Spring AI的向量数据库操纵本领,代码如下
  1. package com.wanganui.chat;
  2. import com.alibaba.fastjson.JSON;
  3. import com.wanganui.vector.service.MilvusService;
  4. import lombok.RequiredArgsConstructor;
  5. import org.jetbrains.annotations.NotNull;
  6. import org.springframework.ai.document.Document;
  7. import org.springframework.ai.ollama.OllamaEmbeddingModel;
  8. import org.springframework.ai.vectorstore.SearchRequest;
  9. import org.springframework.ai.vectorstore.VectorStore;
  10. import org.springframework.ai.vectorstore.filter.Filter;
  11. import org.springframework.stereotype.Component;
  12. import java.util.List;
  13. /**
  14. * @author xtwang
  15. * @des 自定义向量数据库相关接口实现
  16. * @date 2025/2/26 上午11:06
  17. */
  18. @Component
  19. @RequiredArgsConstructor
  20. public class MilvusVectorStore implements VectorStore {
  21.     private final MilvusService milvusService;
  22.     private final OllamaEmbeddingModel ollamaEmbeddingModel;
  23.     @Override
  24.     public void add(List<Document> documents) {
  25.         if (!documents.isEmpty()) {
  26.             for (Document document : documents) {
  27.                 milvusService.insert(ollamaEmbeddingModel.embed(document), document.getText(), JSON.toJSONString(document.getMetadata()), null);
  28.             }
  29.         }
  30.     }
  31.     @Override
  32.     public void delete(List<String> idList) {
  33.         if (!idList.isEmpty()) {
  34.             // idList转换为id数组
  35.             String[] ids = idList.toArray(new String[0]);
  36.             milvusService.delete(ids);
  37.         }
  38.     }
  39.     @Override
  40.     public void delete(Filter.Expression filterExpression) {
  41.         milvusService.delete(filterExpression.toString());
  42.     }
  43.     @Override
  44.     public List<Document> similaritySearch(@NotNull SearchRequest request) {
  45.         return milvusService.search(request);
  46.     }
  47. }
复制代码
然后在谈天客户端设置ChatConfig中将本领提供给Spring AI
  1. package com.wanganui.chat;
  2. import org.springframework.ai.chat.client.ChatClient;
  3. import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
  4. import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor;
  5. import org.springframework.ai.ollama.OllamaChatModel;
  6. import org.springframework.context.annotation.Bean;
  7. import org.springframework.context.annotation.Configuration;
  8. /**
  9. * @author xtwang
  10. * @des AI聊天配置
  11. * @date 2025/2/11 上午9:39
  12. */
  13. @Configuration
  14. public class ChatConfig {
  15.     @Bean
  16.     public ChatClient chatClient(OllamaChatModel ollamaChatModel, RedisChatMemory redisChatMemory, MilvusVectorStore milvusVectorStore) {
  17.         return ChatClient.builder(ollamaChatModel)
  18.                 .defaultSystem("你是一个RAG知识库问答机器人,致力于解决用户提出的问题,并给出详细的解决方案")
  19.                 .defaultAdvisors(new MessageChatMemoryAdvisor(redisChatMemory), new QuestionAnswerAdvisor(milvusVectorStore))
  20.                 .build();
  21.     }
  22. }
复制代码
3、测试大模型

运行步调,通过谈天接口测试之前上传到向量数据库的文档可否被Spring AI检索出来

可以看到,Spring AI已成功从向量数据库中检索出了相关文本添加到了上下文中,并且根据文本作出了准确的回答。
六、总结

在本篇文章中,我们深入探究了如何利用 Spring AI 和 Milvus 向量数据库 实现检索增强生成(RAG)应用。通过联合 Apache Tika 进行多格式文档剖析、HanLP 和 LangChain4J 进行文天职片,以及 Spring AI 的 Advisors API 进行上下文增强,我们成功构建了一个可以或许从知识库中检索相关信息并生成准确回答的智能问答体系。
通过本文的学习,我们掌握了如何利用 Spring AI 和 Milvus 向量数据库构建一个高效的检索增强生成体系。无论是企业知识库管理还是智能问答体系,这一技能组合都显现了强大的潜力。希望本文能为开发者提供有价值的参考,助力 AI 应用的创新与落地。
参考资料:
Apache Tika::Apache Tika
LangChain4J:introduction | LangChain中文网
HanLP:HanLP | 在线演示
Spring AI:Milvus :: Spring AI 参考 - Spring 框架
代码参考:ai-chat: Spring AI 相关技能先容

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

东湖之滨

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表