0.写在前面

因为Spring Ai版本迭代很快,并且迭代后会有接口的一些变化,所以本文讲解如何在全网都教你该怎么做的情况下,理解为什么要这么做

1.引入Spring AI

Spring AI官方文档

1.1-选择正确的版本

为了给自己的汐落记账增加AI功能,遂探索Spring AI

首先搞明白,目前maven仓库存在两个Spring AI

实际上最新的版本是org.springframework.ai » spring-ai-core
虽然这个依赖的版本号是1.0.0开始的,但这确实是新的版本,我们开发应该使用这个

(实际开发选用的版本)

1.2-引入依赖

因为openai的接口已经成为行业规范,很多大模型都支持openai的接口规范,包括本次我要调用的deepseek官方大模型api也是支持openai规范的,所以这里我选择引入了spring-ai-starter-model-openai,spring ai提供了很多不同的组件可以使用,可以选择自己需要的

这里的版本号为什么是M7,因为我想学最新的,所以就使用了M7,值得注意的是,不同版本之间变化较大,所以本文最终的目的是在你知道怎么做的情况下,尽可能帮助你理解为什么要这样做,这样面对不同的版本,你都可以用深刻的理解快速适应接口的变更

<dependency>  
    <groupId>org.springframework.ai</groupId>  
    <artifactId>spring-ai-starter-model-openai</artifactId>  
    <version>1.0.0-M7</version>  
</dependency>

这里可以考虑使用BOM管理依赖的版本,点击下方文字跳转文档
Spring AI官方文档

1.3-配置讲解(重要)

这是一个很重要的板块,因为不同版本的Spring AI这里是有变化的,所以不能直接按照网上的教程写,需要根据自己的版本号判断如何进行配置

下面是yaml错误示范

一般来说网上很多教程会这样写,但实际上这样的写法在1.0.0M7的版本中是不能实现SpringBoot的自动装配的,如果这样写甚至还需要写一个配置类 - AiConfig

这个配置类是这样的

丢份代码在这里

@Configuration  
public class AiConfig {  
    @Value("${ai.openai.api-key}")  
    private String API_KEY;  
    @Value("${ai.openai.base-url}")  
    private String BASE_URL;  
    @Value("${ai.openai.default-deepseek-model}")  
    private String DEFAULT_DEEPSEEK_MODEL;  
  
    @Bean  
    public OpenAiApi chatCompletionApi() {  
        return OpenAiApi.builder()  
                .apiKey(API_KEY) // 替换为你的API密钥  
                .baseUrl(BASE_URL) // 替换为你的API基础URL  
                .build();  
    }  
  
    @Bean  
    public OpenAiChatModel openAiClient(OpenAiApi openAiApi) {  
        return OpenAiChatModel.builder()  
                .openAiApi(openAiApi)  
                .defaultOptions(OpenAiChatOptions.builder().model(DEFAULT_DEEPSEEK_MODEL).build())  
                .build();  
    }  
}

可以看到手动的进行了参数的注入

为什么会这样呢?
因为在这个M7版本的Spring AI中,参数的配置发生了一些变化,如果还按照旧版本来配置就会导致SpringBoot无法正确的将配置注入到需要的类

那么能不能通过正确的配置yaml实现SpringBoot的自动装配呢?
可以的!
正确的写法如下

spring:  
  ai:  
    openai:  
      api-key: ${ai.openai.api-key}  
      base-url: ${ai.openai.base-url}  
      chat:  
        options:  
          model: ${ai.openai.default-deepseek-model}

可以看到多了个 chatoptions 的配置层级
api-keybase-url需要写到openai的层级下,chat-options里面需要指定model
这几个选项缺一不可,否则无法正常实现装配!

这样一来,即使不需要配置类也可以实现正确的SpringBoot自动装配!

不同版本Spring AI的配置

我怎么知道自己的版本应该怎么写?

一般来说,文档里会写的很清楚,但是如果你使用的版本较新,可能文档来不及更新,那么你可以在引入依赖后,输入spring.ai来查看配置推荐

2.调用Spring AI

2.1-Service层处理

注入OpenAiChatModel

这里需要使用构造函数注入,如果使用Lombok的注解@RequiredArgsConstructor是无法顺利注入的,这里需要注意一下!(具体原因暂不了解,可能是还没有适配)

// 注入OpenAiChatModel  
private final OpenAiChatModel openAiChatModel;  

