Spring AI 多模态实战:让 AI 看图、听声音、生成图片

Spring AI 多模态实战:让 AI 看图、听声音、生成图片

本文是《Spring AI 实战》系列第 13 章。前面 12 章我们一直在和文字打交道:文字输入、文字输出、文字检索、文字生成。但现实世界远不止文字------用户拍一张产品图发过来问"这个颜色有其他款吗?"、客服接到一段语音投诉、运营需要一张宣传海报。这些场景都需要 AI 具备多模态能力:能看图、能听声、能生成图片。本章详解 Spring AI 对多模态的四大能力支持,并通过一个"多模态智能助手"把所有能力串起来。


一、开篇:AI 不只属于文字世界

大模型的能力演进有一个清晰的脉络:从最初只能处理纯文本,到能理解图片,再到能识别语音、生成图片和视频。GPT-4V 的发布标志着多模态时代的正式到来------AI 终于"长出了眼睛"。

在真实业务中,多模态需求无处不在。电商用户拍照搜索商品、医疗场景中上传 CT 片辅助诊断、客服系统接收语音消息并自动转文字、内容运营用 AI 生成营销图片。这些能力在 Spring AI 2.0.0 中都有对应的抽象和 API 支持。

多模态不是噱头,而是 AI 从"实验室玩具"变成"生产力工具"的关键一步。试想一下,一个只能打字的客服助手和一个能看图、听语音、回答问题的助手,用户体验天差地别。本章我们就把 Spring AI 的多模态能力逐一拆解,写出能直接上生产的代码。


二、Spring AI 多模态支持全景

Spring AI 2.0.0 对多模态的支持覆盖了四大核心能力,每个能力对应一个独立的 Model 抽象:

能力 Spring AI 模型接口 典型实现 输入 输出
图片理解(Vision) ChatModel(多模态消息) GPT-4o、Claude 3.5 Sonnet、Qwen-VL 图片 + 文字 Prompt 文字描述/分析
语音转文字(ASR) TranscriptionModel OpenAI Whisper、阿里 Paraformer 音频文件 文字
文字转语音(TTS) SpeechModel OpenAI TTS、Azure TTS 文字 音频文件
文生图(Image Generation) ImageModel DALL-E 3、Stable Diffusion 文字 Prompt 图片 URL / Base64

注意一个细节:图片理解不是独立的模型接口 ,而是通过 ChatModel 发送包含图片的多模态消息来实现的。这是因为视觉理解本质上还是"对话"------只是对话中多了一个"图片"这个输入模态。

下面逐一讲解,每个能力都配有完整可运行的代码。


三、图片理解 Vision

3.1 Media 类详解

Spring AI 中,图片理解的核心是 Media 类。它代表一个多模态消息中的媒体附件(图片、音频、视频等):

java 复制代码
package org.springframework.ai.content;

/**
 * Media ------ 多模态消息中的媒体附件
 *
 * Media 是 Spring AI 对多模态输入的核心抽象。
 * 它包装了媒体数据的字节内容和 MIME 类型,
 * 可以附加到 UserMessage 中,让大模型"看到"图片。
 */
public class Media {

    /** 媒体的 MIME 类型,如 image/png、image/jpeg、audio/wav */
    private final MimeType mimeType;

    /** 媒体数据的来源:可以是 URL、Resource 或 byte[] */
    private final Object data;

    // --- 构造方式 1:从 URL 加载远程图片 ---
    // 适用于图片已经在 OSS、S3 等对象存储上的场景
    Media(MimeType mimeType, URL url);

    // --- 构造方式 2:从 Spring Resource 加载 ---
    // 适用于本地文件或classpath资源
    Media(MimeType mimeType, Resource resource);

    // --- 构造方式 3:直接传入字节数据 ---
    // 适用于用户上传的文件(MultipartFile.getBytes())
    Media(MimeType mimeType, byte[] data);
}

Media 支持三种数据来源,对应三种常见场景:

