Model

AI模型

  • Model是SpringAI对AI模型的抽象。学习中一直使用的ChatModel是对聊天模型的抽象。

  • SpringAI除了聊天模型ChatModel,还抽象了比如嵌入模型、图像、音频模型等等。下面是官方的模型层次图:

    模型层次结构

  • StreamingModel本质是与Model一致的,只是对支持流式响应的模型的抽象。

ChatModel

  • ChatModelModel以及StreamingModel的子接口,是SpringAI专门对聊天模型的抽象。

  • 下面两个方法是ChatModel接口的核心方法。

    1
    2
    3
    4
    5
    @Override
    ChatResponse call(Prompt prompt);

    @Override
    Flux<ChatResponse> stream(Prompt prompt);
  • 从参数及返回上看输入是提示Prompt,返回是AI的响应ChatResponse。(这两块前面都有学习记录到)

  • 前面学习记录的案例中使用到的DeepSeekChatModel以及OpenAiChatModel都是其子类。

    DeepSeekChatModel

  • 除了上面提到的两个,SpringAI还封装了其他聊天模型的实现。下面是官方给出的实现图(现在应该更多了)

    chat completions clients

  • 由此可见ChatModel是与AI模型交互的核心接口。那么开发者也可以通过实现其来对接SpringAI未提供的聊天模型。

  • 下面模拟一个自行实现的聊天模型。

    • 大致需求:

      • 将DIFY平台视为一个聊天模型来模拟自行实现一个DifyChatModel

      • 当然DIFY并不是一个AI模型,只是模拟当成聊天模型来使用一下。

      • 用到的API是一个聊天助手。是以前学习遗留下来的。

        聊天助手

    • Code

      • 创建DifyChatModel,实现ChatModel接口,并实现了构造者模式。这里的DifyApiClient是以前写好的调用DIFY平台的客户端,直接引入用一下。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        public class DifyChatModel implements ChatModel {

        /** 这是作者以前写的一个调用 Dify 的客户端,直接引入用一用 */
        private DifyApiClient difyApiClient;

        /** apiKey */
        private String difyApiKey;

        private String conversationId = "";

        public DifyChatModel(Builder builder) {
        this.difyApiClient = builder.difyApiClient;
        this.difyApiKey = builder.difyApiKey;
        }

        @Override
        public ChatResponse call(Prompt prompt) {
        return null;
        }

        @Override
        public Flux<ChatResponse> stream(Prompt prompt) {
        return ChatModel.super.stream(prompt);
        }

        public static Builder builder() {
        return new Builder();
        }

        public static final class Builder {
        private DifyApiClient difyApiClient;

        private String difyApiKey;

        private Builder() {
        }

        public Builder difyApiClient(DifyApiClient difyApiClient) {
        this.difyApiClient = difyApiClient;
        return this;
        }

        public Builder difyApiKey(String difyApiKey) {
        this.difyApiKey = difyApiKey;
        return this;
        }

        public DifyChatModel build() {
        return new DifyChatModel(this);
        }
        }
        }
      • 这里只模拟一下.call()方法的具体实现

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        @Override
        public ChatResponse call(Prompt prompt) {
        DifyClientContext<ChatMessagesRequest> requestContext = createRequestContext(prompt);
        ChatCompletionResponse sync = difyApiClient.onChatMessagesRequest(requestContext).sync();
        return ChatResponse.builder()
        .metadata(createMetadata(sync))
        .generations(createGeneration(sync))
        .build();
        }

        /**
        * 将Dify返回的元数据转换
        * @param response
        * @return
        */
        private ChatResponseMetadata createMetadata(ChatCompletionResponse response) {
        conversationId = response.getConversationId();
        return ChatResponseMetadata.builder()
        .id(response.getMessageId())
        .usage(new DefaultUsage(
        response.getMetadata().getUsage().getPromptTokens(),
        response.getMetadata().getUsage().getCompletionTokens(),
        response.getMetadata().getUsage().getTotalTokens())
        ).build();
        }

        /**
        * 将Dify返回的消息转换
        * @param response
        * @return
        */
        private List<Generation> createGeneration(ChatCompletionResponse response) {
        AssistantMessage assistantMessage = new AssistantMessage(response.getAnswer());
        return List.of(new Generation(assistantMessage, ChatGenerationMetadata.builder().build()));
        }

        /**
        * 将提示转换成Dify的请求
        * @param prompt
        * @return
        */
        private DifyClientContext<ChatMessagesRequest> createRequestContext(Prompt prompt) {
        ChatMessagesRequest request = new ChatMessagesRequest();
        request.setQuery(prompt.getContents());
        request.setUser("123456");
        request.setConversationId(conversationId);

        return new DifyClientContext<>(difyApiKey, request);
        }
      • DifyChatModel注册到容器

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        @Value("${dify.api.key}")
        private String difyApiKey;

        @Bean
        public DifyChatModel difyChatModel(DifyApiClient difyApiClient) {
        return DifyChatModel.builder()
        .difyApiKey(difyApiKey)
        .difyApiClient(difyApiClient)
        .build();
        }
      • DifyApiClient相关的配置。需要开启加载和配置调用API的KEY。

        1
        2
        3
        4
        5
        # 开启 DifyApiClient 的加载
        dify:
        enable: true
        api:
        key: ${DIFY_API_KEY}
      • 直接使用ChatModel调用。

        1
        2
        3
        4
        5
        6
        private final DifyChatModel difyChatModel;

        public void modelExample() {
        String call = difyChatModel.call("你好啊!");
        log.info("\n modelExample call -> \n{}", call);
        }
      • 调用结果。可以看到是正常调用的。

        1
        2
        3
        4
        2025-07-17T18:53:11.893+08:00  INFO 57364 --- [spring-ai-example] [           main] c.s.ai.example.models.DifyModelExample   : 
        modelExample call ->
        (●'◡'●)嗨~我家主人这会正在开会呢,我是他的小助手微微~有什么好玩的事可以先跟我说说呀~

      • 使用ChatClient来调用。并使用LogExampleAdvisor做日志输出。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        @Bean
        public ChatClient difyChatClient(DifyChatModel difyChatModel) {
        return ChatClient.builder(difyChatModel)
        // 添加 Advisors
        .defaultAdvisors(spec -> {
        spec.params(Map.of(
        // 放置客户端名称
        ContextKeys.ClientName.name(), "difyChatClient"
        ));

        spec.advisors(
        new LogExampleAdvisor() // Log
        );
        })
        .build();
        }
        1
        2
        3
        4
        5
        6
        7
        8
        private final ChatClient difyChatClient;

        public void clientExample() {
        difyChatClient.prompt()
        .user("你谁啊?")
        .call()
        .chatResponse();
        }
      • 调用结果。也是完全没问题的。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        2025-07-21T18:42:47.039+08:00  INFO 90099 --- [spring-ai-example] [           main] c.s.a.e.advisor.three.LogExampleAdvisor  : 
        Chat client request to AI
        prompt text -> 你谁啊?
        context -> {
        "ClientName" : "difyChatClient"
        }

        2025-07-21T18:42:56.629+08:00 INFO 90099 --- [spring-ai-example] [ main] c.s.a.e.advisor.three.LogExampleAdvisor :
        Chat client response from AI
        output text -> (≧▽≦) 哈喽~我是主人的小助手微微呀!主人现在正在开会忙得团团转呢,特意让我先来陪您聊天~有什么想说的都可以告诉我哦!
        context -> {ClientName=difyChatClient}
  • 整个模拟的DifyChatModel并不复杂,下面继续围绕其做一些优化。

ChatResponse

ChatResponse

  • ChatResponse在前面的学习中经常用到过,这里再进一步了解其内部结构及作用。

  • ChatResponse是SpringAI封装 AI 模型输出的响应,从源码上看是实现了ModelResponse接口。

  • 且定义了chatResponseMetadatagenerations两个属性

    • ChatResponseMetadata chatResponseMetadata:用来封装 AI 模型响应的元数据。
    • List<Generation> generations:用来封装 AI 模型响应的输出消息。

    ChatResponse

Generation

  • Generation实现了ModelResult接口。主要用于封装模型输出(Assistant 消息)及相关元数据。

  • 其内部有assistantMessagechatGenerationMetadata两个属性

    • AssistantMessage assistantMessage:Assistant 助手消息。

    • ChatGenerationMetadata chatGenerationMetadata:用来封装相关元数据。

      Generation

  • 将上面案例优化一下,把DifyChatModel中调用Dify返回的conversationId(会话ID)封装到元数据中。

    1
    2
    3
    4
    5
    6
    7
    private List<Generation> createGeneration(ChatCompletionResponse response) {
    AssistantMessage assistantMessage = new AssistantMessage(response.getAnswer());
    ChatGenerationMetadata chatGenerationMetadata = ChatGenerationMetadata.builder()
    .metadata("CONVERSATION_ID", response.getConversationId())
    .build();
    return List.of(new Generation(assistantMessage, chatGenerationMetadata));
    }
  • 可以在返回中获取

    1
    2
    3
    4
    5
    ChatResponse chatResponse = difyChatClient.prompt()
    .user("你谁啊?")
    .call()
    .chatResponse();
    chatResponse.getResult().getMetadata().get("CONVERSATION_ID");

