JB5-1-SpringAI(一)
Java道经第5卷 - 第1阶 - SpringAI(一)
传送门:JB5-1-SpringAI(一)
传送门:JB5-2-SpringAI(二)
文章目录
- S01. SpringAI入门
- E01. SpringAI概念
- 1. 传统VS生成式
- 2. SpringAI功能
- 3. 获取API_KEY
- E02. 整合后端项目
- 1. 添加三方依赖
- 2. 开发主配文件
- 3. 开发启动类
- 4. 开发控制器
- E03. 整合前端项目
- 1. 添加Axios依赖
- 2. 添加ElementPlus
- 3. 开发全局样式
- 4. 开发Vue页面
- S02. SpringAI初级
- E01. 预设角色
- 1. 开发控制器
- E02. 日志通知
- 1. 开发主配文件
- 2. 开发控制器
- E03. 自定义通知
- 1. 开发通知类
- 2. 开发控制器
- E04. 记忆通知
- 1. 开发控制器
- E05. 工具调用
- 1. 开发配置类
- 2. 开发控制器
- E06. 检索增强
- 1. 开发检索文件
- 2. 开发配置类
- 3. 开发控制器
- E07. 文生图
- 1. 开发控制器
心法:本章使用 Maven 父子结构项目进行练习
练习项目结构如下:
|_ v5-1-llm-springai |_ 5501 test |_ 6501 springai-web
武技:搭建练习项目结构
- 创建父项目 v5-1-llm-springai,删除 src 目录。
- 在父项目中锁定版本:注意目前 SpringAI 支持 SpringBoot3.2.x 和 SpringBoot 3.3.x 版本:
17 17 UTF-8 4.13.2 1.18.24 5.8.25 3.2.5 1.0.0-M6 1.0.0-M6.1 1.0.0-M7
- 在父项目中配置仓库:因为 Spring 官方目前并没有将 spring-ai 相关的包发布到 Maven 的中央仓库中,所以需要使用 标签单独配置特定的,可能包含 spring-ai 依赖的仓库,参考官方文档 最终配置如下:
spring-snapshots Spring Snapshots https://repo.spring.io/snapshot false Central Portal Snapshots central-portal-snapshots https://central.sonatype.com/repository/maven-snapshots/ false true spring-milestones Spring Milestones https://repo.spring.io/milestone false
- 在父项目中管理依赖:需要同时管理 SpringBoot 依赖和 SpringAI 依赖:
org.springframework.boot spring-boot-starter-parent ${springboot.version} pom import org.springframework.ai spring-ai-bom ${spring-ai-bom.version} pom import
- 在父项目中引入通用依赖:
junit junit ${junit.version} test org.projectlombok lombok ${lombok.version} provided cn.hutool hutool-all ${hutool-all.version}
- 创建子项目 test,不引入任何依赖。
S01. SpringAI入门
E01. SpringAI概念
心法:SpringAI 项目致力于简化集成人工智能功能的应用程序开发流程,解决了 AI 集成的问题,避免引入不必要的复杂性,它从 LangChain 和 LlamaIndex 等知名 Python 项目中获取灵感,但它并非这些项目的直接移植版本。
SpringAI 理念:下一波生成式 AI 应用程序不会局限于 Python 开发者群体,而是会在多种编程语言中广泛普及。
SpringAI 特点:
- API 支持:提供跨 AI 提供商的可移植 API,支持聊天、文本到图像、嵌入等模型,具备同步和流式处理 API 选项。
- 模型支持:涵盖所有主流的 AI 模型提供商,如 OpenAI、Microsoft、Amazon、Google、Ollama 等,支持的模型类型包括聊天完成、嵌入、文本到图像、音频转录、文本到语音、适度、结构化输出等。
- 数据库支持:适配众多主流矢量数据库提供商,如 Apache Cassandra、Azure Vector Search 等。
- SpringBoot 支持:通过 SpringBoot 自动配置和启动器,方便在项目中快速集成 AI 模型和向量存储。
SpringAI 交互流程:核心包括 Application 应用程序和 Generative AI 生成式人工智能两个角色:
- To There:App 将自身的数据和通过 API 获取的信息发送给 AI,供其处理和利用。
- To Here:AI 处理完信息后,将结果返回给应用程序。
1. 传统VS生成式
心法:生成式人工智能是能依据提示生成文本、图像、音频、视频、软件代码等全新内容的人工智能。
传统的 AI 和 生成式 AI 对比如下:
技术原理方面:
- 传统的 AI:常基于规则和算法,通过对大量标注数据的学习来提取特征,实现分类、预测等任务。
- 生成式 AI:以生成模型为核心,通过对海量数据的无监督或半监督学习,掌握数据内在模式和分布,进而生成新数据。
功能特点方面:
- 传统的 AI:擅长执行特定任务,像语音识别、图像识别、医疗影像诊断、金融风险预测等,专注在已知模式和规则下对输入数据分类、判断和预测,不具备内容创造能力。
- 生成式 AI:突出特点是创造新内容,涵盖文本、图像、音频、视频等,还能进行代码补全、场景模拟等。例如根据文本描述生成对应图像,或依据简单旋律拓展成完整乐曲。
应用场景方面:
- 传统的 AI:广泛用于需要精准判断和预测的领域,比如在安防监控中识别异常行为,电商推荐系统依据用户行为和偏好推荐商品,工业生产中检测产品缺陷等。
- 生成式 AI:多用于创意和内容生成领域,比如广告营销生成创意文案和设计,游戏开发自动生成地图、角色和剧情,影视制作生成特效和虚拟场景。
2. SpringAI功能
心法:目前 SpringAI 包基础会话,会话记忆,RAG 增强,通知助手,工具调用,模型评估,数据提取和模型观察等核心功能。
基础会话功能 ChatClient API:通过这个 API,你可以方便地向 AI 聊天模型发送消息,并且接收它回复的消息,就像在和一个真实的人聊天一样。
会话记忆功能 Chat Conversation Memory:该功能可以让聊天机器人记住之前的对话内容,就像人有记忆一样,这样在新的对话中,它就能根据之前的交流来更好地回答问题。
RAG 增强功能 Retrieval Augmented Generation:该功能就像是给聊天机器人一个知识宝库,当它回答问题时,不仅可以依靠自己学到的知识,还能从这个宝库里快速找到相关的信息来丰富答案,让回答更准确、更全面。
通知助手功能 Advisors API:该功能就像是一个智能顾问助手,可以在 Spring 应用程序中对 AI 驱动的交互进行干预和优化,提供灵活且强大的支持,包括拦截请求、修改参数以及增强交互效果等,以满足不同的业务需求和优化应用程序的 AI 功能。
工具调用功能 Tools/Function Calling:该功能可以让模型能按需请求执行客户端工具函数获取实时信息:
- 比如 AI 模型觉得需要知道今天的天气,于是它请求执行一个获取天气信息的函数。
- 这个函数可以是本地开发的,其内部会从相关的数据源(比如天气网站的接口)获取实时的天气数据。
- 然后 AI 模型就能根据这些数据继续完成它的任务,比如给你提供出行建议等。
模型评估功能 AI Model Evaluation:该功能就像一个质检员,专门负责检查 AI 生成的文本、图像等内容是否符合要求,有没有出现幻觉响应,通过 AI 模型评估,我们可以及时发现这些问题,采取措施来改进 AI 模型,让它生成更准确、更有用的内容:
- 幻觉响应:就是 AI 生成了一些不符合事实或者与给定信息不相关的内容。
数据提取功能 ETL framework:支持 ETL框架,即提取(Extract)、转换(Transform)、加载(Load )流程:
- 先把各种文档数据从不同地方提取出来,比如从文件系统、数据库等。
- 接着按照一定规则对文档数据处理转换,像把文本格式统一、提取关键信息。
- 最后把处理好的数据加载到目标存储系统里,方便后续 AI 模型使用这些数据进行训练或推理等操作。
模型观察功能 Observability:可以收集和分析 AI 运行时产生的数据,像模型处理请求的耗时、资源使用量、生成结果的质量等,通过这些数据,开发人员能知道 AI 系统是否正常运行、有没有性能问题,还能找到出错原因,方便优化和改进 AI 应用。
3. 获取API_KEY
武技:获取阿里云百炼的 API_KEY
- 登录 阿里云百炼 页面。
- 依次点击 右上角头像 - API KEY - 创建我的 API KEY,然后将 API KEY 记录下来(SK 开头的),归属业务空间选择默认业务空间即可,描述可省略。
- 回到首页,根据提示开通模型调用服务:
- 将API-KEY设置到环境变量:
# 设置系统环境变量 setx DASHSCOPE_API_KEY sk-xxxxxxx # 检查 API-KEY 的环境变量是否生效 echo %DASHSCOPE_API_KEY%
E02. 整合后端项目
武技:在 test 子项目中整合 SpringAI
1. 添加三方依赖
org.springframework.boot spring-boot-starter-web com.alibaba.cloud.ai spring-ai-alibaba-starter ${spring-ai-alibaba-starter.version}
2. 开发主配文件
server: port: 5501 # 端口号 spring: application: name: test # 项目名称 ai: dashscope: api-key: ${DASHSCOPE_API_KEY} # 阿里云百炼 API_KEY
3. 开发启动类
package com.joezhou; /** @author 周航宇 */ @SpringBootApplication public class SpringAiApp { public static void main(String[] args) { SpringApplication.run(SpringAiApp.class, args); } }
4. 开发控制器
心法:ChatClient 用于与 AI 模型通信,支持同步和流式编程模型。
- 开发控制器:推荐使用 ChatClient.Builder.build() 的方式创建 ChatClient 客户端对象:
package com.joezhou.controller; /** @author 周航宇 */ @RequestMapping("/api/v1/aiChat") @RestController @CrossOrigin public class AiChatController { private final ChatClient chatClient; public AiChatController(ChatClient.Builder chatClientBuilder) { this.chatClient = chatClientBuilder.build(); } }
- 开发控制方法 base():
@GetMapping("base") public String base(@RequestParam("msg") String msg) { return chatClient // 初始化新会话并创建一个包含消息上下文的提示对象,可以理解为“开启新对话” .prompt() // 设置客户端传递过来的对话内容 .user(msg.trim()) // 同步发送消息,此时该方法会将响应结果一次性响应给前端 .call() // 获取响应消息,此时需要将这个消息返回给前端 .content(); }
- 测试 base() 控制方法:
### base() GET http://localhost:5501/api/v1/aiChat/base?msg=讲个笑话
- 开发控制方法 stream():
/** * 流式对话中,控制方法的返回值类型需改更改为 Flux 类型 * 流式对话中,需要在 @RequestMapping 注解中指定 produces=MediaType.TEXT_EVENT_STREAM_VALUE 项 */ @GetMapping(value = "stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux stream(@RequestParam("msg") String msg) { Flux content = chatClient // 初始化新会话并创建一个包含消息上下文的提示对象,可以理解为“开启新对话” .prompt() // 设置客户端传递过来的对话内容 .user(msg.trim()) // 流式响应,将响应结果以流的形式推送给前端 .stream() // 获取响应消息,此时需要将这个消息返回给前端 .content(); // 约定返回一个结束标记,方便前端灵活终止 SSE 推送 return content.concatWith(Flux.just("[over]")); }
- 测试 stream() 控制方法:
### stream():中文可能会乱码,可以暂时使用英文对话进行测试。 GET http://localhost:5501/api/v1/aiChat/stream?msg=用纯英文讲个笑话
E03. 整合前端项目
武技:创建 v5-1-llm-springai/springai-web 项目
- 使用 vite 创建 vue 项目:
# 切换到工作空间目录,注意路径中不要有中文 cd D:\workspace\java\v5-1-llm-springai # 创建Vue项目(第一遍安装需要输入y安装vite) npm create vite@5.5.1 springai-web -- --template vue
- 在 vite.config.js 文件中修改项目端口:
import {defineConfig} from 'vite' import vue from '@vitejs/plugin-vue' // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], server: { host: 'localhost',//ip地址 port: 6051, // 设置服务启动端口号 } })
1. 添加Axios依赖
# 安装Axios组件组件 npm install axios@1.6.7 --save
2. 添加ElementPlus
- 安装 ElementPlus 依赖:
# 局部安装ElementPlus组件库 npm install element-plus@2.5.3 --save
- 在 main.js 文件中引入 ElementPlus 依赖:
import { createApp } from 'vue'; import './style.css'; import App from './App.vue'; // ElementPlus组件库: 核心对象,核心CSS,显隐CSS,国际化对象 import ElementPlus from 'element-plus'; import 'element-plus/dist/index.css'; import 'element-plus/theme-chalk/display.css'; import {zhCn} from "element-plus/es/locale/index"; // 使用ElementPlus组件库 let app = createApp(App); app.use(ElementPlus, {locale: zhCn}); app.mount('#app');
3. 开发全局样式
- 在 style.css 文件中开发全局样式:
body { margin: 0; /* 外边距 */ padding: 20px 20%; /* 内边距 */ } .timeline { background-color: #F5F5F5; /* 背景颜色 */ padding-top: 20px; /* 内边距 */ padding-right: 40px; /* 内边距 */ border-radius: 10px; /* 圆角 */ box-sizing: border-box; /* 忽略边框和内边距影响 */ height: 75vh; /* 75% 视窗高度 */ overflow-y: auto; /* 超出部分显示滚动条 */ } .el-checkbox { margin-top: 40px; /* 外边距 */ margin-right: 0 !important; /* 外边距 */ } .image { width: 200px; /* 宽度 */ height: 200px; /* 高度 */ } .send-ipt { height: 60px; /* 高度 */ } .userContent { background-color: #f1d2d2; /* 背景颜色 */ display: inline-block; /* 内联块元素 */ padding: 10px 20px; /* 内边距 */ border-radius: 10px; /* 圆角 */ }
4. 开发Vue页面
- 布局:开发 HTML 代码如下:
{{ e.content }}{{ e.content }}- 脚本:开发 JS 代码如下:
相关 API 方法 描述 sendMsg() 当用户发送消息时触发 baseQA(url) 向指定 URL 发送消息,使用 AJAX 接收同步响应内容 streamQA(url) 向指定 URL 发送消息,使用 EventSource 接收流式响应内容 imageQA(url) 向指定 URL 发送消息,使用 URL.createObjectURL() 接收二进制响应内容 import {ref} from "vue"; import axios from "axios"; // AI接口地址 const API_URL = 'http://localhost:5501/api/v1/aiChat/base'; // 设置超时时间为 300 秒,因为文生图比较慢,需要等待较长时间 const AJAX = axios.create({timeout: 300000}); // AI欢迎词配置 let activities = ref([{content: '我是AI,有何指教?', timestamp: now(), role: 'ai'}]); // 用户输入的内容 let msg = ref(''); // AI是否正在回复中 let isReplying = false; // 是否开启流式问答 let enableStream = ref(true); // 是否开启文生图 let enableImage = ref(false); // 时间线对象 let timeline; // 当用户发送消息时触发 function sendMsg() { // 如果用户输入的内容为空,则不处理 if (msg.value === '') return; // 如果AI正在回复中,则不处理 if (isReplying) return; // 将AI回复中标记为true isReplying = true; // 加入用户输入的信息和AI回复的信息 activities.value.push({content: msg.value, timestamp: now(), role: 'user'}); activities.value.push({content: 'waiting...', timestamp: now(), role: 'ai'}); // 文生图 if (enableImage.value) imageQA(API_URL); // 流式问答 else if (enableStream.value) streamQA(API_URL.replace('base', 'stream')); // 基础问答 else baseQA(API_URL); // 清空用户输入的内容 msg.value = ''; } /*========== 基础对话 ==========*/ async function baseQA(url) { // 发送请求 let res = await AJAX.get(`${url}?msg=${msg.value}`); // 如果请求成功,则加入AI回复的信息,如果请求失败,则加入错误信息 activities.value[activities.value.length - 1].content = res.status === 200 ? res.data : '请求失败,请稍后重试!'; // 始终滚动到底部 timeline.scrollTop = timeline.scrollHeight - timeline.clientHeight; // 将AI回复中标记为false isReplying = false; } /*========== 流式对话 ==========*/ // SSE客户端对象:用于接收服务端推送的消息 let sse; function streamQA(url) { // 关闭上一个SSE连接 if (sse) sse.close(); // SSE服务端推送时 sse = new EventSource(`${url}?msg=${msg.value}`); // SSE客户端接收到消息时 sse.onmessage = (ev) => { // 如果读取到 [over] 结束标记,则关闭 SSE 连接,否则1秒执行一次 if (ev.data === '[over]') { sse.close(); isReplying = false; return; } // 拼接AI回复的信息 activities.value[activities.value.length - 1].content += ev.data; // 始终滚动到底部 timeline.scrollTop = timeline.scrollHeight - timeline.clientHeight; }; // SSE连接成功时 sse.onopen = () => activities.value[activities.value.length - 1].content = ''; } /*========== 文生图 ==========*/ async function imageQA(url) { // 发送请求 let res = await AJAX.get(`${url}?msg=${msg.value}`, {responseType: 'blob'}); // 如果请求成功,则加入AI回复的图片 if (res.status === 200) { activities.value[activities.value.length - 1].type = 'image'; activities.value[activities.value.length - 1].content = ""; activities.value[activities.value.length - 1].content += "图片已生成!"; activities.value[activities.value.length - 1].src = URL.createObjectURL(res.data); // 始终滚动到底部 timeline.scrollTop = timeline.scrollHeight - timeline.clientHeight; // 将AI回复中标记为false isReplying = false; } } // 获取当前时间 function now() { let now = new Date(); return now.toLocaleDateString() + " " + now.toLocaleTimeString(); } onload = () => timeline = document.querySelector("#timeline");
S02. SpringAI初级
E01. 预设角色
心法:在使用 ChatClient.Builder 构建 ChatClient 时,可以使用 builder.defaultSystem() 方法设置预设的系统信息(字符串),该信息通常会作为初始指令传递给 AI 模型,帮助其理解对话的上下文、角色或任务要求。
注意事项:
- 系统信息通常是纯文本,但某些 API 可能支持 Markdown 或其他格式(需查阅具体文档)。
- 避免使用特殊字符或格式,除非 API 明确支持。
- 系统信息会应用于所有对话轮次,除非在单次请求中覆盖它。
- 某些 API 可能对系统信息的长度有限制(例如 OpenAI 的 GPT 模型建议控制在几百个 token 内)。
- 不要在系统信息中包含敏感信息(如 API 密钥、用户数据等)。
武技:测试 SpringAI 的预设角色功能
1. 开发控制器
- 开发控制器类:
package com.joezhou.controller; /** @author 周航宇 */ @RequestMapping("/api/v1/systemRole") @RestController @CrossOrigin public class SystemRoleController { private static final String SYSTEM_ROLE_PROMPT = """ 你叫詹姆斯9527,你是一个脾气非常不好的人; 你从来不会用 “我” 来指代自己,你只会用 “老子” 来指代自己; 今天的日期是 {today}; """; private final ChatClient chatClient; public SystemRoleController(ChatClient.Builder chatClientBuilder) { this.chatClient = chatClientBuilder .defaultSystem(SYSTEM_ROLE_PROMPT) .build(); } @GetMapping("base") public String base(@RequestParam("msg") String msg) { return chatClient .prompt() .user(msg) .system(e -> e.param("today", LocalDate.now())) .call() .content(); } @GetMapping(value = "stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux stream(@RequestParam("msg") String msg) { return chatClient .prompt() .user(msg) .system(e -> e.param("today", LocalDate.now())) .stream() .content() .concatWith(Flux.just("[over]")); } }
- 测试控制类:
### base() GET http://localhost:5501/api/v1/systemRole/base?msg=你是谁啊 ### stream() GET http://localhost:5501/api/v1/systemRole/stream?msg=用纯英文告诉我你是谁啊
- 在 App.vue 文件中,将前端代码替换为对应路径进行测试:
// AI接口地址 const URL = 'http://localhost:5501/api/v1/systemRole/base';
E02. 日志通知
心法:SimpleLoggerAdvisor 是一个极为实用的 AOP(面向切面编程)增强器,它的主要功能是自动记录 AI 服务交互过程中的请求(Request)和响应(Response)数据,常用于开发调试,生产监控,合规审计等场景。
SimpleLoggerAdvisor 核心特性如下:
- 全链路数据捕获:
- 能够精准记录请求到 AI 服务的完整提示信息(包含用户消息、系统消息、历史对话等内容)。
- 可以详细记录从 AI 服务返回的响应数据,涵盖生成的文本、元数据(像 token 数量、模型信息)以及其他相关信息。
- 灵活的日志级别控制:
- 支持根据不同的日志级别(例如 DEBUG、INFO)来开启或关闭日志记录功能。
- 可以对日志的格式进行自定义设置,从而满足不同的调试和审计要求。
- 非侵入式设计:
- 采用 AOP 切面方式实现,不会对现有的业务逻辑造成影响。
- 能够通过简单的配置进行启用或禁用操作。
SimpleLoggerAdvisor 具体工作流程如下:
- 在使用 ChatClient.Builder 构建 ChatClient 时,使用 builder.defaultAdvisors() 方法设置默认的 SimpleLoggerAdvisor 实例。
- 在发送请求之前,SimpleLoggerAdvisor 会捕获并记录所有即将发送给 AI 服务的消息内容。
- 在收到响应之后,SimpleLoggerAdvisor 会提取并记录响应中的关键信息。
- 这些记录的信息会被输出到应用的日志系统中(如 SLF4J、Logback 等),方便后续查看和分析。
武技:添加日志通知
1. 开发主配文件
心法:想要使用日志通知,则需要将 advisor 包的日志记录级别设置为 DEBUG 级别。
logging: level: org: springframework: ai: chat: client: advisor: DEBUG # advisor 日志级别
2. 开发控制器
- 开发控制器:
package com.joezhou.controller; /** @author 周航宇 */ @RequestMapping("/api/v1/logAdvisor") @RestController @CrossOrigin public class LogAdvisorController { private final ChatClient chatClient; public LogAdvisorController(ChatClient.Builder chatClientBuilder) { this.chatClient = chatClientBuilder // p1:请求日志打印内容(lambda 方式) // p2:响应日志打印内容(lambda 方式) // p3:通知顺序,值越小越靠前 .defaultAdvisors(new SimpleLoggerAdvisor( req -> req.userText(), res -> res.getResult().getOutput().getText(), 0 )) .build(); } @GetMapping(value = "base") public String base(@RequestParam("msg") String msg) { return chatClient .prompt() .user(msg) .call() .content(); } @GetMapping(value = "stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux stream(@RequestParam("msg") String msg) { return chatClient .prompt() .user(msg) .stream() .content().concatWith(Flux.just("[over]")); } }
- 测试控制器:发送请求后,观察控制台是否记录了对应的请求和响应的日志:
### base():发送请求后,观察控制台是否记录了对应的请求和响应的日志 GET http://localhost:5501/api/v1/logAdvisor/base?msg=讲个笑话 ### stream():发送请求后,观察控制台是否记录了对应的请求和响应的日志 GET http://localhost:5501/api/v1/logAdvisor/stream?msg=用纯英文讲个笑话
E03. 自定义通知
心法:Advisors API 提供了一种灵活而强大的方法来拦截、修改和增强 Spring 应用程序中的 AI 驱动的交互,它允许在 ChatClient 执行 call() 或 stream() 等方法时自动触发特定行为。
Advisors 核心组件如下:
核心组件 描述 CallAroundAdvisor 非流式环绕通知器 CallAroundAdvisorChain 非流式环绕通知链 StreamAroundAdvisor 流式环绕通知器 StreamAroundAdvisorChain 流式环绕通知链 AdvisedRequest 建议请求对象,和 AdvisedResponse 共享一个上下文对象 AdvisedResponse 建议响应对象,和 AdvisedRequest 共享一个上下文对象 Advisors 执行流程如下:
- SpringAI 框架根据用户的提示(Prompt)创建一个建议请求对象(AdvisedRequest)对象,并附带一个空的通知上下文对象(AdvisorContext)。
- 通知链(Advisor Chain)中的每个通知(Advisor)都会处理或修改该请求:
- SpringAI 框架中,后一个通知(Adivsor)会将请求发送给聊天模型(Chat Model)。
- 聊天模型(Chat Model)返回聊天响应(ChatResponse)给通知链(Advisor Chain)并转换为建议响应对象(AdvisedResponse),该对象中包含共享的通知上下文对象(AdvisorContext)实例。
- 通知链(Advisor Chain)中的每个通知(Advisor)都会处理或修改该响应。
- 最终将建议响应对象(AdvisedResponse)转换为聊天响应(ChatResponse)并返回给客户端。
武技:开发并使用自定义通知类
1. 开发通知类
自定义通知类要求如下:
- 重写 CallAroundAdvisor -> aroundCall():对应 chatClient 的 call() 方法的环绕通知。
- 重写 StreamAroundAdvisor -> aroundStream():对应 chatClient 的 stream() 方法的环绕通知。
- 重写 Advisor -> getName():用于获取唯一的 Advisor 名称。
- 重写 Ordered -> getOrder():用于设置在通知链中 Advisor 的执行顺序,数字越小越靠前。
package com.joezhou.advisor; /** @author 周航宇 */ @SuppressWarnings("all") @Slf4j public class MyAdvisor implements CallAroundAdvisor, StreamAroundAdvisor { /** * 对应 chatClient 的 call() 方法的环绕通知 * * @param advisedRequest 请求对象 * @param chain 通知链,用于管理这些通知的执行顺序 * @return 响应对象 */ @Override public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) { log.info("进入 aroundCall() 方法"); log.info("请求参数:{}", advisedRequest.userText()); // 继续执行通知链中的下一个通知 // 如果已经是最后一个通知,则会调用目标方法 // 并将目标方法的执行结果封装在 AdvisedResponse 中返回 AdvisedResponse advisedResponse = chain.nextAroundCall(advisedRequest); log.info("响应结果:{}", advisedResponse .response() .getResult() .getOutput() .getText()); log.info("离开 aroundCall() 方法"); return advisedResponse; } /** * 对应 chatClient 的 stream() 方法的环绕通知 * * @param advisedRequest 请求对象 * @param chain 通知链,用于管理这些通知的执行顺序 * @return 响应对象 */ @Override public Flux aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) { log.info("进入 aroundStream() 方法"); log.info("请求参数:{}", advisedRequest.userText()); // 继续执行通知链中的下一个通知 // 如果已经是最后一个通知,则会调用目标方法 // 并将目标方法的执行结果封装在 Flux 中返回。 Flux advisedResponseFlux = chain.nextAroundStream(advisedRequest); log.info("离开 aroundStream() 方法"); advisedResponseFlux = advisedResponseFlux.doOnNext(advisedResponse -> { log.info("响应结果:{}", advisedResponse .response() .getResult() .getOutput() .getText()); }); return advisedResponseFlux; } /** 唯一的 Advisor 名称 */ @Override public String getName() { return this.getClass().getSimpleName(); } /** 在通知链中 Advisor 的执行顺序,数字越小越靠前 */ @Override public int getOrder() { return 0; } }
2. 开发控制器
- 开发控制器:
package com.joezhou.controller; /** @author 周航宇 */ @RestController @CrossOrigin @RequestMapping("/api/v1/myAdvisor") public class MyAdvisorController { private final ChatClient chatClient; public MyAdvisorController(ChatClient.Builder chatClientBuilder) { this.chatClient = chatClientBuilder .defaultAdvisors(new MyAdvisor()) .build(); } @GetMapping("base") public String base(@RequestParam("msg") String msg) { return chatClient .prompt() .user(msg) .call() .content(); } @GetMapping(value = "stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux stream(@RequestParam("msg") String msg) { return chatClient .prompt() .user(msg) .stream() .content() .concatWith(Flux.just("[over]")); } }
- 测试控制器:发送请求后,观察控制台是否记录了对应的请求和响应的日志:
### base():发送请求后,观察控制台是否记录了对应的请求和响应的日志 GET http://localhost:5501/api/v1/myAdvisor/base?msg=讲个笑话 ### stream():发送请求后,观察控制台是否记录了对应的请求和响应的日志 GET http://localhost:5501/api/v1/myAdvisor/stream?msg=用纯英文讲个笑话
E04. 记忆通知
心法:ChatMemory 接口用于存储聊天对话历史记录,它提供了添加消息到对话、从对话中检索消息和清除对话历史记录的方法。
ChatMemory 常用实现:
- InMemoryChatMemory:基于内存存储:
- 线程安全:底层使用 CopyOnWriteArrayList 实现,线程安全。
- 临时存储:应用重启后数据丢失,适用于短期会话。
- 简单轻量:无需外部依赖,直接使用 JVM 内存。
- 典型场景:测试环境或开发调试。
- LimitedSizeChatMemory:基于内存存储:
- 容量限制:通过 maxMessages 参数控制历史消息数量(如保留最近 10 条)。
- 自动截断:超出容量时移除最早的消息,避免内存溢出。
- 适配性强:可包装其他实现,如 InMemoryChatMemory 等。
- 典型场景:需控制内存使用的生产环境。
- RedisChatMemory:基于 Redis 存储:
- 分布式共享:支持多实例应用共享会话历史。
- 持久化:数据可配置为持久化存储(如 RDB/AOF)。
- 高性能:基于内存的缓存,读写速度快。
- 典型场景:微服务架构或需要长时间保留对话历史的应用。
- JdbcChatMemory:基于关系型数据库:
- 结构化存储:消息存储在数据库表中,支持复杂查询。
- 事务支持:与数据库事务集成,确保数据一致性。
- 扩展性:可利用数据库索引和备份机制。
- 典型场景:需要深度分析对话数据的应用(如客服系统)。
- SessionChatMemory:基于 Spring 会话:
- Web 集成:自动与用户 HTTP 会话绑定。
- 会话生命周期:数据随会话过期而清除。
- 适配性:依赖 Spring Web 模块,需配置 HttpSession 对象。
- 典型场景:Web 应用中的用户对话管理。
- CompositeChatMemory:组合多个 ChatMemory 实现:
- 多策略支持:同时使用多种存储方式(如内存 + Redis)。
- 分层存储:例如,近期消息存内存,历史消息存数据库。
- 自定义路由:通过策略决定消息存储位置。
- 典型场景:混合性能与持久化需求的场景。
ChatMemory 使用流程:
- 创建 InMemoryChatMemory 实例:仅做测试。
- 创建 PromptChatMemoryAdvisor 实例:它是一个专门负责自动管理对话历史的 Advisor:
- 在发送请求前,自动从 ChatMemory 中提取历史消息并添加到请求的 messages 列表中。
- 在收到响应后,自动将新生成的消息(用户消息和助手回复)保存到 ChatMemory 中。
- 在使用 ChatClient.Builder 构建 ChatClient 时,使用 builder.defaultAdvisors() 方法设置默认的 Advisor 对象。
- 使用 chatClient.advisors(e -> e.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10)) 设置记忆大小。
武技:测试使用记忆通知功能
1. 开发控制器
- 开发控制器类:
package com.joezhou.controller; /** @author 周航宇 */ @RequestMapping("/api/v1/chatMemory") @RestController @CrossOrigin public class ChatMemoryController { private final ChatClient chatClient; public ChatMemoryController(ChatClient.Builder chatClientBuilder) { this.chatClient = chatClientBuilder .defaultAdvisors(new PromptChatMemoryAdvisor(new InMemoryChatMemory())) .build(); } @GetMapping("base") public String base(@RequestParam("msg") String msg) { return chatClient .prompt() .user(msg) // 设置记忆大小 .advisors(e -> e.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10)) .call() .content(); } @GetMapping(value = "stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux stream(@RequestParam("msg") String msg) { return chatClient .prompt() .user(msg) // 设置记忆大小 .advisors(e -> e.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10)) .stream() .content() .concatWith(Flux.just("[over]")); } }
- 测试控制类:
### base() - 01 GET http://localhost:5501/api/v1/chatMemory/base?msg=我今年100岁了 ### base() - 02 GET http://localhost:5501/api/v1/chatMemory/base?msg=我多大了 ### stream() - 01 GET http://localhost:5501/api/v1/chatMemory/stream?msg=I am 101 years old. ### stream() - 02 GET http://localhost:5501/api/v1/chatMemory/stream?msg=how old are me?
- 在 App.vue 页面中,将前端代码替换为对应路径进行测试:
// AI接口地址 const URL = 'http://localhost:5501/api/v1/chatMemory/base';
E05. 工具调用
心法:Tool Calling 是一种提升大语言模型(LLM)功能的技术,通过定义一组工具(如当前本地时间查询、当前天气查询等),使 LLM 能够调用这些工具以完成特定任务,其核心思想是将 LLM 的生成能力与外部工具的功能相结合,从而增强其解决问题的能力。
Tools Calling 的主要流程如下:
- 定义工具:通过注解或接口明确工具的功能及其描述。
- 注册工具:将工具集成到 LLM 的调用环境中。
- 调用工具:在用户输入中触发工具调用,LLM 根据工具的描述生成相应的调用请求。
武技:测试工具调用
1. 开发配置类
- 开发函数调用配置类:
相关注解 描述 @Tool 用于将方法标记为可被 AI 调用的工具 @ToolParam 描述方法参数的含义,帮助 AI 理解参数用途 package com.joezhou.tools; /** @author 周航宇 */ public class WeatherTool { @Tool(description = "获取今天的天气") String getWeather(@ToolParam(description = "城市名称") String cityName) { if ("上海".equals(cityName)) { return "今天上海的天气是:晴转多云,再转大雨,再转冰雹,1103摄氏度"; } else if ("北京".equals(cityName)) { return "今天北京的天气是:多云转晴,再转小雨,再转闪电,1104摄氏度。"; } else { return "不知道"; } } }
2. 开发控制器
- 开发控制器:在使用 ChatClient.Builder 构建 ChatClient 时,使用 builder.defaultTools() 方法设置默认的 Tool Calling 对象:
package com.joezhou.controller; /** @author 周航宇 */ @RequestMapping("/api/v1/toolCalling") @RestController @CrossOrigin public class ToolCallingController { private final ChatClient chatClient; public ToolCallingController(ChatClient.Builder chatClientBuilder) { this.chatClient = chatClientBuilder .defaultTools(new WeatherTool()) .build(); } @GetMapping("base") public String base(@RequestParam("msg") String msg) { return chatClient .prompt() .user(msg) .call() .content(); } @GetMapping(value = "stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux stream(@RequestParam("msg") String msg) { return chatClient .prompt() .user(msg) .stream() .content() .concatWith(Flux.just("[over]")); } }
- 测试控制器:
### base() GET http://localhost:5501/api/v1/toolCalling/base?msg=北京天气如何 ### stream() GET http://localhost:5501/api/v1/toolCalling/stream?msg=上海天气如何
- 在 App.vue 文件中,将前端代码替换为对应路径进行测试:
// AI接口地址 const URL = 'http://localhost:5501/api/v1/toolCalling/base';
E06. 检索增强
心法:RAG,全称 Retrieval Augmented Generation,检索增强生成。
RAG 理解:
- RAG 就像给语言模型请了个 “专属小助手”,传统语言模型只能靠提前背好的 “知识点”(预训练知识)回答问题,一旦遇到新知识或特定领域问题,可能就答不上来,或者答案不准确。
- 而 RAG 额外设置了一个 “知识仓库”,当遇到问题时,它先快速从这个仓库(比如外部文档、数据库)里,用高效的检索算法找到和问题最相关的信息,再把这些信息交给语言模型,让模型结合它们给出答案。这样一来,模型不仅能拿到最新鲜、准确的知识,在回答专业问题时也更靠谱,相当于随时有新 “教材” 辅助答题,而不是只靠老本行了。
QuestionAnswerAdvisor:Spring AI 提供的拦截器,负责自动执行 RAG 流程,当调用 chatClient.call() 时,QuestionAnswerAdvisor 会自动执行以下操作:
- 拦截用户发送的问题内容。
- 将用户问题转换为向量(Embedding)。
- 在 VectorStore 中检索相关文档(最相似的文档片段)。
- 将检索结果作为上下文注入 AI 模型的提示(Prompt)中。
- 将增强后的提示被发送给 AI 模型,生成回答时会参考检索到的上下文。
武技:搭建检索增强环境
1. 开发检索文件
- 开发 classpath:rag/company.txt 检索文件如下:
公司成立于1945年10月1号。 公司法人是塔拉。 公司经理是周杰伦。 公司法务代表是林俊杰。 公司销售团队包括赵四,刘能,广坤,王云等。
2. 开发配置类
package com.joezhou.config; /** @author 周航宇 */ @Configuration public class RagConfig { @Autowired private ResourceLoader resourceLoader; /** * 构建一个VectorStore对象,用于存储文档 * * @param embeddingModel 嵌入模型,用于将文本转换为向量 * @return VectorStore对象 */ @SneakyThrows @Bean VectorStore vectorStore(EmbeddingModel embeddingModel) { // 创建一个SimpleVectorStore对象,用于存储文档 SimpleVectorStore simpleVectorStore = SimpleVectorStore.builder(embeddingModel).build(); // 从classpath下加载文件 Resource resource = resourceLoader.getResource("classpath:rag/company.txt"); // 使用Java 8的Stream API读取文件内容 String content = new String( resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8); // 生成一个 RAG 文档 List documents = List.of(new Document(content)); // 将文档添加到SimpleVectorStore中 simpleVectorStore.add(documents); // 返回SimpleVectorStore对象 return simpleVectorStore; } }
3. 开发控制器
- 开发控制器:
package com.joezhou.controller; /** @author 周航宇 */ @RequestMapping("/api/v1/rag") @RestController @CrossOrigin public class RagController { private final ChatClient chatClient; public RagController(ChatClient.Builder chatClientBuilder, VectorStore vectorStore) { this.chatClient = chatClientBuilder .defaultAdvisors(new QuestionAnswerAdvisor(vectorStore)) .build(); } @GetMapping("base") public String base(@RequestParam("msg") String msg) { return chatClient .prompt() .user(msg) .call() .content(); } @GetMapping(value = "stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux stream(@RequestParam("msg") String msg) { return chatClient .prompt() .user(msg) .stream() .content() .concatWith(Flux.just("[over]")); } }
- 测试控制类:
### base() GET http://localhost:5501/api/v1/rag/base?msg=公司成员架构是怎么样的 ### stream() GET http://localhost:5501/api/v1/rag/stream?msg=公司成立时间
- 在 App.vue 文件中,将前端代码替换为对应路径进行测试:
// AI接口地址 const URL = 'http://localhost:5501/api/v1/rag/base';
E07. 文生图
心法:ImageModel 是 SpringAI 提供的,与图像生成模型(如通义万相)交互的服务接口。
文生图流程:
- 使用 ImageOptionsBuilder.builder().build() 创建图像的配置对象:
- width/height:图像尺寸(1024×1024 像素)。
- model:使用的模型(阿里云通义万相的基础文生图模型)。
- N:生成图像的数量(此处为 1 张)。
- 将用户描述和配置封装为 ImagePrompt 对象。
- 调用模型服务生成图像,返回 ImageResponse 响应对象。
- 检查结果列表是否为空(防止无生成结果)。
- 获取第一个生成图像的 URL(模型通常返回临时存储的 URL)。
- 从 URL 下载图像到内存(BufferedImage)。
- 通过 HttpServletResponse 的输出流将图像以 JPEG 格式返回给客户端。
- 使用 try-with-resources 确保流自动关闭,避免资源泄漏。
武技:根据用户输入的文本描述,调用阿里云通义万相模型生成图像,并将图像返回给客户端。
1. 开发控制器
- 开发控制器:
package com.joezhou.controller; import java.net.URL; /** @author 周航宇 */ @RequestMapping("/api/v1/image") @RestController @CrossOrigin public class ImageController { private final ImageModel imageModel; public ImageController(ImageModel imageModel) { this.imageModel = imageModel; } @GetMapping({"base", "stream"}) public void base(@RequestParam("msg") String msg, HttpServletResponse resp) throws IOException { // 创建一个 ImageOptions 对象,使用 ImageOptionsBuilder 构建器模式进行配置 ImageOptions options = ImageOptionsBuilder.builder() // 设置生成图片的宽度为 1024 像素 .width(1024) // 设置生成图片的高度为 1024 像素 .height(1024) // 指定使用的图像生成模型为 "wanx-v1",是阿里云通义万相模型中的基础文生图模型 .model("wanx-v1") // 设置生成图片的数量为 1 张 .N(1) // 构建 ImageOptions 对象 .build(); // 创建一个 ImagePrompt 对象,用于封装生成图片的提示信息和配置选项 // msg 是用户传入的生成图片的提示文本,options 是前面配置好的图片生成选项 ImagePrompt imagePrompt = new ImagePrompt(msg, options); // 调用 imageModel 对象的 call 方法,传入 ImagePrompt 对象,发起图片生成请求 // 并将生成结果存储在 ImageResponse 对象中 ImageResponse imageResponse = imageModel.call(imagePrompt); // 从 ImageResponse 对象中获取生成的图片结果列表 List results = imageResponse.getResults(); // 检查生成结果列表是否为空 if (results.isEmpty()) { // 如果为空,抛出一个运行时异常,提示没有找到生成结果 throw new RuntimeException("No results found"); } // 从结果列表中获取第一个生成结果 // 并从该结果中获取生成图片的 URL String imageUrl = results.get(0).getOutput().getUrl(); // 创建一个 URL 对象,用于表示生成图片的 URL URL url = new URL(imageUrl); // 使用 ImageIO 类的 read 方法从指定的 URL 读取图片,并将其存储为 BufferedImage 对象 BufferedImage bufferedImage = ImageIO.read(url); // 从 HttpServletResponse 对象中获取输出流,使用 try-with-resources 语句确保流会自动关闭 try (ServletOutputStream outputStream = resp.getOutputStream()) { // 使用 ImageIO 类的 write 方法将 BufferedImage 对象以 JPEG 格式写入响应输出流 ImageIO.write(bufferedImage, "jpg", outputStream); // 刷新输出流,确保所有数据都被发送到客户端 outputStream.flush(); } } }
- 在 App.vue 文件中,将前端代码替换为对应路径进行测试:
// AI接口地址 const URL = 'http://localhost:5501/api/v1/image/base';
Java道经第5卷 - 第1阶 - SpringAI(一)
传送门:JB5-1-SpringAI(一)
传送门:JB5-2-SpringAI(二)
- 幻觉响应:就是 AI 生成了一些不符合事实或者与给定信息不相关的内容。
- 1. 开发控制器