数据来源 适用场景 示例
URL 图片已存储在 OSS/S3/CDN 商品图片、用户头像
Resource 本地文件或 classpath 资源 配置的默认图片、测试图片
byte[] 用户通过 API 上传的文件 MultipartFile.getBytes()

3.2 发送图片给大模型

将图片"嵌入"到 UserMessage 中有专门的构建器方法。以下是三种典型用法:

java 复制代码
package com.example.springaidemo.multimodal;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.Media;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.util.MimeTypeUtils;

import java.net.URL;

/**
 * 图片理解服务 ------ 让大模型"看懂"图片
 *
 * 核心原理:在 UserMessage 中通过 .media() 方法附加图片,
 * 大模型会同时分析文字问题和图片内容,给出综合回答。
 */
@Service
public class VisionService {

    private final ChatClient chatClient;

    public VisionService(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    /**
     * 方式一:通过 URL 分析远程图片
     *
     * 最常用的方式------图片已在对象存储上,
     * 直接传 URL 给大模型,不需要下载到本地。
     */
    public String analyzeImageUrl(String imageUrl, String question) {
        return chatClient.prompt()
                .user(u -> u
                        .text(question)                                    // 文字问题
                        .media(MimeTypeUtils.IMAGE_PNG, new URL(imageUrl)) // 附加图片
                )
                .call()
                .content();
    }

    /**
     * 方式二:分析用户上传的图片(byte[])
     *
     * 用户通过 HTTP POST 上传图片文件,
     * Controller 层把 MultipartFile 转为 byte[] 传入。
     */
    public String analyzeUploadedImage(byte[] imageBytes, String mimeType,
                                       String question) {
        return chatClient.prompt()
                .user(u -> u
                        .text(question)
                        .media(MimeType.valueOf(mimeType), imageBytes) // 直接传字节数组
                )
                .call()
                .content();
    }

    /**
     * 方式三:分析本地文件
     *
     * 从磁盘或 classpath 读取图片文件。
     * 适用于处理预置的模板图片、配置图片等。
     */
    public String analyzeLocalFile(String filePath, String question) {
        var resource = new org.springframework.core.io.FileSystemResource(filePath);
        return chatClient.prompt()
                .user(u -> u
                        .text(question)
                        .media(MimeTypeUtils.IMAGE_JPEG, resource) // 传入 Resource
                )
                .call()
                .content();
    }
}

3.3 REST Controller

java 复制代码
package com.example.springaidemo.multimodal;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.Map;

/**
 * 图片理解 REST 接口
 */
@RestController
@RequestMapping("/api/vision")
public class VisionController {

    private final VisionService visionService;

    public VisionController(VisionService visionService) {
        this.visionService = visionService;
    }

    /**
     * 上传图片并提问
     *
     * curl 示例:
     * curl -X POST http://localhost:8080/api/vision/upload \
     *   -F "image=@product.jpg" \
     *   -F "question=这个产品是什么颜色?什么材质?"
     */
    @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public Map<String, String> analyzeUpload(
            @RequestParam("image") MultipartFile image,
            @RequestParam("question") String question) {

        String answer = visionService.analyzeUploadedImage(
                image.getBytes(),
                image.getContentType(),
                question
        );
        return Map.of("question", question, "answer", answer);
    }

    /**
     * 通过 URL 分析图片
     *
     * curl 示例:
     * curl "http://localhost:8080/api/vision/url?imageUrl=https://...&question=描述这张图片"
     */
    @GetMapping("/url")
    public Map<String, String> analyzeUrl(
            @RequestParam String imageUrl,
            @RequestParam String question) {

        String answer = visionService.analyzeImageUrl(imageUrl, question);
        return Map.of("question", question, "answer", answer);
    }
}

3.4 实际应用场景

图片理解在以下场景有真实的生产价值:

场景 具体做法 业务价值
电商商品识别 用户拍照搜索,Vision 识别商品类别+特征 提升搜索转化率 30%+
内容审核 检测上传图片中的违规内容 自动化审核,降低人力成本
发票/证件 OCR 上传发票图片,提取金额、日期等结构化信息 替代传统 OCR,准确率更高
质检缺陷检测 上传产品照片,AI 判断是否有缺陷 辅助人工质检,提升效率
医疗影像辅助 上传 X 光/CT 片,AI 给出初步分析 辅助医生诊断(注意:不能替代医生)

四、语音转文字 ASR(Automatic Speech Recognition)

4.1 TranscriptionModel 接口

Spring AI 的语音转文字能力通过 TranscriptionModel 接口抽象:

java 复制代码
package org.springframework.ai.chat.model;

/**
 * TranscriptionModel ------ 语音转文字模型接口
 *
 * 将音频文件转换为文字。支持多种音频格式(mp3、wav、m4a 等)。
 * 典型实现:OpenAI Whisper、阿里 Paraformer。
 */
public interface TranscriptionModel {

