什么是RAG呢?
RAG(Retrieval-Augmented Generation):是检索增强生成,是一种结合信息检索技术和AI内容生成的混合架构,可以解决大模型的知识时效性限制和幻觉问题。
Spring AI框架实现RAG的两种方式:
1、Spring AI+本地知识库
2、Spring AI+云知识库服务
RAG工作流程(核心重要):
主要包含以下4个步骤:
1、文档收集和切割
2、向量转换和存储
3、文档过滤和检索
4、查询增强和关联
文档收集:从各种来源收集原始文档(网页、PDF、数据库)
文档预处理:清洗、标准化文本格式
文档切割:将长文档分割成适当大小的片段(chunks)----基于固定大小、语义边界、递归分割策略,来分割
向量转换:通过Embedding嵌入模型将文本块转换为高维向量表示,可以捕获到文本的语义特征
向量存储:将生成的向量和对应的文本存储到向量数据库,支持高效的相似性搜索
查询处理:将用户的问题也转换为向量表示
过滤机制:基于元数据、关键词、自定义规则进行过滤
相似度搜索:在向量数据库中查找与问题向量最相似的文档块,常用的相似度搜索算法有余弦相似度、欧式距离等
上下文组装:将检索到的多个文档块组装成连贯的上下文,通过Rank模型进行精排然后TopK最相关的文档切片
提示词组装:将检索到的相关文档和用户问题组合成增强提示
上下文融合:大模型基于增强提示生成回答
源引用:在回答中添加信息来源引用
后处理:格式化、摘要、其他处理来优化最终输出
RAG实战:
1、DocumentReader实现Supplier<List<Document>>读取文档,得到文档列表
2、DocumentTransformer实现Function<List<Document>, List<Document>>转换文档,得到处理后的转换文档
3、DocumentWriter实现Consumer<List<Document>>将文档列表保存到存储中
首先引入依赖(官方没提,可以在Maven中央仓库找到)
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-markdown-document-reader</artifactId>
<version>1.0.0-M6</version>
</dependency>在dogaiagent包下新建一个rag包,在包下建一个TravelAppDocumentLoader类,编写以下代码:
@Component
@Slf4j
class TravelAppDocumentLoader {
private final ResourcePatternResolver resourcePatternResolver; TravelAppDocumentLoader(ResourcePatternResolver resourcePatternResolver) { this.resourcePatternResolver = resourcePatternResolver;
}
public List<Document> loadMarkdowns() {
List<Document> allDocuments = new ArrayList<>();
try {
// 这里可以修改为你要加载的多个 Markdown 文件的路径模式
Resource[] resources =
resourcePatternResolver.getResources("classpath:document/*.md");
for (Resource resource : resources) {
String fileName = resource.getFilename();
MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder() .withHorizontalRuleCreateDocument(true)
.withIncludeCodeBlock(false)
.withIncludeBlockquote(false)
.withAdditionalMetadata("filename", fileName)
.build();
MarkdownDocumentReader reader = new MarkdownDocumentReader(resource, config);
allDocuments.addAll(reader.get());
}
} catch (IOException e) {
log.error("Markdown 文档加载失败", e);
}
return allDocuments;
}
}生成单元测试进行Debug测试一下:
@SpringBootTest
class TravelAppDocumentLoaderTest {
@Resource
private TravelAppDocumentLoader travelAppDocumentLoader;
@Test
void loadMarkdowns() {
travelAppDocumentLoader.loadMarkdowns();
}
}注意:如果下面这行代码报错,说明你引入的Document类引错了,正确的应该引的是Spring AI的类
allDocuments.addAll(reader.get());向量转换和存储:将document写入向量数据库,在rag包下新建一个TravelAppVectorStoreConfig类,编写以下代码:
@Configuration
public class TravelAppVectorStoreConfig {
@Resource
private TravelAppDocumentLoader travelAppDocumentLoader;
@Bean
//注意这里的EmbeddingModel导入的是Spring AI的
VectorStore travelAppVectorStore(EmbeddingModel dashscopeEmbeddingModel){
SimpleVectorStore simpleVectorStore =
SimpleVectorStore.builder(dashscopeEmbeddingModel).build();
List<Document> documentList = travelAppDocumentLoader.loadMarkdowns();
simpleVectorStore.add(documentList);
return simpleVectorStore;
}
}根据Spring AI官方文档引入依赖,然后在TravelApp编写以下代码:
//知识库问答功能
@Resource
private VectorStore travelAppVectorStore;
/**
* RAG知识库进行对话
* @param message
* @param chatId
* @return
*/
public String doChatWithRag(String message,String chatId){
ChatResponse chatResponse = chatClient
.prompt()
.user(message)
.advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY,chatId)
.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY,10))
//开启日志顾问,观察效果
.advisors(new MyLoggerAdvisor())
//应用RAG知识库进行问答
.advisors(new QuestionAnswerAdvisor(travelAppVectorStore))
.call()
.chatResponse();
String content = chatResponse.getResult().getOutput().getText();
log.info("content: {}",content);
return content;
}接着生成单元测试进行测试一下,这边的message需要根据前面写的markdown文档特定设计一下,测试是否会根据向量知识库回答,测试代码如下:
@Test
void doChatWithRag() {
String chatId = UUID.randomUUID().toString();
String message ="我的预算不多,只有500块钱怎么规划才能穷游3天";
String answer = travelApp.doChatWithRag(message, chatId);
Assertions.assertNotNull(answer);
}在rag包下新建TravelAppRagCloudAdvisorConfig类,编写以下代码:
/**
* 自定义基于阿里云知识库服务的 RAG 增强顾问
*/
@Configuration
@Slf4j
public class TravelAppRagCloudAdvisorConfig {
@Value("${spring.ai.dashscope.api-key}")
private String dashScopeApiKey;
@Bean
public Advisor loveAppRagCloudAdvisor() {
DashScopeApi dashScopeApi = new DashScopeApi(dashScopeApiKey);
final String KNOWLEDGE_INDEX = "AI旅游规划大师";
DocumentRetriever dashScopeDocumentRetriever = new DashScopeDocumentRetriever(dashScopeApi,
DashScopeDocumentRetrieverOptions.builder()
.withIndexName(KNOWLEDGE_INDEX)
.build());
return RetrievalAugmentationAdvisor.builder()
.documentRetriever(dashScopeDocumentRetriever)
.build();
}
}注意:这边的@Value注解是用的字段级别注解,不是lombok的类级别注解
然后在TravelApp里调用Bean管理的travelAppRagCloudAdvisor,补充后的代码如下:
@Resource
private Advisor travelAppRagCloudAdvisor;
public String doChatWithRag(String message,String chatId){
ChatResponse chatResponse = chatClient
.prompt()
.user(message)
.advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY,chatId)
.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY,10))
//开启日志顾问,观察效果
.advisors(new MyLoggerAdvisor())
//本地RAG和云知识库开启一个就好了
//应用本地RAG知识库进行问答
.advisors(new QuestionAnswerAdvisor(travelAppVectorStore))
//应用RAG检索增强服务(基于阿里云知识库服务进行问答)
.advisors(travelAppRagCloudAdvisor)
.call()
.chatResponse();
String content = chatResponse.getResult().getOutput().getText();
log.info("content: {}",content);
return content;
}最后Debug测试运行一下
(重点)总结RAG整个工作流程:首先建立原始文档,收集处理切割,然后进行向量转换存到向量数据库中,用户提问时候,将用户的问题通过Embedding模型转换成向量,再通过一些相似度搜索或条件搜索从向量数据库中把相关的文档检索出来,然后通过Rank模型等进行一个排序,取出最相关的知识点,最后把相关的知识点和用户的问题结合在一起,变成一个提示词再输入给AI大模型,得到最终的回答。
1、文档收集和切割:整个过程俗称,ETL(抽取、转换、加载)
2、向量转换和存储:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
<version>1.0.0-M7</version>
</dependency>spring:
datasource:
url: jdbc:postgresql://改为你的公网地址/yu_ai_agent
username: 改为你的用户名
password: 改为你的密码
ai:
vectorstore:
pgvector:
index-type: HNSW
dimensions: 1536
distance-type: COSINE_DISTANCE
max-document-batch-size: 10000 # Optional: Maximum number of documents per batch注意:这里的datasource:url、username、password写到application-local.yml中,不然git开源提交会暴露出去,然后呢就是这个dimensions是向量维度值,越高越精确,如果不指定会默认,建表前一定要选择好,因为建完后就不能改了
<!-- 手动整合向量数据库-->
<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>@Configuration
public class PgVectorVectorStoreConfig {
@Resource
private TravelAppDocumentLoader travelAppDocumentLoader;
@Bean
public VectorStore pgVectorVectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel
dashscopeEmbeddingModel){
PgVectorStore vectorStore = PgVectorStore.builder(jdbcTemplate,
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();
return vectorStore;
}
}然后创建对应单元测试,复制粘贴官方给的代码,测试的代码如下:
@SpringBootTest
class PgVectorVectorStoreConfigTest {
@Resource
private VectorStore pgVectorVectorStore;
@Test
void pgVectorVectorStore() {
List<Document> documents = List.of(
new Document("我是小楼,很高兴认识你", Map.of("meta1", "meta1")),
new Document("旅游如何预订景点"),
new Document("有什么美食推荐", Map.of("meta2", "meta2")));
// 添加文件
pgVectorVectorStore.add(documents);
// 相似度查询
List<Document> results =
this.pgVectorVectorStore.similaritySearch(SearchRequest.builder().query("我是谁,阿巴阿巴").topK(3).build());
}
}注意:启动时会自动加载PGVectorStore,会出现需要1个但是找到2个情况的报错,这里需要给启动类如下添加来配合使用:
@SpringBootApplication(exclude = {
// 为了便于大家开发调试和部署,取消数据库自动配置,需要使用 PgVector 时把 DataSourceAutoConfiguration.class 删除
DataSourceAutoConfiguration.class
})
public class DogAiAgentApplication {
public static void main(String[] args) {
SpringApplication.run(DogAiAgentApplication.class, args);}
}然后,如果你要用这个PGVector向量存储来替代原本的本地VectorStore,就按以下操作调用,然后将TravelApp类里之前的基于本地和基于云知识库服务注释掉:
@Resource
private VectorStore pgVectorVectorStore;
//本地RAG和云知识库开启一个就好了
//应用本地RAG知识库进行问答
// .advisors(new QuestionAnswerAdvisor(travelAppVectorStore))
//应用RAG检索增强服务(基于阿里云知识库服务进行问答)
// .advisors(travelAppRagCloudAdvisor)
//应用RAG检索增强服务(基于PGVector向量存储)
.advisors(new QuestionAnswerAdvisor(pgVectorVectorStore))然后在PgVectorVectorStoreConfig类中加上两行加载文档的代码即可:
//载入文档
List<Document> documents =travelAppDocumentLoader.loadMarkdowns();
vectorStore.add(documents);
return vectorStore;(了解)批处理策略:Spring AI实现了批处理策略(Batching Strategy),将大量文档分解为较小的批次,使其适合嵌入模型的最大上下文窗口,还可以提高性能更有效的利用API速率限制。
3、文档过滤和检索:Spring AI官方提供了一个“模块化”的RAG架构,就是把整个文档过滤检索拆为:检索前、检索时、检索后,然后针对每个阶段提供了可自定义的组件。
查询转换-查询重写、查询转换-查询翻译、查询转换-查询压缩
查询扩展-多查询扩展
查询搜索、文档合并
4、查询增强和关联:QuestionAnswerAdvisor和RetrievalAugmentationAdvisor
/**
* 自定义基于token的切词器
*/
@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);
}
}然后在TravelAppVectorStoreConfig类里调用加上这个切词器,最终代码如下:
@Resource
private MyTokenTextSplitter myTokenTextSplitter;
@Bean
VectorStore travelAppVectorStore(EmbeddingModel dashscopeEmbeddingModel){
SimpleVectorStore simpleVectorStore =
SimpleVectorStore.builder(dashscopeEmbeddingModel).build();
//加载文档
List<Document> documentList = travelAppDocumentLoader.loadMarkdowns();
//自主切分文档
List<Document> splitDocuments = myTokenTextSplitter.splitCustomized(documentList);
simpleVectorStore.add(splitDocuments);
return simpleVectorStore;
}当然了,这种切词器不好,切出来的一整段会被分开,不完整。如果使用云服务推荐在创建知识库时使用阿里云百炼的智能切分。
//提取文档状态作为标签
String status = filename.substring(filename.length() - 6, filename.length() - 3);
MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder()
.withHorizontalRuleCreateDocument(true)
.withIncludeCodeBlock(false)
.withIncludeBlockquote(false)
.withAdditionalMetadata("filename", filename)
.withAdditionalMetadata("status", status)@Component
public class MyKeywordEnricher {
@Resource
private ChatModel dashscopeChatModel;
List<Document> enrichDocuments(List<Document> documents){
KeywordMetadataEnricher enricher = new
KeywordMetadataEnricher(this.dashscopeChatModel,5);
return enricher.apply(documents);
}
}然后在TravelAppVectorStoreConfig类里使用它,将前面的token自动切分文档注释掉,最终代码如下:
@Bean
VectorStore travelAppVectorStore(EmbeddingModeldashscopeEmbeddingModel){
SimpleVectorStore simpleVectorStore =
SimpleVectorStore.builder(dashscopeEmbeddingModel).build();
//加载文档
List<Document> documentList = travelAppDocumentLoader.loadMarkdowns();
//自主切分文档//
List<Document> splitDocuments = myTokenTextSplitter.splitCustomized(documentList);
//自动补充关键词元信息
List<Document> enrichedDocuments = myKeywordEnricher.enrichDocuments(documentList);
simpleVectorStore.add(enrichedDocuments);
return simpleVectorStore;
}最后Debug测试一下
@Component
public class MultiQueryExpanderDemo {
private final ChatClient.Builder chatClientBuilder;
public MultiQueryExpanderDemo(ChatModel dashscopeChatModel) {
this.chatClientBuilder = ChatClient.builder(dashscopeChatModel);
}
public List<Query> expand(String query) {
MultiQueryExpander queryExpander = MultiQueryExpander.builder()
.chatClientBuilder(chatClientBuilder)
.numberOfQueries(3)
.build();
List<Query> queries = queryExpander.expand(new Query("谁是小楼?"));
return queries;
}
}然后编写对应的单元测试,进行测试:
@SpringBootTest
class MultiQueryExpanderDemoTest {
@Resource
private MultiQueryExpanderDemo multiQueryExpanderDemo;
@Test
void expand() {
List<Query> queries = multiQueryExpanderDemo.expand("谁是小楼?搞快点回答我谢谢");
Assertions.assertNotNull(queries);
}
}获得多查询后,可以用于检索文档、或者提取查询文本,来改写提示词,不过不建议使用多查询扩展这种优化方式,因为会增加查询次数和成本,不易于量化评估。
/**
* 查询重写器
*/
@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 doQueryRewriter(String prompt){
Query query = new Query(prompt);
//执行查询重写
Query transformedQuery = queryTransformer.transform(query);
//输出重写后的查询
return transformedQuery.text();
}
}然后在TravelApp中使用调用查询重写器,最终代码如下:
@Resource
private QueryRewriter queryRewriter;
public String doChat(String message,String chatId){
//查询重写
String rewrittenMessage = queryRewriter.doQueryRewriter(message);
ChatResponse chatResponse = chatClient
.prompt()
//使用改写后的查询信息
.user(rewrittenMessage)最后Debug测试一下
当然要是在云服务中,创建时可以勾选多轮会话改写,自动将用户的提示词转换为更完整的形式。
3、配置文档过滤规则,根据场景不同可以为文档添加标签或提取元数据
实现配置文档过滤:在rag包下新建一个TravelAppRagCustomAdvisorFactory类,编写以下代码:
/**
* 创建自定义RAG检索增强顾问工厂
*/
public class TravelAppRagCustomAdvisorFactory {
public static Advisor createTravelAppRagCustomAdvisorFactory(VectorStore vectorStore,
String status){
//过滤特定状态的文档
Filter.Expression expression = new FilterExpressionBuilder()
.eq("status", status)
.build();
DocumentRetriever documentRetriever = VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.filterExpression(expression) // 过滤条件
.similarityThreshold(0.5) // 相似度阈值
.topK(3) // 返回文档数量
.build();
return RetrievalAugmentationAdvisor.builder()
.documentRetriever(documentRetriever)
// .queryAugmenter()
.build();
}
}然后在TravelApp类里添加上RAG检索增强顾问工厂,代码如下:.
advisors(
TravelAppRagCustomAdvisorFactory.createTravelAppRagCustomAdvisorFactory(
travelAppVectorStore,"钱"
)最后Debug测试一下。
The user query is outside your knowledge base.
Politely inform the user that you can't answer it.
public class TravelAppContextualQueryAugmenterFactory {
public static ContextualQueryAugmenter createInstance(){
PromptTemplate emptyContextPromptTemplate = new PromptTemplate("""你应该输出下面的内 容: 抱歉,我只能回答旅游规划相关的问题,别的没办法帮到您嘞,请见谅,有问题可以联系人工客服 """);
return ContextualQueryAugmenter.builder()
.allowEmptyContext(false)
.emptyContextPromptTemplate(emptyContextPromptTemplate)
.build();
}
}如果这里找不到相关的问题时,要让它继续按大模型回答就把false改成true。
然后在TravelAppRagCustomAdvisorFactory类的.queryAugmenter()运用它,代码如下:
.queryAugmenter(TravelAppContextualQueryAugmenterFactory.createInstance())最后Debug测试一下看看是否回答的是我们设定的结果。测试的结果:抱歉,我只能回答旅游规划相关的问题,别的没办法帮到您嘞,请见谅,有问题可以联系人工客服。
如果项目不用它可以将下面这段代码注释掉:
// advisors(
// TravelAppRagCustomAdvisorFactory.createTravelAppRagCustomAdvisorFactory(
// travelAppVectorStore,"钱"
// )建议:除了上述的优化策略外,还可以选择合适的大模型、优化提示词模板、开启拒识功能(限制只基于知识库回答)、调整大模型参数,来优化。
1、混合检索策略:并行混合检索、级联混合检索、动态混合检索
2、大模型幻觉:事实性幻觉、逻辑性幻觉、自洽性幻觉
3、RAG应用评估
4、高级RAG架构:自纠错RAG、自省式RAG、检索树RAG、多智能体RAG系统
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。