RAG概念

什么是RAG?

RAG(Retrieval-Augmented Generation,检索增强生成)是一种结合信息检索技术和AI内容生成的混合架构,可以解决大模型的知识时效性和幻觉问题。

简单来说,RAG就像给AI配了一个“小抄本”,让AI回答问题前先查一查特定的知识库来获取知识,确保回答是基于真实资料而不是凭空想象。

image-20251117155115386

从技术角度看,RAG在大语言模型生成回答之前,会先从外部知识库中检索相关信息,然后将这些检索到的内容作为额外上下文提供给模型,引导其生成更准确、更相关的回答。

RAG工作流程

RAG技术实现主要包含以下4个核心步骤:

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

文档收集和切割

文档收集:从各种来源(网页、PDF、数据库等)收集原始文档

文档预处理:清洗、标准化文本格式

文档切割:将长文档分割成适当大小的片段(俗称chunks)

  • 基于固定大小(如512个token)
  • 基于语义边界(如段落、章节)
  • 基于递归分割策略(如递归字符n-gram切割)

image-20251117155652245

向量转换和存储

向量转换:使用Embedding模型将文本块转换为高维向量表示,可以捕获到文本的语义特征

向量存储:将生成的向量和对应文本存入向量数据库,支持高效的相似性搜索

image-20251117155834640

文档过滤和检索

查询处理:将用户问题也转换为向量表示

过滤机制:基于元数据、关键词或自定义规则进行过滤

相似度搜索:在向量数据库中查找与问题向量最相似的文档块,常用的相似度搜索算法有余弦相似度、欧式距离等

上下文组装:将检索到的多个文档块组装成连贯上下文

image-20251117160115964

查询增强和关联

提示词组装:将检索到的相关文档与用户问题组合成增强提示

上下文融合:大模型基于增强提示生成回答

源引用:在回答中添加信息来源引用

后处理:格式化、摘要或其他处理以优化最终输出

image-20251117160751068

完整工作流程

image-20251117160814457

RAG相关技术

Embedding和Embedding模型

Embedding嵌入是将高维离散数据(如文字、图片)转换为低维连续向量的过程。这些向量能在数学空间中表示原始数据的语义特征,使计算机能够理解数据间的相似性。

Embedding 模型是执行这种转换算法的机器学习模型,如 Word2Vec(文本),ResNet(图像)等。不同的 Embedding 模型产生的 向量表示和维度数不同,一般维度越高表达能力更强,可以捕获更丰富的语义信息和更细微的差别,但同样占用更多存储空间。

举个例子,“鱼皮”和“鱼肉”的Embedding 向量在空间中较接近,而“鱼皮”和“帅哥”则相距较远,反映了语义关系。

image-20251117161017148

向量数据库

向量数据库是专门存储和检索向量数据的数据库系统。通过高效索引算法实现快速相似性搜索,支持K近邻查询等操作。

image-20251117161146032

注意,并不是只有向量数据库才能存储向量数据,只不过与传统数据库不同,向量数据库优化了高维向量的存储和检索。

image-20251117161303713

召回

召回是信息检索中的第一阶段,目标是从大规模数据集中快速筛选出可能相关的候选项子集。强调速度和广度,而非精确度。

精排和Rank模型

精排(精确排序)是搜索/推荐系统的最后阶段,使用计算复杂度更高的算法,考虑更多特征和业务规则,对少量候选项进行更复杂、精细的排序。

比如,短视频推荐先通过召回获取数万个可能相关视频,再通过粗排缩减至数百条,最后精排阶段会考虑用户最近的互动、视频热度、内容多样性等复杂因素,确定最终展示的10个视频及顺序。

image-20251117161540842

Rank模型(排序模型)负责对召回阶段筛选出的候选集进行精确排序,考虑多种特征评估相关性。

现代 Rank 模型通常基于深度学习,如BERT,LambdaMART等,综合考虑查询与候选项的相关性、用户历史行为等因素。举个例子,电商推荐系统会根据商品特征、用户偏好、点击率等给每个候选商品打分并排序。

