从原理到实战:基于SpringAI的RAG应用探索

一、前言

上一篇文章(从原理到落地:MCP在Spring AI中的工程实践)介绍了 MCP 在 LLM 中的作用,其中提到 MCP 让 LLM "看起来"具备了调用外部程序的能力,进而能够完成一些自动化工作,如自动获取上下文、操作文件系统等。而本篇文章主要介绍 RAG 在 LLM 中的作用,与 MCP 相同的是,RAG 也能够让 LLM "看起来"可以自动获取外部信息,进而增强其上下文;不同的是,MCP 更偏向于工具调用,由于可以调用各种不同的工具,因此其用途会更加广泛。而 RAG 更偏向于知识检索,可以从数据库中检索出与问题相关联的知识,来增强 LLM 的上下文信息,相当于一个增强知识的工具。

本篇文章将基于 RAG 的背景、原理,以及其在 Spring AI 框架下的实践展开介绍。

二、概述

2.1 背景

目前 LLM 生成的内容都是基于其训练时已知的信息,其无法访问外部的信息,因此无法回答训练数据以外的内容。例如,我们提出了一个问题,而这个问题是关于某个内部文档的,那么 LLM 就很有可能一本正经的胡说八道,这种情况称为大模型的幻觉。针对幻觉问题,我们可以把文档的内容和问题一起发送给 LLM,这样 LLM 就具有充足的上下文来回答问题。但是,当文档十分庞大时,与问题有关联的上下文可能只是文档中的某一小段话,这时候 LLM 就很可能无法准确找到重点,于是又胡乱地回答问题。那此时我们可能又想到一种解决方案,就是不把整个文档都发送给 LLM,而只发送与问题相关联的几段话给它,这个动作由我们人工来完成会显得很低效,因此需要一个具备"检索"能力的工具来帮我们找出与问题相关度最高的上下文,并将其交给 LLM,实际上 RAG 解决的正是这个问题。

2.2 RAG

RAG(Retrieval Augmented Generation),即检索增强生成,其核心思想是在 LLM 回答之前,先通过检索系统从外部知识库找出与问题相关的内容,然后将这些内容与原始问题一起输入到 LLM 中。

结合了 RAG 的 LLM 在回答内容时的大致流程如下所示:

(1)用户提出问题。

(2)检索系统搜索知识库中与问题相关联的内容。

(3)知识库返回相关知识给检索系统。

(4)检索系统将问题、相关知识都发送给 LLM。

(5)LLM 生成回答内容并展示给用户。

2.3 Embedding

基于上面的流程,我们不难发现这里会存在几个关键的问题:

(1)如何高效地检索出与问题相关联的知识内容?

(2)知识库采用何种方式存储知识,是采用关系型数据库直接存储,还是采取其他方式?

针对第一个问题,RAG 引入一种新的模型,称为 Embedding 模型,其输入是一段文字,而输出是一个固定长度的浮点型数组,例如 OpenAI 的 text-embedding-3-small 模型,其输出的数组长度为 1536,而 text-embedding-3-large 模型输出的数组长度为 3072。内容越相似,其经过 Embedding 模型生成的数组则也会越相似,因此我们可以通过数组之间的距离来判断两段文字的相似程度。

这就类似于我们以往在学校中学习过的坐标系,输出的数组也可以映射在一个很大维数的坐标系中的某个点,例如 1536 维坐标系中的某个点,而我们可以通过两个点的距离来判断其相似程度(相关计算方式在原理部分会介绍),这里以三维坐标系举例,如下图所示,可以看到越接近的文字,其映射的点也会越接近。

针对第二个问题,传统的关系型数据库存储的是结构化数据,适合于检索精确匹配的数据,而 RAG 中需要进行语义相似度检索,非精确匹配,所以不适用于传统的关系型数据库,因此,RAG 引入向量数据库作为知识库。

向量数据库是一种专门用于存储和检索高维向量数据的数据库,主要用于处理相似性搜索的任务,其可以存储非结构化数据(如文本、音频、视频等)经过 Embedding 模型后生成的向量,并可以通过一个给定的向量来迅速找到最相似的若干个向量。向量数据库在存储向量时,不仅会存储向量本身,还会存储其原始文本和元信息(如时间、语言等),来方便通过向量找到其原始文本。目前市面上常用的向量数据库有 Pinecone、Chroma、PostgreSQL + PGVector 等。

三、原理

3.1 工作流程

RAG 的工作流程可以分为离线在线两个部分:

  • 离线部分:指的是知识准备的过程。我们可以提前上传文档资料,这些文档会经过 Embedding 模型转换为高维向量,然后存储进向量数据库。
  • 在线部分:指的是实时问答的过程。用户提出问题后,问题文本会经过 Embedding 模型转换为高维向量,然后依据这个向量在知识库中找寻最相似的若干个知识片段,之后将知识和问题一起传入 LLM,最后由 LLM 生成答案。

