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章:完整内容与源码

相关推荐
在水一缸1 小时前
苹果AI国行版过审背后的技术架构深度解析:端侧模型与私有云计算的融合实践
人工智能·架构·云计算·技术架构·苹果ai·端侧模型·私有云计算
乔氪智造1 小时前
我给 Agent 写的循环,一半是护栏
人工智能
武子康1 小时前
Shippy 全拆:3 个集合收缩(动作 / 状态 / 后果)+ 4 类边界(版本化行为 / Typed API / CLI / Sandbox)
人工智能·agent·aiops
QXWZ_IA2 小时前
桥梁数字孪生怎么落地?
人工智能·科技·算法·智能硬件·政务
Kel2 小时前
输出层与反分词(Output Layer & Detokenization)
人工智能·算法·架构
Nturmoils2 小时前
三伏天正是减肥天:我在 EdgeOne Makers 上从 0 到 1 上线了一个 AI 健康教练
人工智能
猿的天空2 小时前
机器人双手迎来全栈训练系统:灵初智能EgoSteer让灵巧手无所不能
网络·人工智能·计算机·ai·程序员·机器人·编程
刘海东刘海东2 小时前
人工智能图论(提纲)
人工智能
陕西企来客2 小时前
2026年7月技术好GEO优化方案:算法适配与内容策略
人工智能·算法·机器学习·技术好geo优化