当前位置: 首页 > news >正文

第五章 RAG知识库进阶

代码仓库地址:https://github.com/Liucc-123/ai-agent

项目目标:通过项目实战掌握AI 大模型应用开发的常见知识,包括有:掌握AI 应用平台使用、AI 大模型接入、开发框架(Spring AI + LangChain4j)、本地部署、Prompt 工程、多模态特性、RAG 知识库、工具调用、MCP 服务开发、智能体原理和开发、服务化部署等技术。

本节重点


以 Spri؜ng AI 框架为例,‍学习 RAG 知识库应‌用开发的核心特性和高级⁡知识点,并且掌握 RA‏G 最佳实践和调优技巧。

具体内容包括:

  • RAG 核心特性
  • 文档收集和切割(ETL)
  • 向量转换和存储(向量数据库)
  • 文档过滤和检索(文档检索器)
  • 查询增强和关联(上下文查询增强器)
  • RAG 最佳实践和调优
  • RAG 高级知识
  • 检索策略
  • 大模型幻觉
  • 高级 RAG 架构

一、RAG核心特性


在上一节中,我们了解到了RAG的工作流程,如下:

在流程中的四个步骤都有很多SpringAI的特性值得学习。流程步骤如下:

  • 文档收集与切割
  • 向量转换与存储
  • 文档过滤和检索
  • 查询增强和关联

文档收集和切割 - ETL

文档收集与切割阶段是针对用户上传的知识库文档进行处理,最终保存到向量数据中。这个过程称之为ETL(抽取、转换和加载),Spring AI提供了对ETL的支持,ETL 流程管理从原始数据源到结构化向量存储的流程,确保数据以最适合 AI 模型检索的格式呈现。官方文档ETL

文档

Spring AI的Document类包含文本、元数据和可选的额外媒体类型,如图像、音频和视频。

ETL

在Spring AI中对Document的处理通常遵循以下流程:

  1. 读取文档:使用DocumentReader组件从数据源(本地文件、数据库、网络资源等)中加载文档;
  2. 转换文档:使用DocumentTransform组件对原始Document列表进行分片
  3. 写入文档:使用DocumentWriter组件将文档以特定格式保存到存储中,比如将文档以嵌入向量的方式写入到向量数据库中,或以键值对的形式保存到redis中

流程如如:

抽取(Extract)

Spring AI通过DocumentReader组件实现文档读取,也即将文件加载到内存中。

DocumentReader源码如下:

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

  1. JsonReader:读取 JSON 文档
  2. TextReader:读取纯文本文件
  3. MarkdownReader:读取 Markdown 文件
  4. PDFReader:读取 PDF 文档,基于 Apache PdfBox 库实现
  • PagePdfDocumentReader:按照分页读取 PDF
  • ParagraphPdfDocumentReader:按照段落读取 PDF
  1. HtmlReader:读取 HTML 文档,基于 jsoup 库实现
  2. TikaDocumentReader:基于 Apache Tika 库处理多种格式的文档,更灵活

Json؜Reader 为例,支‍持 JSON Poin‌ters 特性,能够快⁡速指定从 JSON 文‏档中提取哪些字段和内容:

// 从 classpath 下的 JSON 文件中读取文档
@Component
class MyJsonReader {private final Resource resource;MyJsonReader(@Value("classpath:complex_data.json") Resource resource) {this.resource = resource;}// 基本用法List<Document> loadBasicJsonDocuments() {JsonReader jsonReader = new JsonReader(this.resource);return jsonReader.get();}// 指定使用哪些 JSON 字段作为文档内容List<Document> loadJsonWithSpecificFields() {JsonReader jsonReader = new JsonReader(this.resource, "description", "features");return jsonReader.get();}// 使用 JSON 指针精确提取文档内容List<Document> loadJsonWithPointer(String pointer) {JsonReader jsonReader = new JsonReader(this.resource);return jsonReader.get(pointer); // 提取 items 数组内的内容}
}
  • json附件complex_data.json
  • 单元测试
@Test
public void loadJsonWithPointerTest() {// 读取/company/headquarters对应地值到内存中List<Document> documents = myJsonReader.loadJsonWithPointer("/company/headquarters");// List<Document> documents = myJsonReader.loadBasicJsonDocuments();log.info("documents:{}", documents);
}

转换(Transform)

Sprin؜g AI 通过 D‍ocumentTr‌ansformer组件实现文档转换,也就是切片‏。

看一下DocumentTransformer的源码,实现了Function<List<Document>, List<Document>>接口,将一组文档列表转换为另一组文档列表。

public interface DocumentTransformer extends Function<List<Document>, List<Document>> {default List<Document> transform(List<Document> transform) {return apply(transform);}}

D‍ocumentTr‌ansformer⁡有很多实现类:

  • TextSplitter

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

TokenTextSplitterTextSplitter 的一个实现,它根据 token 数量将文本分割成多个片段,使用 CL100K_BASE 编码。

@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);}
}
  • SummaryMetadataEnricher(摘要元信息增强器)

SummaryMetadataEnricher 是一个 DocumentTransformer ,它使用生成式 AI 模型为文档创建摘要,并将这些摘要作为元数据添加。它可以生成当前文档的摘要,以及相邻文档(前一个和后一个)。

@Configuration
class EnricherConfig {@Beanpublic SummaryMetadataEnricher summaryMetadata(OpenAiChatModel aiClient) {return new SummaryMetadataEnricher(aiClient,List.of(SummaryType.PREVIOUS, SummaryType.CURRENT, SummaryType.NEXT));}
}@Component
class MySummaryEnricher {private final SummaryMetadataEnricher enricher;MySummaryEnricher(SummaryMetadataEnricher enricher) {this.enricher = enricher;}List<Document> enrichDocuments(List<Document> documents) {return this.enricher.apply(documents);}
}
  • ContentFormatTransformer:确保所有文档的内容格式一致。
  • KeywordMetadataEnricher(关键词元信息增强器)

KeywordMetadataEnricher 是一个使用生成式 AI 模型从文档内容中提取关键词并将其作为元数据添加的 DocumentTransformer

@Component
class MyKeywordEnricher {private final ChatModel chatModel;MyKeywordEnricher(ChatModel chatModel) {this.chatModel = chatModel;}List<Document> enrichDocuments(List<Document> documents) {KeywordMetadataEnricher enricher = new KeywordMetadataEnricher(this.chatModel, 5);return enricher.apply(documents);}
}

加载(Load)

Sprin؜g AI 通过 D‍ocumentWr‌iter 组件实现⁡文档加载(写入)。

public interface DocumentWriter extends Consumer<List<Document>> {default void write(List<Document> documents) {accept(documents);}}

Spring AI提供了两种内置的DocumentWriter实现:

  • FileDocumentWriter:将文档写入到文件系统中
