欢迎光临
我们一直在努力

做AI智能体的宝子们!Spring AI Alibaba 搭配 MySQL 存聊天回忆,流式输出真的绝,结合Function Calling增强大模型能力,建立属于你的私人Agent!

在这里插入图片描述

文章目录

  • 0、准备环境:
  • 1、yml配置和引入必要依赖
    • 1.1、在SpringBoot中配置dashscope
      • Coding:application.yml
    • 1.2、添加pom
      • Coding:pom.xml
  • 2、创建记忆表和实体
    • 2.1、LLM记忆存储方式和区别
    • 2.2、创建MySQL记忆表
      • Coding:chat_messages表sql
    • 2.3、创建记忆实体类
      • Coding:ChatMessage.java
      • 小知识点:
  • 3、准备对记忆表的数据库的操作
      • Coding:IChatMessageService.java
  • 4、实现对数据库的CRUD
    • 分别要实现如下方法:
      • 4.1、findConversationIds
      • 4.2、findByConversationId
      • 4.3、saveAll
      • 4.4、deleteByConversationId
      • Coding:ChatMessageServiceImpl.java
  • 5、【本文重点🔥】基于 MySQL 的 ChatMemory 实现
    • 5.1、为什么要写MysqlChatMemory?
    • 5.2、ChatMemory 是什么?
    • 5.3、为什么要实现 ChatMemory?
    • 5.4、MysqlChatMemory 的实现解析
      • Coding:MysqlChatMemory.java
  • 6、定义业务接口
      • Coding:IDashScopeService.java
  • 7、实现dashScopeChatByMemory方法
    • 7.1、先看下这个构造函数:
    • 🔥7.2、dashScopeChatByMemory方法:流式输出LLM对话
      • 7.2.1、构建消息
      • 7.2.2、发起LLM请求
      • Coding:DashScopeServiceImpl
  • 8、🔥Function Calling
    • 8.1、Function Calling是什么?
    • 8.2、Function Calling 的作用包括以下几个方面:
    • 8.3、实现TimeTools工具
      • Coding:TimeTools.java

*如果您不关心原由,可以通过Coding页签跳转至代码

0、准备环境:

JDK:17 SprintBoot:3.2.3 SpringAI: 1.0.0 SpingAI AIibaba: 1.0.0.3

1、yml配置和引入必要依赖

1.1、在SpringBoot中配置dashscope

Coding:application.yml

spring:
datasource:
url: jdbc:mysql://
username:
password:
driver-class-name: com.mysql.cj.jdbc.Driver
data:
redis:
host:
port: 6379
password:
database: 0
ai:
dashscope:
api-key: #阿里云百炼获取key
chat:
options:
model: qwenmaxlatest #全局模型

1.2、添加pom

Coding:pom.xml

<properties>
<!– 基础配置 –>
<java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<!– Spring Boot 版本 –>
<spring-boot.version>3.4.2</spring-boot.version>

<!– Spring AI 版本 –>
<spring-ai.version>1.0.0</spring-ai.version>
<spring-ai-openai-starter.version>1.0.0</spring-ai-openai-starter.version>

<!– Spring AI Alibaba 版本 –>
<spring-ai-alibaba.version>1.0.0.3</spring-ai-alibaba.version>

<!– 依赖版本 –>
<mybatis-plus.version>3.5.11</mybatis-plus.version>
<mysql.version>8.0.28</mysql.version>
<lombok.version>1.18.30</lombok.version>
<fastjson.version>2.0.57</fastjson.version>

</properties>

<dependencyManagement>
<dependencies>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<!– SpringAI Alibaba bom–>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-bom</artifactId>
<version>${spring-ai-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>

</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!– MyBatis Plus 相关依赖 –>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>

<!– MySQL 驱动 –>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>

<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-memory-jdbc</artifactId>
</dependency>

</dependencies>

2、创建记忆表和实体

2.1、LLM记忆存储方式和区别

记忆类型简介持久化性能扩展性适用场景主要缺点
内存记忆 基于内存的临时存储 ❌否 ⚡ 极快 ❌ 差 开发、测试、简单Demo 服务重启数据丢失
JDBC记忆 基于关系型数据库存储 ✅ 是 🐢 一般 ✅ 好 需要持久化、审计、生产环境 配置稍复杂,性能低于内存/Redis
Redis记忆 基于Redis的缓存存储 ✅ 是(可配置) ⚡ 快 ✅好 高并发、临时会话、需要速度 需要维护Redis服务,数据结构需自定义

2.2、创建MySQL记忆表

Coding:chat_messages表sql

create table chat_messages
(
id bigint auto_increment
primary key,
conversation_id varchar(255) not null,
message_type enum ('USER', 'ASSISTANT') not null,
content text not null,
created_at datetime not null,
type int null comment '类型'
)
engine = InnoDB;

create index idx_conversation_id
on chat_messages (conversation_id);

2.3、创建记忆实体类

Coding:ChatMessage.java

@Data
@TableName("chat_messages")
public class ChatMessage {

@TableId(value = "id", type = IdType.AUTO)
private Long id;

@TableField("conversation_id")
private String conversationId;

@TableField(value = "message_type", typeHandler = EnumTypeHandler.class)
private MessageType messageType;

@TableField("content")
private String content;

@TableField("created_at")
private LocalDateTime createdAt;

@TableField("type")
private Integer type;
}

小知识点:

这里的必要依赖是

<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-memory-jdbc</artifactId>
</dependency>

聪明的你会发现,这个依赖没有<version>标签。 这是因为:前面引入spring-ai-alibaba-bom后,Maven 会自动管理其范围内依赖的版本,无需显式指定 ,确保组件版本一致、兼容,避免冲突。

3、准备对记忆表的数据库的操作

Coding:IChatMessageService.java

public interface IChatMessageService extends IService<ChatMessage> {

List<String> findConversationIds();

List<ChatMessage> findByConversationId(String conversationId);

void saveAll(String conversationId, List<ChatMessage> messages);

void deleteByConversationId(String conversationId);
}

4、实现对数据库的CRUD

分别要实现如下方法:

4.1、findConversationIds

查询数据库中所有不重复的对话ID列表。

4.2、findByConversationId

根据指定的对话ID,查询该对话中的所有聊天消息,并按消息ID升序排列。

4.3、saveAll

批量保存一组聊天消息到数据库,如果消息中没有设置对话ID,则使用传入的 conversationId。

4.4、deleteByConversationId

根据指定的对话ID,删除该对话中的所有聊天消息。

Coding:ChatMessageServiceImpl.java

@Service
public class ChatMessageServiceImpl extends ServiceImpl<ChatMessageMapper, ChatMessage> implements IChatMessageService {

@Override
public List<String> findConversationIds() {
QueryWrapper<ChatMessage> qw = new QueryWrapper<>();
qw.select("DISTINCT conversation_id AS conversationId");
return this.list(qw).stream()
.map(ChatMessage::getConversationId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
}

@Override
public List<ChatMessage> findByConversationId(String conversationId) {
LambdaQueryWrapper<ChatMessage> lqw = new LambdaQueryWrapper<>();
lqw.eq(ChatMessage::getConversationId, conversationId)
.orderByAsc(ChatMessage::getId);
return this.list(lqw);
}

@Override
public void saveAll(String conversationId, List<ChatMessage> messages) {
if (messages == null || messages.isEmpty()) {
return;
}
for (ChatMessage m : messages) {
if (m.getConversationId() == null) {
m.setConversationId(conversationId);
}
}
this.saveBatch(messages);
}

@Override
public void deleteByConversationId(String conversationId) {
LambdaQueryWrapper<ChatMessage> lqw = new LambdaQueryWrapper<>();
lqw.eq(ChatMessage::getConversationId, conversationId);
this.remove(lqw);
}
}

5、【本文重点🔥】基于 MySQL 的 ChatMemory 实现

5.1、为什么要写MysqlChatMemory?

这个 MysqlChatMemory 类是 Spring AI 框架中一个核心组件的定制实现,它负责使用 MySQL 数据库 来持久化存储和管理聊天记录(即“记忆”)。

5.2、ChatMemory 是什么?

ChatMemory 是 Spring AI 框架中定义的一个接口,它抽象了 聊天应用中的对话历史(记忆)的存储和检索机制。它为 AI 聊天会话提供了一种持久化存储消息的方式,使得模型在后续交互中能够“记住”之前的对话内容,从而维持聊天的上下文。

5.3、为什么要实现 ChatMemory?

  • 维持上下文: 聊天模型要进行连贯的对话,必须知道之前的交流内容。ChatMemory 确保每条新的用户消息发送给模型时,都附带了该对话的完整或部分历史记录。
  • 可插拔性: Spring AI 框架通过接口实现了解耦。通过实现 ChatMemory,开发者可以自定义存储介质(咱们这里用的 MySQL,也就是:关系型数据库JDBC作为媒介,除此之外还有:内存存储、NoSQL 数据库、图数据库等等),而不必修改核心的 AI 聊天逻辑。
  • 对话管理: 接口提供了按 conversationId 隔离、检索和清除对话记录的标准方法。
  • 5.4、MysqlChatMemory 的实现解析

    ChatMemory 接口方法MysqlChatMemory 如何实现?主要作用
    add(String conversationId, Message message) 内部调用重载的 add 方法,将单条消息转换为列表。 向指定对话添加单条消息。
    add(String conversationId, List<Message> messages) 核心实现:1. 遍历 Spring AI 的 Message 列表,将其映射(Mapp)为数据库实体类 ChatMessage。2. 区分 UserMessage 和 AssistantMessage,设置 messageType 和 content。3. 使用 chatMessageService.saveAll() 批量保存到 MySQL。 向指定对话添加多条消息并持久化。
    get(String conversationId) 1. 调用 chatMessageService.findByConversationId(conversationId) 从 MySQL 按顺序检索所有 ChatMessage。2. 遍历结果,将数据库实体 ChatMessage 映射回 Spring AI 的 UserMessage 或 AssistantMessage 对象。 获取指定对话的完整历史记录。
    clear(String conversationId) 调用 chatMessageService.deleteByConversationId(conversationId)。 删除指定对话的所有历史消息。

    Coding:MysqlChatMemory.java

    @Component
    @RequiredArgsConstructor
    public class MysqlChatMemory implements ChatMemory {

    private final IChatMessageService chatMessageService;

    @Override
    public void add(String conversationId, Message message) {
    if (message == null) {
    return;
    }
    add(conversationId, Collections.singletonList(message));
    }

    @Override
    public void add(String conversationId, List<Message> messages) {
    if (messages == null || messages.isEmpty()) {
    return;
    }
    List<ChatMessage> toSave = messages.stream().map(m -> {
    ChatMessage cm = new ChatMessage();
    cm.setConversationId(conversationId);
    String text;
    if (m instanceof UserMessage) {
    text = ((UserMessage) m).getText();
    cm.setMessageType(MessageType.USER);
    } else if (m instanceof AssistantMessage) {
    text = ((AssistantMessage) m).getText();
    cm.setMessageType(MessageType.ASSISTANT);
    } else {
    text = String.valueOf(m);
    cm.setMessageType(MessageType.ASSISTANT);
    }
    cm.setContent(text);
    cm.setCreatedAt(LocalDateTime.now());
    // 从消息元数据中提取自定义的 type(如果存在)
    try {
    Object t = m.getMetadata() != null ? m.getMetadata().get("type") : null;
    if (t != null) {
    cm.setType(Integer.valueOf(String.valueOf(t)));
    }
    } catch (Exception ignore) { }
    return cm;
    }).collect(Collectors.toList());
    chatMessageService.saveAll(conversationId, toSave);
    }

    public void add(String conversationId, List<Message> messages, DashScopeChatInput input) {
    if (messages == null || messages.isEmpty()) {
    return;
    }
    List<ChatMessage> toSave = messages.stream().map(m -> {
    ChatMessage cm = new ChatMessage();
    cm.setConversationId(conversationId);
    String text;
    if (m instanceof UserMessage) {
    text = ((UserMessage) m).getText();
    cm.setMessageType(MessageType.USER);
    } else if (m instanceof AssistantMessage) {
    text = ((AssistantMessage) m).getText();
    cm.setMessageType(MessageType.ASSISTANT);
    } else {
    text = String.valueOf(m);
    cm.setMessageType(MessageType.ASSISTANT);
    }
    cm.setContent(text);
    cm.setCreatedAt(LocalDateTime.now());
    cm.setType(input.getType());
    return cm;
    }).collect(Collectors.toList());
    chatMessageService.saveAll(conversationId, toSave);
    }

    @Override
    public List<Message> get(String conversationId) {
    return chatMessageService.findByConversationId(conversationId).stream()
    .map(cm -> cm.getMessageType() == MessageType.USER
    ? new UserMessage(cm.getContent())
    : new AssistantMessage(cm.getContent()))
    .collect(Collectors.toList());
    }

    @Override
    public void clear(String conversationId) {
    chatMessageService.deleteByConversationId(conversationId);
    }
    }

    6、定义业务接口

    Coding:IDashScopeService.java

    public interface IDashScopeService {
    Flux<String> dashScopeChatByMemory(DashScopeChatInput input);
    }

    7、实现dashScopeChatByMemory方法

    7.1、先看下这个构造函数:

    这个构造函数是进行 ChatClient 配置和初始化的核心,它利用依赖注入传入的 ChatClient.Builder 来定制客户端的行为。

    • ChatClient: Spring AI 的核心接口,用于与底层 LLM (大语言模型) 进行通信,是发送 Prompt 并获取响应的入口。

    • builder: Spring 框架会自动提供这个 Builder,用于构建 ChatClient 实例。

    • mysqlChatMemory : 这是上面实现的基于 MySQL 的聊天记忆实现的方法。

    • 配置步骤

      • .defaultSystem(DEFAULT_PROMPT): 设置默认的系统级 Prompt。这个 Prompt 会添加到每一个对话的开头,用于指导模型的基本行为和角色设定。
      • .defaultAdvisors(new SimpleLoggerAdvisor()): 添加一个简单的日志 AOP (切面),用于记录请求和响应的日志信息。
      • .defaultAdvisors(MessageChatMemoryAdvisor.builder(mysqlChatMemory).build()): 这是关键。它注册了一个 MessageChatMemoryAdvisor,并将我们定制的 mysqlChatMemory 实例注入其中。
        • 作用: 这个 Advisor 负责在每次调用 chatClient.prompt() 之前,自动从 mysqlChatMemory 中读取历史消息,并将它们添加到当前请求的 Prompt 中,从而实现上下文记忆。同时,它也会将最新的用户消息和模型回复自动保存回 mysqlChatMemory(即保存到 MySQL 数据库)。
      • .defaultOptions(…): 设置 DashScope 模型的默认配置参数,例如:
        • .withTopP(0.7): 调整模型的随机性,TopP 值越小,模型的输出越集中和确定。

    🔥7.2、dashScopeChatByMemory方法:流式输出LLM对话

    构建包含记忆和工具的 Prompt,并发起流式 (Streaming) 请求。

    7.2.1、构建消息

    UserMessage user = UserMessage.builder()
    .text(input.getQuestion())
    .metadata(Map.of("type", input.getType())) // 提取额外的业务类型
    .build();

    SystemMessage system = SystemMessage.builder()
    .text("当用户问到时间时,你需要调用TimeTools获得当前准确时间").build();

    • UserMessage,包含用户的具体问题和任何自定义元数据(如 type,便于数据库存储)。
    • SystemMessage,用于告诉模型在特定情况下(用户问时间)需要使用 Tool Calling 功能。

    7.2.2、发起LLM请求

    return chatClient.prompt(new Prompt(List.of(user, system)))
    .tools(new TimeTools())
    .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, input.getConversationId()))
    .stream()
    .content();

    Coding:DashScopeServiceImpl

    @Service
    @Slf4j
    public class DashScopeServiceImpl implements IDashScopeService {
    private final ChatClient chatClient;

    public DashScopeServiceImpl(ChatClient.Builder builder, MysqlChatMemory mysqlChatMemory) {
    this.chatClient = builder.defaultSystem(DEFAULT_PROMPT)
    .defaultAdvisors(new SimpleLoggerAdvisor())
    // 注册Advisor
    .defaultAdvisors(MessageChatMemoryAdvisor.builder(mysqlChatMemory).build())
    .defaultOptions(
    DashScopeChatOptions.builder()
    .withTopP(0.7)
    .build()
    )
    .build();
    }

    @Override
    public Flux<String> dashScopeChatByMemory(DashScopeChatInput input) {
    UserMessage user = UserMessage.builder()
    .text(input.getQuestion())
    .metadata(Map.of("type", input.getType()))
    .build();
    SystemMessage system = SystemMessage.builder()
    .text("当用户问到时间时,你需要调用TimeTools获得当前准确时间").build();
    return chatClient.prompt(new Prompt(List.of(user, system)))
    .tools(new TimeTools())
    .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, input.getConversationId()))
    .stream()
    .content();
    }
    }

    上面代码中有一行代码是.tools(new TimeTools()),他就是标题中提到的Function Calling,是用来增强大模型能力的。下面我们来具体实现这个“时间工具”:

    8、🔥Function Calling

    8.1、Function Calling是什么?

    Function Calling是LLM与外部工具、API 或代码进行结构化交互,从而扩展其自身能力。

    8.2、Function Calling 的作用包括以下几个方面:

    1、增强模型能力:通过 Function Calling,它可以实时调用外部服务,获取最新或特定领域的信息,从而提供更准确、更实用的回答。 2、执行特定任务:模型可以决定在适当的时候调用预定义的函数,将语言理解能力转化为实际操作。 3、实现多步骤交互:在一次用户请求中,系统可以先让模型判断是否需要调用外部工具,再根据工具返回的结果生成最终回答。 4、提升应用集成度:Function Calling使得大模型可以深度集成到各类应用程序中,作为“智能大脑”协调多个外部系统,实现复杂业务逻辑 。

    8.3、实现TimeTools工具

    众所周知,大模型在回答问题时无法自动获取当前真实时间,这是由其架构和运行机制决定的,主要原因如下: 1、大模型在训练完成后,其参数就固定了,本质上是一个庞大的“函数映射器”。它没有连接实时系统时钟的能力 2、大多数大模型的训练数据截止于某个特定时间点。模型“认知”中的“现在”通常不会超过这个时间,除非通过工具或提示注入新信息。

    Coding:TimeTools.java

    @Slf4j
    public class TimeTools {
    @Tool(description = "Get time by zone id")
    public String getTimeByZoneId(@ToolParam(description = "Time zone id, such as Asia/Shanghai")
    String zoneId) {
    log.info("🔧调用工具-[⏰getTimeByZoneId] zoneId:{}",zoneId);
    ZoneId zid = ZoneId.of(zoneId);
    ZonedDateTime zonedDateTime = ZonedDateTime.now(zid);
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
    return zonedDateTime.format(formatter);
    }
    }

    这里的时间只是抛砖引玉,你完全可以在 chatClient.tool() 中注册多个工具,提高大模型在特定问题下的回答准确性,例如,你可以调用 地图 实现路径规划,做一个专属你的 旅游AI助手

    如果发现文中存在错误,欢迎在评论区留言,暴露问题,大家一起解决!
    如果觉得小名的文章帮助到了您,请关注小名,支持一下小名😄,给小名的文章点赞👍、评论✍、收藏🤞谢谢大家啦~♥♥♥

    赞(0)
    未经允许不得转载:171主机测评 » 做AI智能体的宝子们!Spring AI Alibaba 搭配 MySQL 存聊天回忆,流式输出真的绝,结合Function Calling增强大模型能力,建立属于你的私人Agent!
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址