public ChatService(OpenAiChatModel openAiChatModel) {  
    this.openAiChatModel = openAiChatModel;  
}
可以看到在Model中是有两个方法的,一个是**stream**,一个是**call**

简单来说
stream是流式输出,ai输出到哪里,spring ai就返回给前端到哪里
call是等待ai输出所有内容后一并返回给前端

他们分别对应两种支持的传参,MessagePrompt

观察call内部代码

这里跟进去看一下代码是怎么实现的
call为例

可以看到如果直接使用 message 询问,spring ai会将message封装到prompt里面,并且将得到的ChatResponse类型进行解析,取出里面的String类型的ai回复

观察stream内部代码

我们再跟进一下stream

可以看到,在调用stream或者是call的方法时都可以直接返回String类型的响应,转换的过程已经在内部帮我们封装好了,相较于call方法,stream方法的返回值用Flux<>进行的封装,这就是流式输出的精髓

所以我们如果没有特别需要创建新的Prompt的情况,可以直接发送问题给方法,直接输出返回的String数据

观察Prompt内部

这个Prompt是什么呢?跟进去看一下

可以看到Prompt里面可以接受几种属性,其中眼熟的就是MessageList<Message>,如果是列表结构的信息,那么Prompt会将里面的信息解析出来用不同的方式处理

不同的Message种类

我们跟进Message看看

Message接口继承自Content,提供了一个getter方法,我们跟进去看看

可以得到这段代码

package org.springframework.ai.chat.messages;  
  
/**  
 * Enumeration representing types of {@link Message Messages} in a chat application. It  
 * can be one of the following: USER, ASSISTANT, SYSTEM, FUNCTION. */public enum MessageType {  
  
    /**  
     * A {@link Message} of type {@literal user}, having the user role and originating  
     * from an end-user or developer.     * @see UserMessage  
     */  
    USER("user"),  
  
    /**  
     * A {@link Message} of type {@literal assistant} passed in subsequent input  
     * {@link Message Messages} as the {@link Message} generated in response to the user.  
     * @see AssistantMessage  
     */  
    ASSISTANT("assistant"),  
  
    /**  
     * A {@link Message} of type {@literal system} passed as input {@link Message  
     * Messages} containing high-level instructions for the conversation, such as behave  
     * like a certain character or provide answers in a specific format.     * @see SystemMessage  
     */  
    SYSTEM("system"),  
  
    /**  
     * A {@link Message} of type {@literal function} passed as input {@link Message  
     * Messages} with function content in a chat application.  
     * @see ToolResponseMessage  
     */  
    TOOL("tool");  
  
    private final String value;  
  
    MessageType(String value) {  
       this.value = value;  
    }  
  
    public static MessageType fromValue(String value) {  
       for (MessageType messageType : MessageType.values()) {  
          if (messageType.getValue().equals(value)) {  
             return messageType;  
          }  
       }  
       throw new IllegalArgumentException("Invalid MessageType value: " + value);  
    }  
  
    public String getValue() {  
       return this.value;  
    }  
  
}

可以看到MessageType有很多枚举类型,其中USER是用来指代用户提问的信息,而SYSTEM则是给大模型系统的提示词,这里我们了解即可

分析ChatOptions

再回到Prompt,可以看到有个私有内部类 ChatOptions,让我们跟进去看看