RAG 的完整流程如下图所示。后续也会介绍关键部分的技术原理细节。

3.2 Chunking

Chunking,即分块,指的是将文档分割成若干个片段,文档分割的质量将直接决定了后续检索的准确性和 LLM 回答的效果。在 RAG 中做 Chunking 操作的原因正如前面提到过的,有时与问题相关联的知识片段可能只是文档中的一小部分,如果将所有的文档都交给 LLM,它可能无法马上理解到重点,因此需要先将文档切分成若干个片段,再将每个片段转换成各自的向量。

常见的 Chunking 策略有:

名字 描述 优点 缺点
固定大小拆分 按指定字数或 token 数来切分 实现简单、速度块 可能会割裂语句,打断语义完整性
结构拆分 基于文档格式(如 Html、Markdown)拆分,本质上是借助这些文档特有的格式来拆分语句 可以保留原始文档的结构逻辑,语义完整度高 依赖于结构清晰、规范的文档,对于无结构的语句(如纯文本)无法使用
语义拆分 根据语义边界(如段落、句子、主题变化等)拆分,可以采用 NLP 方法,如如分句、主题检测或 Embedding 聚类 最符合人类理解,可保留语义一致性 实现复杂,需要依赖 NLP 模型、Embedding 计算或聚类等,且计算开销大,效率低
递归拆分 先按大分隔符(如段落)拆分,再按句子等进行拆分,直到拆分得到的块满足长度限制 在语义完整性和长度控制两个度量之间保持平衡 设计较复杂,需要合适的层级和递归停止条件

这些策略可以组合使用,即一类文档可以使用多种策略,但目前没有一种策略适用于所有的文档,因此需要根据情况来选择合适的策略。

3.3 Indexing

在 RAG 中,需要对向量构建索引以便能够高效地计算向量之间的相似程度。向量索引是一种用于高效索引和检索高维向量的数据结构,能帮助我们高效筛选与查询向量最接近的少量数据。

目前常用的构建向量索引的方法是 ANN(Approximate Nearest Neighbor,近似最近邻搜索算法),其能够在牺牲少量准确性的同时,显著提高搜索速度和计算性能,常见的几种实现方式如下:

(1)LSH

LSH(Locality Sensitive Hashing,局部敏感哈希算法),是一种基于哈希结构的算法,其通过设计一种哈希函数族,使得相似的向量被映射到相同哈希桶的概率高,而不相似的概率低。在查询时,只会搜索同一个桶或若干相似的痛中的数据,进而能够避免全表扫描。

(2)Annoy

Annoy 算法是一种基于树结构的算法,其核心思想是构建一棵"随机投影二叉树",每一棵树就是向量空间划分后的小区域。

算法核心流程如下:

① 在所有向量中随机挑选两个向量,用它们的方向生成超平面,进而来切割向量空间。

② 将所有的向量都投影到这个方向上,得到每个向量在这个方向上的数值。

③ 根据投影值的中位数,把向量空间分成左右两部分(类似于左侧数值小,右侧数值大)

④ 对左右两个空间继续重复上面的过程,直到每个空间的向量数量小于指定阈值,就不再进行划分。

⑤ 每个节点的父节点就是切割前的空间,子节点就是当前空间切割后的左右子空间。

在搜索时,会在每棵树中递归查找与查询向量最接近的叶结点,然后将这些叶结点表示的向量作为候选向量,并计算所有的候选向量与查询向量的实际距离,最后选择距离最近的 k 个向量作为近似最近邻。

(3)HNSW

HNSW(Hierarchical Navigable Small World,分层导航小世界算法),是一种基于图结构的算法,该算法会构建一个分层图的结构,每一层都是一个由相互连接的节点组成的可导航小世界网络,图的高层用于快速定位,跳跃大量的无关节点,低层则用于精细搜索近似节点,有点类似于跳表。

(4)IVF

IVF(Inverted File Index,倒排文件索引),是一种基于聚类的算法,通过 k-means 聚类把向量分为多个簇,然后对每个簇建立一个倒排表,存放簇内的所有向量。查询时,会首先找到最近的几个簇,之后就只在这些簇中找到最近邻的向量,进而避免了全局搜索。

从以上四种算法可以看出,ANN 本质上并不保证找出真正最相近的向量,而是找到一个足够接近的向量,以换取更快的搜索速度。原因在于 ANN 中并不是全局搜索,而是在部分候选区域中查找,这样就有概率漏掉实际上最近的向量。而每种算法都有其独特的优势和局限性,目前还不存在一种适用于所有场景的算法,需要权衡性能、准确性和计算资源三个方面。

在 RAG 中,需要通过计算向量之间的距离来判断两个向量的相似程度,常见的计算方式有以下几种:

(1)欧几里得距离

用于计算两个向量之间的直线距离,其公式如下:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> d ( A , B ) = ∑ i = 1 n ( A i − B I ) 2 d(A,B)=\sqrt{\sum_{i=1}^{n}(A_i-B_I)^2} </math>d(A,B)=i=1∑n(Ai−BI)2

其优点是简单直观,适合表示距离感的任务,如定位或聚类,但缺点是对长度比较敏感,不适合语义相似的向量,因为有时两个语义相似的向量在长度上会有所差别。

(2)点积

用于直接计算两个向量的乘积之和,其公式如下:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> D o t ( A , B ) = ∣ A ∣ ⋅ ∣ B ∣ ⋅ c o s ( θ ) Dot(A,B)=|A|·|B|·cos(\theta) </math>Dot(A,B)=∣A∣⋅∣B∣⋅cos(θ)

其优点是简单高效,适合于含有权重意义的 Embedding 模型,如推荐场景,原因是它的计算公式的组成如下:

  • <math xmlns="http://www.w3.org/1998/Math/MathML"> ∣ A ∣ |A| </math>∣A∣:向量 A 的长度。
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> ∣ B ∣ |B| </math>∣B∣:向量 B 的长度。
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> c o s ( θ ) cos(\theta) </math>cos(θ):两个向量的语义方向相似性。

因此,点积不仅可以衡量语义相似性(方向),也可以根据向量的长度进行加权。

(3)余弦相似度

用于计算两个向量之间夹角的余弦值,其公式如下:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> c o s ( A , B ) = A ⋅ B ∣ A ∣ ∣ B ∣ cos(A,B)=\frac{A·B}{|A||B|} </math>cos(A,B)=∣A∣∣B∣A⋅B

其优点是度量与语义方向一致,即两段语义相近的文本,即使它们的长度不同,也会有较高的余弦相似度,因此这种方式在自然语言处理领域很常用。余弦相似度的取值范围为 [-1, 1],越接近 1 表示两个向量越相似。

3.5 Re-ranking

前面介绍了向量索引构建的几种常用算法,而这些算法虽然计算速度快,但牺牲了一些准确性,即最终检索出来的文档虽然相似度高,但实际并不是真正相关,因为"相似度"不等于"相关性",相似度只是衡量语义是否相似,而不一定对问题有帮助。这里举个例子:

查询:"介绍一下李清照的文学风格"

此时向量相似度高的文档可能有:

(1)文档 A:"李清照是宋代著名女词人,擅长婉约词,情感细腻......"(真正相关)

(2)文档 B:"杜甫是唐代伟大诗人,以沉郁顿挫著称,写了许多反映民生疾苦的诗......"(不相关)

向量模型可能会觉得李清照和杜甫都是古代诗人,属于语义相似的场景,因此文档 B 的相似度也会很高,但这与问题并不相关。

因此这里需要做 Re-ranking 的操作,即重排序,假如我们最终要交给 LLM 的文档数量为 5,那么一般在初步检索阶段,也就是从向量数据库查询相似性靠前的文档时,会检索出数量比 5 大的候选文档集合(如 top-20),然后再进行重排序,此时会使用其他模型对候选文档与查询条件更精细的匹配,选出最相关的 5 个文档。其中重排序模型在文档相关性排序上会更加准确,但计算代价会更高,因此通常只在 top-20 或 top-50 上运行。

3.6 Prompt Template

在 RAG 中,Prompt Template 会将检索到的文档(context)和用户的问题(User Query)组合成一个格式化的 prompt,最终交给 LLM。其最大的好处在于能够让 LLM 更聚焦于上下文,减少幻觉问题,让回答更加稳定和专业。

我们可以根据任务的类型来自定义模板,常见的模板如下:

vbnet 复制代码
You are a helpful assistant. Based on the following context, answer the question.

Context:
{retrieved_documents}

Question:
{user_query}

Answer:

3.7 总结

在介绍完 RAG 工作流程中关键部分的技术细节后,再回看一下流程图,整个流程的详细描述如下。

离线部分

(1)对上传的文档进行分块(Chunking),将文档分割成若干个片段。

(2)使用 Embedding 模型将切分后的片段转换为向量。

(3)将向量存储到向量数据库中,并建立索引(Indexing)。

在线部分

(1)使用 Embedding 模型将用户问题转换为向量。

(2)在向量数据库中检索出与查询向量最相似的 top-k 个知识片段(Similarity Search)。

(3)对检索出的片段进行重排序,保留最相关的 top-n 个片段(Re-ranking)。

(4)将知识片段与问题组合成一个格式化的 prompt(Prompt Template)。

(5)将 prompt 提交给 LLM,最终得到生成的答案。

四、实践

这里主要讲述使用 Spring AI 框架完成基于 RAG 的应用开发的方式。

4.1 环境说明

环境名 说明
JDK 17
SpringBoot 3.5.0
Spring AI 1.0.0
构建工具 Maven
LLM Qwen2.5-72B-Instruct
Embedding text-embedding-ada-002
向量数据库 PostgreSQL + PGVector

4.2 Embedding

本篇文章使用 OpenAI 的 Embedding 模型完成向量化操作。

pom 依赖:

xml 复制代码
<!-- OpenAI -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

配置文件:

yaml 复制代码
spring:
  ai:
    openai:
      base-url: [这里填url]
      api-key: [这里填密钥]
      embedding:
        options:
          model: text-embedding-ada-002 # Embedding 模型

之后就可以注入 EmbeddingModel 的 Bean,并调用相应的 API 完成向量化,代码示例如下:

typescript 复制代码
@Autowired
private EmbeddingModel embeddingModel;

@GetMapping("/embedding")
public void embedding(String input) {
    System.out.println("input = " + input);
    float[] embeddings1 = embeddingModel.embed(input);
    System.out.println("length = " + embeddings1.length + ", array = " + Arrays.toString(embeddings1));
}

测试结果如下,可以看到文本被转换为了一个 1536 维的向量。

4.3 向量数据库

本篇文章使用 PostgreSQL 配合 PGVector 插件作为向量数据库,插件安装方式可以参考 PGVector-github

pom 依赖:

xml 复制代码
<!-- 向量数据库(pgvector) -->
<dependency>
		<groupId>org.springframework.ai</groupId>
		<artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
</dependency>

配置文件:

yaml 复制代码
spring:
  ai:
    vectorstore:
      pgvector:
        initialize-schema: true # 是否自动初始化 schema
        index-type: HNSW # 最近邻搜索索引类型
        distance-type: COSINE_DISTANCE # 计算向量距离方式
        dimensions: 1536 # 向量维度
        max-document-batch-size: 10000 # 单次批量处理的最大文档数
  datasource:
    url: jdbc:postgresql://localhost/postgres
    username: [这里填用户名]
    password: [这里填密码]

initialize-schema 为 true 时,Spring AI 会自动初始化向量数据库,上面的配置相当于如下的 sql 脚本:

scss 复制代码
CREATE TABLE IF NOT EXISTS vector_store (
	id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
	content text,
	metadata json,
	embedding vector(1536) // 1536 is the default embedding dimension
);

CREATE INDEX ON vector_store USING HNSW (embedding vector_cosine_ops);

然后就可以注入 VectorStore 的 Bean,并调用相应的 API 完成存储向量和寻找最近相似度向量的操作,代码示例如下:

less 复制代码
@Autowired
private VectorStore vectorStore;

@GetMapping("/storeVector")
public void storeVector(@RequestParam List<String> input) {
    List<Document> documents = input.stream().map(Document::new).collect(Collectors.toList());
    vectorStore.add(documents);
}

@GetMapping("/similaritySearch")
public void similarSearch(String input) {
    // topK 可以指定搜索出的向量数
    SearchRequest query = SearchRequest.builder().query(input).topK(2).build();
    List<Document> similarDocuments = vectorStore.similaritySearch(query);
    String result = similarDocuments.stream()
            .map(Document::getText)
            .collect(Collectors.joining(System.lineSeparator()));
    System.out.println("result:\n" + result);
}

注意,在调用 VectorStore 的 add 方法时时,无需自己调用上面提到的 Embedding API,因为 add 方法底层会去调用 Embedding API 将文本转换为向量,因此无需我们手动转换,但是这里一定要提前在 pom 依赖和配置文件中对 Embedding 模型进行配置,否则启动时会报错缺少 EmbeddingModel 的 Bean,如下所示:

这里先调用 /storeVector 存储一些向量,结果如下:

然后再调用 /similaritySearch 搜索最相似的文本,问题是:"小璐的职业是什么",结果如下,可以看到这里成功搜索出了 2 条最相似的文本。

4.4 ETL

ETL ,即 Extract(提取)、Transform(转换)、Load(加载),用于将数据从不同的来源中提取出来,并经过清洗、格式转换等处理后,加载到目标数据库中。在 RAG 中,ETL 的作用就是对数据进行预处理,是从原始数据源到结构化向量存储的流程。