    /**
     * 执行语音转录
     * @param audioRequest 包含音频数据和语言配置
     * @return 转录结果
     */
    TranscriptionResponse call(TranscriptionRequest audioRequest);
}

4.2 Whisper 模型配置

以 OpenAI Whisper 为例,添加依赖并配置:

xml 复制代码
<!-- pom.xml:OpenAI Whisper(包含在 openai starter 中) -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
yaml 复制代码
# application.yml ------ Whisper 配置
spring:
  ai:
    openai:
      audio:
        transcription:
          options:
            model: whisper-1        # Whisper 模型版本
            language: zh             # 指定中文,提高中文识别准确率
            response-format: text    # 返回纯文本(也支持 json、srt、vtt)
            temperature: 0.0         # 确定性输出,适合转录场景

4.3 完整代码

java 复制代码
package com.example.springaidemo.multimodal;

import org.springframework.ai.audio.transcription.AudioTranscriptionPrompt;
import org.springframework.ai.audio.transcription.AudioTranscriptionResponse;
import org.springframework.ai.chat.model.TranscriptionModel;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

/**
 * 语音转文字服务
 *
 * 基于 OpenAI Whisper 模型。
 * Whisper 支持多种音频格式,包括 mp3、mp4、wav、m4a、webm 等。
 * 支持多语言识别,通过 language 参数指定可提升准确率。
 */
@Service
public class TranscriptionService {

    private final TranscriptionModel transcriptionModel;

    public TranscriptionService(TranscriptionModel transcriptionModel) {
        this.transcriptionModel = transcriptionModel;
    }

    /**
     * 将音频文件转录为文字
     *
     * @param audioBytes 音频文件的字节数据
     * @param mimeType   音频的 MIME 类型,如 audio/mpeg、audio/wav
     * @return 转录后的文字内容
     */
    public String transcribe(byte[] audioBytes, String mimeType) {
        // 构建音频请求对象
        var audioFile = new org.springframework.ai.model.Media(
                org.springframework.util.MimeType.valueOf(mimeType),
                audioBytes
        );
        var request = new AudioTranscriptionPrompt(audioFile);

        // 调用 Whisper 模型进行转录
        AudioTranscriptionResponse response = transcriptionModel.call(request);
        return response.getResult().getOutput();
    }

    /**
     * 转录用户上传的音频文件
     */
    public String transcribeMultipartFile(MultipartFile file) {
        return transcribe(file.getBytes(), file.getContentType());
    }
}
java 复制代码
/**
 * 语音转文字 REST 接口
 */
@RestController
@RequestMapping("/api/asr")
public class TranscriptionController {

    private final TranscriptionService transcriptionService;

    public TranscriptionController(TranscriptionService transcriptionService) {
        this.transcriptionService = transcriptionService;
    }