ChatOptions

  • ChatOptions接口是用来在运行时向ChatModel传递AI模型的选项参数。其定义如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public interface ChatOptions extends ModelOptions {

    @Nullable
    String getModel();
    @Nullable
    Double getFrequencyPenalty();
    @Nullable
    Integer getMaxTokens();
    @Nullable
    Double getPresencePenalty();
    @Nullable
    List<String> getStopSequences();
    @Nullable
    Double getTemperature();
    @Nullable
    Integer getTopK();
    @Nullable
    Double getTopP();

    }
  • ChatOptionsModelOptions 的子接口。说明ChatOptions是专门用于聊天模型的。

  • 基于上面的DifyChatModel定义一个DifyChatOptions,用来传递几个Dify的参数。

    • 需求:

      • 将ApiKey、会话ID、用户唯一标识都定义在DifyChatOptions
    • Code:

      • 定义DifyChatOptions。这里模拟将ApiKey视为Model,ChatOptions`中其他定义暂不实现了。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        public class DifyChatOptions implements ChatOptions {

        // 模拟将 difyApiKey 视为Model,在Dify中一个key对应一个应用。
        private String difyApiKey;

        /** 回话ID **/
        @Getter
        private String conversationId;

        /** 用户唯一标识 **/
        @Getter
        private String user;

        @Override
        public String getModel() {
        // 模拟将 difyApiKey 视为Model
        return difyApiKey;
        }

        public static DifyChatOptions.Builder builder() {
        return new DifyChatOptions.Builder();
        }

        public static class Builder {

        protected DifyChatOptions options;

        public Builder() {
        this.options = new DifyChatOptions();
        }

        public Builder(DifyChatOptions options) {
        this.options = options;
        }

        public DifyChatOptions.Builder difyApiKey(String difyApiKey) {
        this.options.difyApiKey = difyApiKey;
        return this;
        }

        public DifyChatOptions.Builder conversationId(String conversationId) {
        this.options.conversationId = conversationId;
        return this;
        }

        public DifyChatOptions.Builder user(String user) {
        this.options.user = user;
        return this;
        }

        public DifyChatOptions build() {
        return this.options;
        }
        }
        }
      • 将创建请求Dify的参数改造一下

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        ChatMessagesRequest request = new ChatMessagesRequest();
        request.setQuery(prompt.getContents());
        request.setUser("123456");
        request.setConversationId("");

        ChatOptions options = prompt.getOptions();
        // 使用传递的选项参数
        if (options instanceof DifyChatOptions difyChatOptions) {
        request.setUser(difyChatOptions.getUser());
        request.setConversationId(difyChatOptions.getConversationId());
        return new DifyClientContext<>(StringUtils.isBlank(difyChatOptions.getModel()) ? difyApiKey : difyChatOptions.getModel(), request);
        }

        return new DifyClientContext<>(difyApiKey, request);
      • 调用案例改造

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        public void clientExample() {
        String user = "DEFAULT-USER";
        ChatResponse chatResponse = difyChatClient.prompt()
        // 初次交互没有回话ID
        .options(DifyChatOptions.builder()
        .user(user)
        .build())
        .user("你谁啊?")
        .call()
        .chatResponse();

        Object conversationId = chatResponse.getResult().getMetadata().get("CONVERSATION_ID");

        chatResponse = difyChatClient.prompt()
        .options(DifyChatOptions.builder()
        // 使用返回的回话ID
        .conversationId(conversationId.toString())
        .user(user)
        .build())
        .user("你主人在干什么啊?")
        .call()
        .chatResponse();
        }
      • 日志输出结果

        输出结果

      • Dify平台中可以看到,两个消息是同一会话中的。

        会话

  • 最后再看看ChatOptions的子类,基本可以看到每实现一个AI模型ChatModel就会对应有一个ChatOptions的实现

    ChatOptions子类

总结

  • 在SpringAI中Model是对AI模型的抽象。
  • 其中ChatModel是对聊天模型的抽象。当然还包含其他多种模型,后面会学习到。
  • 开发者可以通过实现ChatModel接口来与自定义SpringAI未实现的模型来进行交互扩展。
  • ChatModel中与模型交互后通过ChatResponse封装模型返回的响应。
    • Generation主要用于封装模型输出(Assistant 消息)及相关元数据(额外数据)。
    • ChatResponseMetadata主要用于封装模型返回的一些元数据(一些量化数据)。
  • 可以通过ChatOptions在运行时向ChatModel传递AI模型的选项参数,以便灵活开发。

最后

  • 最近两周发生了一些小插曲,导致没怎么学习。后续努力补课吧。
  • 下一篇大概学习一下SpringAI封装的Deepseek模型或者OpenAI模型。(主要是封装的配置以及模型的特性如何使用)
  • 所有案例的源码,都会提交在GitHub上。本次案例包:com.spring.ai.example.models.one