SpringAI实现聊天记忆和日志打印
大家好,我是 Mr.Sun,一名热爱技术和分享的程序员。
📖 个人博客:Mr.Sun的博客
✨ 微信公众号:「Java技术宇宙」
🤝 个人微信:sunhw0305(备注“加群”免费加入技术交流群)
期待与你交流,让我们一起在技术道路上成长。
请求大模型出入参日志
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);
}
}
- getName 为当前Advisor提供一个唯一的名称
- getOrder 跟Spring的order概念相似,您可以通过设置 order 值来控制执行顺序。值较低的将优先执行。
- 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的感觉


当然,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。每次交互时,它都会从向量存储中检索对话历史记录,并将其以纯文本形式附加到系统消息中。
案例实战
没有聊天记忆



很明显,第三次提问,大模型并没有记住他是小王助手,是个高三学生,而是小孙助手,这不符合我们现在的逻辑,可以让大模型记住上下文再看看
存在聊天记忆
初始化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();
}



从这三次对话中,可以明显看出第三次大模型记住了我第二次告诉他是小王,高三的学生,这样就实现了聊天的记忆了
不同会话的聊天记忆
但是还有个问题,我重新开个浏览器,继续问大模型,他还是说他是小王助手,是个高三学生,我的预期应该你是小孙助手才对呀,新开一个会话才是符合逻辑的

在内存实现聊天记录中,是使用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(备注“加群”),与众多开发者即时讨论。
很高兴在这里遇见你,希望我的分享能对你有所帮助。


浙公网安备 33010602011771号