image-20251117161624329

混合检索策略

混合检索策略结合多种检索方法,提高搜搜效果。常见组合包括关键词检索、语义检索、知识图谱等。

比如在AI大模型开发平台Dify中,就为用户提供了“基于全文检索的关键词搜索+基于向量检索的语义检索”的混合检索策略,用户还可以自己设置不同检索方式的权重。

image-20251117161819828

RAG实战:Spring AI + 本地知识库

标准的RAG开发步骤:

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

简化后的RAG开发步骤:

  • 文档准备
  • 文档读取
  • 向量转换和存储
  • 查询增强

文档准备

首先准备用于给AI知识库提供知识的文档,推荐Markdown格式,尽量结构化。

我们可以利用AI来生成文档,提供一段示例Prompt:

帮我生成 3 篇 Markdown 文章,主题是【恋爱常见问题和回答】,3 篇文章的问题分别针对单身、恋爱、已婚的状态,内容形式为 1 问 1 答,每个问题标题使用 4 级标题,每篇内容需要有至少 5 个问题,要求每个问题中推荐一个相关的课程,课程链接都是 https://www.codefather.cn

image-20251117175909162

文档读取

首先,我们要对自己准备好的知识库文档进行处理,然后保存到向量数据库中。这个过程俗称ETL(抽取、转换、加载),Spring AI 提供了对ETL的支持。

ETL的3大核心组件,按照顺序执行:

  • DocumentReader:读取文档,得到文档列表
  • DocumentTransformer:转换文档,得到处理后的文档列表
  • DocumentWriter:将文档列表保存到存储中(可以是向量数据库,也可以是其他存储)

image-20251117180258481

  • 引入依赖

SpringAI提供了很多种DocumentReaders,用于加载不同类型的文件。

image-20251117180418401

我们可以使用MarkdownDocumentReader来读取Markdown文档。需要 先引入依赖:

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-markdown-document-reader</artifactId>
<version>1.0.0-M6</version>
</dependency>
  • 新建rag包,编写文档加载器类LoveAppDocumentLoader,负责读取所有Markdown文档并转换为Document列表。

    @Component
    @Slf4j
    public class LoveAppDocumentLoader {

    private final ResourcePatternResolver resourcePatternResolver;

    public LoveAppDocumentLoader(ResourcePatternResolver resourcePatternResolver) {
    this.resourcePatternResolver = resourcePatternResolver;
    }

    public List<Document> loadMarkdowns(){
    List<Document> allDocuments = new ArrayList<>();
    try {
    Resource[] resources = resourcePatternResolver.getResources("classpath:document/*.md");
    for (Resource resource : resources) {
    String filename = resource.getFilename();
    MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder()
    .withHorizontalRuleCreateDocument(true) //设为 true 时,Markdown 中的水平分隔符将生成新 Document 对象
    .withIncludeCodeBlock(false) //设为 true 时,代码块将与周边文本合并到同一 Document;设为 false 时,代码块生成独立 Document 对象
    .withIncludeBlockquote(false)//设为 true 时,引用块将与周边文本合并到同一 Document;设为 false 时,引用块生成独立 Document 对象
    .withAdditionalMetadata("filename", filename) //允许为所有生成的 Document 对象添加自定义元数据
    .build();
    MarkdownDocumentReader reader = new MarkdownDocumentReader(resource, config);
    List<Document> documents = reader.get(); //文档的切片集合
    allDocuments.addAll(documents);
    }
    } catch (IOException e) {
    log.error(e.getMessage());
    }
    return allDocuments;
    }
    }

    上述代码中,我们通过 MarkdownDocumentReaderConfig 文档加载配置来指定读取文档的细节,比如是否读取代码块、引用块等。特别需要注意的是,我们还指定了额外的元信息配置,提取文档的文件名(fileName)作为文档的元信息,可以便于后续知识库实现更精确的检索。

    image-20251117180828786

向量转换和存储

为了实现方便,我们先使用Spring AI内置的、基于内存读写的向量数据库SimpleVectorStore来保存文档。

