JB5-1-SpringAI(一)

Java道经第5卷 - 第1阶 - SpringAI(一)


传送门:JB5-1-SpringAI(一)
传送门:JB5-2-SpringAI(二)

心法:本章使用 Maven 父子结构项目进行练习

练习项目结构如下

|_ v5-1-llm-springai
	|_ 5501 test
	|_ 6501 springai-web

武技:搭建练习项目结构

  1. 创建父项目 v5-1-llm-springai,删除 src 目录。
  2. 在父项目中锁定版本:注意目前 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>
  1. 在父项目中配置仓库:因为 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>
  1. 在父项目中管理依赖:需要同时管理 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>
  1. 在父项目中引入通用依赖:
<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>
  1. 创建子项目 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

  1. 登录 阿里云百炼 页面。
  2. 依次点击 右上角头像 - API KEY - 创建我的 API KEY,然后将 API KEY 记录下来(SK 开头的),归属业务空间选择默认业务空间即可,描述可省略。

在这里插入图片描述

在这里插入图片描述

  1. 回到首页,根据提示开通模型调用服务:

在这里插入图片描述

在这里插入图片描述

  1. 将API-KEY设置到环境变量
# 设置系统环境变量
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 模型通信,支持同步和流式编程模型。

  1. 开发控制器:推荐使用 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();
    }
}
  1. 开发控制方法 base()
@GetMapping("base")
public String base(@RequestParam("msg") String msg) {
    return chatClient
            // 初始化新会话并创建一个包含消息上下文的提示对象,可以理解为“开启新对话”
            .prompt()
            // 设置客户端传递过来的对话内容
            .user(msg.trim())
            // 同步发送消息,此时该方法会将响应结果一次性响应给前端
            .call()
            // 获取响应消息,此时需要将这个消息返回给前端
            .content();
}
  1. 测试 base() 控制方法:
### base()
GET http://localhost:5501/api/v1/aiChat/base?msg=讲个笑话
  1. 开发控制方法 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]"));
}
  1. 测试 stream() 控制方法:
### stream():中文可能会乱码,可以暂时使用英文对话进行测试。
GET http://localhost:5501/api/v1/aiChat/stream?msg=用纯英文讲个笑话

E03. 整合前端项目

武技:创建 v5-1-llm-springai/springai-web 项目

  1. 使用 vite 创建 vue 项目:
# 切换到工作空间目录,注意路径中不要有中文
cd D:\workspace\java\v5-1-llm-springai

# 创建Vue项目(第一遍安装需要输入y安装vite)
npm create vite@5.5.1 springai-web -- --template vue
  1. 在 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

  1. 安装 ElementPlus 依赖:
# 局部安装ElementPlus组件库
npm install element-plus@2.5.3 --save
  1. 在 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. 开发全局样式

  1. 在 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页面

在这里插入图片描述

  1. 布局:开发 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>
  1. 脚本:开发 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 模型,帮助其理解对话的上下文、角色或任务要求。

注意事项

  1. 系统信息通常是纯文本,但某些 API 可能支持 Markdown 或其他格式(需查阅具体文档)。
  2. 避免使用特殊字符或格式,除非 API 明确支持。
  3. 系统信息会应用于所有对话轮次,除非在单次请求中覆盖它。
  4. 某些 API 可能对系统信息的长度有限制(例如 OpenAI 的 GPT 模型建议控制在几百个 token 内)。
  5. 不要在系统信息中包含敏感信息(如 API 密钥、用户数据等)。

武技:测试 SpringAI 的预设角色功能

1. 开发控制器

  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]"));
    }
}
  1. 测试控制类:
### base()
GET http://localhost:5501/api/v1/systemRole/base?msg=你是谁啊

### stream()
GET http://localhost:5501/api/v1/systemRole/stream?msg=用纯英文告诉我你是谁啊
  1. 在 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 具体工作流程如下

  1. 在使用 ChatClient.Builder 构建 ChatClient 时,使用 builder.defaultAdvisors() 方法设置默认的 SimpleLoggerAdvisor 实例。
  2. 在发送请求之前,SimpleLoggerAdvisor 会捕获并记录所有即将发送给 AI 服务的消息内容。
  3. 在收到响应之后,SimpleLoggerAdvisor 会提取并记录响应中的关键信息。
  4. 这些记录的信息会被输出到应用的日志系统中(如 SLF4J、Logback 等),方便后续查看和分析。