@Component
class MyDocumentWriter {/*** 将文档列表写入到指定文件中* @param filename 文件路径* @param documents 切片文档列表*/public void writeDocuments(String filename, List<Document> documents) {if (!FileUtil.exist(filename)) {FileUtil.touch(filename);}FileDocumentWriter writer = new FileDocumentWriter(filename, true, MetadataMode.ALL, false);writer.accept(documents);}
}

单元测试

@Test
public void loadJsonWithPointerTest() {List<Document> documents = myJsonReader.loadJsonWithPointer("/company/headquarters");// List<Document> documents = myJsonReader.loadBasicJsonDocuments();log.info("documents:{}", documents);// 将切片文档写入到文件系统myDocumentWriter.writeDocuments("chunks.txt", documents);
}

可以看到,切片内容dockuments被写入到文件里,如下:

  • VectorStore:将文档写入到向量数据库中
@Component
class MyVectorStoreWriter {private final VectorStore vectorStore;MyVectorStoreWriter(VectorStore vectorStore) {this.vectorStore = vectorStore;}public void storeDocuments(List<Document> documents) {vectorStore.accept(documents);}
}

ETL流程示例

将上述 3 大组件组合起来,可以实现完整的 ETL 流程:

// 抽取:从 PDF 文件读取文档
PDFReader pdfReader = new PagePdfDocumentReader("knowledge_base.pdf");
List<Document> documents = pdfReader.read();// 转换:分割文本并添加摘要
TokenTextSplitter splitter = new TokenTextSplitter(500, 50);
List<Document> splitDocuments = splitter.apply(documents);SummaryMetadataEnricher enricher = new SummaryMetadataEnricher(chatModel, List.of(SummaryType.CURRENT));
List<Document> enrichedDocuments = enricher.apply(splitDocuments);// 加载:写入向量数据库
vectorStore.write(enrichedDocuments);// 或者使用链式调用
vectorStore.write(enricher.apply(splitter.apply(pdfReader.read())));

向量存储和转换

向量存储是RAG中的核心组件,它将文档转换为向量并存储起来,以便后续进行向量的相似性搜索。Spring AI官方提供了向量数据库接口VectorStore和向量存储整合包,帮助开发者快速集成第三方向量数据库,比如Mivus、Redis、PGVector、ElasticSearch。

VectorStore接口介绍

VectorStore接口定义了管理和查询向量数据库里的文档集合的操作方法。它继承了DocumentWriter来支持文档写入能力。向量数据库是专为AI应用而设计,不同于传统数据库的的精确匹配,向量数据库基于数据的向量表示进行相似性搜索。VectorStore允许根据给定的查询prompt来添加、删除和搜索文档。

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

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();}
}

搜索请求构建

Sprin؜g AI 提供了 ‍SearchReq‌uest 类,用于⁡构建相似度搜索请求‏:

SearchRequest request = SearchRequest.builder().query("什么是RAG?").topK(5)                  // 返回最相似的5个结果.similarityThreshold(0.7) // 相似度阈值,0.0-1.0之间.filterExpression("category == 'web' AND date > '2025-05-03'")  // 过滤表达式.build();List<Document> results = vectorStore.similaritySearch(request);

接口中的 similaritySearch 方法允许根据给定的查询字符串检索相似的文档。这些方法可以通过以下参数进行微调:

  • query:用户搜素文本
  • topK:相似度最大的前K个文档
  • similarityThreshold:值介于0.0-1.0之间,0.0意味着所有文档都会相关,1.0意味着要求最精确匹配
  • filterExpression:基于文档元数据的过滤表达式,语法类似于SQL语句,基于 ANTLR4 的外部 DSL,接受字符串形式的过滤表达式。例如,对于 country、year 和 isActive 等元数据键,你可以使用类似 country == ‘UK’ && year >= 2020 && isActive == true. 的表达式

参考官方文档

向量存储的工作原理

向量数据库是基于向量表示进行相似度搜索,具体工作流程如下:

  1. 嵌入转换:当文档被添加到向量存储时,Spring AI会使用嵌入模型将文本转换为向量
  2. 相似度计算:查询时,同样会将查询文本转换为向量,然后系统计算此向量与数据库中所有向量的相似度
  3. 过滤与排序:根据相似度阈值结果,按照相似度排序返回最相关的文档

支持的向量数据库

官方文档

以下是可用的向量数据库实现:

Azure Vector Search - The Azure vector store.
Azure 向量搜索 - Azure 向量数据库。Apache Cassandra - The Apache Cassandra vector store.
Apache Cassandra - Apache Cassandra 向量存储。Chroma Vector Store - The Chroma vector store.
Chroma Vector Store - Chroma 向量存储。Elasticsearch Vector Store - The Elasticsearch vector store.
Elasticsearch Vector Store - Elasticsearch 向量存储。GemFire Vector Store - The GemFire vector store.
GemFire Vector Store - GemFire 向量存储。MariaDB Vector Store - The MariaDB vector store.
MariaDB 向量存储 - MariaDB 向量存储。Milvus Vector Store - The Milvus vector store.
Milvus 向量存储 - Milvus 向量存储。MongoDB Atlas Vector Store - The MongoDB Atlas vector store.
MongoDB Atlas 向量存储 - MongoDB Atlas 向量存储。Neo4j Vector Store - The Neo4j vector store.
Neo4j 向量存储 - Neo4j 向量存储。OpenSearch Vector Store - The OpenSearch vector store.
OpenSearch 向量存储 - OpenSearch 向量存储。Oracle Vector Store - The Oracle Database vector store.
Oracle 向量存储 - Oracle 数据库向量存储。PgVector Store - The PostgreSQL/PGVector vector store.
PgVector 向量存储 - PostgreSQL/PGVector 向量存储。Pinecone Vector Store - PineCone vector store.
Pinecone 向量存储 - PineCone 向量存储。Qdrant Vector Store - Qdrant vector store.
Qdrant 向量存储 - Qdrant 向量存储。Redis Vector Store - The Redis vector store.
Redis 向量存储 - Redis 向量存储。SAP Hana Vector Store - The SAP HANA vector store.
SAP HANA 向量存储 - SAP HANA 向量存储。Typesense Vector Store - The Typesense vector store.
Typesense 向量存储 - Typesense 向量存储。Weaviate Vector Store - The Weaviate vector store.
Weaviate 向量存储 - Weaviate 向量存储。SimpleVectorStore - A simple implementation of persistent vector storage, good for educational purposes.
SimpleVectorStore - 一种简单的持久化向量存储实现,适合教育用途。

对于每种 Vecto؜r Store 实现,我们都可以参考对应‍的官方文档进行整合,开发方法基本上一致:‌先准备好数据源 => 引入不同的整合包 ⁡=> 编写对应的配置 => 使用自动注入‏的 VectorStore 即可。

值得一提的是,S؜pring AI Alibaba‍ 已经集成了阿里云百炼平台,可以‌直接使用阿里云百炼平台提供的 V⁡ectorStore API,无‏需自己再搭建向量数据库了。

参考 官方文档,主要是提供了 DashScopeCloudStore 类:

DashScopeCloudStore实现了VectorStore接口,通过调用DashScopeApi来使用阿里云百炼的提供的向量存储服务:

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

数据库准备

首先需要安装 Postgresql 以及向量插件 pgvector

我的操作系统是 mac,通过 homebrew 包管理器安装的,具体的安装步骤也是让 AI 给到的。

以下是我的提示词:

操作系统:mac
需求:通过 homebrew 安装 postgresql,以及 vector 插件
补充:
1、如果需要配置环境变量的话,给出对应步骤说明。
2、数据库环境搭建完成后,创建账号 "tiga"
3、创建数据库“ai-agent”

模型给的方案:

# 1、安装 PostgreSQL 和 Vector 插件
# 安装 PostgreSQL
brew install postgresql# 安装 vector 扩展
brew install pgvector# 2、配置环境变量
echo 'export PATH="/opt/homebrew/opt/postgresql@15/bin:$PATH"' >> ~/.zshrc  # Apple Silicon 芯片
# 或
echo 'export PATH="/usr/local/opt/postgresql@15/bin:$PATH"' >> ~/.zshrc    # Intel 芯片source ~/.zshrc  # 立即生效# 3、启动 PostgreSQL 服务
brew services start postgresql# 4、启用 Vector 插件
# 编辑 PostgreSQL 配置文件:
nano /opt/homebrew/var/postgresql/postgresql.conf
# 找到 shared_preload_libraries 并修改为:
shared_preload_libraries = 'vector'  # 若已有值则追加 ,vector
# 保存后重启服务:
brew services restart postgresql# 5、创建用户和数据库
# 进入 PostgreSQL 控制台
psql postgres# 创建新用户
CREATE USER tiga WITH PASSWORD 'your_password_here';  -- 替换为实际密码# 创建数据库
CREATE DATABASE "ai-agent" OWNER tiga;# 在新数据库启用 vector 扩展
\c "ai-agent"
CREATE EXTENSION vector;# 验证扩展
\dx
# 应看到 vector 扩展已启用# 退出
\q# 6、验证步骤
# 用 tiga 用户连接测试
psql -U tiga -d ai-agent# 执行测试 SQL
ai-agent=> SELECT 1;
# 应返回数字 1

本地idea数据库客户端连接 postgresql 服务:

可以看到 vector 向量存储插件已经安装好了。

至此,我们的数据库环境就准备好了。


编写代码

下面开始结合 SpringAI 官方文档整个 PGVector

1、引入依赖
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-vector-store-pgvector</artifactId><version>1.0.0-M7</version>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId><scope>runtime</scope>
</dependency>
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-pgvector-store</artifactId><version>1.0.0-M6</version>
</dependency>
2、编写配置,建立数据库连接

:::info
说明:因为我项目中之前已经引入 mysql 数据源,现在引入 postgresql 数据源,所以需要进行多数据源配置,否则项目启动时会有冲突。

:::

  • 配置文件
spring:application:name: spring-ai-alibaba-qwq-chat-client-exampleai:dashscope:api-key: '你的 apikey'chat:options:model: qwen-plusdatasource:mysql:jdbc-url: jdbc:mysql://localhost:3306/ai-agent?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghaiusername: rootpassword: ''driver-class-name: com.mysql.cj.jdbc.Driverpostgresql:jdbc-url: jdbc:postgresql://localhost:5432/ai-agentusername: 'tiga'password: ''driver-class-name: org.postgresql.Driver# 禁用Spring Boot自动配置数据源autoconfigure:exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
  • 配置类

config包下新建DataSourceConfig类:

/*** 多数据源配置*/
@Configuration
@EnableConfigurationProperties
public class DataSourceConfig {/*** mysql作为主数据源* * @return*/@Primary@Bean(name = "mysqlDataSource")@ConfigurationProperties(prefix = "spring.datasource.mysql")public DataSource mysqlDataSource() {return DataSourceBuilder.create().build();}@Bean(name = "postgresqlDataSource")@ConfigurationProperties(prefix = "spring.datasource.postgresql")public DataSource postgresDataSource() {return DataSourceBuilder.create().build();}@Primary@Bean(name = "mysqlJdbcTemplate")public JdbcTemplate mysqlJdbcTemplate(@Qualifier("mysqlDataSource") DataSource dataSource) {return new JdbcTemplate(dataSource);}@Bean(name = "postgresqlJdbcTemplate")public JdbcTemplate postgresqlJdbcTemplate(@Qualifier("postgresqlDataSource") DataSource dataSource) {return new JdbcTemplate(dataSource);}
}

然后我我们手动注册一个VectorStore 类型的 bean来构造PgVectorStore,不用 SpringBoot 自动配置的 bean,否则启动时也会有 bean 类型的冲突。

在 config 包下新建PgVectorVectorStoreConfig类:

@Configuration
public class PgVectorVectorStoreConfig {@Resourceprivate  LoveAppDocumentLoader loveAppDocumentLoader;@Beanpublic VectorStore pgVectorVectorStore(@Qualifier("postgresqlJdbcTemplate") JdbcTemplate postgresqlJdbcTemplate, EmbeddingModel dashscopeEmbeddingModel) {VectorStore vectorStore = PgVectorStore.builder(postgresqlJdbcTemplate, dashscopeEmbeddingModel).dimensions(1536)                    // Optional: defaults to model dimensions or 1536.distanceType(COSINE_DISTANCE)       // Optional: defaults to COSINE_DISTANCE.indexType(HNSW)                     // Optional: defaults to HNSW.initializeSchema(true)              // Optional: defaults to false.schemaName("public")                // Optional: defaults to "public".vectorTableName("vector_store")     // Optional: defaults to "vector_store".maxDocumentBatchSize(10000)         // Optional: defaults to 10000.build();// 加载文档List<Document> markdowns = loveAppDocumentLoader.loadMarkdowns();vectorStore.add(markdowns);return vectorStore;}
}
  • 在主启动类排除PgVectorStoreAutoConfiguration自动注册
@SpringBootApplication(exclude = PgVectorStoreAutoConfiguration.class)
@MapperScan("com.liucc.aiagent.mapper")
public class AiAgentApplication {public static void main(String[] args) {SpringApplication.run(AiAgentApplication.class, args);}
}
单元测试
  • 编写单元测试类
@SpringBootTest
public class PgVectorVectorStoreConfigTest {@ResourceVectorStore pgVectorVectorStore;@Testvoid test() {List<Document> documents = List.of(new Document("Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!!", Map.of("meta1", "meta1")),new Document("The World is Big and Salvation Lurks Around the Corner"),new Document("You walk forward facing the past and you turn back toward the future.", Map.of("meta2", "meta2")));// 添加文档pgVectorVectorStore.add(documents);// 相似度查询List<Document> results = pgVectorVectorStore.similaritySearch(SearchRequest.builder().query("Spring").topK(5).build());Assertions.assertNotNull(results);}
}

启动执行单元测试,可能会遇到以下错误:

原因:我们在PgVectorVectorStoreConfig配置类里添加了initializeSchema设为 true,但程序自动初始化vector_store表仍失败。没关系,我们手动去创建这个表就行了,脚本如下:

-- 首先确保 vector 扩展已安装
CREATE EXTENSION IF NOT EXISTS vector;-- 创建向量存储表
CREATE TABLE IF NOT EXISTS public.vector_store (id varchar(255) PRIMARY KEY,content text,metadata jsonb,embedding vector(1536)
);-- 创建向量索引(可选,但建议创建以提高查询性能)
CREATE INDEX ON public.vector_storeUSING hnsw (embedding vector_cosine_ops);
  • 重新执行单元测试:

可以看到对话信息已经持久化到表,embedding 字段就是存储的向量数据