SimpleVectorStore实现了Vector接口,而VectorStore接口集成了DocumentWriter,所以具备森当写入能力。如图:

image-20251117181104496

简单了解下源码,在将文档写入到数据库前,会先调用Embedding大模型将文档转换为向量,实际保存到数据库中的是向量类型的数据。

image-20251117181159579

在rag包下新建LoveAppVectorStoreConfig类,实现初始化向量数据库并且保存文档的方法。

@Configuration
public class LoveAppVectorStoreConfig {

@Resource
private LoveAppDocumentLoader loveAppDocumentLoader;

@Bean
VectorStore loveAppVectorStore(EmbeddingModel dashscopeEmbeddingModel) {
SimpleVectorStore simpleVectorStore = SimpleVectorStore.builder(dashscopeEmbeddingModel)
.build();
// 加载文档
List<Document> documents = loveAppDocumentLoader.loadMarkdowns();
simpleVectorStore.add(documents);
return simpleVectorStore;
}
}

查询增强

Spring Al 通过 Advisor 特性提供了开箱即用的 RAG 功能。主要是QuestionAnswerAdvisor 问答拦截器和 RetrievalAugmentationAdvisor 检索增强拦截器,前者更简单易用、后者更灵活强大。

查询增强的原理其实很简单。向量数据库存储着AI模型本身不知道的数据,当用户问题发送给AI模型时,QuestionAnswerAdvisor会查询向量数据库,获取与用户问题相关的文档。然后从向量数据库返回的响应会被附加到用户文本中,为AI模型提供上下文,帮助其生成回答。

查看 QuestionAnswerAdvisor 源码,可以看到让AI基于知识库进行问答的Prompt:

image-20251117181542964

此处我们就选用更简单易用的QuestionAnswerAdvisor 问答拦截器,在LoveApp 中新增和RAG知识库进行对话的方法。代码如下:

@Resource
private VectorStore loveAppVectorStore;

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())
// 应用知识库问答
.advisors(new QuestionAnswerAdvisor(loveAppVectorStore))
.call()
.chatResponse();
String content = chatResponse.getResult().getOutput().getText();
log.info("content: {}", content);
return content;
}

测试

@Test
void doChatWithRag() {
String chatId = UUID.randomUUID().toString();
String message = "我已经结婚了,但是婚后关系不太亲密,怎么办?";
String answer = loveApp.doChatWithRag(message, chatId);
Assertions.assertNotNull(answer);
}

运行程序,通过调试发现,加载的文档被自动按照小标题拆分,并且补充了metadata元信息:

image-20251117181756299

查看请求,发现根据用户的问题检索到了4个文档切片,每个切片有对应的分数和元信息:

image-20251117181845163

image-20251117181908785

查看请求,发现用户的提示词被修改了,让AI检索知识库:
image-20251117181936521

查看响应结果,AI的回复成功包含了知识库里的内容:

image-20251117182020379

RAG实战:Spring AI + 云知识库服务

很多AI大模型应用开发平台都提供了云知识库服务,这里我们还是选择阿里云百炼,因为Spring Al Alibaba 可以和它轻松集成,简化RAG开发。

  • 准备数据。在应用数据模块中,上传原始文档数据到平台,由平台来帮忙解析文档中的内容和结构:

    image-20251117182208621

  • 进入阿里云百炼平台的知识库,创建一个知识库,选择默认配置即可:

    image-20251117182406480

  • 导入数据到知识库

image-20251117182501640

  • 导入数据时,可以设置数据预处理规则,智能切分文档为文档切片

    image-20251117182606271

  • 创建好知识库后,进入知识库查看文档和切片

    image-20251117182658352

如果你觉得智能切分得到的切片不合理,可以手动编辑切片内容:

image-20251117182739882

RAG开发

有了知识库后,我们就可以用程序来对接了。开发过程很简单,可以参考 Spring Al Alibaba的官方文档来学习。