在 Spring AI 中,也提供了 ETL 相关的 API,主要包含三个组件:

  • DocumentReader:完成 Extract 操作,实现了 Supplier<List> DocumentReader,常用的实现类有 TextReader(处理纯文本文件)、JsoupDocumentReader(处理 HTML 文件)、MarkdownDocumentReader(处理 MarkDown 文件)、PagePdfDocumentReader(处理 PDF 文件) 等。
  • DocumentTransformer:完成 Transform 操作,实现了 Function<List, List>。常用的实现类有 TokenTextSplitter、ContentFormatTransformer 等。
  • DocumentWriter:完成 Load 操作,实现了 Consumer<List>。常用的实现类有 FileDocumentWriter、各种 VectorStore 类(如本篇文章使用的 PgVectorStore)。

三个组件共同完成 ETL 的流程如下图所示。

这里演示 TextReader、TokenTextSplitter、PgVectorStore 的组合,代码如下所示。

java 复制代码
@Autowired
private VectorStore vectorStore;

@Value("classpath:/file.txt")
private Resource resource;

@GetMapping("/etl")
public void etl() {
    // extract
    TextReader textReader = new TextReader(this.resource);
    List<Document> extractedDoc = textReader.read();
    System.out.println("extract result: " + extractedDoc);
    // transform
    TokenTextSplitter splitter = new TokenTextSplitter(200, 200, 5, 10000, true);
    List<Document> transformedDoc = splitter.apply(extractedDoc);
    System.out.println("transform length = " + transformedDoc.size() + ", result: " + transformedDoc);
    // load
    vectorStore.add(transformedDoc);
}

这里解释一下 TokenTextSplitter 的几个细节,TokenTextSplitter 使用 CL100K_BASE 编码根据标记计数将文本拆分成块,即按 token 对文档进行切分,tokenizer 编码的标准是 CL100K_BASE,对应于前面 3.2 节讲述的 Chunking。其构造函数的几个参数如下:

  • chunkSize:每个文本块的目标 token 数量,用于控制 chunk 的最大长度,默认 800。
  • minChunkSizeChars:每个文本块中,最少必须包含的字符数(不是 token),用于防止生成非常短、碎片化的文本块,默认 350。
  • minChunkLengthToEmbed:对每个 chunk,只有在长度超过这个值时,才会包含进最终的结果(比如用于 Embedding 向量生成),用于避免处理无意义的超短段,默认 5。
  • maxNumChunks:从一段文本中最多能切出多少个 chunk,默认 1000
  • keepSeparator:是否在分割后保留原始文本中的分隔符,如 \n、空格、句号等,默认 true。

这里由于我的文本内容只包含了 652 个字符,因此这里对参数进行了相应的调整,防止只生成一个文本块,对于不同的文本内容,你可以自行设定这些参数。

生成的结果如下所示。

可以看到,最终这一个文件的内容被分成了 4 个文本块,然后再看向量数据库中对应的结果,如下所示。

4.5 RAG

前面的部分大多数都是对数据的处理(对应于 RAG 的离线部分),还没有涉及到 LLM 的交互,这里讲述 RAG 的在线部分。

pom 依赖:

xml 复制代码
<!-- RAG -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>

<!-- RAG -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-rag</artifactId>
</dependency>

这里为了显示 LLM 交互时的日志,在配置文件中声明了日志级别,如下所示:

yaml 复制代码
spring:
  ai:
    openai:
    	base-url: [这里填url]
      api-key: [这里填密钥]
      chat:
        options:
          model: Qwen/Qwen2.5-72B-Instruct # chat 模型
# LLM 对话日志
logging:
  level:
    org:
      springframework:
        ai:
          chat:
            client:
              advisor:
                DEBUG

LLM Bean 的配置:

typescript 复制代码
@Bean
public ChatClient chatClient(ChatClient.Builder chatClientBuilder) {
    return chatClientBuilder
      .defaultAdvisors(new SimpleLoggerAdvisor())
      .build();
}

实现 RAG 流程代码:

scss 复制代码
@Autowired
private ChatClient chatClient;

@Autowired
private VectorStore vectorStore;

@GetMapping("/rag")
public void chatWithRag(String input) {
    System.out.println("input: " + input);
    Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
            .documentRetriever(VectorStoreDocumentRetriever.builder()
                    .similarityThreshold(0.5)
                    .vectorStore(vectorStore)
                    .build())
            .queryAugmenter(ContextualQueryAugmenter.builder()
                    .allowEmptyContext(true)
                    .build())
            .build();
    String result = chatClient.prompt()
            .advisors(retrievalAugmentationAdvisor)
            .user(input)
            .call()
            .content();
    System.out.println("result: " + result);
}