/**  
 * {@link ModelOptions} representing the common options that are portable across different  
 * chat models. */public interface ChatOptions extends ModelOptions {  
  
    /**  
     * Returns the model to use for the chat.     * @return the model to use for the chat     */    @Nullable  
    String getModel();  
  
    /**  
     * Returns the frequency penalty to use for the chat.     * @return the frequency penalty to use for the chat     */    @Nullable  
    Double getFrequencyPenalty();  
  
    /**  
     * Returns the maximum number of tokens to use for the chat.     * @return the maximum number of tokens to use for the chat     */    @Nullable  
    Integer getMaxTokens();  
  
    /**  
     * Returns the presence penalty to use for the chat.     * @return the presence penalty to use for the chat     */    @Nullable  
    Double getPresencePenalty();  
  
    /**  
     * Returns the stop sequences to use for the chat.     * @return the stop sequences to use for the chat     */    @Nullable  
    List<String> getStopSequences();  
  
    /**  
     * Returns the temperature to use for the chat.     * @return the temperature to use for the chat     */    @Nullable  
    Double getTemperature();  
  
    /**  
     * Returns the top K to use for the chat.     * @return the top K to use for the chat     */    @Nullable  
    Integer getTopK();  
  
    /**  
     * Returns the top P to use for the chat.     * @return the top P to use for the chat     */    @Nullable  
    Double getTopP();  
  
    /**  
     * Returns a copy of this {@link ChatOptions}.  
     * @return a copy of this {@link ChatOptions}  
     */    <T extends ChatOptions> T copy();  
  
    /**  
     * Creates a new {@link Builder} to create the default {@link ChatOptions}.  
     * @return Returns a new {@link Builder}.  
     */    static Builder builder() {  
       return new DefaultChatOptionsBuilder();  
    }  
  
    /**  
     * Builder for creating {@link ChatOptions} instance.  
     */    interface Builder {  
  
       /**  
        * Builds with the model to use for the chat.        * @param model  
        * @return the builder  
        */       Builder model(String model);  
  
       /**  
        * Builds with the frequency penalty to use for the chat.        * @param frequencyPenalty  
        * @return the builder.  
        */       Builder frequencyPenalty(Double frequencyPenalty);  
  
       /**  
        * Builds with the maximum number of tokens to use for the chat.        * @param maxTokens  
        * @return the builder.  
        */       Builder maxTokens(Integer maxTokens);  
  
       /**  
        * Builds with the presence penalty to use for the chat.        * @param presencePenalty  
        * @return the builder.  
        */       Builder presencePenalty(Double presencePenalty);  
  
       /**  
        * Builds with the stop sequences to use for the chat.        * @param stopSequences  
        * @return the builder.  
        */       Builder stopSequences(List<String> stopSequences);  
  
       /**  
        * Builds with the temperature to use for the chat.        * @param temperature  
        * @return the builder.  
        */       Builder temperature(Double temperature);  
  
       /**  
        * Builds with the top K to use for the chat.        * @param topK  
        * @return the builder.  
        */       Builder topK(Integer topK);  
  
       /**  
        * Builds with the top P to use for the chat.        * @param topP  
        * @return the builder.  
        */       Builder topP(Double topP);  
  
       /**  
        * Build the {@link ChatOptions}.  
        * @return the Chat options.        */       ChatOptions build();  
  
    }  
  
}

可以看到这里面有很多getter和setter方法,用于定义各种大模型的参数,比如topK,topP,temperature等等

这些参数是在AI模型生成文本时控制输出特性的重要参数,这里简单讲几个:

  1. Temperature(温度):控制生成文本的随机性。较高的温度值(如0.8-1.0)会产生更多样化、创造性的响应,但可能不太准确。较低的温度值(如0.2-0.3)会使输出更加确定和一致,但创造性可能较低。温度为0时,模型总是选择最可能的下一个词。

  2. Top-K:限制模型在每一步只考虑概率最高的K个词。例如,如果设置top-k=50,模型只会从概率最高的50个词中选择下一个词,忽略其他所有可能性。

  3. Top-P(也称为nucleus sampling):模型只考虑累积概率达到P值的最小词集合。例如,如果top-p=0.9,模型会选择概率总和达到90%的最少数量的词,忽略剩余的低概率词。

这些参数就是我们自定义Prompt的意义

通过自定义Prompt调用ai

那么我们怎么通过创建Prompt传参呢?

ChatResponse response = openAiChatModel.call(new Prompt(  
        message,  
        ChatOptions.builder()  
                .temperature(0.7)  
                .topK(40)  
                .build()  
));

这是一段代码示例,通过链式编程的方式传入参数,是建造者模式(Builder Pattern)的实际使用

通过yaml注入Prompt属性

其实我们几乎不会去创建Prompt注入,而是使用修改配置的方式
还记得application.yaml这个文件吗?

其实我们是可以直接通过修改配置文件来实现修改Prompt属性的! (这里的TopP:1是随便填的,具体看你的需求咯) 所以实际上我们使用的比较多的是传入Message属性的message或者用列表数组列表封装的messages

Service层代码示例

最终跨域进行流式聊天的最简代码实现如下所示

