5.1知识库概念进阶

有了之前第四章的知识,我们知道了整一个知识库运行的流程,接下来我们对上一节的内容进行细节划分

先来回顾以下整体流程:

可以大致分为四个部分

1.文档收集与切割

2.向量转换和存储

3.文档过滤与检索

4.查询增强与关联

接下来对这四个部分进行细节的概念解析:

Part1.文档收集与切割--ETL

所谓ETL就是我们的part1的大致概括,三个字母对应着该部分三个步骤

E--extract:提取文档

该步骤主要的人物就是"阅读提取"知识,然后转换为SpringAI框架方便使用的对象--document, 关于document对象的大致内容在上一期文章有讲解,不理解可以移步到这篇文章:4.1RAG知识库基础-CSDN博客

实现方案

Sprin⁠g AI 通过 D‌ocumentRe​ader 组件实现‎文档抽取,也就是把‌文档加载到内存中。

看下源码,DocumentReader 接口实现了 Supplier<List<Document>> 接口,主要负责从各种数据源读取数据并转换为 Document 对象集合。

java 复制代码
public interface DocumentReader extends Supplier<List<Document>> {
    default List<Document> read() {
        return get();
    }
}

实际开发中,我们可以直接使用 Spring AI 内置的多种 DocumentReader 实现类,用于处理不同类型的数据源

举个例子,比如邮件解析器的实现:

java 复制代码
public class MsgEmailParser {

    private MsgEmailParser() {
        
    }

    
    public static Document convertToDocument(MsgEmailElement element) {
        if (element == null) {
            throw new IllegalArgumentException("MsgEmailElement cannot be null");
        }

        
        Map<String, Object> metadata = new HashMap<>();

        
        if (StringUtils.hasText(element.getSubject())) {
            metadata.put("subject", element.getSubject());
        }
    
    

        
        String content = StringUtils.hasText(element.getText()) ? element.getText() : "";
        return new Document(content, metadata);
    }

所以可以看到,本质上就是"读取知识,解析为document列表"即可

T--transform:转换(数据的切分、清洗、增强**)**

Sprin⁠g AI 通过 D‌ocumentTransformer‎ 组件实现文档转换‌。

我们上一章节实战的案例虽然实现了文档的简单切分,但是并没有使用transform来细化工作,因此本章节将会详细讲解transform的使用

文档转换是保证 R⁠AG 效果的核心步骤,也就是如何将大‌文档合理拆分为便于检索的知识碎片,S​pring AI 提供了多种 Doc‎umentTransformer 实‌现类,可以简单分为 3 类。

1.TextSplitter 文本分割器

其中 Te⁠xtSplitte‌r 是文本分割器的​基类,提供了分割单‎词的流程方法:

java 复制代码
// 根据原始文本、对应的元数据以及内容格式化器,重新创建 Document 列表
private List<Document> createDocuments(
        List<String> texts,
        List<ContentFormatter> formatters,
        List<Map<String, Object>> metadataList) {

    // 用于保存切分后重新生成的所有 Document
    List<Document> documents = new ArrayList<>();

    // 依次处理每一份原始文档
    // texts、formatters、metadataList 通过下标一一对应
    for (int i = 0; i < texts.size(); i++) {

        // 获取第 i 份原始文本
        String text = texts.get(i);

        // 获取第 i 份原始文档的元数据
        // 例如:文件名、来源地址、标题等
        Map<String, Object> metadata = metadataList.get(i);

        // 对当前原始文本进行切分
        // 一个原始 Document 可能会被切分成多个文本块
        List<String> chunks = splitText(text);

        // 如果切分后得到多个文本块,则输出日志,方便调试
        if (chunks.size() > 1) {
            logger.info(
                    "Splitting up document into " + chunks.size() + " chunks."
            );
        }

        // 依次处理每一个文本块
        for (String chunk : chunks) {

            // 复制原始文档的元数据
            //
            // 这里不是直接使用 metadata,
            // 而是重新创建一个 Map,避免后续修改子 Document 时影响原始 metadata。
            //
            // 这里只保留 key 和 value 都不为 null 的简单类型数据,
            // 方便后续向量化、序列化或写入向量数据库。
            Map<String, Object> metadataCopy = metadata.entrySet()
                    .stream()
                    .filter(entry ->
                            entry.getKey() != null
                                    && entry.getValue() != null
                    )
                    .collect(Collectors.toMap(
                            Map.Entry::getKey,
                            Map.Entry::getValue
                    ));

            // 使用当前切分出来的文本块,
            // 创建一个新的 Document 对象
            //
            // chunk:
            // 当前文本块的内容
            //
            // metadataCopy:
            // 原始文档携带的元数据副本
            Document newDoc = new Document(chunk, metadataCopy);

            // 如果配置了复制内容格式化器,
            // 就把原始 Document 对应的 formatter 传给新的子 Document
            if (this.copyContentFormatter) {
                newDoc.setContentFormatter(formatters.get(i));
            }

            // 当前切分后的 Document 加入结果列表
            documents.add(newDoc);
        }
    }

    // 返回所有切分后重新生成的 Document
    return documents;
}

解析

这样做很重要,因为后续向量化和检索时,系统检索到的不是整篇大文档,而是其中更加具体的文本块。

其中,TokenTex⁠tSplitter 是其实现类‌,基于 Token 的文本分​割器。它考虑了语义边界(比如句子‎结尾)来创建有意义的文本段落,‌是成本较低的文本切分方式。

java 复制代码
@Component
class MyTokenTextSplitter {

    public List<Document> splitDocuments(List<Document> documents) {
        TokenTextSplitter splitter = new TokenTextSplitter();
        return splitter.apply(documents);
    }

    public List<Document> splitCustomized(List<Document> documents) {
        TokenTextSplitter splitter = new TokenTextSplitter(1000, 400, 10, 5000, true);
        return splitter.apply(documents);
    }
}

Token⁠TextSplit‌ter 提供了两种​构造函数选项:

  1. TokenTextSplitter():使用默认设置创建分割器。
  2. TokenTextSplitter(int defaultChunkSize, int minChunkSizeChars, int minChunkLengthToEmbed, int maxNumChunks, boolean keepSeparator):使用自定义参数创建分割器,通过调整参数,可以控制分割的粒度和方式,适应不同的应用场景。

2.Metada‌taEnriche​r 元数据增强器

在我们介绍这个方面之前,首先我们先来了解一下"元数据"--metadata

元数据介绍

一、什么是元数据

二、Document 中的元数据放在哪里?

Spring AI 的 Document 可以简单理解为:

复制代码
Document
├── text/content:正文内容
└── metadata:描述正文的附加信息

例如:

java 复制代码
Document document = new Document(
        "在恋爱关系中,双方应该保持坦诚沟通。",
        Map.of(
                "filename", "恋爱沟通.md",
                "category", "恋爱",
                "topic", "沟通"
        )
);

其中:

复制代码
"在恋爱关系中,双方应该保持坦诚沟通。"

是正文

元数据是:

复制代码
"filename" -> "恋爱沟通.md"
"category" -> "恋爱"
"topic" -> "沟通"

因此元数据在数据结构的角度上来看他就是一组键值对

元数据增强器说明

了解完"元数据"后,我们就可以了解这个"元数据增强器"了,正如它的名字所说,它的作用就是

文档补‌充更多的元信息,便​于后续检索,而不是‎改变文档本身的‌切分规则。包括:

  • KeywordMetadataEnricher:使用 AI 提取关键词并添加到元数据
  • SummaryMetadataEnricher:使用 AI 生成文档摘要并添加到元数据。不仅可以为当前文档生成摘要,还能关联前一个和后一个相邻的文档,让摘要更完整。

示例代码:

java 复制代码
@Component
class MyDocumentEnricher {

    private final ChatModel chatModel;

    MyDocumentEnricher(ChatModel chatModel) {
        this.chatModel = chatModel;
    }
      
      
    List<Document> enrichDocumentsByKeyword(List<Document> documents) {
        KeywordMetadataEnricher enricher = new KeywordMetadataEnricher(this.chatModel, 5);
        return enricher.apply(documents);
    }
  
    
    List<Document> enrichDocumentsBySummary(List<Document> documents) {
        SummaryMetadataEnricher enricher = new SummaryMetadataEnricher(chatModel, 
            List.of(SummaryType.PREVIOUS, SummaryType.CURRENT, SummaryType.NEXT));
        return enricher.apply(documents);
    }
}
它是如何帮助后续检索的

1. 精确过滤

比如你的知识库中有不同恋爱状态的资料:

复制代码
单身篇.md
恋爱篇.md
已婚篇.md

可以设置:

复制代码
status = "单身"

检索时只查单身资料,而不是整个知识库都查。

这属于:

复制代码
结构化过滤

它和向量相似度检索不同:

复制代码
向量检索:这段内容和问题语义上像不像?

元数据过滤:这段内容是否满足指定条件?

两者可以组合:

复制代码
先过滤 status = "单身"
再在过滤后的文档中进行向量相似度检索

2. 帮助判断文档来源

如果 AI 返回的内容需要带引用,系统可以通过:

复制代码
filename
source
page
url

知道这段知识来自哪里。

这对你之前遇到的"知识库中有链接,但 AI 不返回链接"的问题尤其重要:

复制代码
链接存在于 metadata 中
  -> 检索器是否返回了这份 metadata
  -> 查询增强器是否把它拼进 Prompt
  -> 系统提示词是否要求保留并输出链接

因此,元数据不是装饰信息,它可能直接影响引用和调试。

3. 帮助排序或后处理

例如可以根据:

复制代码
更新时间
文档权重
来源可信度
章节类型

对检索结果进一步排序或筛选。

3.Conten‌tFormatte​r 内容格式化工具

它的作用和它的名字一样"内容格式化",我们前文切割好的一个个document,它们的元数据或者文本可能存在格式不统一等情况,这样交给Embedding模型去处理转换向量可能会造成一些干扰,因此取药该工具去"清洗"、"格式化"他们的文本或者元数据

简单来说:ContentFormatter 决定最终交给 EmbeddingModel 或大模型的文本长什么样。

例如:

复制代码
正文:
如何扩大社交圈,认识更多合适的异性。

metadata:
filename = 单身篇.md
category = 单身
topic = 社交
source = 某个课程链接

经过了ContentFormatter的处理后会变成:

复制代码
文件名:单身篇.md
分类:单身
主题:社交
来源:https://example.com

正文:
如何扩大社交圈,认识更多合适的异性。
常用使用场景

除了对文本和元数据的"格式化",他还会识别某一些元数据火文本是否适合加入到document到后面的embedding模型去识别

如:适合加入的:

复制代码
标题
章节
分类
主题
关键词

不太适合加入的:

复制代码
数据库 ID
内部向量 ID
无意义时间戳
过长的调试信息

因为无关 metadata 可能污染文本语义,导致向量表达变得不准确。

Spring AI 现在仍提供 MetadataMode,包括 ALLEMBEDINFERENCENONE,说明框架仍然区分"用于嵌入"和"用于推理"的元数据场景。(docs.spring.io)

此外,他还方便于统一不同文档读取器的输出格式

你的知识库以后可能同时读取:

复制代码
Markdown
PDF
网页
数据库
Word

不同 Reader 产生的 metadata 结构可能不同。可以通过 formatter 统一成:

复制代码
来源:
标题:
分类:
正文:

这样后面的 EmbeddingModel、检索器和大模型看到的格式更加一致。

当前官方 ETL 设计仍然是:

复制代码
DocumentReader
  -> DocumentTransformer
  -> DocumentWriter

ContentFormatTransformer 就是一个专门对文档批量应用 formatter 的 DocumentTransformer,官方 API 还明确说明它适合处理已经切片后的文档。(docs.spring.io)

L--load:加载写入

本质上就是把我们前文处理好的一个个document写入到"目标存储"中就好了

是的,就这么简单,只不过需要注意的是我们刚刚提及的"目标存储"他不是一个固定的存储位置或者格式,它可以是以文件形式存储,也可以是我们后面要用到的以向量的形式存储到向量数据库中

Sprin⁠g AI 通过 D‌ocumentWr​iter 组件实现‎文档加载(写入)。

DocumentWriter 接口实现了 Consumer<List<Document>> 接口,负责将处理后的文档写入到目标存储中:

java 复制代码
public interface DocumentWriter extends Consumer<List<Document>> {
    default void write(List<Document> documents) {
        accept(documents);
    }
}

Sprin⁠g AI 提供了 ‌2 种内置的 Do​cumentWri‎ter 实现:

1.Fil⁠eDocument‌Writer:将文​档写入到文件系统
java 复制代码
@Component
class MyDocumentWriter {
    public void writeDocuments(List<Document> documents) {
        FileDocumentWriter writer = new FileDocumentWriter("output.txt", true, MetadataMode.ALL, false);
        writer.accept(documents);
    }
}
2.Vec⁠torStoreW‌riter:将文档​写入到向量数据库
java 复制代码
@Component
class MyVectorStoreWriter {
    private final VectorStore vectorStore;
    
    MyVectorStoreWriter(VectorStore vectorStore) {
        this.vectorStore = vectorStore;
    }
    
    public void storeDocuments(List<Document> documents) {
        vectorStore.accept(documents);
    }
}

当然,你也⁠可以同时将文档写入‌多个存储,只需要创​建多个 Write‎r 或者自定义 W‌riter 即可。

简单ETL完整流程

java 复制代码
// ==================== E:Extract 提取 ====================

// 创建 PDF 文档读取器
// PagePdfDocumentReader 会按照 PDF 页码读取文档内容
PDFReader pdfReader =
        new PagePdfDocumentReader("knowledge_base.pdf");

// 正式读取 PDF,得到 Spring AI 的 Document 列表
// 此时 Document 中主要是:正文内容 + 原始元数据
List<Document> documents = pdfReader.read();


// ==================== T:Transform 转换 ====================

// 创建文本切分器
// 这里的 500、50 表示切片相关参数,具体含义要以当前版本 API 为准
TokenTextSplitter splitter =
        new TokenTextSplitter(500, 50);

// 将原始 Document 切分成多个较小的 Document
// 目的是让后续检索时能够定位到更具体的知识片段
List<Document> splitDocuments =
        splitter.apply(documents);


// 创建摘要元数据增强器
// 它会借助 ChatModel,为每个切片生成摘要,
// 并把摘要作为 metadata 补充到 Document 中
SummaryMetadataEnricher enricher =
        new SummaryMetadataEnricher(
                chatModel,
                List.of(SummaryType.CURRENT)
        );

// 对切片后的 Document 进行元数据增强
// enrichedDocuments 仍然是 Document 列表,
// 只是每个 Document 额外拥有了摘要等 metadata
List<Document> enrichedDocuments =
        enricher.apply(splitDocuments);


// ==================== L:Load 加载 / 写入 ====================

// 将处理完成的 Document 写入向量存储
//
// vectorStore.write(...) 内部通常会完成:
// 1. 调用 EmbeddingModel 把文本转换成向量
// 2. 保存向量、原始文本和 metadata
vectorStore.write(enrichedDocuments);

Part2.向量的转换与存储

上一节教程中有介绍过,向量存储是 RAG 应用中的核心组件,它将文档转换为向量(嵌入)并存储起来,以便后续进行高效的相似性搜索。Spring AI 官方 **提供了向量数据库接口 VectorStore**和向量存储整合包,帮助开发者快速集成各种第三方向量存储,比如 Milvus、Redis、PGVector、Elasticsearch 等。

VectorStore 接口介绍(重点)

VectorS⁠tore 是 Spring‌ AI 中用于与向量数据库​交互的核心接口,它继承自 ‎DocumentWrite‌r,主要提供以下功能:

java 复制代码
public interface VectorStore extends DocumentWriter {

    default String getName() {
        return this.getClass().getSimpleName();
    }

    void add(List<Document> documents);

    void delete(List<String> idList);

    void delete(Filter.Expression filterExpression);

    default void delete(String filterExpression) { ... };

    List<Document> similaritySearch(String query);

    List<Document> similaritySearch(SearchRequest request);

    default <T> Optional<T> getNativeClient() {
        return Optional.empty();
    }
}

这个接口定⁠义了向量存储的基本‌操作,简单来说就是​ "增删改查":

  • 添加文档到向量库
  • 从向量库删除文档
  • 基于查询进行相似度搜索
  • 获取原生客户端(用于特定实现的高级操作)

了解完这个规范定义方法的接口,我们就要了解一下它旗下的实现类:

对于我们这次自己的项目用的是:PgVectorStore和SimpleVectorStore

按照他们的名字来理解就很好理解了Pg是Postgre的缩写,意味着数据要存储到postgre数据库中

** Simple就是简单,因此它存储仅仅实在java内存中,**服务器一关数据就全部丢失了,因此接下来的实战我们要存储到postgre数据库

举一个简单的例子说明PgVectorStore

当你执行:

复制代码
vectorStore.add(documents);

它内部大致做:

复制代码
Document 文本
  -> EmbeddingModel 生成向量
  -> JdbcTemplate 执行 SQL
  -> PostgreSQL 保存 content、metadata、embedding

当你查询:

复制代码
vectorStore.similaritySearch("课程链接");

它内部大致做:

复制代码
用户问题
  -> EmbeddingModel 生成查询向量
  -> PostgreSQL 计算相似度
  -> 返回最相关的 Document

所以综上来说,vectorstore接口的实现类他们的不同之处就在于存储位置的不同,其他方法还是和要实现接口的增删改的

因此,对于以下vectorstore的实现类我们就好理解了

复制代码
SimpleVectorStore
  -> Java 内存

PgVectorStore
  -> PostgreSQL + pgvector

RedisVectorStore
  -> Redis

MilvusVectorStore
  -> Milvus

让AI帮我总结了这次项目会出现的vector

概念对照

名称 它是什么 在项目里的角色
vector 一串表达文本语义的数字;在 PostgreSQL 中也是一种字段类型。 其实vector就是向量的意思 最终写进表里的 embedding 数据。
EmbeddingModel 把文本转换成向量的模型。 把 Document 的内容变成固定维度的数字数组。
pgvector PostgreSQL 的扩展,让数据库理解向量并支持相似度运算。 让 PG 不只会存普通字段,还能存和查 embedding。
VectorStore Spring AI 统一抽象,规定如何添加、搜索文档。 业务代码依赖的接口,不绑定具体数据库。
PgVectorStore VectorStore 的 PostgreSQL + pgvector 实现。 负责把 Document、向量和 metadata 写入 PG,并执行相似度检索。
SimpleVectorStore 把向量暂存在 Java 内存中的实现。 适合快速学习,但应用关闭后数据会消失。

**一个生活类比:**EmbeddingModel 像翻译员,vector 是翻译结果,pgvector 是数据库新增的"向量语言能力",PgVectorStore 则是负责把这套能力接进 Spring AI 的仓库管理员。

搜索请求构建

上面我们把知识转换为了向量存储到向量库中,接下来按照流程我们就应该让存储的向量"用起来",因此我们这一步就是针对用户所发出的请求,把用户发出的问题转换为向量,再根据 topK、相似度阈值和 metadata 过滤条件,从向量库中找出最相关的 Document

Sprin⁠g AI 提供了 ‌SearchReq​uest 类,用于‎构建相似度搜索请求‌:

java 复制代码
SearchRequest request = SearchRequest.builder()
    .query("什么是程序员鱼皮的编程导航学习网 codefather.cn?")
    .topK(5)                  
    .similarityThreshold(0.7) 
    .filterExpression("category == 'web' AND date > '2025-05-03'")  
    .build();

List<Document> results = vectorStore.similaritySearch(request);

接下来说明构建请求时每个参数的作用:

query
复制代码
.query("什么是程序员鱼皮的编程导航学习网 codefather.cn?")

表示本次要搜索的问题。

它还是普通文本,不是向量。真正执行搜索时,VectorStore 会借助 EmbeddingModel 把它转换成查询向量。

topK
复制代码
.topK(5)

表示最多返回多少条最相关的文档。

复制代码
topK = 5
= 最多返回 5 个 Document

它不是"只允许 5 条知识存在",而是"这次查询最多拿 5 条结果"。

similarityThreshold
复制代码
.similarityThreshold(0.7)

表示最低相似度要求。

复制代码
相似度 >= 0.7
  -> 保留

相似度 < 0.7
  -> 过滤

所以:

  • topK 控制数量
  • similarityThreshold 控制质量门槛
filterExpression
复制代码
.filterExpression(
    "category == 'web' AND date > '2025-05-03'"
)

这是基于 metadata 的结构化过滤

它不是判断文本语义,而是判断文档的附加信息

复制代码
category 必须等于 web
并且 date 必须晚于 2025-05-03

可以理解为:

复制代码
先筛选符合条件的文档
  -> 再进行向量相似度检索

不过具体执行顺序由 VectorStore 实现决定,有些实现可能将 metadata 过滤与向量搜索组合执行。

向量存储的工作原理

在向量数据库⁠中,查询与传统关系型数据‌库有所不同。向量库执行的​是相似性搜索,而非精确匹‎配,具体流程我们在上一节‌教程中有了解,可以再复习下。

  1. 嵌入转换:当文档被添加到向量存储时,Spring AI 会使用嵌入模型(如 OpenAI 的 text-embedding-ada-002)将文本转换为向量。
  2. 相似度计算:查询时,查询文本同样被转换为向量,然后系统计算此向量与存储中所有向量的相似度。
  3. 相似度度量:常用的相似度计算方法包括:
  • 余弦相似度:计算两个向量的夹角余弦值,范围在 - 1 到 1 之间
  • 欧氏距离:计算两个向量间的直线距离
  • 点积:两个向量的点积值

4.过滤与排序:根据相似度阈值过滤结果,并按相似度排序返回最相关的文档

实战:基于 PGVector 实现向量存储

PGVect⁠or 是经典数据库 P‌ostgreSQL 的​扩展,为 Postgr‎eSQL 提供了存储和‌检索高维向量数据的能力。

为什么选择它来实现向量存⁠储呢?因为很多传统业务都会把数据存储在这种关系‌型数据库中,直接给原有的数据库安装扩展就能实现​向量相似度搜索、而不需要额外搞一套向量数据库,‎人力物力成本都很低,所以这种方案很受企业青睐,‌也是目前实现 RAG 的主流方案之一。

首先我们准备⁠ PostgreSQL ‌数据库

并为其添加扩展,这里由于大⁠家更多的是为了学习‌,我们采用更方便的方式 ------ 使用现‎成的云数据库,下面‌我们来实操下~

1)首先打开 阿里云 PostgreSQL 官网,开通 Serverless 版本,按用量计费,对于学习来说性价比更高

开通 Serverless 数据库服务,填写配置:

++这一步叫做"创建RDS实例"相当于部署一台云端的Postgre服务器++

它负责提供:

  • 外网地址
  • 端口 5432
  • 用户账号
  • 网络访问能力

2)开通成功后,进入控制台,先创建账号:

创建好账号后去创建数据库

进入插件管理,安装 vector 插件:

++这一步目的是让我们的postgre数据库能够识别存储向量++

进入数据库连接,开通公网访问地址:

可以在本地⁠使用 IDEA 自‌带的数据库管理工具​,进行连接测试:

如果你的 ⁠IDEA 版本没有‌这个工具,也不用纠​结,直接在云平台查‎看管理数据库即可

显示连接成功,至此数据库准备完成:(我这里idea的数据库插件并没有显示,因此来到阿里云云数据库做展示)

基于项目配置数据库--初始化vectorstore
1.首先引入PGvector依赖,实现向量存储

依赖1:

复制代码
spring-boot-starter-jdbc

作用:

复制代码
让 Spring 能通过 JdbcTemplate 操作 PostgreSQL

依赖2:

复制代码
postgresql

作用:

复制代码
让 Java 认识 PostgreSQL 数据库

依赖3:

复制代码
spring-ai-pgvector-store

作用:

复制代码
提供 PgVectorStore
让 Spring AI 能操作向量数据
2.根据我们的信息编写yml配置文件(保护隐私,敏感信息已经配置在环境变量中)

项目现在有两个数据库,所以不能把 PostgreSQL 的连接参数直接覆盖默认的 MySQL 配置。我们给 PG 单独加了 app.pgvector 前缀,再用配置类明确创建专用数据源。

这一步的核心不是 YAML 长什么样,而是给两个数据库划清边界:默认连接继续服务 MySQL,PG 连接只服务 PgVectorStore。

3.编写关于PGvector的配置类

目的 是创建 PostgreSQL 专用 DataSource 和 JdbcTemplate,让 Spring 得到一条"明确通往 PG 的路。因为++默认的 JdbcTemplate 已经连接 MySQL++ ,所以这里必须使用专用 Bean 名称。@Qualifier 是在告诉 Spring:向量存储要的是 PG 那条连接,不是默认连接。

java 复制代码
/**
 * PostgreSQL + PGVector 配置。
 *
 * <p>项目中有两个数据库,因此这里不使用 Spring Boot 的默认 DataSource:
 * 默认 DataSource 继续连接 MySQL,供 JDBC ChatMemory 使用;
 * 本配置类单独创建 PostgreSQL DataSource,专门供 PgVectorStore 使用。</p>
 */
@Configuration
public class PgVectorStoreConfig {

    /*
     * 读取 app.pgvector.datasource 下的 PostgreSQL 连接参数。
     * 这些配置来自 application-local.yml,不会覆盖 spring.datasource 下的 MySQL 配置。
     */
    @Value("${app.pgvector.datasource.url}")
    private String pgVectorUrl;

    @Value("${app.pgvector.datasource.username}")
    private String pgVectorUsername;

    @Value("${app.pgvector.datasource.password}")
    private String pgVectorPassword;

    @Value("${app.pgvector.datasource.driver-class-name:org.postgresql.Driver}")
    private String pgVectorDriverClassName;

    /**
     * 创建 PostgreSQL 专用数据源。
     *
     * <p>这里不能直接注入默认 DataSource,因为默认 DataSource 是 MySQL。
     * 通过单独读取 app.pgvector.datasource,明确告诉 Spring:
     * 这个连接只服务 PostgreSQL。</p>
     */
    @Bean(name = "pgVectorDataSource")
    public DataSource pgVectorDataSource() {
        return DataSourceBuilder.create()
                .url(pgVectorUrl)
                .username(pgVectorUsername)
                .password(pgVectorPassword)
                .driverClassName(pgVectorDriverClassName)
                .build();
    }

    /**
     * 创建 PostgreSQL 专用 JdbcTemplate。
     *
     * <p>MySQL 对话记忆使用项目默认的 JdbcTemplate;
     * 这个带有 pgVectorJdbcTemplate 名称的对象只负责访问 PostgreSQL。</p>
     */
    @Bean(name = "pgVectorJdbcTemplate")
    public JdbcTemplate pgVectorJdbcTemplate(
            @Qualifier("pgVectorDataSource") DataSource pgVectorDataSource) {
        return new JdbcTemplate(pgVectorDataSource);
    }

}

它目前负责:

复制代码
读取 app.pgvector.datasource
  -> 创建 PostgreSQL DataSource
  -> 创建 PostgreSQL JdbcTemplate
  -> 创建 PgVectorStore

配置类中的关键职责

  • pgVectorDataSource:连接阿里云 PostgreSQL
  • pgVectorJdbcTemplate:专门操作 PostgreSQL
  • pgVectorStore****:使用 DashScope EmbeddingModel,把文档转换为向量并保存到 PGVector
  • dimensionsHNSWCOSINE_DISTANCE 等参数:从你的 YAML 中读取

同时保留:

复制代码
默认 JdbcTemplate
  -> MySQL
  -> JDBC ChatMemory

一句话总结

PgVectorStoreConfig 解决的是底层连接问题:

复制代码
我怎么连接 PostgreSQL?
4.修改LoveAppVectorStoreConfig配置类

我们之前的学习中也简单的做过向量的存储,当时是存在SimpleVectorStore中,现在我们需要存储到PGvectorstore中,因此需要修改我们之前的配置类

因此

LoveAppVectorStoreConfig 解决的是业务存储选择问题:

复制代码
LoveApp 的知识库使用哪种 VectorStore?
向量维度是多少?
表名是什么?
使用什么距离算法?
是否自动建表?

代码如下:

java 复制代码
/**
 * LoveApp 的向量库配置。
 *
 * <p>这个类只负责使用 PostgreSQL 专用 JdbcTemplate 创建 PgVectorStore。
 * Markdown 文档会在 PgVectorStore 完成初始化后再写入。</p>
 *
 * <p>MySQL 仍然由默认 JdbcTemplate 负责,继续用于 JDBC ChatMemory。
 * 因此这里必须使用 pgVectorJdbcTemplate,不能直接注入默认 JdbcTemplate。</p>
 */
@Configuration
public class LoveAppVectorStoreConfig {

    // 读取 application-local.yml 中 app.pgvector.store 下的配置。
    @Value("${app.pgvector.store.dimensions}")
    private int dimensions;

    @Value("${app.pgvector.store.distance-type:COSINE_DISTANCE}")
    private String distanceType;

    @Value("${app.pgvector.store.index-type:HNSW}")
    private String indexType;

    @Value("${app.pgvector.store.vector-table-name:vector_store}")
    private String vectorTableName;

    @Value("${app.pgvector.store.schema-name:public}")
    private String schemaName;

    @Value("${app.pgvector.store.max-document-batch-size:10000}")
    private int maxDocumentBatchSize;

    @Value("${app.pgvector.store.initialize-schema:true}")
    private boolean initializeSchema;

    /**
     * 创建项目实际使用的 VectorStore。
     *
     * <p>这里返回的仍然是 VectorStore 接口,
     * 但底层实现已经从原来的 SimpleVectorStore 改成 PgVectorStore。</p>
     */
    @Bean(name = "loveAppVectorStore")
    public VectorStore loveAppVectorStore(
            @Qualifier("pgVectorJdbcTemplate") JdbcTemplate pgVectorJdbcTemplate,
            EmbeddingModel dashscopeEmbeddingModel) {

        // 使用 PostgreSQL 专用 JdbcTemplate 创建 PGVector。
        // EmbeddingModel 负责把 Document 文本转换为向量。
        VectorStore vectorStore = PgVectorStore.builder(
                        pgVectorJdbcTemplate,
                        dashscopeEmbeddingModel
                )
                // 必须和 EmbeddingModel 输出的向量维度一致。
                .dimensions(dimensions)
                // 使用余弦距离计算查询向量与文档向量的相似度。
                .distanceType(PgDistanceType.valueOf(distanceType))
                // 使用 HNSW 索引提升向量检索效率。
                .indexType(PgIndexType.valueOf(indexType))
                // 允许 Spring AI 初始化 PGVector 所需的表结构。
                .initializeSchema(initializeSchema)
                // 设置 PostgreSQL schema 和向量表名称。
                .schemaName(schemaName)
                .vectorTableName(vectorTableName)
                // 控制批量写入时每批处理的文档数量。
                .maxDocumentBatchSize(maxDocumentBatchSize)
                .build();

        // 这里只返回向量库,不要在这里调用 add(documents)。
        // Spring 还需要先完成 PgVectorStore 的初始化生命周期,
        // initializeSchema(true) 才能创建 vector_store 表。
        return vectorStore;
    }
}

防止弄混上面的两个配置类,这里给出区分

(我自己在做的时候也很烦,PgVectorStoreConfig里面居然并不负责我们的项目选择什么存储方案,只负责连接PG数据库,按理来说完全可以把这两个配置类合并到一起,但是学习阶段分工明确确实会好很多)

5.添加应用启动完成后读取 Markdown 并写入向量库的LoveAppVectorStoreInitializer

在该类内,工作任务职责是:读取 md → 得到 Document → EmbeddingModel 生成 embedding → PgVectorStore 执行插入 → PostgreSQL 保存正文、metadata 和向量。

java 复制代码
/**
 * 应用启动完成后的向量知识库初始化器。
 *
 * <p>它不负责创建 VectorStore,而是等待 PgVectorStore 完成自身初始化后,
 * 再读取 Markdown 并把文档写入 PostgreSQL。</p>
 */
@Component
@Slf4j
public class LoveAppVectorStoreInitializer {

    private final LoveAppDocumentLoader loveAppDocumentLoader;
    private final VectorStore loveAppVectorStore;

    public LoveAppVectorStoreInitializer(
            LoveAppDocumentLoader loveAppDocumentLoader,
            @Qualifier("loveAppVectorStore") VectorStore loveAppVectorStore) {
        this.loveAppDocumentLoader = loveAppDocumentLoader;
        this.loveAppVectorStore = loveAppVectorStore;
    }

    /**
     * ApplicationReadyEvent 表示 Spring Boot 应用已经完成启动。
     *
     * <p>此时 PgVectorStore 已完成 Bean 初始化,
     * initializeSchema(true) 对应的表结构初始化可以先于文档写入执行。</p>
     */
    @EventListener(ApplicationReadyEvent.class)
    public void loadDocumentsIntoVectorStore() {
        List<Document> documents = loveAppDocumentLoader.loadMarkdowns();

        if (documents.isEmpty()) {
            log.warn("没有读取到 Markdown 文档,跳过 PGVector 写入");
            return;
        }

        // add 内部会调用 EmbeddingModel 生成向量,
        // 再把向量、正文和 metadata 写入 PostgreSQL。
        loveAppVectorStore.add(documents);
        log.info("已将 {} 个 Document 写入 PostgreSQL PGVector", documents.size());
    }
}
6.编写测试方法,查看云知识库应用是否成功

测试方法

运行结果

同时我们查看阿里云的数据库,可以看到我们的知识md文档成功完成切分并且转换为向量上传到我们阿里云的云Postgre数据库里面

Part3.文档过滤和检索

Sprin⁠g AI 官方声称‌提供了一个 "模块​化" 的 RAG ‎架构,用于优化大模‌型回复的准确性。

简单来说,⁠就是把整个文档过滤‌检索阶段拆分为:检​索前、检索时、检索‎后,分别针对每个阶‌段提供了可自定义的组件。

  • 在预检索阶段,系统接收用户的原始查询,通过查询转换查询扩展 等方法对其进行优化,输出增强的用户查询
  • 在检索阶段,系统使用增强的查询从知识库中搜索相关文档,可能涉及多个检索源的合并,最终输出一组相关文档。
  • 在检索后阶段,系统对检索到的文档进行进一步处理,包括排序、选择最相关的子集以及压缩文档内容,输出经过优化的相关文档集。

预检索阶段:优化用户查询

预检索阶段用于优化用户的输入,也就是优化"准备发给大模型去回答"的部分

接下来我们会介绍几种预检索优化的方式:

请注意,这些增强器都是按照需求需要才添加的,不需要一上来就把所有增强器拉满

查询转换方式

1.查询重写

顾名思义就是重新写一遍用户的查询,原来用户的查询可能是模糊不清或者含有无关信息的,这样不方便我们大模型的检索,因此需要用到该增强器

RewriteQueryTransformer 使用大语言模型对用户的原始查询进行改写,使其更加清晰和详细。当用户查询含糊不清或包含无关信息时,这种方法特别有用。

实例代码如下:

java 复制代码
Query query = new Query("啥是程序员鱼皮啊啊啊啊?");

QueryTransformer queryTransformer = RewriteQueryTransformer.builder()
        .chatClientBuilder(chatClientBuilder)
        .build();

Query transformedQuery = queryTransformer.transform(query);

用户输入的查询包含了"啊啊啊"这种无关内容,该增强器就会把这种无效的输入通过大模型去规范,可能优化成"请帮我查找程序员鱼皮这个人"这种说法

实现也很简单,查看它的源代码就可以很清楚的知道其实现就是获取用户的输入,然后给大模型一段规范用户输入的提示词,再把规范好的用户输入输出即可,这个提示词我们也可以自定义

2.查询翻译

没啥用的增强器,这里简单介绍,和名字一样其实就只用"翻译"这个功能,可能输入的是其他语言,最优识别语言又是另一种语言,这个时候就需要"查询翻译"了

TranslationQueryTransformer 将查询翻译成嵌入模型支持的目标语言。如果查询已经是目标语言,则保持不变。这对于嵌入模型是针对特定语言训练而用户查询使用不同语言的情况非常有用,便于实现国际化应用。

示例代码如下:

java 复制代码
Query query = new Query("hi, who is coder yupi? please answer me");

QueryTransformer queryTransformer = TranslationQueryTransformer.builder()
        .chatClientBuilder(chatClientBuilder)
        .targetLanguage("chinese")
        .build();

Query transformedQuery = queryTransformer.transform(query);

语言可以随便⁠指定,因为看源码我们会发‌现,查询翻译器也是通过给​ AI 一段 Promp‎t 来实现翻译,当然也可‌以自定义翻译的 Prompt:

不过不太建⁠议使用这个查询器,因为调用 ‌AI 的成本远比调用第三方翻​译 API 的成本要高,不如‎自己有样学样定义一个 Que‌ryTransformer,调用第三方(比如百度翻译)的API,远比这要划算好用

3.查询压缩

当上下文很长的时候,用户又用一些代词之类去指代前文所提及的内容,大模型看到这些代词就会不清楚指代的内容是什么,因此"查询压缩"增强器就可以解决这个问题。

其原理就是:把"很长的聊天上下文 + 这一次的追问"压成一句独立、完整、适合检索的查询

举个例子:

原始对话:

  • 用户:我们刚才说了 PGVectorStoreConfig 和 LoveAppVectorStoreConfig 的区别
  • 用户:那为什么要拆成两个类?

压缩后可能变成:

  • 为什么在 Spring AI 项目中要把PostgreSQL数据源配置和PgVectorStore 创建拆成两个配置类?

这样检索器就更容易找到相关文档或代码。

因此可以看出它的作用:

  • 用户追问时经常只说"那这个呢""继续说刚才那个"
  • 检索系统如果只看当前这句话,会不知道"这个"指什么
  • 查询压缩会先把上下文补全,再去检索

示例代码如下:

java 复制代码
Query query = Query.builder()
        .text("编程导航有啥内容?")
        .history(new UserMessage("谁是程序员鱼皮?"),
                new AssistantMessage("编程导航的创始人 codefather.cn"))
        .build();

QueryTransformer queryTransformer = CompressionQueryTransformer.builder()
        .chatClientBuilder(chatClientBuilder)
        .build();

Query transformedQuery = queryTransformer.transform(query);

查询扩展方式

多查询扩展

就和它的名字一样,扩展 = "从这句话延伸出几个不同问法。

增强器的大模型会把用户提问的问题发散成多个角度的问题,再把这些问题交给处理的大模型,简单来说就是:把一个问题"扩展成多个不同角度的检索问题"

举个例子

原问题:

  • "PGVectorStoreConfig 为什么要拆成两个类,那个不也是配置吗?"

多查询扩展后可能变成:

  • "Spring AI 中 PgVectorStoreConfig 的职责是什么?"
  • "为什么 PostgreSQL 数据源要和 PgVectorStore 分开配置?"
  • "PgVectorStore 和 DataSource 的关系是什么?"
    所以什么时候用多查询扩展呢?

你怀疑一个问题可以从多个角度命中知识库时,用 多查询扩展

理解完后我们就来看如何实现的:

++MultiQueryExpander++使用大语言模型将一个查询扩展为多个语义上不同的变体,有助于检索额外的上下文信息并增加找到相关结果的机会。就理解为我们在网上搜东西的时候,可能一种关键词搜不到,就会尝试一些不同的关键词。

示例代码如下:

java 复制代码
MultiQueryExpander queryExpander = MultiQueryExpander.builder()
    .chatClientBuilder(chatClientBuilder)
    .numberOfQueries(3)
    .build();
List<Query> queries = queryExpander.expand(new Query("啥是程序员鱼皮?他会啥?"));

上面这个查询可能被扩展为:

  • 请介绍程序员鱼皮,以及他的专业技能
  • 给出程序员鱼皮的个人简介,以及他的技能
  • 程序员鱼皮有什么专业技能,并给出更多介绍

默认情况下,会在扩展查询列表中包含原始查询。可以在构造时通过 includeOriginal 方法改变这个行为:

java 复制代码
MultiQueryExpander queryExpander = MultiQueryExpander.builder()
    .chatClientBuilder(chatClientBuilder)
    .includeOriginal(false)
    .build();

总结对比表

查询重写和多查询扩展可能会互相混淆,因此总结一张表用以区分彼此

简单用一句话区分就是:

  • 查询重写:把一个问题"改写得更适合检索"
  • 多查询扩展:把一个问题"扩展成多个不同角度的检索问题"
增强器 核心动作 适用场景 产物 一句话理解
查询重写 RewriteQueryTransformer 把一个原始问题改写得更清楚、更适合检索 问题冗长、含糊、夹杂无关信息 1 个更规范的查询 把一句话"说顺"
多查询扩展 MultiQueryExpander 把一个问题扩展成多个语义不同的查询 想提高召回率、从多个角度找资料 多个查询 把一句话"发散开"
查询压缩 CompressionQueryTransformer 把对话历史 + 追问压成一个独立查询 多轮对话、追问依赖上下文 1 个独立查询 把前后文"压成一句"

检索阶段

前文我们成功的把知识通过向量的形式存储到了向量库中,并且我们还在预检索阶段通过增强器增强了用户的查询请求,接下来的流程就是框架按照用户的查询请求去RAG知识库检索相关知识文档

但是在此之前需要先介绍一个重要的组件:

Retriever--检索器

如同它的名字一样,就是"检索相关资料"的执行者,他不负责回答问题,就负责找资料
在我们的项目中,用的最多的就是DocumentRetriever这种类型的检索器,负责从数据源里面找回document类型的文件

其中常见的数据源可以是:

复制代码
VectorStore
搜索引擎
数据库
知识图谱
云知识库

用我们项目中的例子来说:

java 复制代码
VectorStoreDocumentRetriever.builder()
        .vectorStore(loveAppVectorStore)
        .similarityThreshold(0.0)
        .topK(8)
        .build()

它的意思是:

复制代码
用 loveAppVectorStore 这个向量库
根据用户问题做语义相似度搜索
最多返回 topK 个相关 Document
相似度低于 threshold 的不要

所以在你项目里:

复制代码
VectorStoreDocumentRetriever
    = 去 PGVector / VectorStore 里找相关恋爱知识文档的检索员

因此我们需要掌握的就是以下这些内容:

1.VectorStoreDocumentRetriever

这是你项目当心的检索器。

复制代码
用户问题
  -> 转成查询向量
  -> 去 PGVector 找相似 Document

2.topK

控制最多拿几条文档。

复制代码
topK = 5
表示最多取 5 条最相关文档

太小可能漏资料,太大可能塞太多噪音。

3.similarityThreshold

控制相似度门槛。

复制代码
threshold 越高
  -> 返回更少但更相关的文档

threshold 越低
  -> 返回更多但可能混入无关文

文档搜索

之前我们有了解过 DocumentRetriever 的概念,这是 Spring AI 提供的文档检索器。每种不同的存储方案都可能有自己的文档检索器实现类,比如 VectorStoreDocumentRetriever,从向量存储中检索与输入查询语义相似的文档。它支持基于元数据的过滤、设置相似度阈值、设置返回的结果数。

java 复制代码
DocumentRetriever retriever = VectorStoreDocumentRetriever.builder()
    .vectorStore(vectorStore)
    .similarityThreshold(0.7)
    .topK(5)
    .filterExpression(new FilterExpressionBuilder()
        .eq("type", "web")
        .build())
    .build();
List<Document> documents = retriever.retrieve(new Query("谁是程序员鱼皮"));

上述代码中的 filterExpression 可以灵活地指定过滤条件。当然也可以通过构造 Query 对象的 FILTER_EXPRESSION 参数动态指定过滤表达式:

java 复制代码
Query query = Query.builder()
    .text("谁是鱼皮?")
    .context(Map.of(VectorStoreDocumentRetriever.FILTER_EXPRESSION, "type == 'boy'"))
    .build();
List<Document> retrievedDocuments = documentRetriever.retrieve(query);

文档合并

也和他的名字一样,就是单纯的"合并文档",把"多次检索"的资料合并到一块,若多次检索的资料又重复部分,则去重保留一份即可

举个例子:

比如使用多查询扩展:

复制代码
原问题:
"PGVectorStore 为什么要单独配置?"

扩展查询 1:
"PgVectorStore 配置类的作用是什么?"

扩展查询 2:
"PostgreSQL DataSource 和 PgVectorStore 为什么分开?"

扩展查询 3:
"Spring AI 如何连接 PostgreSQL 向量库?"

每个查询分别检索:

复制代码
查询 1 -> Document A、Document B
查询 2 -> Document B、Document C
查询 3 -> Document C、Document D

合并后:

复制代码
Document A、Document B、Document C、Document D

其中重复的 Document BDocument C 不会重复保留。

示例代码如下:

java 复制代码
Map<Query, List<List<Document>>> documentsForQuery = ...
DocumentJoiner documentJoiner = new ConcatenationDocumentJoiner();
List<Document> documents = documentJoiner.join(documentsForQuery);

检索后--优化文档处理

检索后模块负责⁠处理检索到的文档,以实现最‌佳生成结果。它们可以解决 ​"丢失在中间" 问题、模型‎上下文长度限制,以及减少检‌索信息中的噪音和冗余。

这些模块可能包括:

  • 根据与查询的相关性对文档进行排序
  • 删除不相关或冗余的文档
  • 压缩每个文档的内容以减少噪音和冗余

这一部分也⁠不是我们实际开发中‌要优化的重点,感兴​趣的同学可以自‎行研究。

Part4.查询增强和关联

这一部分就是把我们第三部分检索出来的相关资料加上用户的问题拼接起来作为上下文发送给回答问题的大模型去回答,简单来说,查询增强与关联,就是把"检索结果"和"用户问题"关联起来,整理成大模型能理解的上下文请求,让 AI 基于资料回答,而不是凭空回答。

核心组件

简单总结了一份表格,快速复习学习阶段用过的组件

组件 它是什么 作用 适合场景
QuestionAnswerAdvisor 简单版 RAG Advisor 自动查 VectorStore,把结果拼进用户问题 快速实现基础知识库问答
RetrievalAugmentationAdvisor 模块化 RAG Advisor 可以组合查询转换、检索器、文档处理、上下文增强器 更复杂、更可控的 RAG
ContextualQueryAugmenter 上下文查询增强器 把检索到的 Document 内容真正组织进查询上下文 控制 Prompt 拼接方式、空上下文策略
查询增强器
QuestionAnswerAdvisor

他就是一个简单的普通查询增强器,就是给他通用的接口VectorStore的查询方法放他去相应的数据源中查询相关资料,然后把相关资料拼接打到上下文中发给AI,基本上和我们上文说的基本步骤是一样的

优点:简单容易学习 缺点:对比其他查询增强器可控性弱

简单示例代码如下

java 复制代码
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;

public class SimpleRagExample {

    private final ChatClient chatClient;

    public SimpleRagExample(
            ChatModel chatModel,
            VectorStore vectorStore) {

        /*
         * SearchRequest:配置"去向量库怎么搜索"
         */
        SearchRequest searchRequest = SearchRequest.builder()
                // 最多返回 6 条相关 Document
                .topK(6)

                // 相似度低于 0.8 的文档不返回
                .similarityThreshold(0.8)

                .build();

        /*
         * QuestionAnswerAdvisor:
         * 1. 接收用户问题
         * 2. 调用 VectorStore 检索相关文档
         * 3. 把文档内容加入用户 Prompt
         * 4. 再让 ChatModel 生成回答
         */
        QuestionAnswerAdvisor questionAnswerAdvisor =
                QuestionAnswerAdvisor.builder(vectorStore)
                        .searchRequest(searchRequest)
                        .build();

        /*
         * defaultAdvisors:
         * 表示这个 ChatClient 的每次调用都默认使用该 RAG Advisor。
         */
        this.chatClient = ChatClient.builder(chatModel)
                .defaultAdvisors(questionAnswerAdvisor)
                .build();
    }

    public String ask(String message) {
        /*
         * 这里看起来只是普通聊天,
         * 但 ChatClient 内部会先经过 QuestionAnswerAdvisor 检索知识库。
         */
        return chatClient.prompt()
                .user(message)
                .call()
                .content();
    }
}

根据上面的例子可以看到,它的可控性是比较弱的,++使用该增强器是无法调用我们之前学习的Retriever检索器以及其他组件的++ ,它只能是我们给他简单的searchRequest以及其他简单组件去**"自动"** 到存储位置找相应资料,"自动"自己拼接上下文发给AI

如果到这里还是对这个所谓**"自动"和"低可控"**还是比较疑惑和模糊的话,不妨来看一下下面这个增强器,看看它的手动和"高可控性"

Retrieval‌Augmentat​ionAdviso‎r

如果把刚刚的QuestionAnswerAdvisor类比成一部构造简单的"傻瓜式自动相机",那么接下来介绍的Retrieval‌Augmentat​ionAdviso‎r那就是"专业版的自动相机"可控性比刚刚介绍的要高很多

RetrievalAugmentationAdvisor 像"专业相机":

复制代码
你可以自己指定:
查询要不要重写
用哪个 Retriever
检索几条
是否按 metadata 过滤
检索结果怎么拼 Prompt
没找到资料怎么办

QuestionAnswerAdvisor

= 简单套餐:给它 VectorStore,它自己检索、自己拼上下文。

RetrievalAugmentationAdvisor

= 自助拼装:你自己指定 Retriever、QueryAugmenter、查询转换器等组件。

接下来用一个代码例子详细看看所谓**"自主拼装"的高可控性**是怎么样的

java 复制代码
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.rag.advisor.RetrievalAugmentationAdvisor;
import org.springframework.ai.rag.generation.augmentation.ContextualQueryAugmenter;
import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever;
import org.springframework.ai.vectorstore.VectorStore;

public class RagExample {

    private final ChatClient chatClient;
    private final VectorStore vectorStore;

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

    public String askWithRag(String message) {

        /*
         * 1. DocumentRetriever:负责"去哪里找资料、怎么找资料"
         *
         * VectorStoreDocumentRetriever 会去 vectorStore 里做相似度检索。
         * 在你的项目里,这个 vectorStore 底层就是 PgVectorStore,
         * 也就是去 PostgreSQL 的 vector_store 表中查相关文档。
         */
        VectorStoreDocumentRetriever documentRetriever =
                VectorStoreDocumentRetriever.builder()
                        .vectorStore(vectorStore)

                        // 最多返回 8 条相关 Document
                        .topK(8)

                        // 相似度阈值。越高越严格,越低越容易召回。
                        .similarityThreshold(0.0)

                        .build();

        /*
         * 2. QueryAugmenter:负责"把检索到的文档怎么塞进 Prompt"
         *
         * Retriever 只负责找资料;
         * ContextualQueryAugmenter 才负责把资料整理成上下文,
         * 然后和用户问题组合起来交给大模型。
         */
        ContextualQueryAugmenter queryAugmenter =
                ContextualQueryAugmenter.builder()

                        // false 表示:如果没有检索到相关文档,就不要让模型硬答。
                        .allowEmptyContext(false)

                        // 自定义 RAG Prompt 模板,控制上下文和用户问题如何组织。
                        .promptTemplate(new PromptTemplate("""
                                请严格根据下面的知识库上下文回答用户问题。
                                如果上下文中没有答案,请直接说明知识库中没有相关信息。
                                
                                知识库上下文:
                                {context}
                                
                                用户问题:
                                {query}
                                """))

                        .build();

        /*
         * 3. RetrievalAugmentationAdvisor:把 RAG 流程组装起来
         *
         * 它本身不直接存文档,也不直接生成向量。
         * 它负责把"检索器 + 上下文增强器"接入 ChatClient 调用链。
         */
        RetrievalAugmentationAdvisor ragAdvisor =
                RetrievalAugmentationAdvisor.builder()
                        .documentRetriever(documentRetriever)
                        .queryAugmenter(queryAugmenter)
                        .build();

        /*
         * 4. 真正调用 AI
         *
         * 表面上是普通聊天:
         * user(message) -> call()
         *
         * 但因为挂了 ragAdvisor,
         * 所以调用模型前会先执行:
         * 检索文档 -> 组装上下文 -> 增强 Prompt。
         */
        ChatResponse chatResponse = chatClient
                .prompt()
                .user(message)
                .advisors(ragAdvisor)
                .call()
                .chatResponse();

        return chatResponse.getResult().getOutput().getText();
    }
}

上面可以看到我们在定义RetrievalAugmentationAdvisor之前我们可以自主定义Retriever检索器,让这条链路中更好的去检索知识库相关文档的内容,

以及例子中还定义了"QueryAugmenter"用于"更好的吧检索到的知识和用户问题拼接为上下文发送给AI"

简单举个例子

复制代码
原始用户问题:
怎么扩大社交圈?

检索到的文档:
Document A:单身阶段建议......
Document B:课程链接......

QueryAugmenter 组装后:
请根据下面上下文回答问题:
[Document A]
[Document B]

用户问题:
怎么扩大社交圈?

综上,Retrieval‌Augmentat​ionAdviso‎r他暴露的可用组件可操作性更高这些暴露出来的组件方法是QuestionAnswerAdvisor无法使用的,这就是两者区别所在

⁠Contextua‌lQueryAug​menter

这就是我们上面刚刚介绍过的QueryAugmenter,用于负责"把资料装到问题里"

简单用代码介绍一下用法

java 复制代码
RetrievalAugmentationAdvisor ragAdvisor =
        RetrievalAugmentationAdvisor.builder()

                // 1. Retriever:负责从向量库里找相关 Document
                .documentRetriever(VectorStoreDocumentRetriever.builder()
                        .vectorStore(loveAppVectorStore)
                        .topK(8)
                        .similarityThreshold(0.0)
                        .build())

                // 2. QueryAugmenter:负责把检索到的 Document 拼进 Prompt
                .queryAugmenter(ContextualQueryAugmenter.builder()

                        // 没有检索到上下文时,不允许模型凭空回答
                        .allowEmptyContext(false)

                        .build())

                .build();

完整链路是:

复制代码
用户问题
  -> VectorStoreDocumentRetriever 去 PGVector 找 Document
  -> ContextualQueryAugmenter 把 Document 组织成上下文
  -> RetrievalAugmentationAdvisor 把增强后的请求交给 ChatClient
  -> AI 基于上下文回答

如果你希望更强地约束 AI,比如"必须保留链接、没有依据就拒答++"或者说上下文是空的,我希望AI可以友好的回答++ ,就可以自定义promptTemplate

java 复制代码
PromptTemplate emptyContextPromptTemplate = new PromptTemplate("""
        当前知识库中没有找到可以回答该问题的资料。
        请不要编造答案。
        请礼貌告诉用户:知识库中暂无相关内容,可以换一种问法,或补充更多背景。
        
        用户问题:
        {query}
        """);

ContextualQueryAugmenter queryAugmenter =
        ContextualQueryAugmenter.builder()

                // false:没有检索结果时,不让模型按常识自由发挥
                .allowEmptyContext(false)

                // 控制"没有检索结果时"的回复规则
                .emptyContextPromptTemplate(emptyContextPromptTemplate)

                .build();
相关推荐
Industio_触觉智能1 小时前
瑞芯微RV1126B安防视觉场景解析|AI门禁+智能IPC+边缘分析
人工智能·硬件工程·智能硬件·核心板·瑞芯微·rv1126b·rv1126bj
guo_xiao_xiao_1 小时前
YOLO三相电能表数字与单位符号目标检测数据集
人工智能·yolo·目标检测
KobeSacre1 小时前
ForkJoinPool 源码
java
DFT计算杂谈1 小时前
C4T交错磁体中的超导交织序向列涨落与自旋流环涨落的选择机制
大数据·数据库·人工智能
2601_965798472 小时前
My Honest Review of Rank Math SEO Pro for Beginner Sites
数据库·人工智能·php
掘金者飞哥2 小时前
SDD,到底是个什么东西?
人工智能
wear工程师2 小时前
Kafka 消费者心跳正常,为什么还会被踢出组?拆清 max.poll.interval.ms
java·kafka
cfm_29142 小时前
基于OAuth2.0实现微服务SSO单点登录
后端·spring·微服务·架构
2601_958352902 小时前
不依赖AI算力,AN-93 纯硬件降噪方案如何实现“零成本、零风险“的清晰拾音?
人工智能·语音模块·降噪处理·降噪消回音
小月土星2 小时前
我把一小时会议录音压成一份能执行的纪要:TRAE Work 的一次真实复盘
人工智能