Java道经第5卷 - 第1阶 - SpringAI(一)
传送门:JB5-1-SpringAI(一)
传送门:JB5-2-SpringAI(二)
心法:本章使用 Maven 父子结构项目进行练习
练习项目结构如下:
|_ v5-1-llm-springai
|_ 5501 test
|_ 6501 springai-web
武技:搭建练习项目结构
- 创建父项目 v5-1-llm-springai,删除 src 目录。
- 在父项目中锁定版本:注意目前 SpringAI 支持 SpringBoot3.2.x 和 SpringBoot 3.3.x 版本:
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>4.13.2</junit.version>
<lombok.version>1.18.24</lombok.version>
<hutool-all.version>5.8.25</hutool-all.version>
<springboot.version>3.2.5</springboot.version>
<spring-ai-bom.version>1.0.0-M6</spring-ai-bom.version>
<spring-ai-alibaba-starter.version>1.0.0-M6.1</spring-ai-alibaba-starter.version>
<spring-ai-starter-vector-store-redis.version>1.0.0-M7</spring-ai-starter-vector-store-redis.version>
</properties>
- 在父项目中配置仓库:因为 Spring 官方目前并没有将 spring-ai 相关的包发布到 Maven 的中央仓库中,所以需要使用
<repositories>
标签单独配置特定的,可能包含 spring-ai 依赖的仓库,参考官方文档 最终配置如下:
<!--指定仓库-->
<repositories>
<!--spring-ai 需要的仓库-->
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
<!--spring-ai 需要的仓库-->
<repository>
<name>Central Portal Snapshots</name>
<id>central-portal-snapshots</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<!--spring-ai-alibaba-starter 需要的仓库-->
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
- 在父项目中管理依赖:需要同时管理 SpringBoot 依赖和 SpringAI 依赖:
<!--管理依赖-->
<dependencyManagement>
<dependencies>
<!--spring-boot-starter-parent-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${springboot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring-ai-bom-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai-bom.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
- 在父项目中引入通用依赖:
<dependencies>
<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<!--hutool-all-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool-all.version}</version>
</dependency>
</dependencies>
- 创建子项目 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 开头的),归属业务空间选择默认业务空间即可,描述可省略。
- 回到首页,根据提示开通模型调用服务:
# 设置系统环境变量
setx DASHSCOPE_API_KEY sk-xxxxxxx
# 检查 API-KEY 的环境变量是否生效
echo %DASHSCOPE_API_KEY%
E02. 整合后端项目
武技:在 test 子项目中整合 SpringAI
1. 添加三方依赖
<dependencies>
<!--spring-boot-starter-web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--spring-ai-alibaba-starter:阿里AI-->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter</artifactId>
<version>${spring-ai-alibaba-starter.version}</version>
</dependency>
</dependencies>
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<String> 类型
* 流式对话中,需要在 @RequestMapping 注解中指定 produces=MediaType.TEXT_EVENT_STREAM_VALUE 项
*/
@GetMapping(value = "stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@RequestParam("msg") String msg) {
Flux<String> 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 代码如下:
<template>
<el-timeline class="timeline" id="timeline">
<el-timeline-item v-for="e in activities"
:key="e"
:color="e.role === 'user' ? 'blue': 'green'"
:style="{textAlign: e.role === 'user' ? 'right': 'left'}"
:timestamp="e.timestamp">
<template #default>
<div v-if="e.type === 'image'">
<img class="image" :src="e.src" alt="示例图片"/>
<br/>
<span :class="e.role === 'user' ? 'userContent' : 'aiContent'">
{{ e.content }}
</span>
</div>
<div v-else>
<span :class="e.role === 'user' ? 'userContent' : 'aiContent'">
{{ e.content }}
</span>
</div>
</template>
</el-timeline-item>
</el-timeline>
<el-checkbox v-model="enableStream"
label="流式问答"
size="small"
border/>
<el-checkbox v-model="enableImage"
size="small"
label="文生图"
border/>
<el-input class="send-ipt"
v-model="msg"
placeholder="请输入您的问题,并按回车发送消息"
@keydown.enter="sendMsg()"/>
</template>
- 脚本:开发 JS 代码如下:
相关 API 方法 | 描述 |
---|---|
sendMsg() | 当用户发送消息时触发 |
baseQA(url) | 向指定 URL 发送消息,使用 AJAX 接收同步响应内容 |
streamQA(url) | 向指定 URL 发送消息,使用 EventSource 接收流式响应内容 |
imageQA(url) | 向指定 URL 发送消息,使用 URL.createObjectURL() 接收二进制响应内容 |
<script setup>
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");
</script>
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<String> 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 文件中,将前端代码替换为对应路径进行测试:
<script setup>
// AI接口地址
const URL = 'http://localhost:5501/api/v1/systemRole/base';
</script>
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<String> 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<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest,
StreamAroundAdvisorChain chain) {
log.info("进入 aroundStream() 方法");
log.info("请求参数:{}", advisedRequest.userText());
// 继续执行通知链中的下一个通知
// 如果已经是最后一个通知,则会调用目标方法
// 并将目标方法的执行结果封装在 Flux<AdvisedResponse> 中返回。
Flux<AdvisedResponse> 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<String> 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<message>
实现,线程安全。 - 临时存储:应用重启后数据丢失,适用于短期会话。
- 简单轻量:无需外部依赖,直接使用 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<String> 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 页面中,将前端代码替换为对应路径进行测试:
<script setup>
// AI接口地址
const URL = 'http://localhost:5501/api/v1/chatMemory/base';
</script>
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<String> 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 文件中,将前端代码替换为对应路径进行测试:
<script setup>
// AI接口地址
const URL = 'http://localhost:5501/api/v1/toolCalling/base';
</script>
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<Document> 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<String> 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 文件中,将前端代码替换为对应路径进行测试:
<script setup>
// AI接口地址
const URL = 'http://localhost:5501/api/v1/rag/base';
</script>
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<ImageGeneration> 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 文件中,将前端代码替换为对应路径进行测试:
<script setup>
// AI接口地址
const URL = 'http://localhost:5501/api/v1/image/base';
</script>
Java道经第5卷 - 第1阶 - SpringAI(一)
传送门:JB5-1-SpringAI(一)
传送门:JB5-2-SpringAI(二)