java 篇: 1.基础地基 2.设计原理 3.项目实战
多智能体协同工作-历史对话-异步设置标题:
历史对话功能是可以让用户查询之前的对话记录,最多查询 30 条,并且按照当天、最近 30 天、最近 1 年、1 年以上分类,还支持更新标题、删除功能,如下:

代码实现:
在 `ChatSessionService` 中新增方法:
java
/**
* 更新会话更新时间
*
* @param sessionId 会话ID,用于标识特定的聊天会话
* @param title 新的会话标题,如果为空则不进行更新
* @param userId 用户ID
*/
void update(String sessionId, String title, Long userId);
编写接口实现类:(注意,这里采用了异步更新,主要是确保 AI 对话聊天时的用户体验)
java
/**
* 异步更新聊天会话的标题
*
* @param sessionId 会话ID,用于标识特定的聊天会话
* @param title 新的会话标题,如果为空则不进行更新
* @param userId 用户ID
*/
@Async
@Override
public void update(String sessionId, String title, Long userId) {
// 查询符合条件的聊天会话列表
List<ChatSession> list = super.lambdaQuery()
.eq(ChatSession::getSessionId, sessionId)
.eq(ChatSession::getUserId, userId)
.list();
// 如果列表为空,直接返回,无需进一步处理
if (CollUtil.isEmpty(list)) {
return;
}
// 获取列表中的第一个聊天会话实例
ChatSession chatSession = list.get(0);
// 如果聊天会话的标题为空,并且新标题不为空,则更新标题
if (StrUtil.isEmpty(chatSession.getTitle()) && !StrUtil.isEmpty(title)) {
chatSession.setTitle(StrUtil.sub(title, 0, 100));
}
// 设置更新字段为updateTime为当前时间
chatSession.setUpdateTime(LocalDateTimeUtil.now());
// 更新数据库中的聊天会话信息
super.updateById(chatSession);
}
这里标题只有为空才更新,但是时间会每次问 ai 都会更新,因为历史记录当中是根据更新时间来的。比如 1 年前的对话,我今天问了句,那它就变成当天的了。
ChatServiceImpl 当中加下:

前端测试一下:


发现 title 写进去了,然后 create_time 和 update_time 也是不一样的,测试通过。
多智能体协同工作-历史对话-查询历史会话记录:

效果如下:

响应结构:
java
{
"code": 200,
"msg": "OK",
"data": {
"1年以上": [
{
"sessionId": "03b6491d3a1949c98cf0f8c37aa623fc",
"title": "水水水水谁谁谁水水水水谁谁谁水水水水水水水水",
"updateTime": "2023-02-26 15:45:31"
}
],
"最近1年": [
{
"sessionId": "53349594acff4a0fb92f71541491dc1b",
"title": "帮我推荐课程",
"updateTime": "2025-01-18 21:33:55"
},
{
"sessionId": "695fdea704254c089da454133a1c17a8",
"title": "你是谁",
"updateTime": "2025-01-18 21:33:37"
}
],
"最近30天": [
{
"sessionId": "e380350f97174313898c214afb37d6d8",
"title": "22222",
"updateTime": "2025-02-25 13:44:44"
}
],
"当天": [
{
"sessionId": "fa046bdb4ffe48fba4915e490e1e0b0e",
"title": "xxxxxx",
"updateTime": "2025-02-26 15:44:01"
}
]
},
"requestId": "bc8d535241104da7802e5d27f229d219"
}
代码实现:
先定义一个 VO:
java
package com.tianji.aigc.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChatSessionVO {
/**
* 会话id
*/
private String sessionId;
/**
* 会话标题
*/
private String title;
private LocalDateTime updateTime;
}
Controller
java
/**
* 查询历史会话列表
*/
@GetMapping("/history")
public Map<String, List<ChatSessionVO>> queryHistorySession() {
return this.chatSessionService.queryHistorySession();
}
Service
java
/**
* 查询历史会话列表
*/
Map<String, List<ChatSessionVO>> queryHistorySession();
Impl
java
@Override
public Map<String, List<ChatSessionVO>> queryHistorySession() {
var userId = UserContext.getUser();
// 查询历史会话,限制返回条数
var list = super.lambdaQuery()
.eq(ChatSession::getUserId, UserContext.getUser())
.isNotNull(ChatSession::getTitle)
.orderByDesc(ChatSession::getUpdateTime)
.last("LIMIT 30")
.list();
if (CollUtil.isEmpty(list)) {
log.info("No chat sessions found for user: {}", userId);
return Map.of();
}
// 转换为 ChatSessionVO 列表
var chatSessionVOS = CollStreamUtil.toList(list, chatSession ->
ChatSessionVO.builder()
.sessionId(chatSession.getSessionId())
.title(chatSession.getTitle())
.updateTime(chatSession.getUpdateTime())
.build()
);
final var TODAY = "当天";
final var LAST_30_DAYS = "最近30天";
final var LAST_YEAR = "最近1年";
final var MORE_THAN_YEAR = "1年以上";
// 当前时间
var now = LocalDateTime.now().toLocalDate();
// 按照更新时间分组
return CollStreamUtil.groupByKey(chatSessionVOS, vo -> {
// 计算两个日期之间的天数差
long between = Math.abs(ChronoUnit.DAYS.between(vo.getUpdateTime().toLocalDate(), now));
if (between == 0) {
return TODAY;
} else if (between <= 30) {
return LAST_30_DAYS;
} else if (between <= 365) {
return LAST_YEAR;
} else {
return MORE_THAN_YEAR;
}
});
}
这段代码的作用是查询用户的历史会话记录,并按时间分组(当天/最近 30 天/最近 1 年/1 年以上)。
查询数据库

最终 SQL:

转换为 VO

转换示例:

定义时间分组常量

按时间分组(核心逻辑)