    /**
     * 上传音频文件,返回转录文字
     *
     * curl 示例:
     * curl -X POST http://localhost:8080/api/asr/transcribe \
     *   -H "Content-Type: multipart/form-data" \
     *   -F "audio=@recording.mp3"
     *
     * 返回:
     * {"text": "你好,我想查询一下我的订单状态"}
     */
    @PostMapping(value = "/transcribe",
                 consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public Map<String, String> transcribe(
            @RequestParam("audio") MultipartFile audio) {

        String text = transcriptionService.transcribeMultipartFile(audio);
        return Map.of("text", text);
    }
}

生产提示:Whisper 模型对音频质量有一定要求。建议在前端做静音检测,过滤掉过短的静音片段,减少不必要的 API 调用。对于长音频(>25MB),需要先切割再分段转录,最后拼接结果。


五、文字转语音 TTS(Text-to-Speech)

5.1 SpeechModel 接口

java 复制代码
/**
 * SpeechModel ------ 文字转语音模型接口
 *
 * 将文字转换为音频。典型实现:OpenAI TTS、Azure Speech Service。
 * 支持多种语音角色、语速调节、输出格式选择。
 */
public interface SpeechModel {
    SpeechResponse call(SpeechPrompt speechPrompt);
}

5.2 完整代码

java 复制代码
package com.example.springaidemo.multimodal;

import org.springframework.ai.chat.model.SpeechModel;
import org.springframework.ai.chat.model.SpeechPrompt;
import org.springframework.ai.chat.model.SpeechResponse;
import org.springframework.ai.openai.api.OpenAiAudioApi;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.*;

/**
 * 文字转语音服务
 *
 * 基于 OpenAI TTS 模型。
 * 支持多种语音角色(alloy、echo、fable、onyx、nova、shimmer),
 * 以及语速和格式调节。
 */
@Service
public class TtsService {

    private final SpeechModel speechModel;

    public TtsService(SpeechModel speechModel) {
        this.speechModel = speechModel;
    }

    /**
     * 将文字转换为语音
     *
     * @param text     要转换的文字内容
     * @param voice    语音角色:alloy(中性)、echo(男声)、nova(女声)、fable(英式) 等
     * @param speed    语速:0.5(慢) ~ 2.0(快),默认 1.0
     * @return 音频字节数据
     */
    public byte[] textToSpeech(String text, String voice, Float speed) {
        // 构建语音请求
        var options = OpenAiAudioApi.SpeechOptions.builder()
                .voice(voice != null ? voice : "alloy")  // 默认使用 alloy 中性音色
                .speed(speed != null ? speed : 1.0f)     // 默认正常语速
                .responseFormat(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3)
                .build();

        var prompt = new SpeechPrompt(text, options);
        SpeechResponse response = speechModel.call(prompt);

        return response.getResult().getOutput();
    }
}
java 复制代码
/**
 * TTS REST 接口 ------ 直接返回音频流
 */
@RestController
@RequestMapping("/api/tts")
public class TtsController {

    private final TtsService ttsService;

    public TtsController(TtsService ttsService) {
        this.ttsService = ttsService;
    }

    /**
     * 文字转语音,返回 MP3 音频流
     *
     * curl 示例:
     * curl "http://localhost:8080/api/tts/speak?text=你好,欢迎光临&voice=nova" \
     *   --output welcome.mp3
     */
    @GetMapping(value = "/speak", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
    public ResponseEntity<byte[]> speak(
            @RequestParam String text,
            @RequestParam(defaultValue = "alloy") String voice,
            @RequestParam(required = false) Float speed) {

        byte[] audio = ttsService.textToSpeech(text, voice, speed);

        // 返回音频流,浏览器可直接播放
        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION,
                        "attachment; filename=\"speech.mp3\"")
                .contentType(MediaType.parseMediaType("audio/mpeg"))
                .body(audio);
    }
}

生产提示:TTS 生成的音频建议在前端做缓存------相同文字内容不需要重复调用 API。可以用 MD5(text + voice) 作为缓存 key,把音频文件存到 CDN。


六、文生图(Image Generation)

6.1 ImageModel 接口

java 复制代码
/**
 * ImageModel ------ 文生图模型接口
 *
 * 根据文字描述生成图片。典型实现:DALL-E 3、Stable Diffusion。
 * 返回图片 URL 或 Base64 编码的图片数据。
 */
public interface ImageModel {
    ImageResponse call(ImagePrompt prompt);
}

6.2 DALL-E 配置

xml 复制代码
<!-- pom.xml:OpenAI DALL-E(同样包含在 openai starter 中) -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
yaml 复制代码
# application.yml ------ DALL-E 配置
spring:
  ai:
    openai:
      image:
        options:
          model: dall-e-3        # DALL-E 3 模型
          quality: hd            # 图片质量:standard(标准)或 hd(高清)
          size: 1024x1024        # 图片尺寸:1024x1024、1792x1024、1024x1792
          style: vivid           # 风格:vivid(生动)或 natural(自然)

6.3 完整代码

java 复制代码
package com.example.springaidemo.multimodal;

import org.springframework.ai.image.*;
import org.springframework.ai.openai.api.OpenAiImageApi;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 文生图服务
 *
 * 基于 OpenAI DALL-E 3 模型。
 * 输入一段文字描述,输出一张 AI 生成的图片。
 * 支持自定义尺寸、质量和风格。
 */
@Service
public class ImageGenerationService {

