Spring AI 系列之一个很棒的 Spring AI 功能——Advisors
1. 概述
由AI驱动的应用程序已成为我们的现实。我们正在广泛地实现各种RAG应用程序、提示API,并利用大型语言模型(LLM)创建项目。借助 Spring AI,我们可以更快速地完成这些任务。
在本文中,我们将介绍一个非常有价值的功能:Spring AI Advisors
,它可以为我们处理各种日常任务。
2. 什么是 Spring AI Advisor?
Advisor 是拦截器,用于处理我们 AI 应用程序中的请求和响应。我们可以使用它们为提示处理流程设置额外的功能。例如,我们可以建立聊天记录、排除敏感词,或者为每个请求添加额外的上下文。
该功能的核心组件是CallAroundAdvisor
接口。我们通过实现这个接口,来创建一系列Advisor,它们会影响我们的请求或响应。下图展示了这些 Advisor 的执行流程:
我们将提示(prompt)发送到一个连接了多个 Advisor 的聊天模型。在提示被发送之前,链中的每个Advisor都会执行它的前置操作(before action)。同样地,在我们从聊天模型获取响应之前,每个Advisor会调用自己的 后置操作(after action)。
3. 聊天记忆Advisor(Chat Memory Advisors)
聊天记忆Advisor是一组非常有用的Advisor实现。我们可以使用这些 Advisor 为聊天提示提供交流历史,从而提升聊天响应的准确性。
3.1. MessageChatMemoryAdvisor
通过使用MessageChatMemoryAdvisor,我们可以在聊天客户端调用中通过messages
属性提供聊天历史。我们将所有消息保存在一个 ChatMemory
实现中,并且可以控制历史记录的大小。
下面我们来实现一个这个 Advisor 的简单示例:
@SpringBootTest(classes = ChatModel.class)
@EnableAutoConfiguration
@ExtendWith(SpringExtension.class)
public class SpringAILiveTest {@Autowired@Qualifier("openAiChatModel")ChatModel chatModel;ChatClient chatClient;@BeforeEachvoid setup() {chatClient = ChatClient.builder(chatModel).build();}@Testvoid givenMessageChatMemoryAdvisor_whenAskingChatToIncrementTheResponseWithNewName_thenNamesFromTheChatHistoryExistInResponse() {ChatMemory chatMemory = new InMemoryChatMemory();MessageChatMemoryAdvisor chatMemoryAdvisor = new MessageChatMemoryAdvisor(chatMemory);String responseContent = chatClient.prompt().user("Add this name to a list and return all the values: Bob").advisors(chatMemoryAdvisor).call().content();assertThat(responseContent).contains("Bob");responseContent = chatClient.prompt().user("Add this name to a list and return all the values: John").advisors(chatMemoryAdvisor).call().content();assertThat(responseContent).contains("Bob").contains("John");responseContent = chatClient.prompt().user("Add this name to a list and return all the values: Anna").advisors(chatMemoryAdvisor).call().content();assertThat(responseContent).contains("Bob").contains("John").contains("Anna");}
}
在这个测试中,我们创建了一个包含 InMemoryChatMemory
的 MessageChatMemoryAdvisor
实例。然后我们发送了几个提示,要求聊天模型返回包含历史数据的人名。正如我们所看到的,聊天中提到的所有人名都被返回了。
3.2. PromptChatMemoryAdvisor
使用 PromptChatMemoryAdvisor
,我们可以实现相同的目标——为聊天模型提供对话历史。不同之处在于,这个Advisor是将聊天记忆直接添加到提示文本(prompt)中。
在底层实现中,我们通过如下方式扩展提示文本:
Use the conversation memory from the MEMORY section to provide accurate answers.
---------------------
MEMORY:
{memory}
---------------------
让我们看看它是怎么工作的
@Test
void givenPromptChatMemoryAdvisor_whenAskingChatToIncrementTheResponseWithNewName_thenNamesFromTheChatHistoryExistInResponse() {ChatMemory chatMemory = new InMemoryChatMemory();PromptChatMemoryAdvisor chatMemoryAdvisor = new PromptChatMemoryAdvisor(chatMemory);String responseContent = chatClient.prompt().user("Add this name to a list and return all the values: Bob").advisors(chatMemoryAdvisor).call().content();assertThat(responseContent).contains("Bob");responseContent = chatClient.prompt().user("Add this name to a list and return all the values: John").advisors(chatMemoryAdvisor).call().content();assertThat(responseContent).contains("Bob").contains("John");responseContent = chatClient.prompt().user("Add this name to a list and return all the values: Anna").advisors(chatMemoryAdvisor).call().content();assertThat(responseContent).contains("Bob").contains("John").contains("Anna");
}
同样地,这一次我们使用
PromptChatMemoryAdvisor创建了几个提示,要求聊天模型考虑对话记忆。正如预期的那样,所有数据都被正确返回了。
3.3. VectorStoreChatMemoryAdvisor
使用VectorStoreChatMemoryAdvisor,我们可以实现更强大的功能。它通过向量存储中的相似度匹配来搜索消息上下文。在搜索相关文档时,我们还会考虑对话的 ID。
在我们的示例中,我们将使用一个稍作修改的SimpleVectorStore
,当然也可以替换为任何向量数据库。
首先,我们来创建一个向量存储的 Bean:
@Configuration
public class SimpleVectorStoreConfiguration {@Beanpublic VectorStore vectorStore(@Qualifier("openAiEmbeddingModel")EmbeddingModel embeddingModel) {return new SimpleVectorStore(embeddingModel) {@Overridepublic List<Document> doSimilaritySearch(SearchRequest request) {float[] userQueryEmbedding = embeddingModel.embed(request.query);return this.store.values().stream().map(entry -> Pair.of(entry.getId(),EmbeddingMath.cosineSimilarity(userQueryEmbedding, entry.getEmbedding()))).filter(s -> s.getSecond() >= request.getSimilarityThreshold()).sorted(Comparator.comparing(Pair::getSecond)).limit(request.getTopK()).map(s -> this.store.get(s.getFirst())).toList();}};}
}
在这里,我们创建了一个SimpleVectorStore
类的Bean,并重写了它的
doSimilaritySearch()
方法。
接下来,让我们测试一下基于相似度匹配的行为表现:
@Test
void givenVectorStoreChatMemoryAdvisor_whenAskingChatToIncrementTheResponseWithNewName_thenNamesFromTheChatHistoryExistInResponse() {VectorStoreChatMemoryAdvisor chatMemoryAdvisor = new VectorStoreChatMemoryAdvisor(vectorStore);String responseContent = chatClient.prompt().user("Find cats from our chat history, add Lion there and return a list").advisors(chatMemoryAdvisor).call().content();assertThat(responseContent).contains("Lion");responseContent = chatClient.prompt().user("Find cats from our chat history, add Puma there and return a list").advisors(chatMemoryAdvisor).call().content();assertThat(responseContent).contains("Lion").contains("Puma");responseContent = chatClient.prompt().user("Find cats from our chat history, add Leopard there and return a list").advisors(chatMemoryAdvisor).call().content();assertThat(responseContent).contains("Lion").contains("Puma").contains("Leopard");
}
我们让聊天模型填充列表中的一些条目;而在底层,我们进行了相似度搜索,以获取所有相似的文档。然后,聊天模型(LLM)在参考这些文档的基础上生成了答案。
4. QuestionAnswerAdvisor
在 RAG(检索增强生成)应用中,QuestionAnswerAdvisor
被广泛使用。使用这个Advisor时,我们会构造一个提示(prompt),请求基于准备好的上下文信息进行回答。
这些上下文信息是通过向量存储中的相似度搜索检索出来的。
让我们来看看它的工作机制:
@Test
void givenQuestionAnswerAdvisor_whenAskingQuestion_thenAnswerShouldBeProvidedBasedOnVectorStoreInformation() {Document document = new Document("The sky is green");List<Document> documents = new TokenTextSplitter().apply(List.of(document));vectorStore.add(documents);QuestionAnswerAdvisor questionAnswerAdvisor = new QuestionAnswerAdvisor(vectorStore);String responseContent = chatClient.prompt().user("What is the sky color?").advisors(questionAnswerAdvisor).call().content();assertThat(responseContent).containsIgnoringCase("green");
}
我们将文档中的特定信息填充到了向量存储中。
随后,我们使用QuestionAnswerAdvisor来创建提示,并验证其响应是否与文档内容一致,结果确实如此。
5. SafeGuardAdvisor
有时候我们需要阻止客户端提示中使用某些敏感词。毫无疑问,我们可以使用SafeGuardAdvisor来实现这一目标——只需指定一组禁止词,并将其包含在提示的 Advisor 实例中即可。
如果搜索请求中使用了这些敏感词,Advisor会拒绝该请求,并提示我们重新描述内容:
@Test
void givenSafeGuardAdvisor_whenSendPromptWithSensitiveWord_thenExpectedMessageShouldBeReturned() {List<String> forbiddenWords = List.of("Word2");SafeGuardAdvisor safeGuardAdvisor = new SafeGuardAdvisor(forbiddenWords);String responseContent = chatClient.prompt().user("Please split the 'Word2' into characters").advisors(safeGuardAdvisor).call().content();assertThat(responseContent).contains("I'm unable to respond to that due to sensitive content");
}
在这个示例中,我们首先创建了一个包含单个禁止词的SafeGuardAdvisor。然后尝试在提示中使用这个词,结果如预期所示,我们收到了禁止词验证的提示信息。
6. 实现自定义 Advisor
当然,我们可以根据自己的需要实现任意逻辑的自定义 Advisor。下面我们来创建一个 CustomLoggingAdvisor
,它会记录所有的聊天请求与响应:
public class CustomLoggingAdvisor implements CallAroundAdvisor {private final static Logger logger = LoggerFactory.getLogger(CustomLoggingAdvisor.class);@Overridepublic AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {advisedRequest = this.before(advisedRequest);AdvisedResponse advisedResponse = chain.nextAroundCall(advisedRequest);this.observeAfter(advisedResponse);return advisedResponse;}private void observeAfter(AdvisedResponse advisedResponse) {logger.info(advisedResponse.response().getResult().getOutput().getContent());}private AdvisedRequest before(AdvisedRequest advisedRequest) {logger.info(advisedRequest.userText());return advisedRequest;}@Overridepublic String getName() {return "CustomLoggingAdvisor";}@Overridepublic int getOrder() {return Integer.MAX_VALUE;}
}
在这里,我们实现了CallAroundAdvisor接口,并在调用前后添加了日志记录逻辑。此外,我们在getOrder() 方法中返回了最大整数值,因此这个 Advisor 会被排在调用链的最后一个执行。
现在,让我们测试这个新的 Advisor:
@Test
void givenCustomLoggingAdvisor_whenSendPrompt_thenPromptTextAndResponseShouldBeLogged() {CustomLoggingAdvisor customLoggingAdvisor = new CustomLoggingAdvisor();String responseContent = chatClient.prompt().user("Count from 1 to 10").advisors(customLoggingAdvisor).call().content();assertThat(responseContent).contains("1").contains("10");
}
我们已经创建了
CustomLoggingAdvisor并将其附加到了提示(prompt)上。现在让我们看看执行后的日志中发生了什么:
c.b.s.advisors.CustomLoggingAdvisor : Count from 1 to 10
c.b.s.advisors.CustomLoggingAdvisor : 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
正如我们所见,我们的 Advisor 成功地记录了提示文本和聊天响应内容。
7. 总结
在本教程中,我们探讨了一个很棒的Spring AI 功能——Advisors。通过 Advisors,我们获得了聊天记忆功能、敏感词控制以及与向量存储的无缝集成。
此外,我们还可以轻松地创建自定义扩展,以添加特定功能。
使用 Advisors,能够让我们以一致且简洁的方式实现上述所有能力。
私信1v1直连大厂总监解决职业发展问题「免.米」
相关文章:

Spring AI 系列之一个很棒的 Spring AI 功能——Advisors
1. 概述 由AI驱动的应用程序已成为我们的现实。我们正在广泛地实现各种RAG应用程序、提示API,并利用大型语言模型(LLM)创建项目。借助 Spring AI,我们可以更快速地完成这些任务。 在本文中,我们将介绍一个非常有价值…...

Vue3 + TypeScript + el-input 实现人民币金额的输入和显示
输入人民币金额的参数要求: 输入要求: 通过键盘,只允许输入负号、小数点、数字、退格键、删除键、方向左键、方向右键、Home键、End键、Tab键;负号只能在开头;只保留第一个小数点;替换全角输入的小数点&a…...

2.1 C++之条件语句
学习目标: 理解程序的分支逻辑(根据不同条件执行不同代码)。掌握 if-else 和 switch 语句的用法。能编写简单的条件判断程序(如成绩评级、游戏选项等)。 1 条件语句的基本概念 什么是条件语句? 程序在执…...
ZYNQ实战:可编程差分晶振Si570的配置与动态频率切换
为什么需要可编程差分晶振? 在现代FPGA和嵌入式系统中,高速串行通信(如GTP/GTX收发器)对参考时钟的精度和灵活性要求极高。例如,1G以太网需要125MHz时钟,SATA协议需120MHz,而DisplayPort则需135MHz。传统固定频率晶振无法满足多协议动态切换需求,而Si570凭借其10MHz~8…...