分组规则
|------------|-------|-----------|
| 天数差 | 分组 | 说明 |
| `0` | 当天 | 今天更新的会话 |
| `1-30` | 最近30天 | 30天内更新的会话 |
| `31-365` | 最近1年 | 1年内更新的会话 |
| `>365` | 1年以上 | 超过1年的会话 |
执行示例
输入数据

输出结果

测试一下:

Map>,这里的 String 就是分组时间。

并且点击可以调整对应的对话。这里也可以验证之前课程卡片的 bug。



继续对话后,更新时间更新

测试通过。
多智能体协同工作-历史对话-删除历史会话:

对于历史对话的删除,实现物理删除即可,但是要注意,Redis 中的对话数据也要相应的删除。
代码实现:
Controller:
java
/**
* 删除历史会话列表
*/
@DeleteMapping("/history")
public void deleteHistorySession(@RequestParam("sessionId") String sessionId) {
this.chatSessionService.deleteHistorySession(sessionId);
}
Service:
javascript
/**
* 删除历史会话
*
* @param sessionId 会话id
*/
void deleteHistorySession(String sessionId);
Impl:
java
@Override
public void deleteHistorySession(String sessionId) {
//删除数据库的数据
var queryWrapper = Wrappers.<ChatSession>lambdaQuery()
.eq(ChatSession::getSessionId, sessionId)
.eq(ChatSession::getUserId, UserContext.getUser());
super.remove(queryWrapper);
//删除redis中的数据
var conversationId = ChatService.getConversationId(sessionId);
this.chatMemory.clear(conversationId);
}
前端测试一下:

然后看数据库,没了这记录,测试成功,redis 当中也没有了记忆,成功。
多智能体协同工作-实战任务:
练习 1:
历史会话的标题,默认是用户第一次发出的问题,后面用户可以修改标题,如下:
点击编辑按钮:



接口文档
参数有:sessionId 和 title 。

代码实现:
Controller:
java
/**
* 更新历史会话标题
*/
@PutMapping("/history")
public void updateTitle(@RequestParam("sessionId") String sessionId,
@RequestParam("title") String title) {
this.chatSessionService.updateTitle(sessionId, title);
}
Service:
java
/**
* 更新历史会话标题
*
* @param sessionId 会话id
* @param title 标题
*/
void updateTitle(String sessionId, String title);
Impl:
java
@Override
public void updateTitle(String sessionId, String title) {
//更新数据
super.lambdaUpdate()
// 设置更新条件, 更新字段为title(最多设置前100个字符),更新条件为sessionId和userId
.set(ChatSession::getTitle, StrUtil.sub(title, 0, 100))
.eq(ChatSession::getSessionId, sessionId)
.eq(ChatSession::getUserId, UserContext.getUser())
.update();
}
测试一下:


测试通过
多智能体协同工作-智能体架构模型:
前面我们已经完成了天机 AI 助手智能体功能的开发,实际上我们实现的方式只是最为基础的一种模式,一般应用系统中的智能体架构有 6 种,分别是:
- 增强型智能体
- 链式工作流智能体
- 路由工作流智能体
- 并行工作流智能体
- 协调器工作流智能体
- 评估优化工作流智能体
下面,我们一起来了解下这 6 种架构模式,重点要关注:**路由工作流智能体**。
**1.增强型智能体**
增强型智能体的基本构建块是一个增强的 LLM,其中包含检索、工具和记忆等增强功能。

因为复杂的场景,那调用的工具,以及提示词会很多,就会很臃肿。
**2.链式工作流智能体**
工作流智能体模式,就是将任务分解为一系列步骤,其中每个 LLM 调用处理上一个步骤的输出,通过多步骤 LLM 调用分阶段处理复杂任务。


用意就是不同阶段,调用不同的模型,因为每个模型擅长点不同。
**3.路由工作流智能体**
路由工作流智能体,这种模式是将输入通过 **LLM Call Router **对意图识别,再交由下游的 **LLM **执行。

例如智能客服,根据不同需求,转发到不同的智能体,相当于微服务网关。这样一个智能体就不会那么臃肿。
**4.并行工作流智能体**
并行工作智能体,是值一个输入同时交给多个 LLM 去执行,再将这些大模型的输出进行汇总处理,再输出。

①:为了效率,可以将复杂任务拆分,给多个大模型执行不同的任务。
②:为了准确,每个模型执行相同任务,最后再去整合。
**5.协调器工作流智能体**
协调器工作流智能体,这种模式是,由 `Orchestrator LLM` 作为智能调度中心,**动态生成**子任务列表,子任务可以是并行或串行执行,结果由 `Synthesizer` 进行聚合输出。


前面的两种模式,简单的任务也会执行多个。现在就是为了优化这点,所以可以判断,选择用几个模型。
**评估优化工作流智能体**
评估优化工作流智能体,是这一种 生成 → 评估 → 反馈 循环反馈的机制。


**总结**
|----------------|--------------|--------------|-------------|--------------------|---------------|
| **模式名称** | **控制方式** | **延迟水平** | **可靠性** | **典型应用场景** | **开发复杂度** |
| **增强型智能体** | 直接输出 | 最低 | 低 | 简单问答、内容润色 | 简单 |
| 链式工作流智能体 | 线性顺序执行 | 中等 | 中高 | 分阶段任务(如大纲→内容→格式优化) | 中等 |
| 路由工作流智能体 | 条件分支选择 | 低-中等 | 中 | 多领域处理(如客服分流转人工) | 中等 |
| 并行工作流智能体 | 多模型并发执行 | 中等 | 高 | 可靠性敏感任务(如医疗诊断辅助) | 较高 |
| 协调器工作流智能体 | 动态任务分解+调度 | 高 | 最高 | 复杂业务(如商业智能分析系统) | 极高 |
| 评估优化工作流智能体 | 迭代优化+反馈修正 | 最高 | 极高 | 质量敏感场景(如法律文件生成) | 高 |
**模式选型建议,根据业务需求选择:**
- **简单任务** → 增强型智能体 / 链式工作流智能体
- **多分支处理** → 路由模式
- **高实时性** → 并行化(需任务可拆分)
- **超复杂任务** → 协调器工作流智能体
- **超高可靠性** → 评估优化工作流智能体
多智能体协同工作-路由工作流智能体实现:
根据前面的分析,我们的天机 AI 助理,比较适合用路由工作流模式,接下来,我们将把之前的增强型智能体,改造成路由工作流模式。
实现流程如下:

- 我们把原来的单一智能体改成了 5 个智能体一起协同工作。
- 当用户提出问题时,首先会发送给【意图分析智能体】,它会判断用户是想让我们推荐课程、查询课程信息还是购买课程。
- 一旦明确了用户的意图,就会根据不同的需求调用相应的智能体来完成任务,比如推荐课程或购买课程等。
- 这样做的好处是每个智能体都有明确的任务分工,并且只有在需要时才会调用特定的工具,不需要所有智能体都配备全套工具。这样一来,整个系统变得更加灵活高效了。
代码实现:
定义类型枚举:
不同的智能体,是需要通过类型来区分的,比较好的一种方式就是定义类型枚举。
java
package com.tianji.aigc.enums;
import cn.hutool.core.util.EnumUtil;
import lombok.Getter;
/**
* 智能体类型
*/
@Getter
public enum AgentTypeEnum {
ROUTE("ROUTE", "路由智能体"),
RECOMMEND("RECOMMEND", "课程推荐智能体"),
CONSULT("CONSULT", "课程咨询智能体"),
BUY("BUY", "课程购买智能体"),
KNOWLEDGE("KNOWLEDGE", "知识讲解智能体");
private final String agentName;
private final String desc;
AgentTypeEnum(String agentName, String desc) {
this.agentName = agentName;
this.desc = desc;
}
@Override
public String toString() {
return this.name();
}
/**
* 通过智能体的名称查找枚举
*/
public static AgentTypeEnum agentNameOf(String agentName) {
return EnumUtil.getBy(AgentTypeEnum::getAgentName, agentName);
}
}
定义 Agent 接口:
我们可以想一下,每个智能体都有什么相关的方法,就把他们抽象出来,形成一个 `Agent interface`,子类只需要实现接口即可。
应该有的方法:
- process (普通对话)
- processStream (流式对话)
- getAgentType (获取智能体类型)
- stop (停止方法)
- systemMessage (获取系统提示词方法)
以上这些都是基本的操作方法。实际上,对于一个智能体而言,与大模型或 Tools 交互,还需要一些设定,比如 toolContext、advisors 等,所以还需要额外的加一个方法:
- tools (工具集)
- toolContext (工具上下文参数)
- advisors (Advisor 列表)
- advisorParams (Advisor 参数列表)
- systemMessageParams (系统提示词中的参数列表)
所以,基于上面的分析,就可以定义 `Agent interface` 了:
java
package com.tianji.aigc.agent;
import com.tianji.aigc.enums.AgentTypeEnum;
import com.tianji.aigc.vo.ChatEventVO;
import org.springframework.ai.chat.client.advisor.api.Advisor;
import reactor.core.publisher.Flux;
import java.util.List;
import java.util.Map;
/**
* AI代理接口,定义处理聊天事件和会话的核心能力
*/
public interface Agent {
/**
* 表示空参数的预定义数组
*/
Object[] EMPTY_OBJECTS = new Object[0];
/**
* 处理流式请求(如流式回答)
*
* @param question 用户输入的问题
* @param sessionId 会话唯一标识
* @return 包含中间结果的反应式事件流(Flux)
*/
Flux<ChatEventVO> processStream(String question, String sessionId);
/**
* 处理标准请求(非流式)
*
* @param question 用户输入的问题
* @param sessionId 会话唯一标识
* @return 最终处理结果字符串
*/
String process(String question, String sessionId);
/**
* 获取智能体类型标识
*
* @return 代理类型枚举值(如:ROUTE、RECOMMEND等)
*/
AgentTypeEnum getAgentType();
/**
* 停止指定会话的处理
*
* @param sessionId 需要终止的会话ID
*/
void stop(String sessionId);
/**
* 获取系统提示信息模板,默认为空字符串,子类可以覆盖重写该方法以返回自定义的系统提示信息。
*
* @return 系统提示的文本模板
*/
default String systemMessage() {
return "";
}
/**
* 获取工具列表,默认返回空数组。子类需根据需求覆盖此方法。
*/
default Object[] tools() {
return EMPTY_OBJECTS;
}
/**
* 创建并返回一个工具上下文的空Map对象。
*
* @param sessionId 会话标识符
* @param requestId 请求标识符
* @return 默认返回一个空的Map对象,子类可以覆盖重写该方法以返回自定义的工具上下文。
*/
default Map<String, Object> toolContext(String sessionId, String requestId) {
return Map.of();
}
/**
* Advisor列表,默认返回空对象
*/
default List<Advisor> advisors() {
return List.of();
}
/**
* 创建并返回一个Advisor的空Map对象。
*
* @param sessionId 会话标识符
* @param requestId 请求标识符
* @return 默认返回一个空的Map对象,子类可以覆盖重写该方法以返回自定义的工具上下文。
*/
default Map<String, Object> advisorParams(String sessionId, String requestId) {
return Map.of();
}
/**
* 获取系统提示信息模板的参数,默认为空Map,子类可以覆盖重写该方法以返回自定义的系统提示信息参数。
*/
default Map<String, Object> systemMessageParams() {
return Map.of();
}
}
编写抽象类:
去实现通用的方法:

java
package com.tianji.aigc.agent;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.tianji.aigc.config.ToolResultHolder;
import com.tianji.aigc.constants.Constant;
import com.tianji.aigc.enums.ChatEventTypeEnum;
import com.tianji.aigc.service.ChatService;
import com.tianji.aigc.service.ChatSessionService;
import com.tianji.aigc.vo.ChatEventVO;
import com.tianji.common.utils.UserContext;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.data.redis.core.StringRedisTemplate;
import reactor.core.publisher.Flux;
import java.util.Map;
@Slf4j
public abstract class AbstractAgent implements Agent {
public static final ChatEventVO _STOP_EVENT _= ChatEventVO.builder().eventType(ChatEventTypeEnum._STOP_.getValue()).build();
@Resource
private ChatClient chatClient;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private ChatMemory chatMemory;
@Resource
private ChatSessionService chatSessionService;
private static final String _GENERATE_STATUS_KEY _= "GENERATE_STATUS";
@Override
public Flux<ChatEventVO> processStream(String question, String sessionId) {
// 生成请求id
var requestId = this.generateRequestId();
var hashOps = this.stringRedisTemplate.boundHashOps(_GENERATE_STATUS_KEY_);
// 将会话id转化为对话id
var conversationId = ChatService._getConversationId_(sessionId);
// 大模型输出内容的缓存器,用于在输出中断后的数据存储
var outputBuilder = new StringBuilder();
// 获取到当前登录的用户id
var userId = UserContext.getUser();
//更新会话时间
this.chatSessionService.update(sessionId, question, userId);
return this.getChatClientRequest(question, sessionId, requestId)
.stream()
.chatResponse()
.doFirst(() -> hashOps.put(sessionId, "true")) // 生成开始时,设置标识
.doOnError(throwable -> hashOps.delete(sessionId)) // 异常结束时,删除标识
.doOnComplete(() -> hashOps.delete(sessionId)) // 正常结束时,删除标识
.doOnCancel(() -> {
// 当输出被取消时,保存输出的内容到历史记录中
this.saveStopHistoryRecord(conversationId, outputBuilder.toString());
}) // 打断输出的事件
.takeWhile(response -> hashOps.get(sessionId) != null) // 后续生成的条件,true:继续生成,false:停止生成
.map(chatResponse -> {
// 大模型生成的内容
var text = chatResponse.getResult().getOutput().getText();
// 追加到输出内容中
outputBuilder.append(text);
// 获取到消息的结束原因
var finishReason = chatResponse.getResult().getMetadata().getFinishReason();
if (StrUtil.equals(finishReason, Constant._STOP_)) {
// 获取到消息id
var messageId = chatResponse.getMetadata().getId();
// 将消息id与请求id进行关联
ToolResultHolder._put_(messageId, Constant._REQUEST_ID_, requestId);
}
return ChatEventVO.builder()
.eventData(text)
.eventType(ChatEventTypeEnum._DATA_.getValue())
.build();
})
.concatWith(Flux.defer(() -> {
var result = ToolResultHolder._get_(requestId);
if (ObjectUtil.isNotEmpty(result)) {
ToolResultHolder._remove_(requestId);
// 工具被调用了,需要向前端传递参数
return Flux.just(ChatEventVO.builder()
.eventType(ChatEventTypeEnum._PARAM_.getValue())
.eventData(result)
.build(), _STOP_EVENT_);
}
return Flux.just(_STOP_EVENT_); // 结束标识
}));
}
@Override
public String process(String question, String sessionId) {
// 生成请求id
var requestId = this.generateRequestId();
// 获取到当前登录的用户id
var userId = UserContext.getUser();
//更新会话时间
this.chatSessionService.update(sessionId, question, userId);
return this.getChatClientRequest(question, sessionId, requestId)
.call()
.content();
}
private ChatClient.ChatClientRequestSpec getChatClientRequest(String question, String sessionId, String requestId) {
return this.chatClient.prompt()
.system(promptSystemSpec -> promptSystemSpec.text(this.systemMessage()).params(this.systemMessageParams()))
.advisors(advisorSpec -> advisorSpec.advisors(this.advisors()).params(this.advisorParams(sessionId, requestId)))
.tools(this.tools())
.toolContext(this.toolContext(sessionId, requestId))
.user(question);
}
_/**_
_ * 保存停止输出的记录_
_ *_
_ * @param conversationId 会话id_
_ * @param content 大模型输出的内容_
_ */_
_ _private void saveStopHistoryRecord(String conversationId, String content) {
this.chatMemory.add(conversationId, new AssistantMessage(content));
}
private String generateRequestId() {
return IdUtil.fastSimpleUUID();
}
@Override
public void stop(String sessionId) {
var hashOps = this.stringRedisTemplate.boundHashOps(_GENERATE_STATUS_KEY_);
hashOps.delete(sessionId);
}
@Override
public Map<String, Object> advisorParams(String sessionId, String requestId) {
// 将会话id转化为对话id
var conversationId = ChatService._getConversationId_(sessionId);
return Map._of_(ChatMemory.CONVERSATION_ID, conversationId);
}
}
这是一个抽象 Agent 类,实现了 AI 对话的核心流程,包括流式对话、工具调用、对话中断恢复等功能。
整体架构

依赖注入

常量定义

