springAI利用【AOP】提供了会话时的拦截、增强等功能。
在发送请求给大模型之前,以及把大模型的思考结果返回给客户之前,可以做一些日志打印(会话日志)。还可以把这些提示词、日志等写入数据库(会话记忆)。

1. 会话日志

添加 defaultAdvisors(new SimpleLoggerAdvisor())

@Configuration
public class CommonConfig {

    @Bean
    public ChatClient chatClient(OllamaChatModel model){
        return ChatClient
                .builder(model)
                .defaultSystem("你是一个热心、可爱的智能助手,你的名字叫小团团,请以小团团的身份和语气回答问题,同时请一定要将<think>的内容输出出来。")
                .defaultAdvisors(new SimpleLoggerAdvisor())
                .build();
    }
}

配置文件中,指定日志级别
其中 org.springframework.ai.chat.client.advisor是 SimpleLoggerAdvisor所在的包;

logging:
  level:
    org.springframework.ai.chat.client.advisor: debug
    com.example.spring_ai_demo: debug

浏览器请求:
在这里插入图片描述

控制台输出:

可以看到,请求被包装成AdvisedRequest。

2026-10-20T21:21:30.123+08:00 DEBUG 29860 --- [spring-ai-demo] [io-9090-exec-10] o.s.a.c.c.advisor.SimpleLoggerAdvisor    : request: AdvisedRequest[chatModel=org.springframework.ai.ollama.OllamaChatModel@3218b21c, userText=你是谁, systemText=你的名字叫丽莎,请以丽莎的身份和语气回答问题,同时请一定要将你思考的过程输出出来。, chatOptions=org.springframework.ai.ollama.api.OllamaOptions@6913c33b, media=[], functionNames=[], functionCallbacks=[], messages=[], userParams={}, systemParams={}, advisors=[org.springframework.ai.chat.client.DefaultChatClient$DefaultChatClientRequestSpec$1@417489d8, org.springframework.ai.chat.client.DefaultChatClient$DefaultChatClientRequestSpec$2@4a095086, SimpleLoggerAdvisor, org.springframework.ai.chat.client.DefaultChatClient$DefaultChatClientRequestSpec$1@2a47c32a, org.springframework.ai.chat.client.DefaultChatClient$DefaultChatClientRequestSpec$2@24803812], advisorParams={}, adviseContext={}, toolContext={}]
2026-10-20T21:22:41.978+08:00 DEBUG 29860 --- [spring-ai-demo] [oundedElastic-2] o.s.a.c.c.advisor.SimpleLoggerAdvisor    : response: {
  "result" : {
    "metadata" : {
      "finishReason" : "stop",
      "contentFilters" : [ ],
      "empty" : true
    },
    "output" : {
      "messageType" : "ASSISTANT",
      "metadata" : {
        "messageType" : "ASSISTANT"
      },
      "toolCalls" : [ ],
      "media" : [ ],
      "text" : "您好!我是丽莎,一个由我设计并开发的虚拟 entity。我的设计基于一系列目标,包括提供有用的信息、回答问题以及与用户进行互动。\n\n在技术层面上,我是通过先进的自然语言处理模型生成的,能够理解和生成人类语言。我对数据进行训练和学习,以便能够以人类方式回应您的问题。\n\n希望我的回答对您有所帮助!如果还有其他问题,请随时咨询我。"
    }
  },
  "metadata" : {
    "id" : "",
    "model" : "deepseek-r1:7b",
    "rateLimit" : {
      "requestsRemaining" : 0,
      "requestsReset" : 0.0,
      "tokensRemaining" : 0,
      "requestsLimit" : 0,
      "tokensLimit" : 0,
      "tokensReset" : 0.0
    },
    "usage" : {
      "promptTokens" : 32,
      "completionTokens" : 314,
      "totalTokens" : 346,
      "nativeUsage" : {
        "promptTokens" : 32,
        "totalTokens" : 346,
        "completionTokens" : 314
      },
      "generationTokens" : 314
    },
    "promptMetadata" : [ ],
    "empty" : true
  },
  "results" : [ {
    "metadata" : {
      "finishReason" : "stop",
      "contentFilters" : [ ],
      "empty" : true
    },
    "output" : {
      "messageType" : "ASSISTANT",
      "metadata" : {
        "messageType" : "ASSISTANT"
      },
      "toolCalls" : [ ],
      "media" : [ ],
      "text" : "您好!我是丽莎,一个由我设计并开发的虚拟 entity。我的设计基于一系列目标,包括提供有用的信息、回答问题以及与用户进行互动。\n\n在技术层面上,我是通过先进的自然语言处理模型生成的,能够理解和生成人类语言。我对数据进行训练和学习,以便能够以人类方式回应您的问题。\n\n希望我的回答对您有所帮助!如果还有其他问题,请随时咨询我。"
    }
  } ]
}

(未知原因)没有思考过程
springAI调用本地模型的时候,没有思考过程,即使添加以下内容,思考过程也是时有时无,不知道如何控制。

defaultSystem("你是一个热心、可爱的智能助手,你的名字叫小团团,请以小团团的身份和语气回答问题,同时请一定要将<think>的内容输出出来。")

2. 会话记忆

大模型不具备记忆能力,要想让大模型记住之前聊天的内容,唯一的办法就是把之前聊天的内容与新的提示词一起发送给大模型。

给大模型发送的message消息中,除了user和system,还可以发送assistant,是有大模型生成的消息,可能是上一轮对话生成的结果。

2.1 没有会话记忆的效果

可以看到两次提问没有任何关联
在这里插入图片描述
在这里插入图片描述

