langchain4j搭建失物招领系统(三)---优化对话流历史

381 阅读3分钟

一、背景描述

当前对话流历史有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、修改系统提示词

增加提示词

  • 每次对话,需要能够读取用户历史对话内容

这样,大模型就能主动获取用户对话记录了。

三、功能测试

页面输入:

控制台输出:

重启服务,再次调用:

数据库记录如下:

重启服务再次调用:

最终记录如下: