一、 defaultOptions 作用解析

1. 核心概念
defaultOptions 用于在 ChatClient 构建阶段配置全局默认的大模型调用参数。

2. 主要作用

  • 统一配置:避免在每次调用 .prompt() 时重复编写相同的控制参数,提高代码复用性。
  • 核心参数
    • Model:指定调用的具体模型(如 qwen-maxqwen-vl-max)。
    • Temperature:控制生成的随机性与创造力(值越低越严谨,越高越发散)。
    • MaxTokens:限制单次输出的最大 Token 数量。

3. 覆盖机制
支持局部覆盖。在具体发起请求时,可通过 client.prompt().options(...) 传入新的参数,以覆盖全局 defaultOptions 中的同名配置。


二、 多模态参数 .withMultiModel(true) 原理

在使用视觉等多模态模型(如 qwen-vl-max)时,必须在 DashScopeChatOptions 中开启 .withMultiModel(true)
在这里插入图片描述

原因涉及底层 API 协议的差异:

1. 请求报文结构(JSON Payload)差异

  • 纯文本模式(默认):底层序列化时,content 字段仅支持单一字符串格式。如果传入包含媒体资源(Media)的请求,媒体数据会被忽略或丢弃。
  • 多模态模式(开启后):转换器切换协议,将 content 字段构建为包含文本和媒体 URL 的对象数组(Array),符合阿里云多模态 API 的标准入参格式。

2. 流式响应(Streaming)解析差异

  • 多模态模型与纯文本模型在返回 SSE(Server-Sent Events)数据流时,报文结构不一致。
  • 如果不开启该参数,Spring AI 底层反序列化器会默认使用纯文本规则解析多模态响应包,导致数据格式不匹配,进而引发流式输出解析异常或静默失败。

三、 多模态消息构建与动态调用策略

1. 客户端动态切换
在实际业务中,接口通常需要兼容“纯文本”和“图文混合”两种对话形式。应根据用户请求中是否包含媒体附件,动态选择对应的 ChatClient 实例:

  • 包含附件:使用预先配置了 .withMultiModel(true) 的多模态客户端(如 qwen-vl-max)。
  • 无附件:使用基础文本客户端(如 qwen-max)。

2. UserMessage 的条件构建原则(核心踩坑点)
在组装发给大模型的消息体(UserMessage)时,必须严格通过 if-else 区分纯文本与多模态场景,不可混用构建逻辑:

  • 多模态场景(有附件)
    需将文件流或 URL 转换为 List<Media>,并显式调用 .media() 方法附加到消息中。

    userMessage = UserMessage.builder()
            .text(message)
            .media(memoryResources) // 传入 List<Media>
            .build();
    
  • 纯文本场景(无附件)
    仅传入文本内容。绝对不要调用 .media() 方法(即使传入 null 或空集合)。如果强行调用,底层 Converter 可能会将其误判为格式错误的多模态请求,从而引发校验失败或 JSON 序列化异常。

    userMessage = UserMessage.builder()
            .text(message)
            // 严禁在此处调用 .media(null) 或 .media(emptyList)
            .build();
    

3. 消息参数注入
无论底层走哪种逻辑构建的 UserMessage 对象,最终都统一通过 .prompt().messages(userMessage) 注入到 ChatClient 中。此方式解耦了请求的构建与执行,保证了后续 Advisors(如上下文记忆)和流式(.stream())处理代码的通用性。

参考代码示例:
在这里插入图片描述

/**
     * 生成流式对话响应
     * @param message 用户消息
     * @param files 附件列表
     * @param fileUrls 附件URL列表
     * @param sessionId 会话ID
     * @param userId 用户ID
     * @return Flux<ServerSentEvent<String>> 流式响应
     */
    public Flux<ServerSentEvent<String>> generateStreamResponse(String message, List<MultipartFile> files, List<String> fileUrls, Long sessionId, Long userId) {
        boolean hasFiles = files != null && !files.isEmpty();
        ChatClient clientToUse = hasFiles ? multiModalChatClient : dashScopeChatClient;

        log.info("使用模型: {}, 是否包含附件: {}", hasFiles ? "多模态" : "基础文本", hasFiles);

        // 如果有附件,使用多模态模型
        List<Media> memoryResources = hasFiles ? convertMultipartFilesToMedia(files) : null;

        // 根据是否有附件,分别构建 UserMessage
        UserMessage userMessage;
        if (hasFiles && memoryResources != null && !memoryResources.isEmpty()) {
            // 多模态情况:同时传入文字和媒体文件
            userMessage = UserMessage.builder()
                    .text(message)
                    .media(memoryResources)
                    .build();
        } else {
            // 纯文本情况:只传入文字,绝对不要调用 .media() 方法
            userMessage = UserMessage.builder()
                    .text(message)
                    .build();
        }

        // --- 持久化记录:直接存前端传来的 fileUrls ---
        String urlsString = (fileUrls != null) ? String.join(",", fileUrls) : "";

        // 保存用户消息,同时记录附件URL
        chatMessageService.saveMessage(sessionId, userId, 1, message, urlsString);

        // 创建会话标识
        String chatId = String.valueOf(sessionId);

        // 为当前会话创建流式状态标识
        AtomicBoolean isStreaming = streamingStates.computeIfAbsent(chatId, k -> new AtomicBoolean(true));
        isStreaming.set(true);
        
        // 重置消息保存标识(开始新的流式对话)
        AtomicBoolean messageSaved = messageSavedFlags.computeIfAbsent(chatId, k -> new AtomicBoolean(false));
        messageSaved.set(false);

        // 用于累积AI回复内容
        StringBuilder aiResponse = new StringBuilder();

        // 生成流式对话
        return clientToUse.prompt()
                .messages(userMessage)
                .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, sessionId))
                .stream()
                .content()
                .takeWhile(data -> isStreaming.get())
                .map(content -> {
                    aiResponse.append(content);
                    return ServerSentEvent.<String>builder()
                            .data(content)
                            .build();
                })
                .concatWith(Flux.just(ServerSentEvent.<String>builder()
                        .data("\u0003")
                        .build()))
                .doOnComplete(() -> handleStreamComplete(sessionId, userId, message, aiResponse.toString(), chatId))
                .doOnCancel(() -> handleStreamCancel(sessionId, userId, aiResponse.toString(), chatId))
                .doOnError(error -> handleStreamError(sessionId, userId, chatId, error))
                .onErrorResume(error -> handleStreamErrorResponse(userId, sessionId, error));
    }
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