文章目录
- 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: qwen–max–latest #全局模型
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?
5.4、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助手
如果发现文中存在错误,欢迎在评论区留言,暴露问题,大家一起解决!
如果觉得小名的文章帮助到了您,请关注小名,支持一下小名😄,给小名的文章点赞👍、评论✍、收藏🤞谢谢大家啦~♥♥♥



![[特殊字符]DeepSeek‑Harness(DSH)小白保姆教程-171主机测评](https://www.171host.com/wp-content/uploads/2026/08/20260816085112-6a817a009aabf-220x150.png)