/**  
 * 流式聊天  
 *  
 * @param message 消息  
 * @return 响应  
 * Author: Anson  
 */
	public Flux<String> fluxchat(String message) {  
    Message usermessage = new UserMessage(message);  
    Message systemMessage = new SystemMessage(systemResource);  
    List<Message> messages = new ArrayList<>();  
    messages.add(systemMessage);  
    messages.add(usermessage);  
  
    // 处理聊天逻辑  
    return openAiChatModel.stream(String.valueOf(messages));  
}

完整的Service层级代码

@Service  
public class ChatService {  
  
    // 注入OpenAiChatModel  
    private final OpenAiChatModel openAiChatModel;  
    @Value("classpath:/prompts/alice-system-message.st")  
    private Resource systemResource;  
  
    public ChatService(OpenAiChatModel openAiChatModel) {  
        this.openAiChatModel = openAiChatModel;  
    }  
  
    /**  
     * 流式聊天  
     *  
     * @param message 消息  
     * @return 响应  
     * Author: Anson  
     */    
	    public Flux<String> fluxchat(String message) {  
        Message usermessage = new UserMessage(message);  
        Message systemMessage = new SystemMessage(systemResource);  
        List<Message> messages = new ArrayList<>();  
        messages.add(systemMessage);  
        messages.add(usermessage);  
  
        // 处理聊天逻辑  
        return openAiChatModel.stream(String.valueOf(messages));  
    }  
}

我们提供了一个可以接受String数据类型的方法 fluxchat
在方法内部我们创建了两个不同类型的Message,一个是user,一个是system,分别指代用户的提问和大模型的提示词

之后我们使用了一个ArrayList将信息收集起来,一起传给了stream方法,通过String.valueOf(messages) 解析里面的内容,这样就可以把系统提示词和用户问题一起输入进去

这样我们的一个简单的Service就写好了

提示词传递

值得一提的是这里的提示词使用了一个systemResource

@Value("classpath:/prompts/alice-system-message.st")  
private Resource systemResource;

这是我创建的一个用于获取提示词的变量

通过@Value注解,systemResource可以获取st(StringTemplate)文件,这样就可以作为提示词使用

2.2-Controller层处理

现在我们需要在Controller层级调用方法

@GetMapping(value = "/flux-alice", produces = "text/html;charset=UTF-8")  
@Operation(summary = "聊天接口")  
public Flux<String> fluxchat(@RequestParam(value = "message", defaultValue = "给我讲个笑话?") String message) {  
    System.out.println();  
    Flux<String> fluxchat = chatService.fluxchat(message);  
    return fluxchat;  
}

简单测试一下


可以看到现在已经顺利流式输出了!
(好冷的烂笑话)

3.最终代码

3.1-Service层级

@Service  
public class ChatService {  
  
    // 注入OpenAiChatModel  
    private final OpenAiChatModel openAiChatModel;  
    @Value("classpath:/prompts/alice-system-message.st")  
    private Resource systemResource;  
  
    public ChatService(OpenAiChatModel openAiChatModel) {  
        this.openAiChatModel = openAiChatModel;  
    }  
  
    /**  
     * 流式聊天  
     *  
     * @param message 消息  
     * @return 响应  
     * Author: Anson  
     */    
	    public Flux<String> fluxchat(String message) {  
        Message usermessage = new UserMessage(message);  
        Message systemMessage = new SystemMessage(systemResource);  
        List<Message> messages = new ArrayList<>();  
        messages.add(systemMessage);  
        messages.add(usermessage);  
  
        // 处理聊天逻辑  
        return openAiChatModel.stream(String.valueOf(messages));  
    }  
}

3.2-Controller层级

@RestController  
@RequestMapping("/ai")  
@Tag(name = "ai聊天控制器")  
public class ChatController {  
    private final ChatService chatService;  
  
    public ChatController(ChatService chatService) {  
        this.chatService = chatService;  
    }  
  
    @GetMapping(value = "/flux-alice", produces = "text/html;charset=UTF-8")  
    @Operation(summary = "聊天接口")  
    public Flux<String> fluxchat(@RequestParam(value = "message", defaultValue = "给我讲个笑话?") String message) {  
        System.out.println();  
        Flux<String> fluxchat = chatService.fluxchat(message);  
        return fluxchat;  
    }  
}

4.ChatClient

这里介绍一下ChatClient

因为ai模型的提供厂商很多,也有不同的规范,为了方便调用,统一接口,Spring AI为我们提供了ChatClient,在ChatClient里面提供了一些统一的写法

  1. ChatModel (例如 OpenAiChatModel):
    这是与底层 AI 模型(如 OpenAI 的 GPT 模型)进行交互的直接、低级别接口。
    它负责将 Prompt 对象发送给 AI 服务,并接收原始的 ChatResponse。
    你可以把它看作是与特定 AI 提供商 API 通信的核心组件。

  2. ChatClient:
    这是一个更高级别、更流畅的 API,构建在 ChatModel 之上
    它旨在简化与 AI 模型的交互,提供更方便的链式调用方法来构建请求(例如 .prompt(), .user(), .system(), .stream(), .call())。

  • 封装了常见的模式: 例如,更容易地管理对话历史、设置系统消息、处理用户消息、添加工具/函数调用等。

  • 提供附加功能: 如请求/响应的拦截器(Advisors)、默认提示设置、结构化输出转换(将 AI 的响应直接映射到 Java 对象)以及与可观测性(如 Micrometer)的集成。

总结:
OpenAiChatModel 是基础,负责与 OpenAI API 的直接通信。
ChatClient 是一个便利层,它使用 OpenAiChatModel,但提供了更简单、更强大的 API 来构建 AI 交互逻辑,减少了样板代码,并集成了更多高级功能。

下面讲讲怎么用他

4.1-创建需要的Client