    private final ImageModel imageModel;

    public ImageGenerationService(ImageModel imageModel) {
        this.imageModel = imageModel;
    }

    /**
     * 根据文字描述生成图片
     *
     * @param promptText 文字描述,如"一只戴着墨镜的猫在海滩上"
     * @param size       图片尺寸,如 "1024x1024"、"1792x1024"
     * @param quality    图片质量:"standard" 或 "hd"
     * @return 生成的图片信息(包含 URL)
     */
    public ImageGenerationResult generate(String promptText,
                                          String size, String quality) {
        // 构建图片生成选项
        var options = ImageOptionsBuilder.builder()
                .model("dall-e-3")
                .width(Integer.parseInt(size.split("x")[0]))   // 宽度
                .height(Integer.parseInt(size.split("x")[1]))  // 高度
                .quality(quality != null ? quality : "standard")
                .n(1)  // 生成 1 张图片(DALL-E 3 仅支持 1)
                .build();

        // 发起文生图请求
        var prompt = new ImagePrompt(promptText, options);
        ImageResponse response = imageModel.call(prompt);

        // 提取结果
        ImageGeneration generation = response.getResult();
        var imageInfo = generation.getOutput();

        return new ImageGenerationResult(
                imageInfo.getUrl(),                      // 图片 URL
                imageInfo.getB64Json() != null           // 是否返回了 Base64
        );
    }

    /**
     * 批量生成图片(不同风格的同一主题)
     */
    public List<ImageGenerationResult> generateVariants(String promptText) {
        // 生成三个不同风格的变体
        var styles = List.of(
                "照片级真实风格,专业摄影",
                "水彩画风格,柔和色调",
                "赛博朋克风格,霓虹灯光"
        );

        return styles.stream()
                .map(style -> generate(promptText + "," + style, "1024x1024", "hd"))
                .toList();
    }

    /** 图片生成结果封装 */
    public record ImageGenerationResult(String url, boolean isBase64) {}
}
java 复制代码
/**
 * 文生图 REST 接口
 */
@RestController
@RequestMapping("/api/image")
public class ImageGenerationController {

    private final ImageGenerationService imageService;

    public ImageGenerationController(ImageGenerationService imageService) {
        this.imageService = imageService;
    }

    /**
     * 文生图接口
     *
     * curl 示例:
     * curl -X POST "http://localhost:8080/api/image/generate" \
     *   -H "Content-Type: application/json" \
     *   -d '{"prompt":"一只戴着墨镜的猫在海滩上,日落光线","size":"1024x1024"}'
     */
    @PostMapping("/generate")
    public Map<String, Object> generate(@RequestBody ImageRequest request) {
        var result = imageService.generate(
                request.prompt(),
                request.size() != null ? request.size() : "1024x1024",
                request.quality()
        );
        return Map.of("url", result.url(), "isBase64", result.isBase64());
    }

    /** 请求参数 */
    public record ImageRequest(
            String prompt,
            String size,     // "1024x1024"、"1792x1024"、"1024x1792"
            String quality   // "standard" 或 "hd"
    ) {}
}

生产提示:DALL-E 3 的调用比较慢(10-30 秒),建议异步处理。前端提交请求后轮询结果,或者用 WebSocket 推送完成通知。另外 DALL-E 3 对 Prompt 中的内容有安全过滤,某些描述可能被拒绝生成。


七、实战:多模态智能助手

7.1 设计思路

把四种能力整合到一个 Controller 中,用户可以:

  1. 上传图片 -> AI 看图回答问题

  2. 上传语音 -> AI 转文字后回答

  3. 提问 -> AI 用语音回答(TTS)

  4. 描述场景 -> AI 生成对应图片

    用户输入(文字/图片/语音)
    |
    v
    MultimodalAssistantController
    - /chat+image -> Vision + Chat
    - /chat+voice -> ASR + Chat
    - /chat/speak -> Chat + TTS
    - /generate -> Text to Image

7.2 完整代码

java 复制代码
package com.example.springaidemo.multimodal;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.SpeechModel;
import org.springframework.ai.chat.model.TranscriptionModel;
import org.springframework.ai.image.ImageModel;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.Map;

/**
 * 多模态智能助手 Controller
 *
 * 整合 Vision、ASR、TTS、文生图四大能力。
 * 一个入口处理多种输入模态,返回对应格式的响应。
 */
@RestController
@RequestMapping("/api/assistant")
public class MultimodalAssistantController {

    private final ChatClient chatClient;
    private final TranscriptionModel transcriptionModel;
    private final SpeechModel speechModel;
    private final ImageModel imageModel;

    public MultimodalAssistantController(
            ChatClient chatClient,
            TranscriptionModel transcriptionModel,
            SpeechModel speechModel,
            ImageModel imageModel) {
        this.chatClient = chatClient;
        this.transcriptionModel = transcriptionModel;
        this.speechModel = speechModel;
        this.imageModel = imageModel;
    }