武技:添加日志通知

1. 开发主配文件

心法:想要使用日志通知,则需要将 advisor 包的日志记录级别设置为 DEBUG 级别。

logging:
  level:
    org:
      springframework:
        ai:
          chat:
            client:
              advisor: DEBUG # advisor 日志级别

2. 开发控制器

  1. 开发控制器:
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]"));
    }
}
  1. 测试控制器:发送请求后,观察控制台是否记录了对应的请求和响应的日志:
### 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 执行流程如下

  1. SpringAI 框架根据用户的提示(Prompt)创建一个建议请求对象(AdvisedRequest)对象,并附带一个空的通知上下文对象(AdvisorContext)。
  2. 通知链(Advisor Chain)中的每个通知(Advisor)都会处理或修改该请求:
  3. SpringAI 框架中,后一个通知(Adivsor)会将请求发送给聊天模型(Chat Model)。
  4. 聊天模型(Chat Model)返回聊天响应(ChatResponse)给通知链(Advisor Chain)并转换为建议响应对象(AdvisedResponse),该对象中包含共享的通知上下文对象(AdvisorContext)实例。
  5. 通知链(Advisor Chain)中的每个通知(Advisor)都会处理或修改该响应。
  6. 最终将建议响应对象(AdvisedResponse)转换为聊天响应(ChatResponse)并返回给客户端。

在这里插入图片描述

武技:开发并使用自定义通知类

1. 开发通知类

自定义通知类要求如下:

  1. 重写 CallAroundAdvisor -> aroundCall():对应 chatClient 的 call() 方法的环绕通知。
  2. 重写 StreamAroundAdvisor -> aroundStream():对应 chatClient 的 stream() 方法的环绕通知。
  3. 重写 Advisor -> getName():用于获取唯一的 Advisor 名称。
  4. 重写 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. 开发控制器

  1. 开发控制器:
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]"));
    }
}
  1. 测试控制器:发送请求后,观察控制台是否记录了对应的请求和响应的日志:
### base():发送请求后,观察控制台是否记录了对应的请求和响应的日志
GET http://localhost:5501/api/v1/myAdvisor/base?msg=讲个笑话

### stream():发送请求后,观察控制台是否记录了对应的请求和响应的日志
GET http://localhost:5501/api/v1/myAdvisor/stream?msg=用纯英文讲个笑话

E04. 记忆通知

心法:ChatMemory 接口用于存储聊天对话历史记录,它提供了添加消息到对话、从对话中检索消息和清除对话历史记录的方法。

ChatMemory 常用实现

  1. InMemoryChatMemory:基于内存存储:
    • 线程安全:底层使用 CopyOnWriteArrayList<message> 实现,线程安全。
    • 临时存储:应用重启后数据丢失,适用于短期会话。
    • 简单轻量:无需外部依赖,直接使用 JVM 内存。
    • 典型场景:测试环境或开发调试。
  2. LimitedSizeChatMemory:基于内存存储:
    • 容量限制:通过 maxMessages 参数控制历史消息数量(如保留最近 10 条)。
    • 自动截断:超出容量时移除最早的消息,避免内存溢出。
    • 适配性强:可包装其他实现,如 InMemoryChatMemory 等。
    • 典型场景:需控制内存使用的生产环境。
  3. RedisChatMemory:基于 Redis 存储:
    • 分布式共享:支持多实例应用共享会话历史。
    • 持久化:数据可配置为持久化存储(如 RDB/AOF)。
    • 高性能:基于内存的缓存,读写速度快。
    • 典型场景:微服务架构或需要长时间保留对话历史的应用。
  4. JdbcChatMemory:基于关系型数据库:
    • 结构化存储:消息存储在数据库表中,支持复杂查询。
    • 事务支持:与数据库事务集成,确保数据一致性。
    • 扩展性:可利用数据库索引和备份机制。
    • 典型场景:需要深度分析对话数据的应用(如客服系统)。
  5. SessionChatMemory:基于 Spring 会话:
    • Web 集成:自动与用户 HTTP 会话绑定。
    • 会话生命周期:数据随会话过期而清除。
    • 适配性:依赖 Spring Web 模块,需配置 HttpSession 对象。
    • 典型场景:Web 应用中的用户对话管理。
  6. CompositeChatMemory:组合多个 ChatMemory 实现:
    • 多策略支持:同时使用多种存储方式(如内存 + Redis)。
    • 分层存储:例如,近期消息存内存,历史消息存数据库。
    • 自定义路由:通过策略决定消息存储位置。
    • 典型场景:混合性能与持久化需求的场景。