**公共方法(public)- 对外暴露**
|-------------------|---------------------------------------|------------|-------------|--------------|
| 方法名 | 参数 | 返回值 | 作用 | 调用场景 |
| `processStream` | `String question, String sessionId` | `Flux` | 流式对话,实时返回内容 | 前端需要实时显示AI回答 |
| `process` | `String question, String sessionId` | `String` | 同步对话,等待完整结果 | 后端内部调用、非实时场景 |
| `stop` | `String sessionId` | `void` | 停止当前正在生成的对话 | 用户点击停止按钮 |
**受保护方法(protected)- 子类可覆盖**
|-------------------------|----------------------------------------|----------------|--------------------|--------------------------|
| 方法名 | 参数 | 返回值 | 作用 | 默认实现 |
| `systemMessage` | 无 | `String` | 定义AI的系统提示词 | `null`(子类必须实现) |
| `systemMessageParams` | 无 | `Map` | 系统提示词的参数 | `Map.of()`(空Map) |
| `advisors` | 无 | `Object\[\]` | 定义AI的Advisor(如RAG) | `new Object0`(空数组) |
| `advisorParams` | `String sessionId, String requestId` | `Map` | Advisor的参数 | 返回包含conversationId的Map |
| `tools` | 无 | `Object\[\]` | 定义AI可调用的工具 | `new Object0`(空数组) |
| `toolContext` | `String sessionId, String requestId` | `Map` | 工具调用的上下文 | 返回包含requestId的Map |
私有方法(private)- 内部使用
|---------------------------|---------------------------------------------------------|---------------------------|----------------|
| 方法名 | 参数 | 返回值 | 作用 |
| `generateRequestId` | 无 | `String` | 生成唯一请求ID(UUID) |
| `getChatClientRequest` | `String question, String sessionId, String requestId` | `ChatClientRequestSpec` | 构建AI请求 |
| `saveStopHistoryRecord` | `String conversationId, String content` | `void` | 保存中断时的输出内容 |
**核心方法详解**
1.`processStream()` - 流式对话(核心)
这是最重要的方法,处理 SSE 流式响应。

流式处理链

2.`process()` - 同步对话

3.`stop()` - 停止生成

4.`saveStopHistoryRecord()` - 保存中断记录

当用户手动停止时,把已生成的内容保存到对话历史中。
数据流图

添加路由智能体 nacos 配置:
java
# 角色
天机AI意图分析师
## 能力
1. 识别用户意图并匹配对应编号:
- RECOMMEND(课程推荐)
- BUY(课程购买)
- CONSULT(课程咨询)
- KNOWLEDGE(知识讲解)
2. 特殊场景处理:
- 识别关键词触发意图:
- BUY: 确认购买/下单/是的确认
- RECOMMEND: 包含年龄/学历/兴趣信息
- 识别问候语并礼貌回应:你好/您好
3. 非相关提问时礼貌拒答
## 约束
精准识别,避免误判
## 输出
- 匹配意图时返回编号
- 问候语场景返回「您好!有什么可以帮您?」
- 无匹配时用自然语言回复
## 示例
输入:20岁本科想学Java → RECOMMEND
输入:现在要下单 → BUY
输入:这个课程多少钱 → CONSULT
输入:java是什么 → KNOWLEDGE
输入:你好 → 您好!有什么可以帮您?
输入:今天天气 → 抱歉我只处理课程相关问题

AIProperties 读取配置:

`application.yml` 中增加配置:

SystemPromptConfig 加载配置:

**路由智能体**
java
package com.tianji.aigc.agent;
import com.tianji.aigc.config.SystemPromptConfig;
import com.tianji.aigc.enums.AgentTypeEnum;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* 路由智能体
*/
@Component
@RequiredArgsConstructor
public class RouteAgent extends AbstractAgent {
private final SystemPromptConfig systemPromptConfig;
@Override
public String systemMessage() {
return this.systemPromptConfig.getRouteAgentSystemMessage().get();
}
@Override
public AgentTypeEnum getAgentType() {
return AgentTypeEnum.ROUTE;
}
}
测试用例
java
package com.tianji.aigc.agent;
import cn.hutool.core.lang.Assert;
import com.tianji.aigc.enums.AgentTypeEnum;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class RouteAgentTest {
@Resource
private RouteAgent routeAgent;
@Test
public void testChat(){
Assert.equals(this.routeAgent.process("最新有哪些课程", "1"), AgentTypeEnum.RECOMMEND.getAgentName());
Assert.equals(this.routeAgent.process("下单购买这个课程", "1"), AgentTypeEnum.BUY.getAgentName());
Assert.equals(this.routeAgent.process("这个课程是多少钱", "1"), AgentTypeEnum.CONSULT.getAgentName());
Assert.equals(this.routeAgent.process("java是什么", "1"), AgentTypeEnum.KNOWLEDGE.getAgentName());
}
}
SpringAIConfig 删除不必要的配置:
删除添加课程工具,因为有些智能体不用工具

为什么要用断言验证?

测试结果:

**推荐智能体**
系统提示词