    /**
     * 场景一:看图回答
     * 用户上传图片 + 文字问题,AI 看图后用文字回答
     */
    @PostMapping(value = "/vision-chat",
                 consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public Map<String, String> visionChat(
            @RequestParam("image") MultipartFile image,
            @RequestParam("question") String question) {

        // 通过 ChatClient 发送多模态消息
        String answer = chatClient.prompt()
                .user(u -> u
                        .text(question)
                        .media(org.springframework.util.MimeType.valueOf(
                                image.getContentType()), image.getBytes())
                )
                .call()
                .content();

        return Map.of("answer", answer);
    }

    /**
     * 场景二:语音提问 + 文字回答
     * 用户发语音消息,系统先 ASR 转文字,再调用 AI 回答
     */
    @PostMapping(value = "/voice-chat",
                 consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public Map<String, String> voiceChat(
            @RequestParam("audio") MultipartFile audio) {

        // Step 1:语音转文字(ASR)
        var audioFile = new org.springframework.ai.model.Media(
                org.springframework.util.MimeType.valueOf(audio.getContentType()),
                audio.getBytes()
        );
        var transcription = transcriptionModel.call(
                new org.springframework.ai.audio.transcription.AudioTranscriptionPrompt(audioFile)
        );
        String userText = transcription.getResult().getOutput();

        // Step 2:AI 回答
        String answer = chatClient.prompt()
                .user(userText)
                .call()
                .content();

        return Map.of("transcribedText", userText, "answer", answer);
    }

    /**
     * 场景三:文字提问 + 语音回答
     * 用户文字提问,AI 回答后用 TTS 转为语音
     */
    @GetMapping("/speak-answer")
    public ResponseEntity<byte[]> speakAnswer(
            @RequestParam String question) {

        // Step 1:AI 文字回答
        String answer = chatClient.prompt()
                .user(question)
                .call()
                .content();

        // Step 2:文字转语音(TTS)
        var speechPrompt = new org.springframework.ai.chat.model.SpeechPrompt(answer);
        byte[] audio = speechModel.call(speechPrompt).getResult().getOutput();

        // 返回音频流
        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType("audio/mpeg"))
                .header(HttpHeaders.CONTENT_DISPOSITION,
                        "attachment; filename=\"answer.mp3\"")
                .body(audio);
    }

    /**
     * 场景四:文生图
     * 用户描述场景,AI 生成对应图片
     */
    @PostMapping("/generate-image")
    public Map<String, Object> generateImage(
            @RequestBody Map<String, String> request) {
        String prompt = request.get("prompt");
        var imagePrompt = new ImagePrompt(prompt);
        ImageResponse response = imageModel.call(imagePrompt);

        String imageUrl = response.getResult().getOutput().getUrl();
        return Map.of("prompt", prompt, "imageUrl", imageUrl);
    }
}

7.3 测试

bash 复制代码
# 场景一:看图回答
curl -X POST "http://localhost:8080/api/assistant/vision-chat" \
  -F "image=@product.jpg" \
  -F "question=描述这个产品的外观特征和可能的用途"

# 场景二:语音提问
curl -X POST "http://localhost:8080/api/assistant/voice-chat" \
  -F "audio=@question.mp3"
# 返回:{"transcribedText":"我的订单什么时候发货?","answer":"您的订单..."}

# 场景三:语音回答
curl "http://localhost:8080/api/assistant/speak-answer?question=什么是SpringAI" \
  --output answer.mp3

# 场景四:文生图
curl -X POST "http://localhost:8080/api/assistant/generate-image" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"一只柴犬在樱花树下微笑"}'

八、踩坑总结

在多模态开发中,我踩过不少坑,总结如下:

问题 原因 解决方案
Vision 返回乱码或拒绝 图片格式不支持或图片过大 转为 JPEG/PNG,压缩到 20MB 以内
Whisper 中文识别不准 未指定 language 参数 配置 language: zh,准确率提升明显
TTS 音频为空 text 超过模型长度限制(4096 字符) 分段生成再拼接
DALL-E 请求被拒 Prompt 包含敏感内容 前端做内容预过滤,添加异常处理
大文件上传超时 音频/视频文件过大 增大 spring.servlet.multipart.max-file-size
Media URL 不可访问 私有云存储的 URL 需要签名 使用预签名 URL 或直接传 byte\[\]
多模态调用链路长,整体延迟高 ASR + Chat + TTS 串行调用 对非依赖步骤做并行处理
不同模型能力差异大 开源模型的 Vision 能力远弱于 GPT-4o 根据场景选模型,不要一刀切

一句话总结:多模态能力的价值在于拓展 AI 的输入输出边界------从纯文字到"能看、能听、能说、能画"。Spring AI 统一了这四种能力的编程模型,开发者只需要关注业务逻辑,不用操心底层模型适配。


Spring AI 实战 -- 第13章:完整内容与源码

相关推荐
Navicat中国1 小时前
Navicat 18 内置 AI 助手让数据库开发从繁琐操作迈向智能协作
大数据·人工智能·数据库开发
4SAPI1 小时前
多模型请求路由怎么设计?成本、速度和能力如何平衡
人工智能·python·gateway·php
魔乐社区2 小时前
翼芯・智算测试与适配优化专区,让国产算力“选得准、跑得通、用得好“
人工智能·开源·大模型
Java爱好狂.3 小时前
Java初学者如何一周速通SpringBoot!
java·spring boot·后端·spring·高并发·架构师·java面试
XCDoris3 小时前
郴州logo设计机构
大数据·人工智能·python
Goboy4 小时前
一个人带三个Agent,我用全新TRAE从0到1搭了个资讯周刊站
人工智能·trae
俊哥V4 小时前
每日 AI 研究简报 · 2026-10-19
人工智能·ai
AI_AGENT_DEV_AI4 小时前
AI英语教培智能体的开发
人工智能
物联网软硬件开发-轨物科技4 小时前
【技术白皮书】光伏清检一体化机器人系统解决方案技术白皮书
大数据·运维·网络·人工智能·机器人
海宇数据4 小时前
零信任架构实战:基于海宇网上踪迹欺诈行为识别构建自动化信贷资信评估网关
运维·人工智能·架构·自动化