Linux `ls` 命令深度解析与高阶应用指南
Linux `ls` 命令深度解析与高阶应用指南 一、核心功能解析1. 基本作用2. 与类似命令对比二、选项系统详解1. 常用基础选项2. 进阶筛选选项三、高阶应用技巧1. 组合过滤查询2. 格式化输出控制3. 元数据深度分析四、企业级应用场景1. 存储空间监控2. 安全审计3. 自动化运维五、特…...

【MPC控制 - 从ACC到自动驾驶】5. 融会贯通:MPC在ACC中的优势总结与知识体系构建
【MPC控制 - 从ACC到自动驾驶】融会贯通:MPC在ACC中的优势总结与知识体系构建 在过去的四天里,我们一起经历了一段奇妙的旅程: Day 1: 我们认识了自适应巡航ACC这位“智能领航员”,并初见了模型预测控制MPC这位“深谋远虑的棋手…...
Day3 记忆内容:map set 高频操作
以下是 第三天 的详细学习内容,聚焦 map和set的高效应用,重点突破查找类题型和去重逻辑,助你提升代码效率! 📚 Day3 记忆内容:map & set 高频操作 1. map 核心操作(手写3遍) /…...

初等数论--Garner‘s 算法
0. 介绍 主要通过混合积的表示来逐步求得同余方程的解。 对于同余方程 { x ≡ v 0 ( m o d m 0 ) x ≡ v 1 ( m o d m 1 ) ⋯ x ≡ v k − 1 ( m o d m k − 1 ) \begin{equation*} \begin{cases} x \equiv v_0 \quad (\ \bmod \ m_0)\\ x \equiv v_1 \quad (\ \bmod \ m_1)…...

NV211NV212美光科技颗粒NV219NV220
NV211NV212美光科技颗粒NV219NV220 技术架构解析:从颗粒到存储系统 近期美光科技发布的NV211、NV212、NV219、NV220系列固态颗粒,凭借其技术突破引发行业关注。这些颗粒基于176层QLC堆叠工艺,单Die容量预计在2026年可达1Tb,相当…...

SQL解析工具JSQLParser
目录 一、引言二、JSQLParser常见类2.1 Class Diagram2.2 Statement2.3 Expression2.4 Select2.5 Update2.6 Delete2.7 Insert2.8 PlainSelect2.9 SetOperationList2.10 ParenthesedSelect2.11 FromItem2.12 Table2.13 ParenthesedFromItem2.14 SelectItem2.15 BinaryExpressio…...

Wave Terminal + Cpolar:SSH远程访问的跨平台实战+内网穿透配置全解析
文章目录 前言1. Wave Terminal安装2. 简单使用演示3. 连接本地Linux服务器3.1 Ubuntu系统安装ssh服务3.2 远程ssh连接Ubuntu 4. 安装内网穿透工具4.1 创建公网地址4.2 使用公网地址远程ssh连接 5. 配置固定公网地址 前言 各位开发者朋友,今天为您介绍一款颠覆性操…...

html使用JS实现账号密码登录的简单案例
目录 案例需求 思路 错误案例及问题 修改思路 案例提供 所需要的组件 <input>标签,<button>标签,<script>标签 详情使用参考:HTML 教程 | 菜鸟教程 案例需求 编写一个程序,最多允许用户尝试登录 3 次。…...
sorted() 函数和sort()函数的区别
在Python中,sorted() 函数和列表的 sort() 方法都用于排序,但它们之间有一些关键的区别: 返回值: sorted():返回一个新的列表,包含所有排序后的元素,原始列表不会被修改。sort():对列…...
Solr搜索:比传统数据库强在哪?
Solr 是一个基于 Apache Lucene 的开源搜索平台,广泛用于全文检索和数据分析。与传统的关系型数据库查询相比,Solr 在某些方面具有明显的优势,特别是在处理大规模文本数据和复杂的搜索需求时。以下是 Solr 相对于传统数据库查询的主要优势&am…...

【数据集】基于ubESTARFM法的100m 地温LST数据集(澳大利亚)
目录 数据概述一、输入数据与处理二、融合算法1. ESTARFM(Enhanced STARFM)2. ubESTARFM(Unbiased ESTARFM)代码实现数据下载参考根据论文《Generating daily 100 m resolution land surface temperature estimates continentally using an unbiased spatiotemporal fusion…...

51c自动驾驶~合集55
我自己的原文哦~ https://blog.51cto.com/whaosoft/13935858 #Challenger 端到端碰撞率暴增!清华&吉利,框架:低成本自动生成复杂对抗性驾驶场景~ 自动驾驶系统在对抗性场景(Adversarial Scenarios)中的可靠性是安全落…...

【前端基础】Promise 详解
文章目录 什么是 Promise?为什么要使用 Promise?创建 Promise消费 Promise (使用 Promise)1. .then(onFulfilled, onRejected)2. .catch(onRejected)3. .finally(onFinally) Promise 链 (Promise Chaining)Promise 的静态方法1. Promise.resolve(value)2…...

高性能管线式HTTP请求
高性能管线式HTTP请求:原理、实现与实践 目录 高性能管线式HTTP请求:原理、实现与实践 1. HTTP管线化的原理与优势 1.1 HTTP管线化的基本概念 关键特性: 1.2 管线化的优势 1.3 管线化的挑战 2. 高性能管线式HTTP请求的实现方案 2.1 技术选型与工具 2.2 Java实现:…...
c/c++的opencv膨胀
使用 OpenCV (C) 进行图像膨胀操作详解 图像膨胀 (Dilation) 是形态学图像处理中的另一种基本操作,与腐蚀操作相对应。它通常用于填充图像中的小孔洞、连接断开的物体部分、以及加粗二值图像中的物体。本文将详细介绍膨胀的原理,并演示如何使用 C 和 Op…...
react native搭建项目
React Native 项目搭建指南 React Native 是一个使用 JavaScript 和 React 构建跨平台移动应用的框架。以下是搭建 React Native 项目的详细步骤: 1. 环境准备 安装 Node.js 下载并安装 Node.js (推荐 LTS 版本) 安装 Java Development Kit (JDK) 对于 Androi…...

【CSS】九宫格布局
CSS Grid布局(推荐) 实现代码: <!doctype html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0"…...

Python用Transformer、Prophet、RNN、LSTM、SARIMAX时间序列预测分析用电量、销售、交通事故数据
原文链接: tecdat.cn/?p42219 在数据驱动决策的时代,时间序列预测作为揭示数据时序规律的核心技术,已成为各行业解决预测需求的关键工具。从能源消耗趋势分析到公共安全事件预测,不同领域的数据特征对预测模型的适应性提出了差异…...

java基础(面向对象进阶高级)泛型(API一)
认识泛型 泛型就等于一个标签(比如男厕所和女厕) 泛型类 只能加字符串: 把别人写好的东西,自己封装。 泛型接口 泛型方法、泛型通配符、上下限 怎么解决下面的问题? API object类 toString: equals: objects类 包装类 为什么上面的Integer爆红…...

学习心得(17--18)Flask表单
一. 认识表单:定义表单类 password2中末端的EqualTo(password)是将密码2与密码1进行验证,看是否相同 二.使用表单: 运行 如果遇到这个报错,就在该页面去添加 下面是举例: 这就是在前端的展示效…...
AI测试和敏捷测试有什么联系与区别?
AI测试与敏捷测试作为软件质量保障领域的两种重要方法,既有紧密联系也存在显著区别。以下是两者的联系与区别分析: 一、联系 共同目标:提升测试效率与质量 敏捷测试强调通过快速迭代、持续反馈和团队协作确保交付价值,而AI测试通…...

微信小程序进阶第2篇__事件类型_冒泡_非冒泡
在小程序中, 事件分为两种类型: 冒泡事件, 当一个组件上的事件被触发后,该事件会向父节点传递非冒泡事件, 当一个组件上的事件被触发后, 该事件不会向父节点传递。 一 冒泡事件 tap, touchst…...

电机控制学习笔记
文章目录 前言一、电机二、编码器三、开环控制和闭环控制总结 前言 学习了解电机控制技术的一些原理和使用的方法。 一、电机 直流有刷电机 操作简单 使用H桥驱动直流有刷电机 直流有刷电机驱动板 电压检测 电流检测以及温度检测 直流无刷电机 使用方波或者正弦波进行换向…...
什么是前端工程化?它有什么意义
前端工程化是指通过工具、流程和规范,将前端开发从手工化、碎片化的模式转变为系统化、自动化和标准化的生产过程。其核心目标是 提升开发效率、保障代码质量、增强项目可维护性,并适应现代复杂 Web 应用的需求。 一、前端工程化的核心内容 1. 模块化开发 代码模块化:使用 …...

企业网站架构部署与优化-Nginx性能调优与深度监控
目录 #1.1Nginx性能调优 1.1.1更改进程数与连接数 1.1.2静态缓存功能设置 1.1.3设置连接超时 1.1.4日志切割 1.1.5配置网页压缩 #2.1nginx的深度监控 2.1.1GoAccess简介 2.1.2nginx vts简介 1.1Nginx性能调优 1.1.1更改进程数与连接数 (1)进程数 进程数…...

行列式的线性性质(仅限于单一行的加法拆分)
当然可以,以下是经过排版优化后的内容,保持了原始内容不变,仅调整了格式以提升可读性: 行列式的线性性质(加法拆分) 这个性质说的是:如果行列式的某一行(或某一列)的所有…...