  • 我们修改 LoveApp基于 RAG 聊天方法的向量存储,改为通过 pgVector存储,代码如下:
/*** 基于 RAG 的聊天* * @param message* @param chatId* @return*/
public String doChaWithRag(String message, String chatId) {String content = chatClient.prompt().user(message).advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId).param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10))// 开启日志记录顾问、QA顾问.advisors(new MyLoggerAdvisor())// 采取pgVector进行存储.advisors(new QuestionAnswerAdvisor(pgVectorVectorStore)).call().chatResponse().getResult().getOutput().getText();return content;
}
  • 执行对应的单元测试:
@Test
void doChaWithRagTest() {String message = "你好,我是tiga";String chatId = UUID.randomUUID().toString();loveApp.doChaWithRag(message, chatId);loveApp.doChaWithRag("我和我的女朋友是异地恋,我该怎么维持这段关系呢?", chatId);
}

扩展知识 - 批处理策略

使用向量存储时,通常需要嵌入大量文档。虽然一次调用即可嵌入所有文档看似简单,但这种方法可能会导致问题。嵌入模型将文本作为token处理,并且具有最大token限制,通常称为上下文窗口大小。此限制约束了单个嵌入请求中可处理的文本量。尝试在一次调用中嵌入过多的标记可能会导致错误或嵌入被截断。

为了解决token限制问题,Spring AI 实现了批处理策略。此方法将大量文档分解为适合嵌入模型最大上下文窗口的较小批次。批处理不仅解决了token限制问题,还可以提高性能并更有效地利用 API 速率限制。

Spring AI 通过 BatchingStrategy 接口提供此功能,该接口允许根据文档的token计数以子批次的形式处理文档。

public interface BatchingStrategy {List<List<Document>> batch(List<Document> documents);
}

Spring AI 提供了一个名为 TokenCountBatchingStrategy 的默认实现。此策略根据文档的 token 计数对文档进行批处理,确保每个批次不超过计算的最大输入 token 计数。

核心特性:

  1. 使用 OpenAI 的最大输入令牌数 (8191)作为默认上限。
  2. 包含储备百分比(默认 10%)来为潜在开销提供缓冲。
  3. 计算实际最大输入令牌数为: actualMaxInputTokenCount = originalMaxInputTokenCount * (1 - RESERVE_PERCENTAGE)

该策略估计每个文档的token数,将它们分组为批次,且不超过最大输入标记数,如果单个文档超过此限制,就会抛出异常。

SpringAI 还支持自定义实现TokenCountBatchingStrategy,以更好满足用户自己的特定需求。

创建自定义 TokenCountBatchingStrategy bean 的示例:

@Configuration
public class EmbeddingConfig {@Beanpublic BatchingStrategy customTokenCountBatchingStrategy() {return new TokenCountBatchingStrategy(EncodingType.CL100K_BASE,  // 指定编码方式8000,                      // 设置最大输入 token 数0.1                        // 设置缓冲预留百分比);}
}

文档过滤和检索

SpringAI RAG文档

SpringAI官方实现了一个模块化的RAG架构,灵感来源于论文《模块化 RAG:将 RAG 系统转变为乐高式可重构框架》中详细阐述的模块化概念。

论文链接:https://arxiv.org/abs/2407.21059

把整个文档的过滤检索阶段拆分为:检索前、检索中和检索后,针对于每个阶段SpringAI都提供了可自定义的组件。

  • 预检索阶段: 系统接收用户的原始查询,通过查询重写、查询压缩、查询扩展等组件进行优化,输出增强后的用户查询
  • 检索阶段: 检索模块负责查询数据系统(如向量存储)并检索最相关的文档,通过Document SearchDocument Join文档合并等组件,将最和用户query相关的文档检索出来
  • 检索后阶段: 可以对检索到的文档做一些后续处理,比如减少检索信息中的噪声和冗余、根据文档与查询的相关性进行排序等

检索前:优化用户查询

与检索阶段对于用户的原始查询,通过查询重写、查询压缩、查询扩展等组件进行优化,输出增强后的用户查询。Spring AI提供了多种查询处理组件。

查询转换 - 查询重写

RewriteQueryTransformer 组件使用大型语言模型来改写用户查询,旨在提供语义更精简、更精准的用户query。在用户查询内容冗长、模糊或包含可能影响搜索结果质量的不相关信息时,该组件将会很有用。

Query query = new Query("I'm studying machine learning. What is an LLM?");QueryTransformer queryTransformer = RewriteQueryTransformer.builder()
.chatClientBuilder(chatClientBuilder)
.build();Query transformedQuery = queryTransformer.transform(query);

实现改写的原理很简单,通过查看源码可以发现,就是给大模型提示词,基于用户原始query改写,确保结果是精准的、明确的。

也可以通过RewriteQueryTransformer构造器的promptTemplate属性来定制自己的重写查询prompt:

简单的示例代码:

@Test
public void queryRewrite(){Query query = new Query("我正在学习机器学习。什么是大语言模型?");QueryTransformer queryTransformer = RewriteQueryTransformer.builder().chatClientBuilder(chatClientBuilder).build();Query transformedQuery = queryTransformer.transform(query);log.info("原始查询: {}", query.text());log.info("转换后的查询: {}", transformedQuery.text());
}

测试效果如下:

查询转换 - 查询翻译

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

示例代码:

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);

查看源码,可以发现查询翻译也是通过给大模型提示词来实现的:

所以,没必要使用这个来实现语言翻译,毕竟大模型的使用成本还是很高的,可以通过继承Google或百度翻译工具来实现。

查询转换 - 查询压缩

CompressionQueryTransformer组件使用大型语言模型将对话历史和后续查询压缩成一个独立的查询,该查询能够捕捉对话的精髓。当对话历史较长且后续查询与对话上下文相关时,这个转换器很有用。

示例代码:

Query query = Query.builder().text("And what is its second largest city?").history(new UserMessage("What is the capital of Denmark?"),new AssistantMessage("Copenhagen is the capital of Denmark.")).build();QueryTransformer queryTransformer = CompressionQueryTransformer.builder().chatClientBuilder(chatClientBuilder).build();Query transformedQuery = queryTransformer.transform(query);

查询压缩也是通过提示词来实现的:

检索:提高查询相关性

检索模块负责从存储中查询出和用户问题相关的文档。

文档搜索

VectorStoreDocumentRetriever

VectorStoreDocumentRetriever组件从向量存储中检索与输入查询语义相似的文档,它支持基于元数据的过滤、相似度阈值和查询top-k个结果。

DocumentRetriever retriever = VectorStoreDocumentRetriever.builder().vectorStore(vectorStore).similarityThreshold(0.73).topK(5).filterExpression(new FilterExpressionBuilder().eq("genre", "fairytale").build()).build();
List<Document> documents = retriever.retrieve(new Query("What is the main character of the story?"));

过滤表达式可以是静态或动态的,对于动态过滤表达式,可以传递一个supplier,官方示例:

DocumentRetriever retriever = VectorStoreDocumentRetriever.builder().vectorStore(vectorStore).filterExpression(() -> new FilterExpressionBuilder().eq("tenant", TenantContextHolder.getTenantIdentifier()).build()).build();
List<Document> documents = retriever.retrieve(new Query("What are the KPIs for the next semester?"));

SpringAI还支持通过在QueryAPI中使用FILTER_EXPRESSION来构建过滤表达式,且如果在query和retriever中同时定义了过滤表达式,那么query中定义的表达式优先级更高。

Query query = Query.builder().text("Who is Anacletus?").context(Map.of(VectorStoreDocumentRetriever.FILTER_EXPRESSION, "location == 'Whispering Woods'")).build();
List<Document> retrievedDocuments = documentRetriever.retrieve(query);
文档合并

ConcatenationDocumentJoiner组件将来自多个查询及其对应查询到的多个文档全部合并到一个文档集合中。并且在合并处理过程中,该组件可以处理重复的文档。

示例用法:

Map<Query, List<List<Document>>> documentsForQuery = ...
DocumentJoiner documentJoiner = new ConcatenationDocumentJoiner();
List<Document> documents = documentJoiner.join(documentsForQuery);

现在看一下ConcatenationDocumentJoiner源码的核心处理,它是如何将多个查询的多个文档集合合并到一起的?

public class ConcatenationDocumentJoiner implements DocumentJoiner {private static final Logger logger = LoggerFactory.getLogger(ConcatenationDocumentJoiner.class);@Overridepublic List<Document> join(Map<Query, List<List<Document>>> documentsForQuery) {Assert.notNull(documentsForQuery, "documentsForQuery cannot be null");Assert.noNullElements(documentsForQuery.keySet(), "documentsForQuery cannot contain null keys");Assert.noNullElements(documentsForQuery.values(), "documentsForQuery cannot contain null values");logger.debug("Joining documents by concatenation");return new ArrayList<>(documentsForQuery.values().stream().flatMap(List::stream).flatMap(List::stream).collect(Collectors.toMap(Document::getId, Function.identity(), (existing, duplicate) -> existing)).values());}}

核心方法就是join方法,核心处理逻辑:

  • documentsForQuery的结构是Map<Query, List<List<Document>>>,表示每个查询对应多个数据源的文档集合。
  • 通过流式处理,逐层展开嵌套的集合:
    • documentsForQuery.values():获取所有查询对应的全部文档集合
    • .stream():将这些集合转换为stream流,方便处理
    • .flatMap(List::stream):将List<List<Document>>转换为List<Document>的流
    • 再次.flatMap(List::stream):将List<Document>转换为单个Document的流
  • 去重逻辑:使用Collectors.toMap(Document::getId, Function.identity(), (existing, duplicate) -> existing)将文档流转换为一个Map,key是每个文档的Id(Document::getId),值就是文档本身(Function.identity()),出现重复时保留第一次出现的文档( (existing, duplicate) -> existing)
  • 结果返回:将去重后的Map的值转换为集合并返回

检索后:优化文档处理

检索后模块负责对已经检索到的文档集合进行后续处理,目的是解决信息丢失、模型上下文长度限制、减少检索信息中的噪声和冗余等挑战。

不过截止到SpringAI 1.0.0,官方文档对这块目前似乎只有设想,并没有太多的具体案例讲解,后续更新中或许会有。

查询增强和关联

RAG的最后阶段是生成阶段,负责将用户query和检索到的文档结合到一起,为AI提供必要的上下文信息,从而生成更准确、更相关的内容。

之前的章节中已经了解到SpringAI提供了两种实现RAG查询增强的Advisor,一个是QuestionAnswerAdvisorRetrievalAugmentationAdvisor


QuestionAnswerAdvisor 查询增强

当用户问题发送AI时,Advisor会查询向量数据库获取和用户问题相关的文档,并将这些文档作为上下文附加到用户查询中。使用示例如下:

ChatResponse response = ChatClient.builder(chatModel).build().prompt().advisors(new QuestionAnswerAdvisor(vectorStore)).user(userText).call().chatResponse();

还可以通过建造者模式配置更精细的参数,比如文档过滤条件:

var qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)// 相似度阈值为 0.8,并返回最相关的前 6 个结果.searchRequest(SearchRequest.builder().similarityThreshold(0.8d).topK(6).build()).build();

此外,QuestionAnswerAdvisor还支持动态过滤表达式,可以在运行时根据需要调整过滤条件:

ChatClient chatClient = ChatClient.builder(chatModel).defaultAdvisors(QuestionAnswerAdvisor.builder(vectorStore).searchRequest(SearchRequest.builder().build()).build()).build();// 在运行时更新过滤表达式
String content = this.chatClient.prompt().user("看着我的眼睛,回答我!").advisors(a -> a.param(QuestionAnswerAdvisor.FILTER_EXPRESSION, "type == 'web'")).call().content();

QuestionAnswerAdvisor的实现原理很简单,就是将用户问题和检索到的文档拼接成一个新的prompt,再传递给AI。

SpringAI也支持用户自定义提示词模板:

QuestionAnswerAdvisor qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore).promptTemplate(customPromptTemplate).build();

RetrievalAugmentationAdvisor 查询增强

SpringAI提供了另一种RAG实现方式,基于RAG模块化架构,提供了更多的灵活性和扩展性。

一种简单的使用示例:

Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder().documentRetriever(VectorStoreDocumentRetriever.builder().similarityThreshold(0.50).vectorStore(vectorStore).build()).build();String answer = chatClient.prompt().advisors(retrievalAugmentationAdvisor).user(question).call().content();

这段示例代码中,我们配置了VectorStoreDocumentRetriever用于从向量存储中检索文档数据,然后将这个检索器添加到ChatClient的请求当中,让他处理用户的问题。

VectorStoreDocumentRetriever还支持更高级的RAG流程,比如结合查询转换器:

Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder().queryTransformers(RewriteQueryTransformer.builder().chatClientBuilder(chatClientBuilder.build().mutate()).build()).documentRetriever(VectorStoreDocumentRetriever.builder().similarityThreshold(0.50).vectorStore(vectorStore).build()).build();