java
# 在线教育客服&讲师指南
## 核心职责
分步精准推荐:信息采集 → 课程匹配 → 执行推荐
## 强制流程
1. **信息采集(必须优先)**
- 必须收集三项核心数据:
▪ 年龄(数字)
▪ 最高学历(初中/高中/本科/硕士等)
▪ 编程基础(无经验/基础语法/项目经验)
- 任一信息缺失时:立即停止推荐,礼貌追问直至信息完整
2. **课程匹配
- 强制:要通过课程id查询课程之后再输出
- 匹配逻辑:
1) 精准匹配(年龄+学历+兴趣)
2) 向下兼容课程(如学历达标但年龄较小)
3) 关联领域Top3课程
3. **推荐执行
- 每次推荐必须包含:
▪ 数据关联说明(例:"针对25岁本科学历...")
▪ 课程适配点(例:"包含实战项目模块...")
- 禁止推荐未经数据验证的课程
## 关键规则
- 阻断机制:未收齐三项数据前禁用推荐功能
- 数据校验:发现矛盾数据(如"12岁硕士学历")需确认
- 异常处理:无匹配时提供「人工咨询」入口
- 必须要输出课程id、价格、介绍等信息
读取配置:
java
package com.tianji.aigc.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "tj.ai.prompt")
public class AIProperties {
private System system; // 系统提示语,用于课程推荐、购买业务
@Data
public static class System {
private Chat chat; // 系统提示语,用于课程推荐、购买业务
private Chat routeAgent; // 路由智能体系统提示词
private Chat recommendAgent; // 推荐智能体系统提示词
@Data
public static class Chat {
private String dataId;
private String group = "DEFAULT_GROUP";
private long timeoutMs = 20000L; // 读取的超时时间,单位毫秒
}
}
}
`application.yml` 中增加配置:
java
tj:
ai:
prompt:
system:
chat:
data-id: system-chat-message.txt
group: DEFAULT_GROUP
timeout-ms: 20000
route-agent:
data-id: route-agent-system-message.txt
recommend-agent:
data-id: recommend-agent-system-message.txt
加载配置:
java
package com.tianji.aigc.config;
// 省略一些代码........
public class SystemPromptConfig {
// 省略一些代码........
// 使用原子引用,保证线程安全
private final AtomicReference<String> chatSystemMessage = new AtomicReference<>();
private final AtomicReference<String> routeAgentSystemMessage = new AtomicReference<>();
private final AtomicReference<String> recommendAgentSystemMessage = new AtomicReference<>();
@PostConstruct // 初始化时加载配置
public void init() {
// 读取配置文件
loadConfig(aiProperties.getSystem().getChat(), chatSystemMessage);
loadConfig(aiProperties.getSystem().getRouteAgent(), routeAgentSystemMessage);
loadConfig(aiProperties.getSystem().getRecommendAgent(), recommendAgentSystemMessage);
}
// 省略一些代码........
}
编写智能体:
java
package com.tianji.aigc.agent;
import com.tianji.aigc.config.SystemPromptConfig;
import com.tianji.aigc.constants.Constant;
import com.tianji.aigc.enums.AgentTypeEnum;
import com.tianji.aigc.tools.CourseTools;
import com.tianji.common.utils.UserContext;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.advisor.api.Advisor;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
@Component
@RequiredArgsConstructor
public class RecommendAgent extends AbstractAgent {
private final SystemPromptConfig systemPromptConfig;
private final VectorStore vectorStore;
private final CourseTools courseTools;
@Override
public String systemMessage() {
return this.systemPromptConfig.getRecommendAgentSystemMessage().get();
}
@Override
public AgentTypeEnum getAgentType() {
return AgentTypeEnum.RECOMMEND;
}
@Override
public List<Advisor> advisors() {
// 创建RAG增强
var qaAdvisor = QuestionAnswerAdvisor.builder(this.vectorStore)
.searchRequest(SearchRequest.builder().similarityThreshold(0.6d).topK(6).build())
.build();
return List.of(qaAdvisor);
}
@Override
public Object[] tools() {
return new Object[]{courseTools};
}
@Override
public Map<String, Object> toolContext(String sessionId, String requestId) {
var userId = UserContext.getUser();
return Map.of(
//Constant.USER_ID, userId, // 设置用户id参数
Constant.REQUEST_ID, requestId // 设置请求id参数
);
}
}


测试用例:
java
package com.tianji.aigc.agent;
import com.tianji.aigc.vo.ChatEventVO;
import com.tianji.common.utils.UserContext;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import reactor.core.publisher.Flux;
@SpringBootTest
class RecommendAgentTest {
@Resource
private RecommendAgent recommendAgent;
@Test
public void processStream() throws InterruptedException {
String question = "推荐课程,20岁,本科,对java感兴趣";
String sessionId = "123";
UserContext.setUser(123L);
Flux<ChatEventVO> flux = recommendAgent.processStream(question, sessionId);
flux.subscribe(System.out::println);
// 阻塞主线程,防止主线程结束,子线程终止
Thread.sleep(100000);
}
}
运行结果如下:

也有 params 的输出
**课程购买智能体**
系统提示词

读取配置
java
package com.tianji.aigc.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "tj.ai.prompt")
public class AIProperties {
private System system; // 系统提示语,用于课程推荐、购买业务
@Data
public static class System {
private Chat chat; // 系统提示语,用于课程推荐、购买业务
private Chat routeAgent; // 路由智能体系统提示词
private Chat recommendAgent; // 推荐智能体系统提示词
private Chat buyAgent; // 购买智能体系统提示词
@Data
public static class Chat {
private String dataId;
private String group = "DEFAULT_GROUP";
private long timeoutMs = 20000L; // 读取的超时时间,单位毫秒
}
}
}
`application.yml` 中增加配置:
java
tj:
ai:
prompt:
system:
chat:
data-id: system-chat-message.txt
group: DEFAULT_GROUP
timeout-ms: 20000
route-agent:
data-id: route-agent-system-message.txt
recommend-agent:
data-id: recommend-agent-system-message.txt
buy-agent:
data-id: buy-agent-system-message.txt
加载配置:
java
package com.tianji.aigc.config;
// 省略一些代码........
public class SystemPromptConfig {
// 省略一些代码........
// 使用原子引用,保证线程安全
private final AtomicReference<String> chatSystemMessage = new AtomicReference<>();
private final AtomicReference<String> routeAgentSystemMessage = new AtomicReference<>();
private final AtomicReference<String> recommendAgentSystemMessage = new AtomicReference<>();
private final AtomicReference<String> buyAgentSystemMessage = new AtomicReference<>();
@PostConstruct // 初始化时加载配置
public void init() {
// 读取配置文件
loadConfig(aiProperties.getSystem().getChat(), chatSystemMessage);
loadConfig(aiProperties.getSystem().getRouteAgent(), routeAgentSystemMessage);
loadConfig(aiProperties.getSystem().getRecommendAgent(), recommendAgentSystemMessage);
loadConfig(aiProperties.getSystem().getBuyAgent(), buyAgentSystemMessage);
}
// 省略一些代码........
}
编写智能体
java
package com.tianji.aigc.agent;
import com.tianji.aigc.config.SystemPromptConfig;
import com.tianji.aigc.constants.Constant;
import com.tianji.aigc.enums.AgentTypeEnum;
import com.tianji.aigc.tools.OrderTools;
import com.tianji.common.utils.UserContext;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
@RequiredArgsConstructor
public class BuyAgent extends AbstractAgent {
private final SystemPromptConfig systemPromptConfig;
private final OrderTools orderTools;
@Override
public String systemMessage() {
return this.systemPromptConfig.getBuyAgentSystemMessage().get();
}
@Override
public AgentTypeEnum getAgentType() {
return AgentTypeEnum.BUY;
}
@Override
public Object[] tools() {
return new Object[]{orderTools};
}
@Override
public Map<String, Object> toolContext(String sessionId, String requestId) {
var userId = UserContext.getUser();
return Map.of(
Constant.USER_ID, userId, // 设置用户id参数
Constant.REQUEST_ID, requestId // 设置请求id参数
);
}
}
当中涉及到的工具类 copy 一下,常量添加一下。
测试用例
java
package com.tianji.aigc.agent;
import com.tianji.aigc.vo.ChatEventVO;
import com.tianji.common.utils.UserContext;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import reactor.core.publisher.Flux;
@SpringBootTest
class BuyAgentTest {
@Resource
private BuyAgent buyAgent;
@Test
public void processStream() throws InterruptedException {
String question = "下单购买,课程id为:1589905661084430337";
String sessionId = "123";
UserContext.setUser(123L);
Flux<ChatEventVO> flux = buyAgent.processStream(question, sessionId);
flux.subscribe(System.out::println);
// 阻塞主线程,防止主线程结束,子线程终止
Thread.sleep(100000);
}
}
测试结果:

测试成功
多智能体协同工作-整合多智能体:
前面已经实现了多个智能体,这些智能体都是独立运行,接下来我们就需要把他们整合起来,一起协调工作,完成天机 AI 助理。
编写 AgentServiceImpl 实现类
java
package com.tianji.aigc.service.impl;
import cn.hutool.extra.spring.SpringUtil;
import com.tianji.aigc.agent.AbstractAgent;
import com.tianji.aigc.agent.Agent;
import com.tianji.aigc.enums.AgentTypeEnum;
import com.tianji.aigc.enums.ChatEventTypeEnum;
import com.tianji.aigc.service.ChatService;
import com.tianji.aigc.vo.ChatEventVO;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
@Service
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "tj.ai", name = "chat-type", havingValue = "ROUTE")
public class AgentServiceImpl implements ChatService {
@Override
public Flux<ChatEventVO> chat(String question, String sessionId) {
// 先通过路由智能体,分析用户的意图,再执行后面的逻辑
var result = this.findAgentByType(AgentTypeEnum.ROUTE).process(question, sessionId);
var agentTypeEnum = AgentTypeEnum.agentNameOf(result);
var agent = this.findAgentByType(agentTypeEnum);
if (agent == null) {
// 找不到对应的智能体,直接返回结果
var chatEventVO = ChatEventVO.builder()
.eventType(ChatEventTypeEnum.DATA.getValue())
.eventData(result)
.build();
return Flux.just(chatEventVO, AbstractAgent.STOP_EVENT);
}
// 执行智能体的逻辑
return agent.processStream(question, sessionId);
}
/**
* 根据代理类型查找对应的Agent实例
*
* @param agentTypeEnum 要查找的代理类型
* @return 与给定类型匹配的Agent实例,如果未找到或类型为null则返回null
*/
private Agent findAgentByType(AgentTypeEnum agentTypeEnum) {
if (agentTypeEnum == null) {
return null;
}
var beans = SpringUtil.getBeansOfType(Agent.class);
// 遍历所有Agent Bean查找匹配类型
for (var agent : beans.values()) {
if (agentTypeEnum == agent.getAgentType()) {
return agent;
}
}
return null;
}
/**
* 停止生成
*
* @param sessionId 会话ID
*/
@Override
public void stop(String sessionId) {
this.findAgentByType(AgentTypeEnum.ROUTE).stop(sessionId);
}
}

- `SpringUtil`:Hutool 提供的 Spring 工具类
- `getBeansOfType(Agent.class)`:获取所有类型为 `Agent`(或其子类)的 Bean
- 返回值:`Map`
- Key:Bean 的名称(通常是类名首字母小写)
- Value:Agent 实例

|-----------------------------------------------|-----------------|---------------------------------------------------------------|
| 部分 | 说明 | 示例值 |
| `this.findAgentByType(AgentTypeEnum.ROUTE)` | 获取路由 Agent 实例 | `RouteAgent` 对象 |
| `.process(question, sessionId)` | 调用路由 Agent 处理问题 | 返回意图识别结果 |
| `result` | 意图识别的结果 | `"recommend"` / `"buy"` / `"consult"` / `"knowledge"` |
作用:让路由 Agent 分析用户问题,返回应该由哪个 Agent 处理的标识。

|-------------------------|---------------|---------------------------------------------------------|
| 部分 | 说明 | 示例 |
| `AgentTypeEnum` | 代理类型枚举 | 定义 `RECOMMEND`, `BUY`, `CONSULT`, `KNOWLEDGE` 等 |
| `agentNameOf(result)` | 静态方法,将字符串转为枚举 | `"recommend"` → `AgentTypeEnum.RECOMMEND` |
作用:将路由 Agent 返回的字符串(如 `"recommend"`)转换为对应的枚举类型。
完整执行流程

一个 ChatService 接口有两个实现类

一个是 ChatServiceImpl 一个是 AgentServiceImpl,如果直接注入的话,就会出错,所以这里得设置一下。
在 `application.yml` 配置文件中,增强条件配置:
tj:
ai:
chat-type: ROUTE # ROUTE / ENHANCE / APP
改造 ChatServiceImpl
在原 `ChatServiceImpl` 中添加条件:

改造 SpringAIConfig
在 SpringAIConfig 中,就不需要设置默认的 Tool 了,需要改造下,如下:

这个在前面已经处理过了
测试一下:
发送"你好"

发送"课程推荐"

多智能体协同工作-对话记录的 bug 修复:

查看对话记录时,路由智能体发送的部分,也出来了。
那如何解决呢,就是想办法把打叉的部分去掉,这里根据是否为类型,来判断。