2.1 基于内存的实现

Spring AI定义了会话存储的标准接口ChatMemory ,并且提供了一个基于内存的默认实现InMemoryChatMemory

public interface ChatMemory {

	// TODO: consider a non-blocking interface for streaming usages

	default void add(String conversationId, Message message) {
		this.add(conversationId, List.of(message));
	}

	void add(String conversationId, List<Message> messages);

	List<Message> get(String conversationId, int lastN);

	void clear(String conversationId);

}

2.1.1 修改配置类
  • 定义一个ChatMemory,实现选择基于内存的实现;当然,实现可以自定义。
  • 添加一个advisor:MessageChatMemoryAdvisor
@Configuration
public class CommonConfig {

    // 定义会话存储的方式,这里使用基于内存的方式
    @Bean
    public ChatMemory chatMemory(){
        return new InMemoryChatMemory();
    }

    @Bean
    public ChatClient chatClient(OllamaChatModel model, ChatMemory chatMemory){
        return ChatClient
                .builder(model)
                .defaultSystem("你的名字叫丽莎,请以丽莎的身份和语气回答问题,同时请一定要将你思考的过程输出出来。")
                .defaultAdvisors(new SimpleLoggerAdvisor(),// 会话日志
                        new MessageChatMemoryAdvisor(chatMemory))// 会话记忆
                .build();
    }


}

此时已经有会话记忆的功能了,只是大模型没有区分不同客户端的功能;
在这里插入图片描述

如果再打开一个客户端,发现新打开的客户端有上个会话的记忆
在这里插入图片描述

2.1.2 添加会话ID

前端在请求时,携带一个会话id
在这里插入图片描述

import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY;

@RestController
@RequestMapping("/ai")
@RequiredArgsConstructor
public class ChatController {

    private final ChatClient chatClient;

    /**
     * 流式输出
     *
     * @param prompt
     */
    @CrossOrigin
    @RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")
    public Flux<String> chatStream(String prompt, String chatId) {
        return chatClient.prompt()
                .user(prompt)
                .advisors(a ->a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId))
                .stream()
                .content();
    }
}

添加会话ID之后,就可以区分不同的会话
在这里插入图片描述
新开一个会话,没有上一个会话的记忆:
在这里插入图片描述

3.会话历史

在前面的基础上,如果想查看会话历史,发现已经没有了,因为目前还没有将历史的会话保存起来
在这里插入图片描述

要实现会话历史,需实现两个关键接口

  • 查询会话记录列表
  • 查询会话记录详情

以deepseek为例:
会话记录列表会话记录详情
会话记录详情,spring ai已经自动保存到chatMemory中了,但是会话记录列表没有保存。

3.1 保存会话id

public interface ChatHistoryRepository {

    /**
     * 保存会话记录
     * @param type: 业务类型
     * @param chatId: 会话id
     */
    void save(String type, String chatId);

    /**
     * 获取会话id列表
     * @param type: 业务类型
     * @return 会话id列表
     */
    List<String> getChatIdList(String type);
}

@Repository
public class InMemoryChatHistoryRepository implements ChatHistoryRepository {

    // key是具体的业务类型
    // value是业务类型对应的会话记录列表
    private final Map<String, List<String>> chatHistory = new HashMap<>();

    @Override
    public void save(String type, String chatId) {

        if (!chatHistory.containsKey(type)) {
            chatHistory.put(type, new ArrayList<>());
        }


        List<String> chatIds = chatHistory.get(type);
        if (chatIds.contains(chatId)) {
            return;
        }

        chatIds.add(chatId);
    }

    @Override
    public List<String> getChatIdList(String type) {
        List<String> chatIds = chatHistory.get(type);
        return chatIds == null ? new ArrayList<>() : chatIds;
    }
}

每次向大模型发送请求前,保存会话ID

@RestController
@RequestMapping("/ai")
@RequiredArgsConstructor
public class ChatController {

    private final ChatClient chatClient;

    private final ChatHistoryRepository chatHistory;

    /**
     * 流式输出
     *
     * @param prompt
     */
    @CrossOrigin
    @PostMapping(value = "/chat", produces = "text/html;charset=utf-8")
    public Flux<String> chatStream(String prompt, String chatId) {

        // 1. 保存会话
        // 这里的key是业务类型,【对话机器人】
        chatHistory.save("chat",chatId);

        return chatClient.prompt()
                .user(prompt)
                .advisors(a ->a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId))
                .stream()
                .content();
    }
}

3.2 查询会话记录列表,会话记录详情


@RestController
@RequestMapping("/ai/history")
@RequiredArgsConstructor
public class ChatHistoryController {

    private final InMemoryChatHistoryRepository chatHistory;

    private final ChatMemory chatMemory;


    /**
     * 查询会话记录列表
     * @param type
     * @return
     */
    @CrossOrigin
    @GetMapping(value = "/{type}")
    public List<String> getChatList(@PathVariable("type") String type) {
        return chatHistory.getChatIdList(type);
    }

    /**
     * 查询会话详情
     * @param type
     * @return
     */
    @CrossOrigin
    @GetMapping(value = "/{type}/{chatId}")
    public List<MessageVO> getChatHistory(@PathVariable("type") String type, @PathVariable("chatId") String chatId) {
        // 会话详细,spring ai自动按照会话id保存到 chatMemory 了
        List<Message> messages = chatMemory.get(chatId, Integer.MAX_VALUE);
        if (CollectionUtils.isEmpty(messages)){
            return List.of();
        }

        // 处理成前端需要的格式
        return messages.stream().map(MessageVO::new).toList();

    }
}
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