ChatMemory 使用流程

  1. 创建 InMemoryChatMemory 实例:仅做测试。
  2. 创建 PromptChatMemoryAdvisor 实例:它是一个专门负责自动管理对话历史的 Advisor:
    • 在发送请求前,自动从 ChatMemory 中提取历史消息并添加到请求的 messages 列表中。
    • 在收到响应后,自动将新生成的消息(用户消息和助手回复)保存到 ChatMemory 中。
  3. 在使用 ChatClient.Builder 构建 ChatClient 时,使用 builder.defaultAdvisors() 方法设置默认的 Advisor 对象。
  4. 使用 chatClient.advisors(e -> e.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10)) 设置记忆大小。

武技:测试使用记忆通知功能

1. 开发控制器

  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]"));
    }
}
  1. 测试控制类:
### 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?
  1. 在 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. 开发配置类

  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. 开发控制器

  1. 开发控制器:在使用 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]"));
    }
}
  1. 测试控制器:
### base()
GET http://localhost:5501/api/v1/toolCalling/base?msg=北京天气如何

### stream()
GET http://localhost:5501/api/v1/toolCalling/stream?msg=上海天气如何
  1. 在 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 会自动执行以下操作:

  1. 拦截用户发送的问题内容。
  2. 将用户问题转换为向量(Embedding)。
  3. 在 VectorStore 中检索相关文档(最相似的文档片段)。
  4. 将检索结果作为上下文注入 AI 模型的提示(Prompt)中。
  5. 将增强后的提示被发送给 AI 模型,生成回答时会参考检索到的上下文。

武技:搭建检索增强环境

1. 开发检索文件

  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. 开发控制器

  1. 开发控制器:
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]"));
    }
}
  1. 测试控制类:
### base()
GET http://localhost:5501/api/v1/rag/base?msg=公司成员架构是怎么样的

### stream()
GET http://localhost:5501/api/v1/rag/stream?msg=公司成立时间
  1. 在 App.vue 文件中,将前端代码替换为对应路径进行测试:
<script setup>
	// AI接口地址
	const URL = 'http://localhost:5501/api/v1/rag/base';
</script>

E07. 文生图

心法:ImageModel 是 SpringAI 提供的,与图像生成模型(如通义万相)交互的服务接口。

文生图流程

  1. 使用 ImageOptionsBuilder.builder().build() 创建图像的配置对象:
    • width/height:图像尺寸(1024×1024 像素)。
    • model:使用的模型(阿里云通义万相的基础文生图模型)。
    • N:生成图像的数量(此处为 1 张)。
  2. 将用户描述和配置封装为 ImagePrompt 对象。
  3. 调用模型服务生成图像,返回 ImageResponse 响应对象。
  4. 检查结果列表是否为空(防止无生成结果)。
  5. 获取第一个生成图像的 URL(模型通常返回临时存储的 URL)。
  6. 从 URL 下载图像到内存(BufferedImage)。
  7. 通过 HttpServletResponse 的输出流将图像以 JPEG 格式返回给客户端。
  8. 使用 try-with-resources 确保流自动关闭,避免资源泄漏。

武技:根据用户输入的文本描述,调用阿里云通义万相模型生成图像,并将图像返回给客户端。

1. 开发控制器

  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();
        }
    }
}
  1. 在 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(二)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值