advisor 放第一个,虽然进去是第一个先进去,但是出来是最后一个出来呀
**第一步:**编写 `RecordOptimizationAdvisor`
java
package com.tianji.aigc.advisor;
import cn.hutool.core.map.MapUtil;
import com.tianji.aigc.enums.AgentTypeEnum;
import com.tianji.aigc.memory.MyChatMemoryRepository;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.client.advisor.api.Advisor;
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
/**
* 记录优化
*/
public class RecordOptimizationAdvisor implements BaseAdvisor {
private final MyChatMemoryRepository myChatMemoryRepository;
public RecordOptimizationAdvisor(MyChatMemoryRepository myChatMemoryRepository) {
this.myChatMemoryRepository = myChatMemoryRepository;
}
@Override
public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {
return chatClientRequest;
}
@Override
public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {
// 获取大模型的响应内容
var chatResponse = chatClientResponse.chatResponse();
// 获取大模型的响应内容,判断内容是否是智能体的名称,如果是,优化记录,否则无需优化
assert chatResponse != null;
var text = chatResponse.getResult().getOutput().getText();
var agentType = AgentTypeEnum.agentNameOf(text);
if (null != agentType) {
// 需要优化记录
var conversationId = MapUtil.getStr(chatClientResponse.context(), ChatMemory.CONVERSATION_ID);
this.myChatMemoryRepository.optimization(conversationId);
}
return chatClientResponse;
}
@Override
public int getOrder() {
return Advisor.DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER - 100;
}
}
为了消除 idea 为空的警告,增加 `package-info.java` 文件:
java
@NonNullApi
@NonNullFields
package com.tianji.aigc.advisor;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;
**第二步,**创建 MyChatMemoryRepository 接口,定义 `optimization` 方法:
java
package com.tianji.aigc.memory;
public interface MyChatMemoryRepository {
/**
* 根据对话ID优化对话记录,删除最后的2条消息,因为这条消息是从路由智能体存储的,请求由后续的智能体处理
* 为了确保历史消息的完整性,所以需要将中间转发的消息清理掉
*
* @param conversationId 对话的唯一标识符
*/
void optimization(String conversationId);
}
**第三步,**在 `RedisChatMemoryRepository` 中实现 `optimization` 方法:
javascript
/**
* 根据对话ID优化对话记录,删除最后的2条消息,因为这条消息是从路由智能体存储的,请求由后续的智能体处理
* 为了确保历史消息的完整性,所以需要将中间转发的消息清理掉
*
* @param conversationId 对话的唯一标识符
*/
public void optimization(String conversationId) {
var redisKey = this.getKey(conversationId);
var listOps = this.stringRedisTemplate.boundListOps(redisKey);
// 从Redis列表右侧弹出2个元素
listOps.rightPop(2);
}
**第四步,**在 `SpringAIConfig` 中增加配置,使 `Advisor` 生效:
java
/**
* 配置 ChatClient
*/
@Bean
public ChatClient chatClient(ChatClient.Builder chatClientBuilder,
Advisor loggerAdvisor,
Advisor messageChatMemoryAdvisor,
Advisor recordOptimizationAdvisor, // 记录优化
CourseTools courseTools, // 课程工具
OrderTools orderTools // 预下单工具
) { // 日志记录器
return chatClientBuilder
.defaultAdvisors(loggerAdvisor, messageChatMemoryAdvisor, recordOptimizationAdvisor) //添加 Advisor 功能增强
// .defaultTools(courseTools, orderTools) //添加默认工具
.build();
}
/**
* 优化对话历史记录
*/
@Bean
public Advisor recordOptimizationAdvisor(MyChatMemoryRepository myChatMemoryRepository) {
return new RecordOptimizationAdvisor(myChatMemoryRepository);
}
**第五步,**测试:
发现出现了 Bug

bug 修复:
主要思路:

对于路由智能体,输入是"课程推荐",输出是"RECOMMEND",那输出是不能展示出来的,所以得在 after 方法当中处理
RecordOptimizationAdvisor:


前面是弹出 2,这里改成 1。相当于把路由智能体输出的 RECOMMEND 记录删掉。
那对于 RecommendAgent 输入是"RECOMMEND",然后它也会有输出,但是记录当中输入是不需要的,所以得在 before 方法当中进行处理。

那因为 before 那,还没有记录,所以这里只是做一个标记,到后面 after 方法当中,根据这个标记去执行相应的删除操作。

以上的操作就是下面图中所示:

前提基础知识补充讲解:

首先 RecordOptimizationAdvisor 实现了 BaseAdvisor

而 BaseAdvisor 又继承了 CallAdvisor 和 StreamAdvisor,如果是普通的大模型调用,CallAdvisor 生效。如果是流式的话,StreamAdvisor 生效。所以我们只需要实现 before 和 after 方法就行了。
下面重新进行测试:

这里可能也有小 bug,就是打出你好后,之后再去对话,发现发送不了了,这是因为流没有关闭导致的。
修复:
AgentServiceImpl

但是测下来还是有 bug

修复条件是又把它改成 2 了

我们再分析一波


对于指向智能体的就是 jack 那方,智能体吐出来的就是小黄鸡那方。
推测是这样一个流程,所以是删 2 个。


测试通过
java
{
"code": 200,
"msg": "OK",
"data": [
{
"type": "USER",
"content": "你好"
},
{
"type": "ASSISTANT",
"content": "您好!有什么可以帮您?",
"params": {}
},
{
"type": "USER",
"content": "课程推荐"
},
{
"type": "ASSISTANT",
"content": "您好!有什么可以帮您呢?",
"params": {}
}
],
"requestId": "85fe6cff6b4a40acae388601c679fb89"
}
那为啥视频里面是 1 呢

因为视频里面传的是 result 结果,不是 question,因为是有上下文的,所以它也知道问题是什么
如果对你有帮助的话,请点赞,关注,收藏。热爱可抵一切!👍 ❤️ 🔥








