一、defaultOptions作用解析1. 核心概念defaultOptions用于在ChatClient构建阶段配置全局默认的大模型调用参数。2. 主要作用统一配置避免在每次调用.prompt()时重复编写相同的控制参数提高代码复用性。核心参数Model指定调用的具体模型如qwen-max、qwen-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解析差异多模态模型与纯文本模型在返回 SSEServer-Sent Events数据流时报文结构不一致。如果不开启该参数Spring AI 底层反序列化器会默认使用纯文本规则解析多模态响应包导致数据格式不匹配进而引发流式输出解析异常或静默失败。三、 多模态消息构建与动态调用策略1. 客户端动态切换在实际业务中接口通常需要兼容“纯文本”和“图文混合”两种对话形式。应根据用户请求中是否包含媒体附件动态选择对应的ChatClient实例包含附件使用预先配置了.withMultiModel(true)的多模态客户端如qwen-vl-max。无附件使用基础文本客户端如qwen-max。2.UserMessage的条件构建原则核心踩坑点在组装发给大模型的消息体UserMessage时必须严格通过if-else区分纯文本与多模态场景不可混用构建逻辑多模态场景有附件需将文件流或 URL 转换为ListMedia并显式调用.media()方法附加到消息中。userMessageUserMessage.builder().text(message).media(memoryResources)// 传入 ListMedia.build();纯文本场景无附件仅传入文本内容。绝对不要调用.media()方法即使传入null或空集合。如果强行调用底层 Converter 可能会将其误判为格式错误的多模态请求从而引发校验失败或 JSON 序列化异常。userMessageUserMessage.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 FluxServerSentEventString 流式响应 */publicFluxServerSentEventStringgenerateStreamResponse(Stringmessage,ListMultipartFilefiles,ListStringfileUrls,LongsessionId,LonguserId){booleanhasFilesfiles!null!files.isEmpty();ChatClientclientToUsehasFiles?multiModalChatClient:dashScopeChatClient;log.info(使用模型: {}, 是否包含附件: {},hasFiles?多模态:基础文本,hasFiles);// 如果有附件使用多模态模型ListMediamemoryResourceshasFiles?convertMultipartFilesToMedia(files):null;// 根据是否有附件分别构建 UserMessageUserMessageuserMessage;if(hasFilesmemoryResources!null!memoryResources.isEmpty()){// 多模态情况同时传入文字和媒体文件userMessageUserMessage.builder().text(message).media(memoryResources).build();}else{// 纯文本情况只传入文字绝对不要调用 .media() 方法userMessageUserMessage.builder().text(message).build();}// --- 持久化记录直接存前端传来的 fileUrls ---StringurlsString(fileUrls!null)?String.join(,,fileUrls):;// 保存用户消息同时记录附件URLchatMessageService.saveMessage(sessionId,userId,1,message,urlsString);// 创建会话标识StringchatIdString.valueOf(sessionId);// 为当前会话创建流式状态标识AtomicBooleanisStreamingstreamingStates.computeIfAbsent(chatId,k-newAtomicBoolean(true));isStreaming.set(true);// 重置消息保存标识开始新的流式对话AtomicBooleanmessageSavedmessageSavedFlags.computeIfAbsent(chatId,k-newAtomicBoolean(false));messageSaved.set(false);// 用于累积AI回复内容StringBuilderaiResponsenewStringBuilder();// 生成流式对话returnclientToUse.prompt().messages(userMessage).advisors(a-a.param(ChatMemory.CONVERSATION_ID,sessionId)).stream().content().takeWhile(data-isStreaming.get()).map(content-{aiResponse.append(content);returnServerSentEvent.Stringbuilder().data(content).build();}).concatWith(Flux.just(ServerSentEvent.Stringbuilder().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));}