测试结果如下所示,这里由于开启了日志,因此整个整个交互流程都会显示在上面。从结果中还能够看出,Spring AI 在 RAG 中也设置了 Prompt Template(对应于 3.6 节),它不仅将检索到的文档和用户提的问题组合成一个格式化的 prompt,还告诉了 LLM 两条回答的规则,即"如果答案不在上下文中,就说你不知道"、"避免使用"根据上下文......"或"提供的信息......"之类的说法"。

sql 复制代码
input: 小璐是谁,他是干什么的
2025-06-12T16:00:04.143+08:00 DEBUG 1183 --- [nio-8080-exec-1] o.s.a.c.c.advisor.SimpleLoggerAdvisor    : request: ChatClientRequest[prompt=Prompt{messages=[UserMessage{content='Context information is below.

---------------------
小璐的职业是做Java开发的
小璐的职业是计算机相关的
小璐,男(/女),一名专注于Java开发的计算机从业者,自大学起便对编程与软件工程抱有浓厚兴趣。凭借对技术的热爱与不断钻研的精神,他逐步走上了专业的开发之路。
大学期间,小璐主修计算机科学与技术,系统学习了数据结构、操作系统、计算机网络、数据库原理、Java编程语言等核心课程。在课程之外,他积极参与各类编程实践与项目开发,多次参加编程竞赛与开发挑战,积累了
力。
除了日常开发工作,小璐也不断关注新技术的发展,积极学习微服务架构、分布式系统、容器化部署(如Docker、Kubernetes)等前沿知识。他相信技术永无止境,持续学习和思考是保持竞争力的关键。
作为一名开发者,小璐不仅追求技术上的成长,也重视团队协作与沟通效率。他乐于帮助他人,愿意分享自己的经验,同时也虚心接受他人的建议。在工作中,他秉持认真负责、追求完美
---------------------

Given the context information and no prior knowledge, answer the query.

Follow these rules:

1. If the answer is not in the context, just say that you don't know.
2. Avoid statements like "Based on the context..." or "The provided information...".

Query: 小璐是谁,他是干什么的

Answer:
', properties={messageType=USER}, messageType=USER}], modelOptions=OpenAiChatOptions: {"streamUsage":false,"model":"Qwen/Qwen2.5-72B-Instruct","temperature":0.7}}, context={rag_document_context=[Document{id='8d730d2c-5d35-4ff2-89c8-6bbacc642dde', text='小璐的职业是做Java开发的', media='null', metadata={distance=0.113830574}, score=0.8861694261431694}, Document{id='768fd0ba-c853-48dd-ad95-01bd0ababce4', text='小璐的职业是计算机相关的', media='null', metadata={distance=0.11950535}, score=0.8804946467280388}, Document{id='b6d0b193-c852-4e8a-9929-2918932a31c0', text='小璐,男(/女),一名专注于Java开发的计算机从业者,自大学起便对编程与软件工程抱有浓厚兴趣。凭借对技术的热爱与不断钻研的精神,他逐步走上了专业的开发之路。
大学期间,小璐主修计算机科学与技术,系统学习了数据结构、操作系统、计算机网络、数据库原理、Java编程语言等核心课程。在课程之外,他积极参与各类编程实践与项目开发,多次参加编程竞赛与开发挑战,积累了', media='null', metadata={charset=UTF-8, source=file.txt, distance=0.16311505}, score=0.8368849456310272}, Document{id='e3578e33-00f7-4e0a-a046-00ae0bdc8482', text='力。
除了日常开发工作,小璐也不断关注新技术的发展,积极学习微服务架构、分布式系统、容器化部署(如Docker、Kubernetes)等前沿知识。他相信技术永无止境,持续学习和思考是保持竞争力的关键。
作为一名开发者,小璐不仅追求技术上的成长,也重视团队协作与沟通效率。他乐于帮助他人,愿意分享自己的经验,同时也虚心接受他人的建议。在工作中,他秉持认真负责、追求完美', media='null', metadata={charset=UTF-8, source=file.txt, distance=0.17335716}, score=0.8266428411006927}]}]
2025-06-12T16:00:11.587+08:00 DEBUG 1183 --- [nio-8080-exec-1] o.s.a.c.c.advisor.SimpleLoggerAdvisor    : response: {
  "result" : {
    "metadata" : {
      "finishReason" : "STOP",
      "contentFilters" : [ ],
      "empty" : true
    },
    "output" : {
      "messageType" : "ASSISTANT",
      "metadata" : {
        "role" : "ASSISTANT",
        "messageType" : "ASSISTANT",
        "refusal" : "",
        "finishReason" : "STOP",
        "index" : 0,
        "annotations" : [ ],
        "id" : "0197632746fcc873321a1dc9b0fabbcb"
      },
      "toolCalls" : [ ],
      "media" : [ ],
      "text" : "小璐是一名专注于Java开发的计算机从业者。他在大学期间主修计算机科学与技术,系统学习了数据结构、操作系统、计算机网络、数据库原理、Java编程语言等核心课程,并积极参与编程实践与项目开发。在工作中,他不仅专注于技术成长,还重视团队协作与沟通效率。"
    }
  },
  "metadata" : {
    "id" : "0197632746fcc873321a1dc9b0fabbcb",
    "model" : "Qwen/Qwen2.5-72B-Instruct",
    "rateLimit" : {
      "requestsLimit" : null,
      "requestsRemaining" : null,
      "requestsReset" : null,
      "tokensLimit" : null,
      "tokensRemaining" : null,
      "tokensReset" : null
    },
    "usage" : {
      "promptTokens" : 330,
      "completionTokens" : 65,
      "totalTokens" : 395,
      "nativeUsage" : {
        "completion_tokens" : 65,
        "prompt_tokens" : 330,
        "total_tokens" : 395
      }
    },
    "promptMetadata" : [ ],
    "empty" : false
  },
  "results" : [ {
    "metadata" : {
      "finishReason" : "STOP",
      "contentFilters" : [ ],
      "empty" : true
    },
    "output" : {
      "messageType" : "ASSISTANT",
      "metadata" : {
        "role" : "ASSISTANT",
        "messageType" : "ASSISTANT",
        "refusal" : "",
        "finishReason" : "STOP",
        "index" : 0,
        "annotations" : [ ],
        "id" : "0197632746fcc873321a1dc9b0fabbcb"
      },
      "toolCalls" : [ ],
      "media" : [ ],
      "text" : "小璐是一名专注于Java开发的计算机从业者。他在大学期间主修计算机科学与技术,系统学习了数据结构、操作系统、计算机网络、数据库原理、Java编程语言等核心课程,并积极参与编程实践与项目开发。在工作中,他不仅专注于技术成长,还重视团队协作与沟通效率。"
    }
  } ]
}
result: 小璐是一名专注于Java开发的计算机从业者。他在大学期间主修计算机科学与技术,系统学习了数据结构、操作系统、计算机网络、数据库原理、Java编程语言等核心课程,并积极参与编程实践与项目开发。在工作中,他不仅专注于技术成长,还重视团队协作与沟通效率。

4.6 完整代码

pom 依赖:

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
  
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
  
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
  
    <!-- RAG -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-advisors-vector-store</artifactId>
    </dependency>
  
    <!-- RAG -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-rag</artifactId>
    </dependency>
  
    <!-- OpenAI -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-openai</artifactId>
    </dependency>
  
    <!-- 向量数据库(pgvector) -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>1.0.0</version> <!-- spring-ai 版本 -->
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

配置文件:

yaml 复制代码
spring:
  ai:
    openai:
      base-url: [这里填url]
      api-key: [这里填密钥]
      chat:
        options:
          model: Qwen/Qwen2.5-72B-Instruct # chat 模型
      embedding:
        options:
          model: text-embedding-ada-002 # Embedding 模型
    vectorstore:
      pgvector:
        initialize-schema: true # 是否自动初始化 schema
        index-type: HNSW # 最近邻搜索索引类型
        distance-type: COSINE_DISTANCE # 计算向量距离方式
        dimensions: 1536 # 向量维度
        max-document-batch-size: 10000 # 单次批量处理的最大文档数
  datasource:
    url: jdbc:postgresql://localhost/postgres
    username: [这里填用户名]
    password: [这里填密码]

# LLM 对话日志
logging:
  level:
    org:
      springframework:
        ai:
          chat:
            client:
              advisor:
                DEBUG

Java 代码:

kotlin 复制代码
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AIConfig {

    @Bean
    public ChatClient chatClient(ChatClient.Builder chatClientBuilder) {
        return chatClientBuilder
                .defaultAdvisors(new SimpleLoggerAdvisor())
                .build();
    }

}
java 复制代码
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.api.Advisor;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
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.reader.TextReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class QwenController {

    @Autowired
    private ChatClient chatClient;

    @Autowired
    private EmbeddingModel embeddingModel;

    @Autowired
    private VectorStore vectorStore;

    @Value("classpath:/file.txt")
    private Resource resource;

    @GetMapping("/embedding")
    public void embedding(String input) {
        System.out.println("input = " + input);
        float[] embeddings1 = embeddingModel.embed(input);
        System.out.println("length = " + embeddings1.length + ", array = " + Arrays.toString(embeddings1));
    }

    @GetMapping("/storeVector")
    public void storeVector(@RequestParam List<String> input) {
        List<Document> documents = input.stream().map(Document::new).collect(Collectors.toList());
        vectorStore.add(documents);
    }

    @GetMapping("/similaritySearch")
    public void similarSearch(String input) {
        // topK 可以指定搜索出的向量数
        SearchRequest query = SearchRequest.builder().query(input).topK(2).build();
        List<Document> similarDocuments = vectorStore.similaritySearch(query);
        String result = similarDocuments.stream()
                .map(Document::getText)
                .collect(Collectors.joining(System.lineSeparator()));
        System.out.println("result:\n" + result);
    }

    @GetMapping("/etl")
    public void etl() {
        // extract
        TextReader textReader = new TextReader(this.resource);
        List<Document> extractedDoc = textReader.read();
        System.out.println("extract result: " + extractedDoc);
        // transform
        TokenTextSplitter splitter = new TokenTextSplitter(200, 200, 5, 10000, true);
        List<Document> transformedDoc = splitter.apply(extractedDoc);
        System.out.println("transform length = " + transformedDoc.size() + ", result: " + transformedDoc);
        // load
        vectorStore.add(transformedDoc);
    }

    @GetMapping("/rag")
    public void chatWithRag(String input) {
        System.out.println("input: " + input);
        Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
                .documentRetriever(VectorStoreDocumentRetriever.builder()
                        .similarityThreshold(0.5)
                        .vectorStore(vectorStore)
                        .build())
                .queryAugmenter(ContextualQueryAugmenter.builder()
                        .allowEmptyContext(true)
                        .build())
                .build();
        String result = chatClient.prompt()
                .advisors(retrievalAugmentationAdvisor)
                .user(input)
                .call()
                .content();
        System.out.println("result: " + result);
    }
}

五、展望

RAG 作为一种融合外部知识与 LLM 强大生成能力的技术路径,正在成为企业内各种 AI 应用的解决方案,它在增强 LLM 的专业性、个性化能力上展现出了巨大潜力,我相信随着技术的不断演进,以及框架能力的不断完善,RAG 将在更多真实场景中发挥重要的作用。

参考资料

1.这就是RAG 一看就懂的个人知识库架构_哔哩哔哩_bilibili

2.20分钟速成 RAG & 向量数据库核心概念 【小白学AI系列 -1 】_哔哩哔哩_bilibili

3.RAG总结,分块Chuck的策略和实现 - 53AI-AI知识库|大模型知识库|大模型训练|智能体开发

4.15分钟速成 近似最近邻算法 (approximate nearest neighbor, ANN)_哔哩哔哩_bilibili

5.人工智能小白到高手:RAG通过重排(Reranking)提升信息检索的质量-AI.x-AIGC专属社区-51CTO.COM

6.PGVector-github

7.PGvector :: Spring AI Reference

8.ETL Pipeline :: Spring AI Reference

9.Retrieval Augmented Generation :: Spring AI Reference

10.SpringAI开发指南(四):如何实现RAG(Retrieval Augmented Generation)

11.使用 Embedding 模型和向量数据库的 Spring AI RAG - spring 中文网

相关推荐
libo_202513 分钟前
HarmonyOS 5 模型瘦身验证:从200MB到5MB的剪枝后准确率回归测试
ai编程·arkts
玛奇玛丶1 小时前
谨防AICoding之AI幻觉
ai编程
饼干哥哥1 小时前
n8n+fastgpt RAG = 王炸!!!用最强AI知识库MCP Server补全 n8n短板
ai编程·mcp
广州山泉婚姻2 小时前
智慧零工平台后端开发进阶:Spring Boot 3结合MyBatis-Flex的技术实践与优化【无标题】
人工智能·爬虫·spring
阿里云云原生2 小时前
Spring AI Alibaba 1.0 GA 正式发布,Java 智能体开发进入新时代
spring
用户73729113888572 小时前
🔥 AI编程神器大PK!Claude Code vs Cursor:谁才是2025年程序员的最强助手
ai编程
熊猫钓鱼4 小时前
Trae智能体实战:再也不用炒股断手,我用Trae构建股票牛熊市场预测器!
ai编程·trae
该用户已不存在4 小时前
懒人福音!ServBay+n8n,10分钟打造自己的小道消息
github·ai编程
阿灿爱分享4 小时前
AI换衣技术实现原理浅析:基于图像合成的虚拟试衣实践
ai·ai编程·免费开源
Gyoku Mint5 小时前
机器学习×第七卷:正则化与过拟合——她开始学会收敛,不再贴得太满
人工智能·python·算法·chatgpt·线性回归·ai编程