2026最新Spring AI+多智能体版天机学堂(1-2部分)
SpringAI官方文档: https://docs.spring.io/spring-ai/reference/index.html
SpringAI中文官方文档: Spring AI 中文文档
整体架构:
新建会话及热门问题:
生成sessionId:
可以使用uuid来生成,确保唯一性。
sessionId保存到数据库:
页面原型:

配置项:
tj:
ai:
session:
title: Hello,我是天机AI助理
describe: 我是由天机学堂倾力打造的智能助理,我不仅能推荐课程、答疑解惑,还能为您激发创意、畅聊心事。
examples:
- title: "课程推荐"
describe: "能帮我推荐一个合适的课吗?"
- title: "课程推荐"
describe: "最近有什么新课吗?"
- title: "课程推荐"
describe: "如何选择适合我的课程?"
- title: "课程推荐"
describe: "有哪些热门课程推荐?"
- title: "课程购买"
describe: "我想要购买《Java进阶编程实战》"
- title: "课程购买"
describe: "我想要报名《Java高级架构与微服务》"
- title: "课程购买"
describe: "《Java进阶编程实战》的课程价格是多少?"
- title: "课程购买"
describe: "《Java高级架构与微服务》的课程价格是多少?"
- title: "课程介绍"
describe: "请帮我介绍一下《互联网产品运营实战》"
- title: "课程介绍"
describe: "《Java大数据处理与分析》这门课一共有多少节?"
- title: "课程介绍"
describe: "《Java高级架构与微服务》能学到什么内容?"
- title: "课程介绍"
describe: "《互联网产品运营实战》适合我学习吗?"
- title: "知识讲解"
describe: "面向对象的三大特征是什么?"
- title: "知识讲解"
describe: "ArrayList和LinkedList有何区别?"
- title: "知识讲解"
describe: "Java多线程怎么实现?"
- title: "知识讲解"
describe: "Spring Boot和SSM有什么区别?"
- title: "知识讲解"
describe: "列表和元组有什么区别?"
- title: "知识讲解"
describe: " Python如何管理内存?"
- title: "知识讲解"
describe: " 装饰器的作用是什么?"
- title: "知识讲解"
describe: "多线程为什么有GIL限制?"
配置类:
@Data
@Configuration
@ConfigurationProperties(prefix = "tj.ai.session")
public class SessionProperties {
/**
* AI助手的标题,用于显示助手的名称或身份。
*/
private String title;
/**
* AI助手的描述,简要介绍助手的功能或特点。
*/
private String describe;
/**
* 示例列表,包含一些使用助手的示例。
*/
private List<SessionVO.Example> examples;
}
实现类:
@Slf4j
@Service
@RequiredArgsConstructor
public class ChatSessionServiceImpl extends ServiceImpl<ChatSessionMapper, ChatSession> implements ChatSessionService {
private final SessionProperties sessionProperties;
@Override
public SessionVO createSession(Integer num) {
var sessionVO = BeanUtil.toBean(sessionProperties, SessionVO.class);
// 随机获取examples
sessionVO.setExamples(RandomUtil.randomEleList(sessionProperties.getExamples(), num));
// 随机生成sessionId
sessionVO.setSessionId(IdUtil.fastSimpleUUID());
// 构建持久化对象,并持久化
var chatSession = ChatSession.builder()
.sessionId(sessionVO.getSessionId())
.userId(UserContext.getUser())
.build();
super.save(chatSession);
return sessionVO;
}
}
热门问题:
热门问题一般都是固定的问题,所以在后台中存储,随机的返回3个即可,写到nacos的配置中心中,不硬编码到代码。
@Override
public List<SessionVO.Example> hotExamples(Integer num) {
return RandomUtil.randomEleList(sessionProperties.getExamples(), num);
}
流式对话:
响应结构说明:
Spring AI 内部执行工具时会切换线程,ThreadLocal 的值传不过去
1、每行数据,都是一个json数据
2、json中有eventData和eventType,两个属性
3、eventData:文本内容
4、eventType:事件类型,1001-数据事件,1002-停止事件,1003-参数事件
运行结果:
data:{"eventData":"课程","eventType":1001}
data:{"eventData":"id","eventType":1001}
data:{"eventData":":","eventType":1001}
data:{"eventData":"1880","eventType":1001}
data:{"eventData":"5332","eventType":1001}
data:{"eventData":"5357","eventType":1001}
data:{"eventData":"5225","eventType":1001}
data:{"eventData":"346\n\n","eventType":1001}
data:{"eventData":"发展。","eventType":1001}
data:{"eventData":"","eventType":1001}
data:{"eventType":1002}
创建配置类SpringAIConfig:
引入依赖中的ChatClient.Builder创建ChatClient对象:
@Configuration
public class SpringAIConfig {
/**
* 配置 ChatClient
*/
@Bean
public ChatClient chatClient(ChatClient.Builder chatClientBuilder,
Advisor loggerAdvisor) { // 日志记录器
return chatClientBuilder
.defaultAdvisors(loggerAdvisor) //添加 Advisor 功能增强
.build();
}
/**
* 日志记录器
*/
@Bean
public Advisor loggerAdvisor() {
return new SimpleLoggerAdvisor();
}
}
编写Service实现类
@Slf4j
@Service
@RequiredArgsConstructor
public class ChatServiceImpl implements ChatService {
private final ChatClient chatClient;
@Override
public Flux<ChatEventVO> chat(String question, String sessionId) {
return chatClient.prompt()
.user(question)
.stream()//返回的类型:Flux<chatResponse>
.chatResponse()
.map(chatResponse -> {
// 获取大模型的输出的内容
String text = chatResponse.getResult().getOutput().getText();
// 封装响应对象
return ChatEventVO.builder()
.eventData(text)
.eventType(ChatEventTypeEnum.DATA.getValue())
.build();
})
.concatWith(Flux.just(ChatEventVO.builder() // 标记输出结束
.eventType(ChatEventTypeEnum.STOP.getValue())
.build()));
}
}
Spring AI ChatClient 常见API总结:

system提示词:
功能:
- 功能一:课程推荐
- 功能二:课程购买
- 功能三:课程咨询
- 功能四:知识讲解
提示词工程构建:
角色
你作为在线教育平台资深客服代表兼讲师。你的任务根据学员的需求,调用知识库中的课程信息,为学员推荐合适的课程,同时解答学员对课程内容和知识点的疑问。
技能 1: 课程推荐
1. 当学员提出课程推荐需求时,需判断是否提供必要信息。必要信息包含年龄、学历、是否有编程基础。
2. 若缺少必要信息,需礼貌追问。
3. 若学员未提供感兴趣的方向,需追问。若没有明确方向,优先推荐学习人数多的课程。
4. 若信息充足,根据必要信息和感兴趣的课程方向,去知识库匹配合适的课程,获取课程id,调用queryCourseById,根据课程id查询课程详细信息,为学员推荐课程,可推荐单门/多门课程。
5. 若知识库未包含学员感兴趣方向,需明确告知学员未提供该方向课程,并推荐其他课程。
6. 若必要信息未匹配合适课程,需提示学员您的情况与现有课程要求并不完全匹配,说明详细原因后,再推荐其他课程。
7. 推荐课程,必须要通过queryCourseById查询后,才能返回数据。
技能 2: 课程购买
1. 当学员提出购买课程时,需判断此次会话中,学员是否明确提出购买xx课/系统已为学员推荐课程。
2. 若已推荐/明确课程名称,需调用prePlaceOrder,根据此次上文已推荐/学员明确的课程,直接进入预下单流程。
3. 若未推荐课程,需引导学员进入到课程推荐流程。
4. 若学员未明确提出购买某门课程时,需询问用户购买哪门课程。
5. 支持购买一门/多门课程。
技能 3: 课程咨询
1. 当学员咨询课程内容时,需去知识库匹配合适的课程,获取课程id,根据课程id查询课程详细信息。回复的内容要全面,要引导学员报名购买。
2. 若未查询到,需礼貌告知学员未检索到相关的内容,请联系人工客服010-12345678。
3. 若咨询课程有效期,需将当前时间{now} 与 课程有效期相加,回复学员准确日期。课程有效期999天,代表永久有效。
技能 4: 知识讲解
1. 当学员咨询与IT相关的知识点内容时,需详细讲解知识点并提供示例。
限制:
- 推荐的课程只能从知识库中选择,坚决不能凭空编造
- 回答的内容要逻辑清晰、内容全面、不要有遗漏。
- 只能回答与课程和IT知识点相关的内容,若学员咨询与课程无关的内容,你需告知学员不能回答与课程和IT知识点无关的问题,并引导学员咨询与课程/IT知识点相关的问题。
- 若学员询问课程ID,则告知学员无法提供课程ID,引导学员咨询其他的问题。
可以直接用 @RefreshScope + @Value 自动注入和热更新
配置类进行配置:将提示词放入到对应的配置文件中
@Slf4j
@Getter
@RefreshScope // Nacos 配置变更时自动刷新
@Configuration
public class SystemPromptConfig {
@Value("${tj.ai.system}")
private String chatSystemMessage;
}
实现类:
package com.tianji.aigc.service.impl;
import cn.hutool.core.date.DateUtil;
import com.tianji.aigc.config.SystemPromptConfig;
import com.tianji.aigc.enums.ChatEventTypeEnum;
import com.tianji.aigc.service.ChatService;
import com.tianji.aigc.vo.ChatEventVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
@Slf4j
@Service
@RequiredArgsConstructor
public class ChatServiceImpl implements ChatService {
private final ChatClient chatClient;
private final SystemPromptConfig systemPromptConfig;
@Override
public Flux<ChatEventVO> chat(String question, String sessionId) {
return this.chatClient.prompt()
.system(promptSystem -> promptSystem
.text(this.systemPromptConfig.getChatSystemMessage()) // 设置系统提示语
.param("now", DateUtil.now()) // 设置当前时间的参数
)
.user(question)
.stream()
.chatResponse()
.map(chatResponse -> {
// 获取大模型的输出的内容
String text = chatResponse.getResult().getOutput().getText();
// 封装响应对象
return ChatEventVO.builder()
.eventData(text)
.eventType(ChatEventTypeEnum.DATA.getValue())
.build();
})
.concatWith(Flux.just(ChatEventVO.builder() // 标记输出结束
.eventType(ChatEventTypeEnum.STOP.getValue())
.build()));
}
}
停止生成:
基于Redis容器进行控制输出流的中断:
核心思想:
在大模型输出内容时,进行标记正在输出,如果stop的话,需要将这个标记删除,Flux是否继续输出,取决于这个标记。
@Slf4j
@Service
@RequiredArgsConstructor
public class ChatServiceImpl implements ChatService {
private final ChatClient chatClient;
private final SystemPromptConfig systemPromptConfig;
@Override
public Flux<ChatEventVO> chat(String question, String sessionId) {
BoundHashOperations<String, Object, Object> hashOps = stringRedisTemplate.boundHashOps(GENSTATU);
return this.chatClient.prompt()
.system(promptSystem -> promptSystem
.text(this.systemPromptConfig.getChatSystemMessage()) // 设置系统提示语
.param("now", DateUtil.now()) // 设置当前时间的参数
)
.user(question)
.stream()
.chatResponse()
.doFirst(()-> hashOps.put(sessionId,"true"))
.doOnError(throwable -> {hashOps.delete(sessionId);})
.doOnComplete(() -> {hashOps.delete(sessionId);})
.doOnCancel(()->saveStopHistoryRecord(conversationId, outputBuilder.toString()))
.filter(chatResponse -> chatResponse.getResult() != null)
.takeWhile(chatResponse -> {return hashOps.get(sessionId) != null;)
.map(chatResponse -> {
// 获取大模型的输出的内容
String text = chatResponse.getResult().getOutput().getText();
// 封装响应对象
return ChatEventVO.builder()
.eventData(text)
.eventType(ChatEventTypeEnum.DATA.getValue())
.build();
})
.concatWith(Flux.just(ChatEventVO.builder() // 标记输出结束
.eventType(ChatEventTypeEnum.STOP.getValue())
.build()));
}
}
//停止方法
@Override
public void stop(String sessionId) {
stringRedisTemplate.delete(sessionId);
}
会话记忆:
SpringAI官方没有提供redis存储的实现:
实现自定义的Redia存储:
就需要实现SpringAI中的org.springframework.ai.chat.memory.ChatMemoryRepository接口
package com.tianji.aigc.memory;
import cn.hutool.core.collection.CollStreamUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.stream.StreamUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import jakarta.annotation.Resource;
import org.jetbrains.annotations.NotNull;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.ai.chat.messages.Message;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.List;
import java.util.Set;
/**
* 基于Redis实现的ChatMemoryRepository
*/
public class RedisChatMemoryRepository implements ChatMemoryRepository {
// 默认redis中key的前缀
public static final String DEFAULT_PREFIX = "CHAT:";
private final String prefix;
// 注入spring redis模板,进行redis的操作
@Resource
private StringRedisTemplate stringRedisTemplate;
public RedisChatMemoryRepository() {
this.prefix = DEFAULT_PREFIX;
}
public RedisChatMemoryRepository(String prefix) {
this.prefix = prefix;
}
@Override
public List<String> findConversationIds() {
Set<String> keys = this.stringRedisTemplate.keys(DEFAULT_PREFIX + "*");
if (null == keys) {
return List.of();
}
return StreamUtil.of(keys)
.map(key -> StrUtil.replace(key, DEFAULT_PREFIX, ""))
.toList();
}
@Override
public List<Message> findByConversationId(String conversationId) {
// 生成Redis键名用于存储会话消息
var redisKey = this.getKey(conversationId);
// 获取Redis列表操作对象
var listOps = this.stringRedisTemplate.boundListOps(redisKey);
// 从Redis列表中获取所有的数据
var messages = listOps.range(0, -1);
// 将Redis返回的字符串列表转换为Message对象列表
return CollStreamUtil.toList(messages, MessageUtil::toMessage);
}
//全量的传入数据。是整个会话。
@Override
public void saveAll(String conversationId, List<Message> messages) {
Assert.notEmpty(messages, "消息列表不能为空");
var redisKey = this.getKey(conversationId);
var listOps = this.stringRedisTemplate.boundListOps(redisKey);
// 保存数据时,会传入全部的消息数据,包括之前的数据,所以需要先删除之前的数据,再添加新的数据
this.deleteByConversationId(conversationId);
// 将消息序列化并添加到Redis列表的右侧
messages.forEach(message -> listOps.rightPush(MessageUtil.toJson(message)));
}
@Override
public void deleteByConversationId(String conversationId) {
var redisKey = this.getKey(conversationId);
stringRedisTemplate.delete(redisKey);
}
private String getKey(String conversationId) {
return DEFAULT_PREFIX + conversationId;
}
}
基于 chatMemoryRepository 对象构建 chatMemory 对象:
@Configuration
public class SpringAIConfig {
@Value("${tj.ai.memory.max:100}")
private Integer maxMessages;
/**
* 配置 ChatClient
*/
@Bean
public ChatClient chatClient(ChatClient.Builder chatClientBuilder,
Advisor loggerAdvisor, // 日志记录器
Advisor messageChatMemoryAdvisor
) {
return chatClientBuilder
.defaultAdvisors(loggerAdvisor, messageChatMemoryAdvisor) //添加 Advisor 功能增强
.build();
}
/**
* 日志记录器
*/
@Bean
public Advisor loggerAdvisor() {
return new SimpleLoggerAdvisor();
}
@Bean
public ChatMemoryRepository redisChatMemoryRepository() {
return new RedisChatMemoryRepository();
}
@Bean
public ChatMemory chatMemory(ChatMemoryRepository chatMemoryRepository) {
// 基于 chatMemoryRepository 对象构建 chatMemory 对象
return MessageWindowChatMemory.builder()
.chatMemoryRepository(chatMemoryRepository)
.maxMessages(this.maxMessages) // 最多保存 100 条对话, 如果超出的话,会自动删除最旧的对话
.build();
}
/**
* 基于Redis的会话记忆,聊天记忆整合到message列表中实现多轮对话
*/
@Bean
public Advisor messageChatMemoryAdvisor(ChatMemory chatMemory) {
// 创建基于 chatMemory 的 Advisor 对象
return MessageChatMemoryAdvisor.builder(chatMemory).build();
}
}
在application.yml中添加配置:
tj:
ai:
memory:
max: 100 #存储最大的聊天记录数
自定义对象对Message对象序列化和反序列化:
自定义类:
@Data
public class MyMessage {
private String messageType;
private Map<String, Object> metadata = Map.of();
private List<Media> media = List.of();
private List<AssistantMessage.ToolCall> toolCalls = List.of();
private String textContent;
private List<ToolResponseMessage.ToolResponse> toolResponses = List.of();
private Map<String, Object> params = Map.of();
}
自定义工具包:
/**
* 消息转换工具类,提供消息对象与JSON字符串之间的转换功能,主要用于Redis存储格式转换
*/
public class MessageUtil {
/**
* 将Message对象转换为Redis存储格式的JSON字符串
*
* @param message 需要转换的原始消息对象
* @return 符合Redis存储规范的JSON字符串
*/
public static String toJson(Message message) {
var myMessage = BeanUtil.toBean(message, MyMessage.class);
// 设置消息内容
myMessage.setTextContent(message.getText());
if (message instanceof AssistantMessage assistantMessage) {
myMessage.setToolCalls(assistantMessage.getToolCalls());
}
if (message instanceof ToolResponseMessage toolResponseMessage) {
myMessage.setToolResponses(toolResponseMessage.getResponses());
}
return JSONUtil.toJsonStr(myMessage);
}
/**
* 将Redis存储的JSON字符串反序列化为对应的Message对象
*
* @param json Redis存储的JSON格式消息数据
* @return 对应类型的Message对象
* @throws RuntimeException 当无法识别的消息类型时抛出异常
*/
public static Message toMessage(String json) {
var myMessage = JSONUtil.toBean(json, MyMessage.class);
var messageType = MessageType.valueOf(myMessage.getMessageType());
switch (messageType) {
case SYSTEM -> {
return new SystemMessage(myMessage.getTextContent());
}
case USER -> {
return UserMessage.builder()
.text(myMessage.getTextContent())
.metadata(myMessage.getMetadata())
.media(myMessage.getMedia())
.build();
}
case ASSISTANT -> {
return new AssistantMessage(myMessage.getTextContent(), myMessage.getMetadata(), myMessage.getToolCalls());
}
case TOOL -> {
return new ToolResponseMessage(myMessage.getToolResponses(), myMessage.getMetadata());
}
}
throw new RuntimeException("Message data conversion failed.");
}
}
停止输出bug:
停止是通过中断Flux流程完成的,Flux中断了,SpringAI就不会触发
ChatMemory的add方法,也就不会调用ChatMemoryRepository#saveAll方法了,所以就保存数据了。其实,
Flux是doOnCancel方法的,当流中断就会触发这个方法执行,所以,就需要在doOnCancel方法中实现自己存储的逻辑了
在ChatServiceImpl实现类中进行迭代更新:
private final ChatMemory chatMemory;
@Override
public Flux<ChatEventVO> chat(String question, String sessionId) {
// 获取对话id
var conversationId = ChatService.getConversationId(sessionId);
// 大模型输出内容的缓存器,用于在输出中断后的数据存储
var outputBuilder = new StringBuilder();return this.chatClient.prompt()
.system(promptSystem -> promptSystem
.text(this.systemPromptConfig.getChatSystemMessage().get()) // 设置系统提示语
.param("now", DateUtil.now()) // 设置当前时间的参数
)
.advisors(advisor -> advisor.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, conversationId))
.user(question)
.stream()
.chatResponse()
.doFirst(() -> { //输出开始,标记正在输出
GENERATE_STATUS.put(sessionId, true);
})
.doOnComplete(() -> { //输出结束,清除标记
GENERATE_STATUS.remove(sessionId);
})
.doOnError(throwable -> GENERATE_STATUS.remove(sessionId)) // 错误时清除标记
.doOnCancel(() -> {
// 当输出被取消时,保存输出的内容到历史记录中
this.saveStopHistoryRecord(conversationId, outputBuilder.toString());
})
// 输出过程中,判断是否正在输出,如果正在输出,则继续输出,否则结束输出
.takeWhile(s -> Optional.ofNullable(GENERATE_STATUS.get(sessionId)).orElse(false))
.map(chatResponse -> {
// 获取大模型的输出的内容
String text = chatResponse.getResult().getOutput().getText();
// 追加到输出内容中
outputBuilder.append(text);
// 封装响应对象
return ChatEventVO.builder()
.eventData(text)
.eventType(ChatEventTypeEnum.DATA.getValue())
.build();
})
.concatWith(Flux.just(ChatEventVO.builder() // 标记输出结束
.eventType(ChatEventTypeEnum.STOP.getValue())
.build()));
}/**
* 保存停止输出的记录
*
* @param conversationId 会话id
* @param content 大模型输出的内容
*/
private void saveStopHistoryRecord(String conversationId, String content) {
this.chatMemory.add(conversationId, new AssistantMessage(content));
}
实现自定义的Mysql存储:
/**
* 基于MYSQL实现的ChatMemoryRepository
*/
public class JDBCChatMemoryRepository implements ChatMemoryRepository {
@Autowired
private IConversationService conversationService;
@Override
public List<String> findConversationIds() {
List<Conversation> list = conversationService.lambdaQuery().select(Conversation::getConversationId).list();
return CollStreamUtil.toList(list, Conversation::getConversationId);
}
@Override
public List<Message> findByConversationId(String conversationId) {
List<Conversation> list = conversationService.lambdaQuery().eq(Conversation::getConversationId, conversationId).list();
List<Message> list1 = CollStreamUtil.toList(list, conversation -> MessageUtil.toMessage(conversation.getData()));
return list1;
}
@Override
public void saveAll(String conversationId, List<Message> messages) {
deleteByConversationId(conversationId);
Long userId = UserContext.getUser();
long uid = userId == null ? 0L : userId;
List<Conversation> list = CollStreamUtil.toList(messages, message ->
new Conversation()
.setConversationId(conversationId)
.setData(MessageUtil.toJson(message))
.setCreator(uid)
.setUpdater(uid)
);
conversationService.saveBatch(list);
}
@Override
public void deleteByConversationId(String conversationId) {
LambdaQueryWrapper<Conversation> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Conversation::getConversationId, conversationId);
conversationService.remove(wrapper);
}
}
spring:
ai:
memory:
max: 100 #存储最大的聊天记录数
type: Redis
注意:使用注解:@ConditionalOnProperty
@Bean
@ConditionalOnProperty(prefix = "tj.ai.memory", value = "type" ,havingValue = "Redis")
public ChatMemoryRepository redisChatMemoryRepository() {
return new RedisChatMemoryRepository();
}
@Bean
@ConditionalOnProperty(prefix = "tj.ai.memory", value = "type" ,havingValue = "MYSQL")
public ChatMemoryRepository jdbcChatMemoryRepository() {
return new JDBCChatMemoryRepository();
}
Tool调用:

查询课程:
定义Tool类:
package com.tianji.aigc.constants;
public interface Constant {
interface Tools {
String QUERY_COURSE_BY_ID = "根据课程id查询课程详细信息";
}
interface ToolParams {
String COURSE_ID = "课程id";
}
}
@Component
@RequiredArgsConstructor
public class CourseTools {
private final CourseClient courseClient;
/**
* 根据课程id查询课程信息
*
* @param courseId 课程id
* @return 课程信息
*/
@Tool(description = Constant.Tools.QUERY_COURSE_BY_ID)
public CourseInfo queryCourseById(@ToolParam(description = Constant.ToolParams.COURSE_ID) Long courseId) {
return courseId == null ? null : CourseInfo.of(this.courseClient.baseInfo(courseId, true));
}
}
注册tool:
/**
* 配置 ChatClient
*/
@Bean
public ChatClient chatClient(ChatClient.Builder chatClientBuilder,
Advisor loggerAdvisor,
Advisor messageChatMemoryAdvisor,
CourseTools courseTools // 课程工具
) { // 日志记录器
return chatClientBuilder
.defaultAdvisors(loggerAdvisor, messageChatMemoryAdvisor) //添加 Advisor 功能增强
.defaultTools(courseTools) //添加默认工具
.build();
}
预下单:
定义Tool类:
package com.tianji.aigc.constants;
public interface Constant {
String USER_ID = "userId";
String REQUEST_ID = "requestId";
interface Tools {
String QUERY_COURSE_BY_ID = "根据课程id查询课程详细信息";
String PRE_PLACE_ORDER = "购买课程预下单操作";
}
interface ToolParams {
String COURSE_ID = "课程id";
String COURSE_IDS = "课程id列表";
}
}
@Component
@RequiredArgsConstructor
public class OrderTools {
private final TradeClient tradeClient;
@Tool(description = Constant.Tools.PRE_PLACE_ORDER)
public PrePlaceOrder prePlaceOrder(@ToolParam(description = Constant.ToolParams.COURSE_IDS) List<Number> ids,
ToolContext toolContext) {
// 设置用户ID,用于身份验证,否在在Feign调用时会出现401错误
UserContext.setUser(Convert.toLong(toolContext.getContext().get(Constant.USER_ID)));
// 大模型传入的ids,可能是int类型,所以转化为long类型,再调用Feign
var orderConfirmVO = this.tradeClient.prePlaceOrder(CollStreamUtil.toList(ids, Number::longValue));
return Optional.ofNullable(orderConfirmVO)
.map(PrePlaceOrder::of)
.map(prePlaceOrder -> {
var field = StrUtil.lowerFirst(prePlaceOrder.getClass().getSimpleName());
var requestId = Convert.toStr(toolContext.getContext().get(Constant.REQUEST_ID));
ToolResultHolder.put(requestId, field, prePlaceOrder);
return prePlaceOrder;
})
.orElse(null);
}
}
注册tool:
原因很明确:UserContext 用的是 ThreadLocal。
请求线程:UserContext.setUser(123) → ThreadLocal 存入当前线程
↓
ChatClient 流式处理(可能切换线程)
↓
工具方法在另一个线程执行 → UserContext.getUser() → null(拿不到)
Spring AI 内部执行工具时会切换线程,ThreadLocal 的值传不过去。所以需要通过 ToolContext 手动把 userId带过去,在工具方法里重新 UserContext.setUser() 来恢复上下文。
/**
* 配置 ChatClient
*/
@Bean
public ChatClient chatClient(ChatClient.Builder chatClientBuilder,
Advisor loggerAdvisor,
Advisor messageChatMemoryAdvisor,
CourseTools courseTools, // 课程工具
OrderTools orderTools // 预下单工具
) { // 日志记录器
return chatClientBuilder
.defaultAdvisors(loggerAdvisor, messageChatMemoryAdvisor) //添加 Advisor 功能增强
.defaultTools(courseTools, orderTools) //添加默认工具
.build();
}
课程卡片:
需要在AI助手中,如果是查询课程,或推荐课程,需要展示出课程的卡片信息,其中,要显示课程的价格、名称等信息,还可以,点击卡片,跳转到课程详情页面。
实际上,就是在Flux输出流的最后,做判断,如果调用了工具,拿到工具的结果,追加到输出流的结束标签之前即可。
输出流结构:
{
"eventData": {
"courseInfo_1589905661084430337": {
"id": "1589905661084430337",
"name": "可能是史上最全的微服务技术栈课程",
"price": 199.0,
"validDuration": 9999,
"usePeople": "有一定的Java开发基础,熟练使用了SpringBoot、MyBatis等基础框架",
"detail": "可能是史上最全的微服务技术栈课程,由黑马名师授课,你值的拥有"
}
},
"eventType": 1003
}
Redis全局容器
解决方案:Redis全局容器,工具执行完后,将结果放入容器,流输出的最后进行判断,判断这个容器中是否有数据,如果有,就添加到流中,反之,就不需要添加。

同一个sessionid也可能有并发的情况,所以不能使用sessionId,那就需要重新生成一个requestId,这个请求id,每次发起大模型时都会生成一个新的id,用这个请求id和容器的数据关联起来。
保存工具数据:
@Component
@RequiredArgsConstructor
public class CourseTools {
private final CourseClient courseClient;
private final static String FIELD = "courseinfo_";
private final StringRedisTemplate stringRedisTemplate;
/**
* 根据课程id查询课程信息
*
* @param courseId 课程id
* @return 课程信息
*/
@Tool(description = Constant.Tools.QUERY_COURSE_BY_ID)
public CourseInfo queryCourseById(@ToolParam(description = Constant.ToolParams.COURSE_ID) Long courseId, ToolContext toolContext) {
CourseInfo courseInfo = courseId == null ? null : CourseInfo.of(this.courseClient.baseInfo(courseId, true));
String requestId = (String) toolContext.getContext().get(Constant.REQUEST_ID);
stringRedisTemplate.opsForHash().put(requestId,FIELD+courseInfo.getId(), courseInfo);
return courseInfo;
}
}
@Slf4j
@Service
@RequiredArgsConstructor
public class ChatServiceImpl implements IChatService {
private final ChatClient chatClient;
private final SystemPromptConfig systemPromptConfig;
// private final Map<String,Boolean> GENSTATU = new ConcurrentHashMap<>();
private static final ChatEventVO STOP_EVENT = ChatEventVO.builder().eventType(ChatEventTypeEnum.STOP.getValue()).build();
private final ChatMemory chatMemory;
private static final String GENSTATU = "GENSTATU";
private final StringRedisTemplate stringRedisTemplate;
private final VectorStore vectorStore;
@Override
public Flux<ChatEventVO> chat(String question, String sessionId) {
StringBuilder outputBuilder = new StringBuilder();
String conversationId = UserContext.getUser() + sessionId;
BoundHashOperations<String, Object, Object> hashOps = stringRedisTemplate.boundHashOps(GENSTATU);
// 生成请求id
var userId = UserContext.getUser();
var requestId = IdUtil.fastSimpleUUID();
var qaAdvisor = QuestionAnswerAdvisor.builder(this.vectorStore)
.searchRequest(SearchRequest.builder().similarityThreshold(0.6d).topK(6).build())
.build();
return chatClient.prompt()
.system(s -> s
.text(systemPromptConfig.getChatSystemMessage()) // 去掉 .get()
.param("now", DateUtil.now())
)
.advisors(advisor -> advisor.advisors(qaAdvisor).param(ChatMemory.CONVERSATION_ID, conversationId))
.user(question)
.toolContext(Map.of(Constant.REQUEST_ID, requestId, Constant.USER_ID, userId)) //通过工具上下文传递参数
.stream()
.chatResponse()
.doFirst(()-> hashOps.put(sessionId,"true"))
.doOnError(throwable -> {hashOps.delete(sessionId);})
.doOnComplete(() -> {hashOps.delete(sessionId);})
.doOnCancel(()->saveStopHistoryRecord(conversationId, outputBuilder.toString()))
.filter(chatResponse -> chatResponse.getResult() != null)
.takeWhile(chatResponse -> {
return hashOps.get(sessionId) != null;
})
.map(chatResponse -> {
String text = chatResponse.getResult().getOutput().getText();
outputBuilder.append(text);
var finishReason = chatResponse.getResult().getMetadata().getFinishReason();
if (StrUtil.equals(Constant.STOP, finishReason)) {
var messageId = chatResponse.getMetadata().getId();
stringRedisTemplate.opsForHash().put(messageId, Constant.REQUEST_ID, requestId);
}
return ChatEventVO.builder()
.eventData(text)
.eventType(ChatEventTypeEnum.DATA.getValue())
.build();
})
.filter(vo -> vo.getEventData() != null)
.concatWith(Flux.defer(() -> {
// 通过请求id获取到参数列表,如果不为空,就将其追加到返回结果中
Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(requestId);
if (CollUtil.isNotEmpty(map)) {
// 注意:不在这里删除requestId,由MessageUtil.toJson()读取后再清理
// 响应给前端的参数数据
var chatEventVO = ChatEventVO.builder()
.eventData(map)
.eventType(ChatEventTypeEnum.PARAM.getValue())
.build();
return Flux.just(chatEventVO, STOP_EVENT);
}
return Flux.just(STOP_EVENT);
}));
}
Redis存储工具调用数据(bug修复):
课程查询和预下单功能,给前端返回的数据中,包含了eventType为1003的数据,这个叫作额外数据,给前端提供,前端是不会显示到页面的,正常对话是没问题的,但是,数据存储到Redis是没有保存进去的。

Redis中的数据,是与requestId关联的,requestId是我们自己生成的,在RedisChatMemory中是没有的,所以,这个问题的关键就是如何获取到requestId了,只要有了requestId就可以获取到数据,进行保存了。
如何传递requestId?
其实,同样也是可以借助于Redis来完成,通用容器,可以放Tool的结果,也可以放其他的内容,只要及时的删除即可。
Redis通用容器数据结构:messageId, Constant.REQUEST_ID, requestId
@Override
public Flux<ChatEventVO> chat(String question, String sessionId) {
StringBuilder outputBuilder = new StringBuilder();
String conversationId = UserContext.getUser() + sessionId;
BoundHashOperations<String, Object, Object> hashOps = stringRedisTemplate.boundHashOps(GENSTATU);
// 生成请求id
var userId = UserContext.getUser();
var requestId = IdUtil.fastSimpleUUID();
var qaAdvisor = QuestionAnswerAdvisor.builder(this.vectorStore)
.searchRequest(SearchRequest.builder().similarityThreshold(0.6d).topK(6).build())
.build();
return chatClient.prompt()
.system(s -> s
.text(systemPromptConfig.getChatSystemMessage()) // 去掉 .get()
.param("now", DateUtil.now())
)
.advisors(advisor -> advisor.advisors(qaAdvisor).param(ChatMemory.CONVERSATION_ID, conversationId))
.user(question)
.toolContext(Map.of(Constant.REQUEST_ID, requestId, Constant.USER_ID, userId)) //通过工具上下文传递参数
.stream()
.chatResponse()
.doFirst(()-> hashOps.put(sessionId,"true"))
.doOnError(throwable -> {hashOps.delete(sessionId);})
.doOnComplete(() -> {hashOps.delete(sessionId);})
.doOnCancel(()->saveStopHistoryRecord(conversationId, outputBuilder.toString()))
.filter(chatResponse -> chatResponse.getResult() != null)
.takeWhile(chatResponse -> {
return hashOps.get(sessionId) != null;
})
.map(chatResponse -> {
String text = chatResponse.getResult().getOutput().getText();
outputBuilder.append(text);
var finishReason = chatResponse.getResult().getMetadata().getFinishReason();
if (StrUtil.equals(Constant.STOP, finishReason)) {
var messageId = chatResponse.getMetadata().getId();
stringRedisTemplate.opsForHash().put(messageId, Constant.REQUEST_ID, requestId);
}
return ChatEventVO.builder()
.eventData(text)
.eventType(ChatEventTypeEnum.DATA.getValue())
.build();
})
.filter(vo -> vo.getEventData() != null)
.concatWith(Flux.defer(() -> {
// 通过请求id获取到参数列表,如果不为空,就将其追加到返回结果中
Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(requestId);
if (CollUtil.isNotEmpty(map)) {
// 注意:不在这里删除requestId,由MessageUtil.toJson()读取后再清理
// 响应给前端的参数数据
var chatEventVO = ChatEventVO.builder()
.eventData(map)
.eventType(ChatEventTypeEnum.PARAM.getValue())
.build();
return Flux.just(chatEventVO, STOP_EVENT);
}
return Flux.just(STOP_EVENT);
}));
}
public static String toJson(Message message) {
var myMessage = BeanUtil.toBean(message, MyMessage.class);
// 设置消息内容
myMessage.setTextContent(message.getText());
if (message instanceof AssistantMessage assistantMessage) {
myMessage.setToolCalls(assistantMessage.getToolCalls());
// ★ 通过 messageId 中转,从Redis获取工具参数
if (assistantMessage.getMetadata() != null
&& assistantMessage.getMetadata().containsKey(Constant.ID)
&& staticRedisTemplate != null) {
String messageId = assistantMessage.getMetadata().get(Constant.ID).toString();
Object requestIdObj = staticRedisTemplate.opsForHash().get(messageId, Constant.REQUEST_ID);
if (requestIdObj != null) {
String reqId = requestIdObj.toString();
Map<Object, Object> params = staticRedisTemplate.opsForHash().entries(reqId);
if (ObjectUtil.isNotEmpty(params)) {
myMessage.setParams((Map) params);
}
staticRedisTemplate.delete(messageId);
staticRedisTemplate.delete(reqId);
}}}
if (message instanceof ToolResponseMessage toolResponseMessage) {
myMessage.setToolResponses(toolResponseMessage.getResponses());
}
return JSONUtil.toJsonStr(myMessage);
}
自定义AssistantMessage:
在toMessage时使用:
@Setter
@Getter
public class MyAssistantMessage extends AssistantMessage {
private Map<String, Object> params;
public MyAssistantMessage(String content, Map<String, Object> properties, List<ToolCall> toolCalls, List<Media> media, Map<String, Object> params) {
super(content, properties, toolCalls, media);
this.params = params;
}
}
构建知识库:
RAG:

- 文档摄取(ETL)流程(离线处理)
- 数据读取:从数据源(如文档、数据库)读取原始文档。
- 分割文档:通过分割模块(
<<Split>>)将文档切分为更小的数据块(chunks)。- 转换数据:通过转换模块(
Transformer)处理数据块(如向量化、添加元数据)。- 写入存储:将处理后的数据块写入向量数据库(
Vector Store),为后续检索做准备。- 核心目标:将非结构化文档转化为结构化、可检索的向量数据。
- 检索增强生成(RAG)流程(实时处理)
- 用户查询:接收用户提问(
Chat Request)。- 检索相关块:从向量库中检索与查询最相关(相似度高)的数据块(
<<Retrieve>>)。- 增强查询:将检索到的上下文信息(
Context information)与用户问题结合,生成增强后的提示(<<Augment>>)。- 生成响应:通过聊天模型(
Chat Model)生成回答。- 核心目标:通过外部知识库提升生成结果的准确性,解决了大模型信息缺失或滞后的问题。
准备数据存入数据库:
@Slf4j
@RestController
@RequestMapping("/embedding")
@RequiredArgsConstructor
public class EmbeddingController {
private final VectorStore vectorStore;
@PostMapping
public void saveVectorStore(@RequestParam("messages") List<String> messages) {
log.info("保存到向量数据库中,消息数据:{}", messages);
//构建文档
List<Document> documents = CollStreamUtil.toList(messages, message -> Document.builder()
.text(message)
.build());
//存储到向量数据库中
this.vectorStore.add(documents);
log.info("保存到向量数据库成功, 数量:{}", messages.size());
}
}
集成到chatClient:
// 创建RAG增强
var qaAdvisor = QuestionAnswerAdvisor.builder(this.vectorStore)
.searchRequest(SearchRequest.builder().similarityThreshold(0.6d).topK(6).build())
.build();
return this.chatClient.prompt()
.system(promptSystem -> promptSystem
.text(this.systemPromptConfig.getChatSystemMessage().get()) // 设置系统提示语
.param("now", DateUtil.now()) // 设置当前时间的参数
)
.advisors(advisor -> advisor
// 设置RAG增强
.advisors(qaAdvisor)
.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, conversationId)
)........................................................................................
增删改查:
@Slf4j
@RestController
@RequestMapping("/embedding")
@RequiredArgsConstructor
public class EmbeddingController {
private final VectorStore vectorStore;
private final EmbeddingModel embeddingModel;
@PostMapping
public void saveVectorStore(@RequestParam("messages") List<String> messages) {
log.info("保存到向量数据库中,消息数据:{}", messages);
//构建文档
List<Document> documents = CollStreamUtil.toList(messages, message -> Document.builder()
.text(message)
.build());
//存储到向量数据库中
this.vectorStore.add(documents);
log.info("保存到向量数据库成功, 数量:{}", messages.size());
}
@GetMapping
public EmbeddingResponse embed(@RequestParam("message") String message) {
return this.embeddingModel.embedForResponse(List.of(message));
}
@DeleteMapping
public void deleteVectorStore(@RequestParam("ids") List<String> ids) {
// 删除向量数据库中的数据
this.vectorStore.delete(ids);
}
@GetMapping("/search")
public List<Document> search(@RequestParam("message") String message) {
return this.vectorStore.similaritySearch(SearchRequest.builder().query(message).topK(5).build());
}
@GetMapping("/search/all")
public List<Document> searchAll() {
// 搜索全部数据
return this.vectorStore.similaritySearch(SearchRequest.builder().query("").topK(999).build());
}
}
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐





所有评论(0)