Spring Al Alibaba 利用了Spring Al提供的文档检索特性(DocumentRetriever),自定义了一套文档检索的方法,使得程序会调用阿 里灵积大模型API来从云知识库中检索文档,而不是从内存中检索。

使用下列代码就可以创建一个文档检索器并发起查询:

// 调用大模型的 API
var dashScopeApi = new DashScopeApi("DASHSCOPE_API_KEY");
// 创建文档检索器
DocumentRetriever retriever = new DashScopeDocumentRetriever(dashScopeApi,
DashScopeDocumentRetrieverOptions.builder()
.withIndexName("你的知识库名称")
.build());
// 测试从云知识库中查询
List<Document> documentList = retriever.retrieve(new Query("谁是鱼皮"));

如何使用这个文档检索器,让AI从云知识库查询文档呢?

这就需要使用Spring Al提供的另一个 RAG Advisor-RetrievalAugmentationAdvisor检索增强顾问,可以绑定文档检索器、查询转 换器和查询增强器,更灵活地构造查询。

示例代码如下,先仅作了解即可,后面章节中会带大家实战检索增强顾问的更多特性:

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

String answer = chatClient.prompt()
.advisors(retrievalAugmentationAdvisor)
.user(question)
.call()
.content();
  • 回归到我们的项目中,先编写一个配置类,用于初始化基于云知识库的检索增强顾问Bean:

    @Configuration
    @Slf4j
    class LoveAppRagCloudAdvisorConfig {

    @Value("${spring.ai.dashscope.api-key}")
    private String dashScopeApiKey;

    @Bean
    public Advisor loveAppRagCloudAdvisor() {
    DashScopeApi dashScopeApi = new DashScopeApi(dashScopeApiKey);
    final String KNOWLEDGE_INDEX = "恋爱大师";
    DocumentRetriever documentRetriever = new DashScopeDocumentRetriever(dashScopeApi,
    DashScopeDocumentRetrieverOptions.builder()
    .withIndexName(KNOWLEDGE_INDEX)
    .build());
    return RetrievalAugmentationAdvisor.builder()
    .documentRetriever(documentRetriever)
    .build();
    }
    }

    注意上述代码中指定知识库要使用名称(而不是id)。

  • 然后在LoveApp中使用Advisor:

    @Resource
    private Advisor loveAppRagCloudAdvisor;

    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())
    // 应用增强检索服务(云知识库服务)
    .advisors(loveAppRagCloudAdvisor)
    .call()
    .chatResponse();
    String content = chatResponse.getResult().getOutput().getText();
    log.info("content: {}", content);
    return content;
    }
  • 测试一下。通过调试查看请求,能发现检索到了多个文档切片,每个切片有对应的元信息:

    image-20251117183303588

查看请求,发现用户提示词被改写,查询到的关联文档已经作为上下文拼接到了用户提示词中:

image-20251117183409880

查看响应结果,成功包含了知识库里的内容:

image-20251117183432259

扩展

我们可以利用RAG知识库,实现“通过用户的问题推荐可能的恋爱对象”功能。

我们可以新建一个恋爱对象文档,每行数据包含一位用户的基本信息(年龄、星座、职业等等)。示例文档如下:

姓名年龄星座爱好职业
陆星辰28射手座旅行、玩滑板、看纪录片、学外语旅游博主
沈墨言32摩羯座阅读、攀岩、品威士忌、逛博物馆架构师
顾清欢26双鱼座画画、照料植物、做手账、听民谣花艺师
赵知行35处女座马拉松、研究厨艺、参观科技展大学讲师
苏小暖29狮子座看话剧、组织朋友聚会、潜水、跳舞品牌策划经理

将创建好的文档上传到平台依次操作即可。

接下来我们进行测试即可:

@Test
void doChatWithRag() {
String chatId = UUID.randomUUID().toString();
// 第一轮
String message = "你好,我叫小王,我喜欢旅游、读书,请给我推荐一些恋爱人选";
String answer = loveApp.doChatWithRag(message, chatId);
Assertions.assertNotNull(answer);
}

结果如下:

image-20251117183852667

发现结果和我们预期的一样,成功!