在这段示例代码中,我们添加了一个查询转换器用于在真正检索文档前,先将用户问题进行改写,改写为更加准确的query,从而提高的文档检索的质量。一般处理用户问题本身含糊不清的情况。


ContextualQueryAugmenter 空上下文处理

默认情况下,RetrievalAugmentationAdvisor遵循如果文档检索上下文为空的话,指示AI拒绝回答任何内容。默认提示词如下:

但在更多情况下,我们仍然希望即使未检索到相关文档,也为用户提供一些其他特定的回复策略。可以通过配置ContextualQueryAugmenter上下文查询增强器来实现。

Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder().documentRetriever(VectorStoreDocumentRetriever.builder().similarityThreshold(0.50).vectorStore(vectorStore).build()).queryAugmenter(ContextualQueryAugmenter.builder().allowEmptyContext(true).build()).build();

通过设置 allowEmptyContext(true),允许模型在没有找到相关文档的情况下也生成回答。

查看ContextualQueryAugmenter的源码可以发现有两处Prompt:

  • 一处是正常情况下对用户查询增强的提示词
  • 一处是上下文为空时的使用的提示词

为了提供更友好、更符合用户定制化需求的提示词,SpringAI支持自定义提示词模板:

QueryAugmenter queryAugmenter = ContextualQueryAugmenter.builder().promptTemplate(customPromptTemplate).emptyContextPromptTemplate(emptyContextPromptTemplate).build();

二、RAG最佳实践与调优

下面通过实战来具象化地体会RAG四大阶段中的各个组件是怎么协作的。

文档收集和切割

文档的质量决定了大模型回答质量的上线,其他的优化策略也只能让大模型回复质量不断接近这个上限。

因此,对于原始文档的处理是RAG模块中的重要一环。

1、优化原始文档

知识完备性 是文档质量的首要条件。如果知识库缺失相关内容,大模型将无法准确回答对应问题。我们需要通过收集用户反馈或统计知识库检索命中率,不断完善和优化知识库内容。

在知识完整的前提下,我们要注意 3 个方面:

1)内容结构化:Pga3o8dlAlk5jom/XG4u38VZgjWTs6SzXy1vtLtsRaA=

  • 原始文档应保持排版清晰、结构合理,如案例编号、项目概述、设计要点等
  • 文档的各级标题层次分明,各标题下的内容表达清晰
  • 列表中间的某一条之下尽量不要再分级,减少层级嵌套

2)内容规范化:

  • 语言统一:确保文档语言与用户提示词一致(比如英语场景采用英文文档),专业术语可进行多语言标注
  • 表述统一:同一概念应使用统一表达方式(比如 ML、Machine Learning 规范为“机器学习”),可通过大模型分段处理长文档辅助完成
  • 减少噪音:尽量避免水印、表格和图片等可能影响解析的元素

3)格式标准化:

  • 优先使用 Markdown、DOC/DOCX 等文本格式(PDF 解析效果可能不佳),可以通过百炼 DashScopeParse 工具将 PDF 转为Markdown,再借助大模型整理格式
  • 如果文档包含图片,需链接化处理,确保回答中能正常展示文档中的插图,可以通过在文档中插入可公网访问的 URL 链接实现

我们将最终可以给AI大模型直接处理的文档称之为“AI原生文档“,我们可以将⁡上述规则输入给 AI 大模‏型,让它对已有文档进行优化。


2、文档切片

合适的文档切片大小和方式对检索文档的效果至关重要。在进行切片时,避免走两个极端:切片过短导致语义缺失,语义过长引入无关信息。具体需要结合以下因素考虑:

  • 文档类型:对于专业类文献,增加长度通常有助于保留更多上下文信息;而对于社交类帖子,缩短长度则能更准确地捕捉语义
  • 提示词复杂度:如果用户的提示词较复杂且具体,则可能需要增加切片长度;反之,缩短长度会更为合适

最佳文档切片策略是 结合智能分块算法和人工二次校验。智能分块算法基于分句标识符先划分为段落,再根据语义相关性动态选择切片点,避免固定长度切分导致的语义断裂。在实际应用中,应尽量让文本切片包含完整信息,同时避免包含过多干扰信息。

  • 选择智能切分,让AI进行分段,在语义和文本长度之间的权衡会做得更好

  • 人工二次校验

SpringAI提供了TokenTextSplitter组件对读取到的文档进行分片,示例代码如下:

定义文本切分器:

@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(200, 100, 10, 5000, true);return splitter.apply(documents);}
}

使用切分器:

@Bean
public VectorStore loveAppVectorStore(EmbeddingModel dashscopeEmbeddingModel) {// 加载文档List<Document> markdowns = loveAppDocumentLoader.loadMarkdowns();// chunkList<Document> chunks = myTokenTextSplitter.splitCustomized(markdowns);SimpleVectorStore simpleVectorStore = SimpleVectorStore.builder(dashscopeEmbeddingModel).build();// 分段文档存储到向量数据库中simpleVectorStore.add(chunks);return simpleVectorStore;
}

切分效果:可以看到文档语义的连贯性很明显被截断了,因此很不推荐这种固定死长度的token切分方式。

如果使用云服务,如阿里云百炼,推荐在创建知识库时选择 智能切分,这是百炼经过大量评估后总结出的推荐策略:

我们对于平台最终切分的文档也可以进一步人工审核,修改其不符合预期的部分。


3、元数据标注

我们可以为文档添加元信息,以便后面可以通过过滤表达式更加准确的检索到预期文档。下面介绍3种常见的生成文档元信息的方式。

1)手动添加元信息(单个文档):

documents.add(new Document("案例编号:LR-2023-001\n" +"项目概述:180平米大平层现代简约风格客厅改造\n" +"设计要点:\n" +"1. 采用5.2米挑高的落地窗,最大化自然采光\n" +"2. 主色调:云雾白(哑光,NCS S0500-N)配合莫兰迪灰\n" +"3. 家具选择:意大利B&B品牌真皮沙发,北欧白橡木茶几\n" +"空间效果:通透大气,适合商务接待和家庭日常起居",Map.of("type", "interior",    // 文档类型"year", "2025",        // 年份"month", "05",         // 月份"style", "modern",      // 装修风格)));

2)利用DocumentReader批量添加元信息:

比如我们可؜以在 loadMa‍rkdown 时为‌每篇文章添加特定标⁡签,例如"恋爱状态‏":

// 提取文档倒数第 3 和第 2 个字作为标签
String status = fileName.substring(fileName.length() - 6, fileName.length() - 4);
MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder().withHorizontalRuleCreateDocument(true).withIncludeCodeBlock(false).withIncludeBlockquote(false).withAdditionalMetadata("filename", fileName).withAdditionalMetadata("status", status).build();

debug可以看到给文档添加的元信息:

3)自动生成关键词元信息

SpringAI的ETL框架提供了一种Transforms组件–KeywordMetadataEnricher,结合大模型来提取出文档中的关键词作为文档的元信息。

使用示例如下:

rag包下定义MyKeywordEnricher

/*** 关键词元数据生成器*/
@Component
public class MyKeywordEnricher {@Resourceprivate ChatModel dashscopeChatModel;List<Document> enrichDocuments(List<Document> documents){// 生成 3 个关键词的元数据KeywordMetadataEnricher keywordMetadataEnricher = new KeywordMetadataEnricher(dashscopeChatModel, 3);return keywordMetadataEnricher.apply(documents);}
}

调整rag包下的LoveAppVectorStoreConfig类:

@Bean
public VectorStore loveAppVectorStore(EmbeddingModel dashscopeEmbeddingModel) {// 加载文档List<Document> markdowns = loveAppDocumentLoader.loadMarkdowns();// 生成关键词元数据List<Document> enrichDocuments = myKeywordEnricher.enrichDocuments(markdowns);SimpleVectorStore simpleVectorStore = SimpleVectorStore.builder(dashscopeEmbeddingModel).build();// 分段文档存储到向量数据库中simpleVectorStore.add(enrichDocuments);return simpleVectorStore;
}

通过debug,可以发现给每一个切片都自动生成了关键词元信息:

在云服务平台؜中,如阿里云百炼,同样‍支持元数据和标签功能。‌可以通过平台 API ⁡或界面设置标签、以及通‏过标签实现快速过滤:

1)为某个文档设置标签:

2)在创建知؜识库并导入数据时,可以‍配置自动 metada‌ta 抽取(需注意,创⁡建后将无法再配置抽取规‏则或更新已有元信息):

元数据的抽取支持多种规则:

下面给一个配置大模型取值规则的示例:

在知识库首页查看大模型生成的Meta信息:

可以看到的是,大模型生成的元信息并不准确,而且阿里云百炼平台还不支持你手动修改元信息,这很不方便,建议自己手动的创建元信息。再一个也希望平台方后期可以改善下这块的设计。


向量存储和转换

向量转换和؜存储是 RAG 系‍统的核心环节,直接‌影响检索的效率和⁡准确性。但这个环节的主要工作就是选择合适的向量存储源嵌入模型。

向量存储配置

需要根据费؜用成本、数据规模、‍性能、开发成本来选‌择向量存储方案,比⁡如内存 / Red‏is / MongoDB。在编程实现中,可以通过以下方式配置向量存储:

SimpleVectorStore vectorStore = SimpleVectorStore.builder(embeddingModel)
.build();

在云平台中؜,通常提供多种存储‍选项,比如内置的向‌量存储或者云数据库⁡:

选择合适的嵌入模型

嵌入模型负؜责将文本转换为向量‍,其质量直接影响相‌似度计算和检索⁡准确性。可以在代码中修‏改:

SimpleVectorStore vectorStore = SimpleVectorStore.builder(embeddingModel).build();

文档过滤和检索

这个阶段有查询扩展器、查询重写、查询翻译等组件,下面一一使用。

查询扩展器MultiQueryExpander

MultiQueryExpander会将原始query从多个角度进行不同的扩展,以此达到更全面的回复。不过一般建议设置扩展查询的个数在3-5个,因为查询扩展也是走模型,有成本的,扩展的 query 越多,费用自然也就越高。

使用示例如下:

新建单元测试方法:

@Resource
ChatModel dashScopeChatModel;@Test
void queryExpanderTest(){ChatClient.Builder builder = ChatClient.builder(dashScopeChatModel);// 查询扩展MultiQueryExpander multiQueryExpander = MultiQueryExpander.builder().chatClientBuilder(builder).includeOriginal(false) // 是否包含原始query.numberOfQueries(3).build();List<Query> expand = multiQueryExpander.expand(new Query("谁是水冠呢呢呢?"));log.info("扩展后的查询: {}", expand);
}

运行效果如下:

查询重写和翻译

查询重写和؜翻译可以使查询更加‍精确和专业,但是要‌注意保持查询的语义⁡完整性。

主要应用包括:

  • 使用 RewriteQueryTransformer优化查询语义
  • 配置 TranslationQueryTransformer支持多语言

如何使用?

自定义查询重写器

/*** 查询重写器* 用于在查询前对查询进行预处理或修改*/
@Component
public class QueryRewriter {private final QueryTransformer queryTransformer;public QueryRewriter(ChatModel dashscopeChatModel){ChatClient.Builder builder = ChatClient.builder(dashscopeChatModel);queryTransformer = RewriteQueryTransformer.builder().chatClientBuilder(builder).build();}public String doQueryWrite(String prompt){Query rewriteQuery = queryTransformer.apply(new Query(prompt));return rewriteQuery.text();}
}
@Resource
private QueryRewriter queryRewriter;public String doChaWithRag(String message, String chatId) {// 改写后的 promptString rewriteMessage = queryRewriter.doQueryWrite(message);String content = chatClient.prompt().user(rewriteMessage).advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId).param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10))// 开启日志记录顾问、QA顾问.advisors(new MyLoggerAdvisor())
//                                .advisors(new QuestionAnswerAdvisor(pgVectorVectorStore)).advisors(new QuestionAnswerAdvisor(loveAppVectorStore)).call().chatResponse().getResult().getOutput().getText();return content;
}

检索器配置

检索器配置是影响检索质量的关键因素,主要包括三个因素:相似度阈值、返回文档数量和过滤规则。

1)设置合理的相似度阈值

相似度阈值控制文档召回的标准,需要根据实际情况进行调整,具体来说如下:

问题解决方案
知识库的召回结果不完整,没有包含全部相关的文本切片建议降低 相似度阈值,提高 召回片段数,以召回一些原本应被检索到的信息
知识库的召؜回结果中包含大量无‍关的文本切片‌建议提高相似度阈值⁡,以排除与用户提示‏词相似度低的信息

示例代码:

DocumentRetriever documentRetriever = VectorStoreDocumentRetriever.builder().vectorStore(loveAppVectorStore).similarityThreshold(0.5) // 相似度阈值.build();

2)控制文档返回数量(召回片段数)

控制返回文档数,平衡信息完整性和噪声水平。示例使用代码:

DocumentRetriever documentRetriever = VectorStoreDocumentRetriever.builder().vectorStore(loveAppVectorStore).similarityThreshold(0.5) // 相似度阈值.topK(3) // 返回3个文档.build();

3)配置文档过滤规则

配置文档过滤规则可以进一步提高检索的准确率,主要是通过元信息进行过滤。可以参考官方文档设置过滤规则。

1、在rag包下创建一个工厂类创建自定义检索器顾问:

/*** 自定义顾问工厂类*/
public class LoveAppRagCustomAdvisorFactory {/*** 获取自定义顾问* @param vectorStore 向量存储* @param status 标签* @return*/public static Advisor createLoveAppRagCustomAdvisor(VectorStore vectorStore, String status){Filter.Expression expression = new FilterExpressionBuilder().eq("status", status).build();DocumentRetriever documentRetriever = VectorStoreDocumentRetriever.builder().topK(2).filterExpression(expression).similarityThreshold(0.7).vectorStore(vectorStore).build();return RetrievalAugmentationAdvisor.builder().documentRetriever(documentRetriever).build();}
}

