10. Spring AI + RAG
<h1 id="10-spring-ai---rag">10. Spring AI+ RAG</h1><p>@</p><div class="toc"><div class="toc-container-header">目录</div><ul><li>10. Spring AI+ RAG<ul><li>RAG<ul><li>概念<ul><li>向量:</li><li>文本向量化</li><li>向量数据库</li><li>匹配检索<ul><li>SearchRequest</li></ul></li></ul></li><li>接入ChatClient<ul><li>RetrievalAugmentationAdvisor</li></ul></li></ul></li></ul></li><li>最后:</li></ul></div><p></p>
<h2 id="rag">RAG</h2>
<p><font style="color: rgba(51, 51, 51, 1)">检索增强生成(Retrieval-augmented Generation)</font></p>
<p>对于基础大模型来说, 他只具备通用信息,他的参数都是拿公网进行训练,并且有一定的时间延迟, 无法得知一些具体业务数据和实时数据, 这些数据往往在各种文件中(比如txt、word、html、数据库...)</p>
<p>虽然function-call、SystemMessage可以用来解决一部分问题</p>
<p>但是它只能少量,并且针对的场景不一样</p>
<p>如果你要提供大量的业务领域信息, 就需要给他外接一个知识库:</p>
<p>比如</p>
<ol>
<li>我问他退订要多少费用</li>
<li>这些资料可能都由产品或者需求编写在了文档中:
<ol>
<li>所以需要现在需求信息存到向量数据库(这个过程叫Embedding, 涉及到文档读取、分词、向量化存入)</li>
</ol>
</li>
<li>去向量数据库中查询“退订费用相关信息”</li>
<li>将查询到的数据和对话信息再请求大模型</li>
<li>此时会响应退订需要多少费用</li>
</ol>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930214328609-1975516382.png"></p>
<h3 id="概念">概念</h3>
<h4 id="向量">向量:</h4>
<p><font style="color: rgba(64, 64, 64, 1)">向量通常用来做相似性搜索,比如语义的一维向量,可以表示词语或短语的语义相似性。例如,“你好”、“hello”和“见到你很高兴”可以通过一维向量来表示它们的语义接近程度。</font></p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930214329352-574936284.png"></p>
<p><font style="color: rgba(64, 64, 64, 1)">然而,对于更复杂的对象,比如小狗,无法仅通过一个维度来进行相似性搜索。这时,我们需要提取多个特征,如颜色、大小、品种等,将每个特征表示为向量的一个维度,从而形成一个多维向量。例如,一只棕色的小型泰迪犬可以表示为一个多维向量 [棕色, 小型, 泰迪犬]。</font></p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930214329258-1924960461.png"></p>
<p><font style="color: rgba(64, 64, 64, 1)">如果需要检索见过更加精准, 我们肯定还需要更多维度的向量, 组成更多维度的空间,在多维向量空间中,相似性检索变得更加复杂。我们需要使用一些算法,如余弦相似度或欧几里得距离,来计算向量之间的相似性。</font><strong><font style="color: rgba(223, 42, 63, 1)">向量数据库</font></strong><font style="color: rgba(64, 64, 64, 1)">会帮我实现。</font></p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930214328611-1449770395.png"></p>
<h4 id="文本向量化">文本向量化</h4>
<p>通过向量模型即可向量化, 这里我们学到了一种新的模型, 叫“向量模型” 专门用来做文本向量化的。</p>
<p>大语言模型不能做向量化, 所以需要单独找一个向量模型</p>
<ol>
<li>deepseek不支持向量模型</li>
<li>阿里百炼有大量向量模型
<ol>
<li>默认模型DashScopeEmbeddingProperties#DEFAULT_EMBEDDING_MODEL="text-embedding-v1"</li>
</ol>
</li>
</ol>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930214328783-739508978.png"></p>
<pre><code class="language-properties">spring.ai.dashscope.embedding.options.model= text-embedding-v4
</code></pre>
<ol>
<li>ollama有大量向量模型, 自己拉取</li>
</ol>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930214328604-134158686.png"></p>
<p>以ollama为例:</p>
<pre><code class="language-properties">spring.ai.ollama.embedding.model= nomic-embed-text
</code></pre>
<pre><code class="language-java">@SpringBootTest
public class EmbaddingTest {
@Test
public void testEmbadding(@Autowired OllamaEmbeddingModel ollamaEmbeddingModel) {
// .embed() 转换为向量模型
float[] embedded = ollamaEmbeddingModel.embed("我叫徐庶");
System.out.println(embedded.length);
System.out.println(Arrays.toString(embedded));
}
}
</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930214328826-1795532821.png"></p>
<p>从结果可以知道"我叫徐庶"这句话经过OllamaEmbeddingModel向量化之后得到的一个长度为768的float数组。注意,768是向量模型<font style="color: rgba(74, 85, 101, 1)">nomic-embed-text-v1.5</font>固定的,不会随着句子长度而变化,不同的向量模型提供了不同的维度。</p>
<p>那么,我们通过这种向量模型得到一句话对应的向量有什么作用呢?非常有用,因为我们可以基于向量来判断两句话之间的相似度,举个例子:</p>
<p>查询跟秋田犬类似的狗, 在向量数据库中根据每个狗的特点进行多维向量, 你会发现秋田犬的向量数值和柴犬的向量数值最接近, 就可以查到类似的狗。 (当然我这里只是举例,让你对向量数据库有一个印象)</p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930214328590-272452255.png"></p>
<ul>
<li><font style="color: rgba(6, 8, 31, 0.88); background-color: rgba(248, 206, 211, 0.5)">向量模型的本质目标,就是把</font><strong><font style="color: rgba(6, 8, 31, 0.88); background-color: rgba(248, 206, 211, 0.5)">语义相似</font></strong><font style="color: rgba(6, 8, 31, 0.88); background-color: rgba(248, 206, 211, 0.5)">的内容用“相近”的向量表示,把“不相关”内容尽量拉远。 </font></li>
<li><font style="color: rgba(6, 8, 31, 0.88); background-color: rgba(248, 206, 211, 0.5)">所以好的向量模型能够更好的识别语义, 进行向量化.</font></li>
</ul>
<font style="background-color: rgba(248, 206, 211, 0.5)">
</font>
<h4 id="向量数据库">向量数据库</h4>
<p>对于向量模型生成出来的向量,我们可以持久化到向量数据库,并且能利用向量数据库来计算两个向量之间的相似度,或者根据一个向量查找跟这个向量最相似的向量。</p>
<p>在SpringAi中,<font style="color: rgba(20, 24, 24, 1)">VectorStore </font>表示向量数据库,目前支持的向量数据库有</p>
<ul>
<li>Azure Vector Search<font style="color: rgba(25, 30, 30, 1)">- The</font>Azure<font style="color: rgba(25, 30, 30, 1)">vector store.</font></li>
<li>Apache Cassandra<font style="color: rgba(25, 30, 30, 1)">- The</font>Apache Cassandra<font style="color: rgba(25, 30, 30, 1)">vector store.</font></li>
<li>Chroma Vector Store<font style="color: rgba(25, 30, 30, 1)">- The</font>Chroma<font style="color: rgba(25, 30, 30, 1)">vector store.</font></li>
<li>Elasticsearch Vector Store<font style="color: rgba(25, 30, 30, 1)"> - The </font>Elasticsearch<font style="color: rgba(25, 30, 30, 1)"> vector store. </font><font style="color: rgba(6, 8, 31, 0.88)">可以“以向量+关键词”方式做混合检索。深度优化更多针对文本,不是专门“向量搜索引擎”。向量存储和检索容量有限制,查询延迟高于 Milvus。</font></li>
<li>GemFire Vector Store<font style="color: rgba(25, 30, 30, 1)">- The</font>GemFire<font style="color: rgba(25, 30, 30, 1)">vector store.</font></li>
<li>MariaDB Vector Store<font style="color: rgba(25, 30, 30, 1)">- The</font>MariaDB<font style="color: rgba(25, 30, 30, 1)">vector store.</font></li>
<li>Milvus Vector Store<font style="color: rgba(25, 30, 30, 1); background-color: rgba(251, 222, 40, 1)"> - The </font>Milvus<font style="color: rgba(25, 30, 30, 1); background-color: rgba(251, 222, 40, 1)"> vector store.</font></li>
<li>MongoDB Atlas Vector Store<font style="color: rgba(25, 30, 30, 1)">- The</font>MongoDB Atlas<font style="color: rgba(25, 30, 30, 1)">vector store.</font></li>
<li>Neo4j Vector Store<font style="color: rgba(25, 30, 30, 1)"> - The </font>Neo4j<font style="color: rgba(25, 30, 30, 1)"> vector store.</font><font style="color: rgba(6, 8, 31, 0.88)">可以结合结构化图谱查询与向量检索, </font><strong><font style="color: rgba(6, 8, 31, 0.88)">大规模嵌入检索(如千万—亿级高维向量)性能明显落后于 Milvus</font></strong></li>
<li>OpenSearch Vector Store<font style="color: rgba(25, 30, 30, 1)">- The</font>OpenSearch<font style="color: rgba(25, 30, 30, 1)">vector store.</font></li>
<li>Oracle Vector Store<font style="color: rgba(25, 30, 30, 1)">- The</font>Oracle Database<font style="color: rgba(25, 30, 30, 1)">vector store.</font></li>
<li>PgVector Store<font style="color: rgba(25, 30, 30, 1)">- The</font>PostgreSQL/PGVector<font style="color: rgba(25, 30, 30, 1)">vector store.</font></li>
<li>Pinecone Vector Store<font style="color: rgba(25, 30, 30, 1)">-</font>PineCone<font style="color: rgba(25, 30, 30, 1)">vector store.</font></li>
<li>Qdrant Vector Store<font style="color: rgba(25, 30, 30, 1)">-</font>Qdrant<font style="color: rgba(25, 30, 30, 1)">vector store.</font></li>
<li>Redis Vector Store<font style="color: rgba(25, 30, 30, 1)"> - The </font>Redis<font style="color: rgba(25, 30, 30, 1)"> vector store. </font><font style="color: rgba(6, 8, 31, 0.88)">低门槛实现小规模向量检索。对于高维大规模向量(如几百万到上亿条),性能和存储效率不如专用向量库。</font></li>
<li>SAP Hana Vector Store<font style="color: rgba(25, 30, 30, 1)">- The</font>SAP HANA<font style="color: rgba(25, 30, 30, 1)">vector store.</font></li>
<li>Typesense Vector Store<font style="color: rgba(25, 30, 30, 1)">- The</font>Typesense<font style="color: rgba(25, 30, 30, 1)">vector store.</font></li>
<li>Weaviate Vector Store<font style="color: rgba(25, 30, 30, 1)">- The</font>Weaviate<font style="color: rgba(25, 30, 30, 1)">vector store.</font></li>
<li>SimpleVectorStore<font style="color: rgba(25, 30, 30, 1); background-color: rgba(251, 222, 40, 1)"> - A simple implementation of persistent vector storage, good for educational purposes.</font></li>
</ul>
<p>其中有我们熟悉的几个数据库都可以用来存储向量,比如Elasticsearch、MongoDb、Neo4j、Pgsql、Redis。</p>
<p>视频中我会讲解2种:</p>
<ol>
<li>SimpleVectorStore教学版向量数据库</li>
<li>Milvus Vector Store<font style="color: rgba(6, 8, 31, 0.88)">Milvus(国产团队)、文档友好、社区国内活跃、性能最佳、市场占用率大。 </font><font style="color: rgba(25, 30, 30, 1)">实战中使用的向量数据库.</font></li>
</ol>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930214328559-1035810263.png"></p>
<h4 id="匹配检索">匹配检索</h4>
<p>在这个示例中, 我分别存储了预订航班和取消预订2段说明到向量数据库中<br>
然后通过"退票要多少钱" 进行查询</p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930214328728-1639126516.png"></p>
<p>代码执行结果为:</p>
<p>OllamaEmbedding结果</p>
<pre><code class="language-java">@Bean
public VectorStore vectorStore(OllamaEmbeddingModel embeddingModel) {
// 绑定一个向量大模型,进行将数据信息向量化
SimpleVectorStore.SimpleVectorStoreBuilder builder = SimpleVectorStore.builder(embeddingModel);
return builder.build();
}
</code></pre>
<h5 id="searchrequest">SearchRequest</h5>
<p>可以利用searchRequest设置检索请求:</p>
<ul>
<li>query 代表要检索的内容</li>
<li>topK 设置检索结果的前N条
<ul>
<li>通常我们查询所有结果查出来, 因为查询结果最终要发给大模型, 查询过多的结果会:
<ul>
<li>
<ol>
<li>过多的token意味着更长延迟, 更多的费用, 并且过多上下文会超限;</li>
</ol>
</li>
<li>
<ol start="2">
<li><font style="color: rgba(77, 77, 77, 1)">研究表明</font>过多的内容<font style="color: rgba(77, 77, 77, 1)">会降低 LLM 的<em></em></font><em>_**<font style="color: rgba(77, 77, 77, 1)">召回性能</font></em><font style="color: rgba(77, 77, 77, 1)">;</font>**_</li>
</ol>
</li>
</ul>
</li>
</ul>
</li>
<li>similarityThreshold 设置<font style="color: rgba(6, 8, 31, 0.88)">相似度阈值</font>, 可以通关设置分数限制召回内容相似度. 从而过滤掉废料。 (中文语料要适当降低分数) , 所以应遵循**<font style="color: rgba(6, 8, 31, 0.88)">始终以“业务召回效果”为主,而不是追求网上常说的高分阈值。</font></li>
</ul>
<pre><code class="language-java">@BeforeEach
public void init( @Autowired
VectorStore vectorStore) {
// 1. 声明内容文档
Document doc = Document.builder()
.text("""
预订航班:
- 通过我们的网站或移动应用程序预订。
- 预订时需要全额付款。
- 确保个人信息(姓名、ID 等)的准确性,因为更正可能会产生 25 的费用。
""")
.build();
Document doc2 = Document.builder()
.text("""
取消预订:
- 最晚在航班起飞前 48 小时取消。
- 取消费用:经济舱 75 美元,豪华经济舱 50 美元,商务舱 25 美元。
- 退款将在 7 个工作日内处理。
""")
.build();
// 2. 将文本进行向量化,并且存入向量数据库(无需再手动向量化)
vectorStore.add(Arrays.asList(doc,doc2));
}
@Test
void similaritySearchTest(
@Autowired
VectorStore vectorStore) {
// 3. 相似性查询
SearchRequest searchRequest = SearchRequest
.builder().query("预定航班")
.topK(5) // 查询几个
.similarityThreshold(0.3) // 过滤相似度 0.3 的内容,才返回
.build();
List<Document> results = vectorStore.similaritySearch(searchRequest);
// 4.输出
System.out.println(results);
}
</code></pre>
<p>可以看到明显阿里的向量模型归类的更加准确,Ollama的向量模型查出来后结果并不正确。 所以为了你的准确性,请选择性能更好的向量模型。 想要更快更相似的搜索,用好的向量数据库。</p>
<h3 id="接入chatclient">接入ChatClient</h3>
<ol>
<li>依赖</li>
</ol>
<pre><code class="language-json"><dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>
</code></pre>
<ol start="2">
<li>代码</li>
</ol>
<pre><code class="language-java">@Bean
public VectorStore vectorStore(DashScopeEmbeddingModel embeddingModel) {
SimpleVectorStore.SimpleVectorStoreBuilder builder = SimpleVectorStore.builder(embeddingModel);
return builder.build();
}
</code></pre>
<ol start="3">
<li>测试</li>
</ol>
<p>实际你会发现, 最核心的是通过拦截器:QuestionAnswerAdvisor . 你应该能猜到底层肯定会通过拦截对话将相似内容发给大模型。 可以结合SimpleLoggerAdvisor 查看日志内容.</p>
<pre><code class="language-java">@SpringBootTest
public class SimpleVectorStoreTest {
@BeforeEach
public void init( @Autowired
VectorStore vectorStore) {
// 1. 声明内容文档
Document doc = Document.builder()
.text("""
预订航班:
- 通过我们的网站或移动应用程序预订。
- 预订时需要全额付款。
- 确保个人信息(姓名、ID 等)的准确性,因为更正可能会产生 25 的费用。
""")
.build();
Document doc2 = Document.builder()
.text("""
取消预订:
- 最晚在航班起飞前 48 小时取消。
- 取消费用:经济舱 75 美元,豪华经济舱 50 美元,商务舱 25 美元。
- 退款将在 7 个工作日内处理。
""")
.build();
// 2. 将文本进行向量化,并且存入向量数据库(无需再手动向量化)
vectorStore.add(Arrays.asList(doc,doc2));
}
@Test
void chatRagTest(
@Autowired
VectorStore vectorStore,
@Autowired DashScopeChatModel chatModel
) {
ChatClient chatClient = ChatClient.builder(chatModel)
.build();
String message="退费需要多少费用?";
String content = chatClient.prompt().user(message)
// 通过 advisors 查询向量数据库,进行过滤
.advisors(
new SimpleLoggerAdvisor(),
QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(
SearchRequest
.builder().query(message)
.topK(5)
.similarityThreshold(0.3)
.build())
.build()
).call().content();
System.out.println(content);
}
}
</code></pre>
<h4 id="retrievalaugmentationadvisor">RetrievalAugmentationAdvisor</h4>
<ul>
<li><font style="color: rgba(51, 51, 51, 1); background-color: rgba(248, 248, 248, 1)">查询空时扩展策略 :</font></li>
</ul>
<pre><code class="language-java">.queryAugmenter(ContextualQueryAugmenter.builder()
.allowEmptyContext(false)
.emptyContextPromptTemplate(PromptTemplate.builder().template("用户查询位于知识库之外。礼貌地告知用户您无法回答").build())
.build())
</code></pre>
<ul>
<li><font style="color: rgba(51, 51, 51, 1); background-color: rgba(248, 248, 248, 1)">查询检索器</font>
<ul>
<li><font style="color: rgba(51, 51, 51, 1); background-color: rgba(248, 248, 248, 1)">检索提示词重写</font></li>
</ul>
</li>
</ul>
<pre><code class="language-java">.queryTransformers(RewriteQueryTransformer.builder()
.chatClientBuilder(ChatClient.builder(dashScopeChatModel))
.targetSearchSystem("航空票务助手")
.build())
</code></pre>
<p>翻译重写</p>
<pre><code class="language-java">.queryTransformers(TranslationQueryTransformer.builder()
.chatClientBuilder(ChatClient.builder(dashScopeChatModel))
.targetLanguage("中文")
.build())
</code></pre>
<ul>
<li>
<p><font style="color: rgba(51, 51, 51, 1); background-color: rgba(248, 248, 248, 1)">后置处理器:需要文档后处理和重排序</font></p>
</li>
<li>
<p><font style="color: rgba(51, 51, 51, 1); background-color: rgba(248, 248, 248, 1)">实现复杂的 RAG 流水线</font></p>
</li>
</ul>
<pre><code class="language-java">@Test
public void testRag3(@Autowired VectorStore vectorStore,
@Autowired DashScopeChatModel dashScopeChatModel) {
chatClient = ChatClient.builder(dashScopeChatModel)
.defaultAdvisors(SimpleLoggerAdvisor.builder().build())
.build();
// 增强多
Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
// 查 = QuestionAnswerAdvisor
.documentRetriever(VectorStoreDocumentRetriever.builder()
.similarityThreshold(0.50)
.vectorStore(vectorStore)
.build())
// 检索为空时,返回提示
/*.queryAugmenter(ContextualQueryAugmenter.builder()
.allowEmptyContext(false)
.emptyContextPromptTemplate(PromptTemplate.builder().template("用户查询位于知识库之外。礼貌地告知用户您无法回答").build())
.build())*/
// 相似性查询内容转换
/*.queryTransformers(RewriteQueryTransformer.builder()
.chatClientBuilder(ChatClient.builder(dashScopeChatModel))
.targetSearchSystem("航空票务助手")
.build())*/
// 检索后文档监控、操作
/*.documentPostProcessors((query, documents) -> {
System.out.println("Original query: " + query.text());
System.out.println("Retrieved documents: " + documents.size());
return documents;
})*/
.build();
String answer = chatClient.prompt()
.advisors(retrievalAugmentationAdvisor)
.user("退一张票大概要多少费用?希望别扣太多啊")
.call()
.content();
System.out.println(answer);
}
@TestConfiguration
static class TestConfig {
@Bean
public VectorStore vectorStore(DashScopeEmbeddingModel embeddingModel) {
return SimpleVectorStore.builder(embeddingModel).build();
}
}
</code></pre>
<h1 id="最后">最后:</h1>
<blockquote>
<p>“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”</p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930214328889-1941540745.gif"></p>
</blockquote><br><br>
来源:https://www.cnblogs.com/TheMagicalRainbowSea/p/19121826
頁:
[1]