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 中,用户可以:
-
上传图片 -> AI 看图回答问题
-
上传语音 -> AI 转文字后回答
-
提问 -> AI 用语音回答(TTS)
-
描述场景 -> 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 统一了这四种能力的编程模型,开发者只需要关注业务逻辑,不用操心底层模型适配。
