一、背景描述
当前对话流历史有2个问题:
1、没有持久化
当前使用的策略是InMemoryChatMemoryStore,内部实现就是一个Map,系统重启后,对话流就全部清空了。当然,这个问题处理比较简单,只有我们自定义实现一个存储的策略就行,比如保存到数据库中,查询的时候从数据库中查询就行。
2、存储消息过多
这个是什么意思呢?比如之前的那个对话流里面,使用了2个大模型(一个意图分析,一个登记处理),调用之后,会记录4条记录(每个大模型都至少会产生2条消息记录)。
正常来说,我们期望肯定是一条用户消息,最终只会产生2条对话记录(一条用户消息,一条最终回复的AI消息)。
那给每一个大模型都定义一个自己的ChatMemoryProvider呢?这个虽然能保证每个大模型每一次对话,就记录2条消息,但是这个消息没法在整个工作流里面统一,也不是一个很好的解决方案。
当然,也可以只用一个大模型,让这个大模型能处理所有的功能。。。不过,这个需要大模型很强力,并且提示词得调整的很好。
目前我这边想到是我们要自定义存储消息的时机,不需要框架主动存储。但是当前框架我也没找到好的扩展点,下面是我想的一个方案实现。
刚进入对话流时,调用方法保存消息,这个消息肯定是用户消息
db.save(message,user)
执行对话流
aiMessage = doChatFlow(message)
保存ai消息
db.save(aiMessage,ai)
这个流程我们可以通过aop去实现。
二、功能实现
经过我不断尝试后,最终功能实现代码如下:
1、历史消息保存
定义一个数据库对象来保存历史消息:
@EqualsAndHashCode(callSuper = true)
@Table(name = "chat_history")
@Entity
@Data
@Comment("聊天历史")
public class ChatHistoryEntity extends BaseEntity {
@Comment("会话id")
private String sessionId;
@Comment("角色")
private ChatRole role;
@Comment("内容")
private String content;
}
定义一个注解,来标识是一个对话流
/**
* 这个 ChatFlow 注解用于标识一个方法是一个对话流。
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ChatFlow {
}
定义对应的AOP切面
@Aspect
@Component
@Slf4j
public class ChatFlowAspect {
@Autowired
private ChatHistoryRepository chatHistoryRepository;
@Pointcut("@annotation(io.orangewest.ailostproperty.component.ChatFlow)")
public void pointcut() {
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
String sessionId = (String) args[0];
String message = (String) args[1];
log.info("sessionId:{}, message:{}", sessionId, message);
saveChatHistory(sessionId, message, ChatRole.USER);
Object result = joinPoint.proceed();
log.info("sessionId:{}, aiMessage:{}", sessionId, result);
saveChatHistory(sessionId, result.toString(), ChatRole.ASSISTANT);
return result;
}
private void saveChatHistory(String sessionId, String message, ChatRole chatRole) {
ChatHistoryEntity chatHistory = new ChatHistoryEntity();
chatHistory.setSessionId(sessionId);
chatHistory.setRole(chatRole);
chatHistory.setContent(message);
chatHistoryRepository.save(chatHistory);
}
}
2、定义获取历史记录工具类
@Component
@Slf4j
public class ChatHistoryTools {
@Autowired
private ChatHistoryRepository chatHistoryRepository;
@Tool("获取用户聊天历史对话")
public List<ChatHistory> getChatHistory(@P("sessionId") String sessionId) {
log.info("获取用户与ai聊天历史,sessionId: {}", sessionId);
List<ChatHistory> historyList = chatHistoryRepository.findTop21BySessionIdOrderByIdDesc(sessionId)
.stream()
.sorted(Comparator.comparing(ChatHistoryEntity::getId))
.map(chatHistory -> ChatHistory.of(chatHistory.getRole().getRole(), chatHistory.getContent()))
.collect(Collectors.toList());
// 最新一条记录是用户当前输入的记录,移除掉
historyList.remove(historyList.size() - 1);
return historyList;
}
}
3、修改AiServices
修改之前的AiAssistant代码,改动如下:
- 去掉chatMemoryProvider,并且增加chatHistoryTools工具调用。
- @SystemMessage系统提示词必须从类上移除下来,放到方法上才能生效,这样相同的大模型调用也可以合成一个(意图识别和登记)
- 由于没有了chatMemoryProvider,@MemoryId注解也不会生效,我们必须自定义会话id,并且修改@UserMessage用户提示词
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT, chatModel = "qwenChatModel",
streamingChatModel = "qwenStreamingChatModel", tools = {"chatHistoryTools"})
public interface AiAssistant {
/**
* 获取用户意图
*/
@SystemMessage(fromResource = "/message/system/getIntention.txt")
@UserMessage("当前sessionId:{{sessionId}};用户当前消息:{{message}}")
IntentionOutput getIntention(@V("sessionId") String sessionId, @V("message") String message);
/**
* 注册失物信息
*/
@SystemMessage(fromResource = "/message/system/registerLost.txt")
@UserMessage("当前sessionId:{{sessionId}};用户当前消息:{{message}}")
LostPropertyOutput registerLost(@V("sessionId") String id, @V("message") String message);
}
4、修改系统提示词
增加提示词
- 每次对话,需要能够读取用户历史对话内容
这样,大模型就能主动获取用户对话记录了。
三、功能测试
页面输入:
控制台输出:
重启服务,再次调用:
数据库记录如下:
重启服务再次调用:
最终记录如下: