SpringAI实现聊天记忆和日志打印

大家好,我是 Mr.Sun,一名热爱技术和分享的程序员。
​📖 个人博客​:Mr.Sun的博客
​​✨ 微信公众号​:「Java技术宇宙」
🤝 个人微信​:​sunhw0305​(备注“加群”免费加入技术交流群)
期待与你交流,让我们一起在技术道路上成长。
Mr.Sun的个人博客


请求大模型出入参日志

Spring AI Advisors API 提供了一种灵活而强大的方法来拦截、修改和增强 Spring 应用程序中的 AI 驱动交互。通过利用 Advisors API,开发人员可以创建更复杂、可重用且更易于维护的 AI 组件。
我们可以实现一个简单的日志顾问,记录调用链中下一个顾问ChatClientRequest之前和ChatClientResponse之后的操作。需要注意的是,该顾问仅观察请求和响应,而不会对其进行修改。此实现支持非流式和流式场景。

自定义MySimpleLoggerAdvisor

/**
 * @author hwsun3
 * @date 2025/9/18
 */
public class MySimpleLoggerAdvisor implements CallAdvisor, StreamAdvisor {

    private static final Logger logger = LoggerFactory.getLogger(MySimpleLoggerAdvisor.class);

    @Override
    public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {
        logRequest(chatClientRequest);

        ChatClientResponse chatClientResponse = callAdvisorChain.nextCall(chatClientRequest);

        logResponse(chatClientResponse);

        return chatClientResponse;
    }

    @Override
    public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain) {
        logRequest(chatClientRequest);

        Flux<ChatClientResponse> chatClientResponses = streamAdvisorChain.nextStream(chatClientRequest);

        return new ChatClientMessageAggregator().aggregateChatClientResponse(chatClientResponses, this::logResponse);
    }

    @Override
    public String getName() {
        return this.getClass().getSimpleName();
    }

    @Override
    public int getOrder() {
        return 0;
    }


    private void logRequest(ChatClientRequest request) {
        logger.info("自定义request: {}", request);
    }

    private void logResponse(ChatClientResponse chatClientResponse) {
        logger.info("自定义response: {}", chatClientResponse);
    }

}
  1. getName 为当前Advisor提供一个唯一的名称
  2. getOrder 跟Spring的order概念相似,您可以通过设置 order 值来控制执行顺序。值较低的将优先执行。
  3. MessageAggregator一个实用程序类,它将 Flux 响应聚合成单个 ChatClientResponse。这对于日志记录或其他需要观察整个响应(而非流中的单个项目)的处理非常有用。请注意,MessageAggregator由于这是只读操作,因此您无法更改 中的响应

注册defaultAdvisors

创建ChatClient对象,建议在构建时使用构建器的defaultAdvisors()方法注册顾问。

@Bean
ChatClient chatClientByMyAdvisor(ChatClient.Builder builder) {
    return builder.defaultSystem("你是小孙助手,请使用贴吧老哥的语气跟我对话")
            .defaultAdvisors(new MySimpleLoggerAdvisor())
            .build();
}

defaultSystem("你是小孙助手,请使用贴吧老哥的语气跟我对话")
初始化大模型前提,这样你提问的时候,大模型就会以贴吧老哥的语气跟你对话了

@Autowired
private ChatClient chatClientByMyAdvisor;

@GetMapping(value = "/generateByLogAdvisor", produces = "text/html;charset=utf-8")
public Flux<String> generationByMyAdvisor(@RequestParam(value = "message", defaultValue = "你是谁?") String message) {
    return this.chatClientByMyAdvisor.prompt()
            .user(message)
            .stream()
            .content();
}

这时候发送请求,观察控制台日志,可以看到已经打印出我们自定义的日志了,感觉有点像AOP的感觉
Description
Description

当然,SpringAI肯定也内置了大量的Advisor,日志SimpleLoggerAdvisor
但是内置的这个SimpleLoggerAdvisor日志打印是DEBUG级别的,需要额外开启

logging:
  level:
    org:
      springframework:
        ai:
          chat:
            client:
              advisor: debug
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
    return builder.defaultSystem("你是小孙助手,请使用贴吧老哥的语气跟我对话")
            .defaultAdvisors(new SimpleLoggerAdvisor(
                    request -> "Custom request: " + request.prompt().getUserMessage(),
                    response -> "Custom response: " + response.getResult(),
                    0))
            .build();
}

聊天记忆功能

大型语言模型 (LLM) 是无状态的,这意味着它们不会保留先前交互的信息。当您希望在多个交互之间维护上下文或状态时,这可能会造成限制。为了解决这个问题,Spring AI 提供了聊天记忆功能,允许您使用 LLM 跨多个交互存储和检索信息。
该ChatMemory抽象允许您实现各种类型的内存以支持不同的用例。消息的底层存储由 处理ChatMemoryRepository,其唯一职责是存储和检索消息。由ChatMemory实现决定保留哪些消息以及何时删除它们。策略示例包括保留最后 N 条消息、将消息保留一段时间或将消息保留到一定的令牌限制。

在选择记忆类型之前,必须了解聊天记忆和聊天历史之间的区别。

  • 聊天记忆。大型语言模型保留并用于在整个对话过程中保持语境感知的信息。
  • 聊天记录。整个对话历史记录,包括用户和模型之间交换的所有消息。

该ChatMemory抽象旨在管理聊天内存。它允许您存储和检索与当前对话上下文相关的消息。但是,它并非存储聊天历史记录的最佳选择。如果您需要维护所有交换消息的完整记录,则应考虑使用其他方法,例如依赖 Spring Data 来高效地存储和检索完整的聊天历史记录。

快速入门

Spring AI 会自动配置一个ChatMemoryBean,供您在应用程序中直接使用。默认情况下,它使用内存存储库来存储消息(InMemoryChatMemoryRepository),并使用MessageWindowChatMemory实现来管理对话历史记录。如果已配置其他存储库(例如 Cassandra、JDBC 或 Neo4j),Spring AI 将改用该存储库。

@Autowired
ChatMemory chatMemory;

消息窗口聊天记忆

MessageWindowChatMemory维护一个消息窗口,使其不超过指定的最大大小。当消息数量超过最大值时,较旧的消息将被删除,同时保留系统消息。默认窗口大小为 20 条消息。

MessageWindowChatMemory memory = MessageWindowChatMemory.builder()
    .maxMessages(10)
    .build();

这是 Spring AI 用于自动配置ChatMemorybean 的默认消息类型。

内存存储库

InMemoryChatMemoryRepository使用 将消息存储在内存中ConcurrentHashMap。
默认情况下,如果尚未配置其他存储库,Spring AI 会自动配置一个您可以在应用程序中直接使用的ChatMemoryRepository类型的bean,InMemoryChatMemoryRepository

@Autowired
ChatMemoryRepository chatMemoryRepository;

如果您希望InMemoryChatMemoryRepository手动创建,可以按如下方式操作:

ChatMemoryRepository repository = new InMemoryChatMemoryRepository();

聊天客户端中的内存

使用 ChatClient API 时,您可以提供一个ChatMemory实现来维护跨多个交互的对话上下文。
Spring AI 提供了一些内置的 Advisor,您可以ChatClient根据需要使用它们来配置的内存行为。

  • MessageChatMemoryAdvisor。此顾问程序使用提供的实现来管理对话内存ChatMemory。每次交互时,它都会从内存中检索对话历史记录,并将其作为消息集合包含在提示中。
  • PromptChatMemoryAdvisor。此顾问程序使用提供的实现来管理对话内存ChatMemory。每次交互时,它都会从内存中检索对话历史记录,并将其以纯文本形式附加到系统提示中。
  • VectorStoreChatMemoryAdvisor该顾问使用提供的实现来管理对话内存VectorStore。每次交互时,它都会从向量存储中检索对话历史记录,并将其以纯文本形式附加到系统消息中。

案例实战

没有聊天记忆

Description
Description
Description

很明显,第三次提问,大模型并没有记住他是小王助手,是个高三学生,而是小孙助手,这不符合我们现在的逻辑,可以让大模型记住上下文再看看

存在聊天记忆

初始化chatClient,设置MessageChatMemoryAdvisor助手,默认内存存储20条记录

@Autowired
private ChatMemoryRepository chatMemoryRepository;

@Bean
ChatClient chatClientChatMemory(ChatClient.Builder builder) {
    ChatMemory chatMemory = MessageWindowChatMemory.builder()
            .maxMessages(20)
            .chatMemoryRepository(chatMemoryRepository)
            .build();

    return builder
            .defaultSystem("你是小孙助手,请使用贴吧老哥的语气跟我对话")
            .defaultAdvisors(
                    new SimpleLoggerAdvisor(
                            request -> "Custom request: " + request.prompt().getUserMessage(),
                            response -> "Custom response: " + response.getResult(),
                            0),
                    MessageChatMemoryAdvisor.builder(chatMemory).build())
            .build();
}

@Autowired
private ChatClient chatClientChatMemory;

@GetMapping(value = "/chatMemory/generateStream", produces = "text/html;charset=utf-8")
public Flux<String> chatMemoryGenerateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message) {
    return chatClientChatMemory.prompt()
            .user(message)
            .stream()
            .content();
}

Description
Description
Description

从这三次对话中,可以明显看出第三次大模型记住了我第二次告诉他是小王,高三的学生,这样就实现了聊天的记忆了

不同会话的聊天记忆

但是还有个问题,我重新开个浏览器,继续问大模型,他还是说他是小王助手,是个高三学生,我的预期应该你是小孙助手才对呀,新开一个会话才是符合逻辑的
Description
在内存实现聊天记录中,是使用ConcurrentHashMap来存储记录的,通过key来获取当前的聊天记忆,那么我们不同会话设置不同的key,不就可以实现不同的key返回不同的聊天记录了吗

@GetMapping(value = "/chatMemory/generateStream", produces = "text/html;charset=utf-8")
public Flux<String> chatMemoryGenerateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message,
                                             @RequestParam(value = "conversationId", defaultValue = "1") String conversationId) {
    return chatClientChatMemory.prompt()
            .user(message)
            .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
            .stream()
            .content();
}

在执行对话的时候,传入conversationId,这就key,这样不同的conversationId就会返回不同的聊天记忆了


作者:Mr.Sun | 「Java技术宇宙」主理人
专注分享硬核技术干货与编程实践,让编程之路更简单。
找到我,获取更多:​​
​📖 深度文章​:个人博客「Mr.Sun的博客」 ​
🚀 最新推送​:微信公众号「Java技术宇宙
​👥 交流社群​:添加微信​sunhw0305​(备注“加群”),与众多开发者即时讨论。
很高兴在这里遇见你,希望我的分享能对你有所帮助。

posted @ 2025-09-24 10:16  孙半仙人  阅读(21)  评论(0)    收藏  举报