2、使用自定义检索器顾问:

/*** 基于 RAG 的聊天** @param message* @param chatId* @return*/
public String doChaWithRag(String message, String chatId) {// 改写后的 promptString rewriteMessage = queryRewriter.doQueryWrite(message);String content = chatClient.prompt().user(rewriteMessage).advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId).param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10))// 开启日志记录顾问、QA顾问.advisors(new MyLoggerAdvisor())
//                                .advisors(new QuestionAnswerAdvisor(pgVectorVectorStore))
//                                .advisors(new QuestionAnswerAdvisor(loveAppVectorStore)).advisors(LoveAppRagCustomAdvisorFactory.createLoveAppRagCustomAdvisor(pgVectorVectorStore, "恋爱")).call().chatResponse().getResult().getOutput().getText();return content;
}

正面案例

相似度阈值:0.7设置高一些,避免检索到无关文档

过滤表达式:状态为恋爱

query:我和我的女朋友是异地恋,我该怎么维持这段关系呢?

期望的效果:检索到相关的两条文档,控制台输出相关上下文信息

响应结果:

控制台信息:

:::color2
可以看到,召回两条文档,虽然文档片段有些重复,这是因为原始文档内容太少了,且chunk也不好,但是想要的效果达到了。

:::

反面案例

相似度阈值:0.7设置高一些,避免检索到无关文档

过滤表达式:状态为恋爱

query:我和我的女朋友是异地恋,我该怎么维持这段关系呢?

期望效果:请求中没有召回文档,控制台信息输出类似“未检索到相关文档”信息

响应结果:

控制台信息:

:::color2
这次请求上下文的内容为空,说明的确未召回到相关问文档。

控制台是英文,至于为什么输出这样的信息,在后面的章节中会介绍,但可以知道的是,提示词指示用户query超出知识库范围,不能进行回复,达到我们的期望效果!

:::

查询增强和关联

经过文档检索,系统已经获取了与用户查询相关的检索文档,此时,大模型需要根据用户提示词和检索内容生成最终的回答。但是,返回结果可能仍未达到预期效果,需要做进一步的优化。

错误处理机制

在实际应用开发中,可能出现多种异常情况,如检索不到相关文档、相似度低、查询出超时等。良好的错误处理机制可以提升用户的使用体验。

异常处理包括有:

  • 允许空上下文查询(即处理边界情况)
  • 提供有好的错误提示
  • 引导用户提供必要信息

边界情况可以使用SpringAI的Co‌ntextualQu⁡eryAugment‏er 上下文查询增强器:

RetrievalAugmentationAdvisor.builder()
.queryAugmenter(ContextualQueryAugmenter.builder().allowEmptyContext(false).build()
)

如果选择开启允许空上下文.allowEmptyContext(true),那么就会把用户的原始查询原模原样返回,源码如下:

如果选择不启用允许空上下文,那么用户提示词最终会被改写成如下提示词:

我们也可以自定义错误处理机制,使用工厂模式来创建一个自定义的Cont⁡extualQuer‏yAugmenter

/*** 自定义上下文增强器工厂类*/
public class LoveAppContextualAugmenterFactory {public static ContextualQueryAugmenter createInstance(){// 自定义空上下文提示词PromptTemplate emptyContextPromptTemplate = new PromptTemplate("""你应该输出下面的内容:抱歉,我只能回答恋爱相关的问题,别的没办法帮到您哦,有问题可以联系编程导航客服 水冠""");return ContextualQueryAugmenter.builder().emptyContextPromptTemplate(emptyContextPromptTemplate).allowEmptyContext(false).build();}
}

使用自定义查询增强器:

单元测试:

@Test
void doChaWithRagTest() {String message = "你好,我是tiga";String chatId = UUID.randomUUID().toString();
//        loveApp.doChaWithRag(message, chatId);loveApp.doChaWithRag("我和我的女朋友是异地恋,我该怎么维持这段关系呢?", chatId);
}

测试结果:

如果检索上下文为空,用户提示词会被改写为我们自定义的空上下文提示词

其他建议

除了上述优化策略外,还可以考虑以下方面的改进:

问题类型改进策略
大模型并未理解知识和用户提示词之间的关系,答案生硬拼凑建议 选择合适的大模型,提升语义理解能力
返回的结果没有按照要求,或؜者不够全面建议 优化提示词模板,引导模型生成更‍符合要求的回答
返回结果不够准确,混入了模型自身的通‌用知识建议 开启拒识 功能,限制模型只基于知识⁡库回答
相似提示词,希望控制回答的一致性或多样性‏ 建议 调整大模型参数,如温度值等

如果有必要的话,还可以考虑更高级的优化方向,比如:

  1. 分离检索阶段和生成阶段的知识块
  2. 针对不同阶段使用不同粒度的文档,进一步提升系统性能和回答质量
  3. 针对查询重写、关键词元信息增强等用到 AI 大模型的场景,可以选择相对轻量的大模型,不一定整个项目只引入一种大模型
http://www.dtcms.com/a/276590.html

相关文章:

  • Java项目2——增强版飞机大战游戏
  • Linux:信号
  • Redis持久化机制:RDB和AOF
  • 【面试八股文】2025最新软件测试面试
  • 多模态数据解压-Parquet
  • 【数据结构初阶】--顺序表(三)
  • 咨询导览,AI发展趋势
  • 三维点云Transformer局部感受野构建:理论、方法与挑战
  • 【图像处理基石】如何入门大规模三维重建?
  • 宁德时代2025年社招入职Verify测评语言理解数字推理考点及SHL测评真题整理
  • Augmented Nested Arrays With Enhanced DOF and Reduced Mutual Coupling
  • C++面试问题集锦
  • Linux系统编程——目录 IO
  • C++ 算法题常用函数大全
  • 独立开发第二周:构建、执行、规划
  • 数智管理学(三十二)
  • ATE-市场现状及趋势
  • AI:机器人行业发展现状
  • 用 Jpom 10 分钟搭好一套轻量级 CICD + 运维平台
  • 傅里叶方法求解偏微分方程2
  • 【C/C++】迈出编译第一步——预处理
  • 并查集理论以及实现
  • QILSTE/旗光 H6-108QHR
  • SSM项目上传文件的方式及代码
  • Java使用Langchai4j接入AI大模型的简单使用(二)
  • 线程同步:互斥锁与条件变量实战指南
  • 猿人学js逆向比赛第一届第二十题
  • 关于赛灵思的petalinux zynqmp.dtsi文件的理解
  • 二叉树算法进阶
  • 《Spring 中上下文传递的那些事儿》Part 8:构建统一上下文框架设计与实现(实战篇)