要使用Client,需要我们创建一个Client(存在水字数的嫌疑( •̀ ω •́ )

我们需要像使用Model一样配置参数

在进行yaml的正确配置之后,按照上面的章节我们其实可以直接注入Model到service并且使用了

private final OpenAiChatModel openAiChatModel;  
  
public ChatClientService(OpenAiChatModel openAiChatModel) {  
    this.openAiChatModel = openAiChatModel;  
}

而使用Client只需要把这个Model注入到Client里面就可以了(套娃)

ChatClient momoi = ChatClient.create(openAiChatModel);
(momoi - 才羽桃井)

4.2-使用momoi得到midori(流式输出)

// 直接使用链式编程传入参数  
Flux<String> midori = momoi.prompt()  
        .system(systemResource)  
        .user(message)  
        .options(options)  
        .stream()  
        .content();  
return midori;
(midori - 才羽绿)

你可能会留意到在链式编程的过程中存在一些不同
比如我们没有传入用数组列表实现的messages,而是使用.system.user来传入我们需要的系统提示词和用户信息,而且有个没有见过的选项 .options
这里我们直接跟进去,看看这是个什么东西

<T extends ChatOptions> ChatClientRequestSpec options(T options);

可以看到需要一个继承了ChatOptions类型的参数,我们看看这个ChatOptions是何方神圣

public interface ChatOptions extends ModelOptions {  
    static Builder builder() {  
        return new DefaultChatOptionsBuilder();  
    }  
    String getModel();  
    Double getFrequencyPenalty();  
    Integer getMaxTokens();  
    Double getPresencePenalty();  
    List<String> getStopSequences();  
    Double getTemperature();  
    Integer getTopK();  
    Double getTopP();  
    <T extends ChatOptions> T copy();  
  
    interface Builder {  
        Builder model(String model);  
        Builder frequencyPenalty(Double frequencyPenalty);  
        Builder maxTokens(Integer maxTokens);  
        Builder presencePenalty(Double presencePenalty);  
        Builder stopSequences(List<String> stopSequences);  
        Builder temperature(Double temperature);  
        Builder topK(Integer topK);  
        Builder topP(Double topP);  
        ChatOptions build();  
    }  
}

我手动删除了很多注释内容,现在留下了比较干净的类,可以看到这些参数都非常眼熟,实际上这就是我们用来微调模型的一些参数,ChatClient允许我们通过传入options来对模型进行微调

那么我们怎么创建options呢
相当的简单

OpenAiChatOptions options = OpenAiChatOptions.builder()  
        .temperature(1.0)  
        .build();

我们只需要使用链式编程构建一个option就可以了

4.3-Service层级示例

现在看一下我们使用Client返回流式输出的代码

@Service  
public class ChatClientService {  
    private final OpenAiChatModel openAiChatModel;  
    @Value("classpath:/prompts/alice-system-message.st")  
    private Resource systemResource;  
  
    public ChatClientService(OpenAiChatModel openAiChatModel) {  
        this.openAiChatModel = openAiChatModel;  
    }  
  
    /**  
     * 使用Momoi进行聊天(直接使用链式编程传入参数)  
     *  
     * @param message  
     * @return  
     */    public Flux<String> chatWithOpenAiByMomoi(String message) {  
  
        ChatClient momoi = ChatClient.create(openAiChatModel);  
        OpenAiChatOptions options = OpenAiChatOptions.builder()  
                .temperature(1.0)  
                .build();  
  
        // 直接使用链式编程传入参数  
        Flux<String> midori = momoi.prompt()  
                .system(systemResource)  
                .user(message)  
                .options(options)  
                .stream()  
                .content();  
        return midori;  
    }  
}

4.4-Controller层级示例

/**  
 * 链式编程ChatClient聊天接口  
 *  
 * @param message 用户消息  
 * @return 返回流式聊天结果  
 */  
@GetMapping(value = "/flux-ChatClient/OpenAi-momoi", produces = "text/html;charset=UTF-8")  
@Operation(summary = "链式实现ChatClient聊天接口")  
public Flux<String> chatWithOpenAiByChatClientBymomoi(@RequestParam(value = "message", defaultValue = "给我讲个笑话?") String message) {  
    log.info("链式构成ChatClient/OpenAi聊天接口被调用,消息内容:{}", message);  
    Flux<String> fluxchat = chatClientService.chatWithOpenAiByMomoi(message);  
    log.info("Momoi调用完成");  
    return fluxchat;  
}

现在我们就已经掌握了配置不同AI的Model,使用SpringBoot自动装配Model需要的参数,使用ChatClient的统一Api发送请求,至于ChatClient的其他好处,接下来我们讲一个Advisor的用法

5.使用Advisor提供临时会话记忆

先看效果

5.1-创建配置类

我们需要创建一个配置类,这里我命名为AiConfig

5.2-第一种写法(create与mutate)

这个配置类的代码如下

@Configuration  
public class AiConfig {  

	// 依然使用注解导入系统提示词
    @Value("classpath:/prompts/alice-system-message.st")  
    private Resource systemResource;  
  
	// 创建yuukaChat的同时注入两个参数,OpenAiChatModel和ChatMemory
    @Bean  
    public ChatClient yuukaChat(OpenAiChatModel openAiChatModel, ChatMemory chatMemory) {  
	    // mutate不修改调用它的client,而是创建一个新的client
	    // 所以这里是写到链式表达式里面并且直接返回
        return ChatClient.create(openAiChatModel).mutate().defaultAdvisors(  
                new MessageChatMemoryAdvisor(chatMemory) // 使用内存聊天记录  
        ).build();  
    }  

	// 创建一个内存类型的记忆
	// 官方还提供了CassandraChatMemory,Neo4jChatMemory和JdbcChatMemory
    @Bean  
    public ChatMemory memory() {  
        return new InMemoryChatMemory();  
    }  
}

Service层级需要注入并调用yuukaChat
注入yuukaChat

public ChatClientService(ChatClient yuukaChat) {  
    this.yuukaChat = yuukaChat;  
}
(yuuka - 早濑优香)

调用yuukaChat得到noa

public Flux<String> chatWithOpenAiByYuuka(String message) {  
  
    // 直接使用链式编程传入参数  
    Flux<String> noa = yuukaChat.prompt()  
            .user(message)  
            .system(systemResource)  
            .stream()  
            .content();  
    return noa;  
}
(noa - 生盐诺亚)

之后调用controller

@GetMapping(value = "/flux-ChatClient/OpenAi-yuuka", produces = "text/html;charset=UTF-8")  
@Operation(summary = "链式实现ChatClient聊天接口")  
public Flux<String> chatWithOpenAiByYuuka(@RequestParam(value = "message", defaultValue = "给我讲个笑话?") String message) {  
    log.info("链式构成ChatClient/OpenAi聊天接口被调用,消息内容:{}", message);  
    Flux<String> fluxchat = chatClientService.chatWithOpenAiByYuuka(message);  
    log.info("Yuuka调用完成");  
    return fluxchat;  
}

效果实现

(爱丽丝大头)

但是目前的记忆实现是临时的,我们之后再说持久化的事情,先把临时的讲清楚

5.3-第二种写法(builder构建)

第二种写法的区别主要是在Config类中初始化ChatClient上,调用都是一样的

@Bean  
public ChatClient Kei(OpenAiChatModel model, ChatMemory chatMemory) {  
    return ChatClient.builder(model)  
            .defaultSystem(systemResource)  
            .defaultAdvisors(  
                    new MessageChatMemoryAdvisor(chatMemory) // 使用内存聊天记录  
            )  
            .build();  
}
(kei - 嘴上都是爱丽丝,心里都是自己的家伙)

可以看到这里调用了builder方法,那么和第一种ChatClient配置时先用了create,后用了mutate有什么区别呢?

5.4-两种方法的教会了我们什么

我们直接跟到create里面看看

也就是说create本质还是builder,那为什么要有mutate呢?
我们注意到mutate可以用来修改已经存在的实例,对其微调并返回一个新的实例
也就是说

return ChatClient.create(openAiChatModel).mutate().defaultAdvisors(  
        new MessageChatMemoryAdvisor(chatMemory) // 使用内存聊天记录  
).build();

可以拆分为两部分
如下

@Bean  
public ChatClient yuukaChat(OpenAiChatModel openAiChatModel, ChatMemory chatMemory) { 
    // 先用create生成新的client
    ChatClient originClient = ChatClient.create(openAiChatModel);  

	// 再用mutate进行微调并返回新的client
    ChatClient finalClient = originClient.mutate().defaultAdvisors(  
            new MessageChatMemoryAdvisor(chatMemory) // 使用内存聊天记录  
    ).build();  

	最后我们直接返回就可以了
    return finalClient;  
}

这里就展现了mutate可以微调并生成一个新的client并返回
那么下次如果你希望生成一个经过微调的client就可以使用mutate了!

一般来说,我们还是推荐直接使用builder
毕竟生成一次就能直接用,不需要多生成一个实例

@Bean  
public ChatClient Kei(OpenAiChatModel openAiChatModel, ChatMemory chatMemory) {  
    return ChatClient.builder(openAiChatModel)  
            .defaultSystem(systemResource)  
            .defaultAdvisors(  
                    new MessageChatMemoryAdvisor(chatMemory) // 使用内存聊天记录  
            )  
            .build();  
}

5.5-简单看一下Memory实现

在这里我们可以看到memory是一个ChatMemory类型的类

我们直接ctrl + 左键跟进去看看
发现ChatMemory是一个接口,提供了增加,获取,清除的方法

咦,看看怎么实现的这些方法,我们直接跳到Impl类里面

因为跳进来阅读是只读代码,所以我复制一份代码出来给大家加个注释

public class InMemoryChatMemory implements ChatMemory {  
	// 使用ConcurrentHashMap,将对话id作为key,消息列表作为value
    Map<String, List<Message>> conversationHistory = new ConcurrentHashMap<>();  

	// 这里自定义实现了一个put方法 putIfAbsent
	/**
	 如果 Map 中已经存在指定的 key 并且其值不是 null,则此方法不执行任何操作,并返回该 key 当前关联的值。
如果 Map 中不存在指定的 key,或者该 key 存在但其关联的值是 null,则此方法会将指定的 key 和 value 添加到 Map 中,并返回 null(因为之前没有值或值为 null)。
	 */
    @Override  
    public void add(String conversationId, List<Message> messages) {  
       this.conversationHistory.putIfAbsent(conversationId, new ArrayList<>());  
       this.conversationHistory.get(conversationId).addAll(messages);  
    }  
  
    @Override  
    public List<Message> get(String conversationId, int lastN) {  
       List<Message> all = this.conversationHistory.get(conversationId);  
       return all != null ? all.stream().skip(Math.max(0, all.size() - lastN)).toList() : List.of();  
    }  
  
    @Override  
    public void clear(String conversationId) {  
       this.conversationHistory.remove(conversationId);  
    }  
  
}

这里补充一下
ConcurrentHashMap:

  • 优点:线程安全,采用了分段锁或细粒度锁的设计,允许多个线程同时访问map的不同部分,并发性能好
  • 缺点:迭代时不反映最新状态,弱一致性,略微牺牲单线程性能
  • 适用场景:需要线程安全的高并发场景

所以简单的来说,Memory通过Map的方式实现了对话id和内容的存储,每次对话都会将上一次的对话一起发送给大模型,以此实现记忆上下文的功能,但是大模型的输入Token终究有限,所以有些公司会使用自己研发的注意力集中机制,专注于最近的消息

6.配置记忆持久化

TODO