Spring AI Alibaba 多模态全家桶:图片理解、图片生成与语音合成实战

Spring AI Alibaba 多模态全家桶:图片理解、图片生成与语音合成实战

本文围绕 Spring AI Alibaba 的三大多模态能力------图片理解(Vision)、图片生成(通义万象)和语音合成(CosyVoice),从项目搭建到完整业务实战,覆盖发票 OCR、商品鉴定、电商配图生成、风格化出图、文字转语音、AI 语音对话和定时语音播报七个真实场景。面向有 Spring Boot 基础、正在用 Spring AI 做大模型应用开发的后端工程师。


一、为什么选 Spring AI Alibaba

Spring AI 本身提供了统一的大模型抽象层(ChatClientImageModel 等),切换模型供应商时业务代码零改动。但原生 Spring AI 在国内多模态能力上的覆盖是有限的------图片生成、语音合成、文字识别这些能力需要各厂商自己去做扩展。

阿里巴巴基于通义千问全家桶推出了 Spring AI Alibaba,底层对接的是灵积(DashScope)大模型服务平台。它的优势在于:

  1. 多模态能力齐全:文本对话、图片理解、图片生成、语音合成、Embedding 一站式覆盖
  2. 国内合规性好:API 服务节点在国内,不存在访问障碍
  3. 与 Spring AI 抽象层完全兼容:上层代码不需要为供应商做任何适配

二、项目搭建与依赖配置

2.1 Maven 依赖

核心依赖就三块:Spring AI Alibaba BOM 统一管理版本,DashScope Starter 引入对话 + 图片 + Embedding 能力,DashScope SDK 引入语音合成能力(Spring AI Alibaba 目前没有封装 TTS 标准接口,需要额外引入原生 SDK)。

xml 复制代码
<properties>
    <java.version>21</java.version>
    <spring-ai-alibaba.version>1.1.2.0</spring-ai-alibaba.version>
</properties>

<dependencyManagement>
    <dependencies>
        <!-- BOM 统一管理通义相关依赖版本,后续引用不需要写 version -->
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-bom</artifactId>
            <version>${spring-ai-alibaba.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <!-- 通义千问对话 + 图片生成 + Embedding -->
    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
        <version>1.1.2.0</version>
    </dependency>

    <!-- 语音合成需要原生 DashScope SDK -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>dashscope-sdk-java</artifactId>
        <version>2.22.4</version>
    </dependency>
</dependencies>

2.2 application.yml

yaml 复制代码
spring:
  ai:
    dashscope:
      api-key: sk-xxxxxxxxxxxx   # 替换为你的 API Key,生产环境务必用环境变量
      chat:
        options:
          model: qwen3-max       # 默认文本模型
          temperature: 0.7
          max-tokens: 2048
server:
  port: 8083

API Key 从阿里云灵积控制台的密钥管理页面创建,新用户有免费额度。生产环境一定要通过环境变量或配置中心注入,不要硬编码在代码里。


三、图片理解------让 AI 看懂图片

图片理解的核心是通义千问的视觉模型(qwen-vl-max)。普通文本模型(qwen-max / qwen-plus)不支持图片输入,这是第一个需要注意的点。

3.1 核心思路

整个流程分三步:

  1. 构建 Media 对象 :把图片(URL 或文件)封装成 Media
  2. 组装 UserMessage :将 Media 和文字提问一起放入 UserMessage
  3. 指定视觉模型参数withModel("qwen-vl-max") + withMultiModel(true)

其中 withMultiModel(true)必须设置的------它控制请求发送到多模态端点。如果不设置,请求会打到文本端点,而文本端点不认识图片数据,直接报错。

3.2 传 URL 分析图片

最基础的用法:传一个图片 URL,让 AI 描述内容。

java 复制代码
@RestController
@RequestMapping("/api/vision")
public class VisionController {

    private final ChatClient chatClient;

    public VisionController(DashScopeChatModel dashScopeChatModel) {
        this.chatClient = ChatClient.builder(dashScopeChatModel).build();
    }

    // 视觉模型参数------所有视觉接口共用
    private static final DashScopeChatOptions VL_OPTIONS = DashScopeChatOptions.builder()
            .withModel("qwen-vl-max")
            .withMultiModel(true)
            .build();

    @GetMapping("/analyze-url")
    public String analyzeImageUrl(
            @RequestParam String imageUrl,
            @RequestParam(defaultValue = "请描述这张图片的内容") String question) {

        // 1. 封装图片为 Media
        Media media = Media.builder()
                .mimeType(MimeTypeUtils.IMAGE_JPEG)
                .data(URI.create(imageUrl))
                .build();

        // 2. 构建 UserMessage(文字 + 图片一起传)
        UserMessage message = UserMessage.builder()
                .text(question)
                .media(media)
                .build();

        // 3. 调用,指定视觉模型参数
        return chatClient.prompt()
                .messages(message)
                .options(VL_OPTIONS)
                .call()
                .content();
    }
}

如果是上传文件 ,只需要把 Media 的数据源从 URI 换成 MultipartFile.getResource()

java 复制代码
MimeType mimeType = MimeType.valueOf(
        imageFile.getContentType() != null ? imageFile.getContentType() : "image/jpeg");

Media media = Media.builder()
        .mimeType(mimeType)
        .data(imageFile.getResource())   // 文件上传用 getResource()
        .build();

多张图片对比 也是同样的套路------遍历文件列表,构建 List<Media>,然后传入 UserMessage.builder().media(mediaList)

3.3 实战:发票信息结构化提取

理解了基础用法后,来看一个真实业务场景:上传发票图片,自动提取发票号码、日期、金额等结构化数据。

关键点在于结合 System Prompt 规范输出格式,再用 .entity(InvoiceInfo.class) 直接映射到 Java 对象:

java 复制代码
@RestController
@RequestMapping("/api/invoice")
public class InvoiceController {

    private final ChatClient chatClient;

    public InvoiceController(DashScopeChatModel dashScopeChatModel) {
        this.chatClient = ChatClient.builder(dashScopeChatModel)
                .defaultSystem("""
                        你是一个发票信息提取助手。
                        精确提取发票上的信息,不要猜测,看不清楚的字段返回 null。
                        金额统一用数字表示,不要带"元"或"¥"符号。
                        """)
                .build();
    }

    record InvoiceInfo(
            @JsonPropertyDescription("发票号码") String invoiceNumber,
            @JsonPropertyDescription("开票日期,格式 yyyy-MM-dd") String invoiceDate,
            @JsonPropertyDescription("销售方名称(卖家)") String sellerName,
            @JsonPropertyDescription("购买方名称(买家)") String buyerName,
            @JsonPropertyDescription("价税合计(含税总金额),纯数字") Double totalAmount,
            @JsonPropertyDescription("货物或服务名称") String items
    ) {}

    @PostMapping("/extract")
    public InvoiceInfo extractInvoice(@RequestParam("file") MultipartFile file) throws Exception {
        MimeType mimeType = MimeType.valueOf(
                file.getContentType() != null ? file.getContentType() : "image/jpeg");

        Media media = Media.builder().mimeType(mimeType).data(file.getResource()).build();
        UserMessage message = UserMessage.builder().text("请提取这张发票上的所有信息").media(media).build();

        return chatClient.prompt()
                .messages(message)
                .options(DashScopeChatOptions.builder()
                        .withModel("qwen-vl-max")
                        .withMultiModel(true)
                        .build())
                .call()
                .entity(InvoiceInfo.class);   // 直接映射为结构化对象
    }
}

这里的 .entity() 非常实用------Spring AI 会让大模型按照 InvoiceInfo 的字段定义输出 JSON,然后自动反序列化。省去了手动解析的麻烦,拿到的就是可以直接入库的结构化数据。

同理,二手商品鉴定 也是类似模式:设定 System Prompt 为"二手商品鉴定专家",定义 ProductAnalysis Record 包含商品类别、品牌、状态、瑕疵、建议定价等字段,上传商品图片即可自动分析。

3.4 图片理解四条铁律

序号 要点 说明
1 Media 封装图片 不管是 URL 还是文件,都必须通过 Media.builder() 构建
2 放入 UserMessage 传参 文字和图片在同一个 UserMessage
3 指定视觉模型 必须用 qwen-vl-max,且 withMultiModel(true)
4 结合 .entity() 输出结构化数据 利用 Spring AI 的自动映射能力,直接拿到 Java 对象

四、图片生成------通义万象接入实战

生成图片用的是阿里的通义万象模型(wanx2.1-t2i-plus) ,Spring AI 抽象为 ImageModel 接口。

4.1 异步轮询机制

图片生成和文本对话有一个本质区别:它是异步的。API 提交生成任务后不会立即返回图片,而是在后台轮询任务状态。Spring AI Alibaba 内部已经封装了这个轮询逻辑,但日志中会出现 retry 相关的 WARN------这是正常行为。

如果默认的轮询策略不满足需求,可以在 application.yml 中调整 retry 参数,让轮询间隔更均匀。图片生成通常需要 15~30 秒。

4.2 基础生成

最简单的方式:传一段描述文字,拿回图片 URL。

java 复制代码
@RestController
@RequestMapping("/api/image")
public class ImageGenerationController {

    private final ImageModel imageModel;

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

    @GetMapping("/generate")
    public String generateImage(@RequestParam String description) {
        ImageResponse response = imageModel.call(new ImagePrompt(description));
        return response.getResult().getOutput().getUrl();
    }
}

调用 GET /api/image/generate?description=一只可爱的橘猫坐在窗台上,等待约 20 秒,返回一个 CDN 图片链接。

4.3 自定义参数:尺寸与数量

实际业务中通常需要指定图片尺寸和生成数量,通过 DashScopeImageOptions 控制:

java 复制代码
@GetMapping("/generate-custom")
public List<String> generateCustomImage(
        @RequestParam String description,
        @RequestParam(defaultValue = "1") int count,
        @RequestParam(defaultValue = "1024") int width,
        @RequestParam(defaultValue = "1024") int height) {

    ImageResponse response = imageModel.call(
            new ImagePrompt(description,
                    DashScopeImageOptions.builder()
                            .withModel("wanx2.1-t2i-plus")
                            .withN(count)          // 生成几张
                            .withWidth(width)      // 宽度
                            .withHeight(height)    // 高度
                            .build()));

    return response.getResults().stream()
            .map(result -> result.getOutput().getUrl())
            .collect(Collectors.toList());
}

4.4 实战:文本优化 + 图片生成的两步法

直接把中文描述扔给图片生成模型,效果往往不好。更优的做法是两步走

  1. 第一步:用 ChatClient 把商品信息转成专业的英文绘画 Prompt
  2. 第二步:用优化后的 Prompt 调用 ImageModel 生成图片
java 复制代码
@RestController
@RequestMapping("/api/product-image")
public class ProductImageController {

    private final ChatClient chatClient;
    private final ImageModel imageModel;

    public ProductImageController(DashScopeChatModel dashScopeChatModel,
                                  ImageModel imageModel) {
        this.chatClient = ChatClient.builder(dashScopeChatModel).build();
        this.imageModel = imageModel;
    }

    record GenerateRequest(String productName, String productDescription, String style) {}
    record GenerateResult(String imagePrompt, String imageUrl) {}

    @PostMapping("/generate")
    public GenerateResult generateProductImage(@RequestBody GenerateRequest request) {
        // 第一步:商品信息 → 专业英文绘画 Prompt
        String imagePrompt = chatClient.prompt()
                .system("""
                        你是一个专业的 AI 绘画 prompt 工程师。
                        根据商品信息,生成一段用于 AI 图片生成的英文 prompt。
                        要求:用英文写,50-100 词,包含商品外观特征、背景、光线、风格,
                        商业摄影风格,适合电商展示。只输出 prompt 文本,不要其他内容。
                        """)
                .user(String.format("商品名称:%s\n商品描述:%s\n风格要求:%s",
                        request.productName(), request.productDescription(), request.style()))
                .call()
                .content();

        // 第二步:用优化后的 Prompt 生成图片
        ImageResponse imageResponse = imageModel.call(
                new ImagePrompt(imagePrompt,
                        DashScopeImageOptions.builder()
                                .withModel("wanx2.1-t2i-plus")
                                .withN(1).withWidth(1024).withHeight(1024)
                                .build()));

        return new GenerateResult(imagePrompt, imageResponse.getResult().getOutput().getUrl());
    }
}

这个"两步法"在电商场景中非常实用------输入"蓝牙耳机 / 入耳式深蓝色金属质感 / 写实",AI 先生成一段专业的英文摄影描述 Prompt,再用这段 Prompt 去生图,出来的效果远好于直接用中文描述。

4.5 风格化生图

预定义一组常用风格关键词,请求时传风格名就能切换不同出图风格:

java 复制代码
private static final Map<String, String> STYLE_KEYWORDS = Map.of(
        "写实摄影", "professional photography, realistic, high resolution, 8k",
        "扁平插画", "flat design, minimalist illustration, vector style",
        "油画风格", "oil painting style, artistic, textured brushstrokes",
        "水彩风格", "watercolor painting, soft colors, artistic",
        "3D渲染",  "3D rendering, CGI, photorealistic, studio lighting",
        "日系动漫", "anime style, Japanese illustration, clean lines",
        "商务简约", "clean corporate style, white background, professional"
);

调用时只需要把风格关键词拼接到描述后面:

java 复制代码
String fullPrompt = description + ", " + STYLE_KEYWORDS.getOrDefault(style, "");

GET /api/styled-image/generate?description=一杯咖啡&style=水彩风格 就能拿到一张水彩风格的咖啡图。


五、语音合成------CosyVoice 接入实战

Spring AI Alibaba 目前没有封装标准的 TTS 接口,语音合成需要直接使用阿里 DashScope 原生 SDK。核心类只有两个:

  • SpeechSynthesisParam:封装模型名、音色、语速等参数
  • SpeechSynthesizer:发起合成调用,返回音频字节流

当前最新模型是 cosyvoice-v3-flash ,支持多种音色(如 longanyang 阳光大男孩、longxiaocheng 成熟男声等),完整音色列表可在阿里云文档查看。

5.1 基础文字转语音

java 复制代码
@RestController
@RequestMapping("/api/tts")
public class TextToSpeechController {

    @Value("${spring.ai.dashscope.api-key}")
    private String apiKey;

    @GetMapping(value = "/synthesize", produces = "audio/mpeg")
    public ResponseEntity<byte[]> synthesize(@RequestParam String text) throws Exception {
        // 语音合成走 WebSocket 长连接
        Constants.baseWebsocketApiUrl = "wss://dashscope.aliyuncs.com/api-ws/v1/inference";

        SpeechSynthesisParam param = SpeechSynthesisParam.builder()
                .apiKey(apiKey)
                .model("cosyvoice-v3-flash")
                .voice("longanyang")         // 音色
                .build();

        SpeechSynthesizer synthesizer = new SpeechSynthesizer(param, null);
        ByteBuffer audio = synthesizer.call(text);
        synthesizer.getDuplexApi().close(1000, "bye");

        byte[] audioBytes = audio.array();
        Files.write(Path.of("speech.mp3"), audioBytes);  // 同时保存到本地

        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType("audio/mpeg"))
                .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"speech.mp3\"")
                .body(audioBytes);
    }
}

注意 Constants.baseWebsocketApiUrl 必须设置------语音合成通过 WebSocket 长连接完成,默认 HTTP 地址是不对的。

自定义参数(音色 + 语速)也很简单,speechRate 取值范围 0.5(慢)到 2.0(快),1.0 为正常语速:

java 复制代码
SpeechSynthesisParam param = SpeechSynthesisParam.builder()
        .apiKey(apiKey)
        .model("cosyvoice-v3-flash")
        .voice(voice)
        .speechRate(speed)
        .build();

5.2 实战:AI 文字对话 + 语音回答

一个非常实用的组合场景:用户用文字提问,AI 先生成文字回答,再转成语音返回。和前面图片生成的"两步法"思路完全一样。

java 复制代码
@RestController
@RequestMapping("/api/voice-chat")
public class VoiceChatController {

    private final ChatClient chatClient;

    @Value("${spring.ai.dashscope.api-key}")
    private String apiKey;

    public VoiceChatController(DashScopeChatModel dashScopeChatModel) {
        this.chatClient = ChatClient.builder(dashScopeChatModel)
                .defaultSystem("""
                        你是一个语音助手,回答会被转成语音播放。
                        因此:
                        - 回答要口语化,避免用 Markdown 格式
                        - 不要输出代码块、列表符号(*、-)等
                        - 句子要自然流畅,适合朗读
                        - 回答控制在 100 字以内
                        """)
                .build();
    }

    @GetMapping(value = "/ask", produces = "audio/mpeg")
    public ResponseEntity<byte[]> askWithVoice(
            @RequestParam String question,
            @RequestParam(defaultValue = "longanyang") String voice) throws Exception {

        // 第一步:获取文字回答
        String textAnswer = chatClient.prompt().user(question).call().content();

        // 第二步:文字转语音
        Constants.baseWebsocketApiUrl = "wss://dashscope.aliyuncs.com/api-ws/v1/inference";
        SpeechSynthesisParam param = SpeechSynthesisParam.builder()
                .apiKey(apiKey)
                .model("cosyvoice-v3-flash")
                .voice(voice)
                .build();
        SpeechSynthesizer synthesizer = new SpeechSynthesizer(param, null);
        ByteBuffer audio = synthesizer.call(textAnswer);
        synthesizer.getDuplexApi().close(1000, "bye");

        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType("audio/mpeg"))
                .body(audio.array());
    }
}

这里 System Prompt 的设计很关键------必须明确告诉 AI "回答会被转成语音",否则它可能输出 Markdown 格式、代码块或列表符号,这些转成语音后听起来会很奇怪。

5.3 实战:定时语音播报

结合 Spring 的 @Scheduled 做定时任务,每天早上 8 点自动生成早间播报音频:

java 复制代码
@Service
public class DailyBroadcastService {

    private final ChatClient chatClient;

    @Value("${spring.ai.dashscope.api-key}")
    private String apiKey;

    public DailyBroadcastService(DashScopeChatModel dashScopeChatModel) {
        this.chatClient = ChatClient.builder(dashScopeChatModel).build();
    }

    @Scheduled(cron = "0 0 8 * * ?")
    public void generateDailyBroadcast() throws Exception {
        // 第一步:生成播报文案
        String script = chatClient.prompt()
                .system("你是一个播音员,生成简洁的早间播报文案,不要用 Markdown 格式")
                .user("今天是 " + LocalDate.now().format(
                        DateTimeFormatter.ofPattern("yyyy年M月d日")) +
                      ",请生成一段 30 秒的早间播报,包括问候语和今日关键提示")
                .call()
                .content();

        // 第二步:文案转语音
        Constants.baseWebsocketApiUrl = "wss://dashscope.aliyuncs.com/api-ws/v1/inference";
        SpeechSynthesisParam param = SpeechSynthesisParam.builder()
                .apiKey(apiKey)
                .model("cosyvoice-v3-flash")
                .voice("longxiaocheng")   // 成熟男声,适合播报
                .speechRate(0.9f)         // 略慢,播报感
                .build();

        SpeechSynthesizer synthesizer = new SpeechSynthesizer(param, null);
        ByteBuffer audio = synthesizer.call(script);
        synthesizer.getDuplexApi().close(1000, "bye");

        String filename = "broadcast-" + LocalDate.now() + ".mp3";
        Files.write(Path.of(filename), audio.array());
    }
}

使用 @Scheduled 时别忘了在启动类上加 @EnableScheduling 注解。


六、多场景模型切换:Config 统一管理

在实际项目中,不同业务场景对模型的需求不同:客服场景用便宜快速的 qwen-plus(temperature 低,回答克制),内容创作用旗舰的 qwen-max(temperature 高,发散创意),数据分析用快速的 qwen-turbo(temperature 最低,精确严谨)。

推荐做法是在 @Configuration 中注册多个具名 ChatClient Bean:

java 复制代码
@Configuration
public class QwenChatClientConfig {

    @Bean("customerServiceChatClient")
    public ChatClient customerServiceChatClient(DashScopeChatModel dashScopeChatModel) {
        return ChatClient.builder(dashScopeChatModel)
                .defaultSystem("你是一个专业、耐心的电商客服助手。只回答产品、订单相关问题。回答简洁,不超过 200 字。")
                .defaultOptions(DashScopeChatOptions.builder()
                        .withModel("qwen-plus")
                        .withTemperature(0.3)
                        .build())
                .build();
    }

    @Bean("contentChatClient")
    public ChatClient contentChatClient(DashScopeChatModel dashScopeChatModel) {
        return ChatClient.builder(dashScopeChatModel)
                .defaultSystem("你是一个资深文案策划,擅长撰写吸引人的营销文案。")
                .defaultOptions(DashScopeChatOptions.builder()
                        .withModel("qwen-max")
                        .withTemperature(0.9)
                        .build())
                .build();
    }

    @Bean("analysisChatClient")
    public ChatClient analysisChatClient(DashScopeChatModel dashScopeChatModel) {
        return ChatClient.builder(dashScopeChatModel)
                .defaultSystem("你是一个数据分析师,擅长解读数据并给出业务洞察。")
                .defaultOptions(DashScopeChatOptions.builder()
                        .withModel("qwen-turbo")
                        .withTemperature(0.1)
                        .build())
                .build();
    }
}

使用时通过 @Qualifier 注入对应场景的 Client:

java 复制代码
public SomeController(@Qualifier("customerServiceChatClient") ChatClient chatClient) {
    this.chatClient = chatClient;
}

这种方式的好处是业务代码完全不感知模型差异,切换模型只需要改 Config。


七、总结

本文覆盖了 Spring AI Alibaba 三大多模态能力的完整接入路径:

能力 核心类 模型 关键配置
图片理解 ChatClient + Media + UserMessage qwen-vl-max withMultiModel(true) 必须开启
图片生成 ImageModel + ImagePrompt wanx2.1-t2i-plus 异步轮询,注意 retry 配置
语音合成 SpeechSynthesizer + SpeechSynthesisParam cosyvoice-v3-flash 需额外引入原生 SDK,走 WebSocket

Spring AI 抽象层的价值在这里体现得非常明显 :不管底层是通义千问、GPT-4 还是 Gemini,上层的 ChatClientImageModel 接口不变,切换供应商只改配置,业务代码零改动。


本系列后续内容 :下一篇将探讨多模型切换与异步并发------如何在同一个应用中同时对接多个模型供应商,以及如何利用 Spring AI 的异步能力提升多模态场景下的吞吐量。

相关推荐
码喽7号2 小时前
Springboot学习五:MybatisPlus的快速上手
spring boot·学习·spring
Knight_AL2 小时前
为什么要用 ApplicationReadyEvent 来初始化 RabbitTemplate 回调?
spring boot
CoderJia程序员甲2 小时前
GitHub 热榜项目 - 日榜(2026-03-16)
人工智能·ai·大模型·github·ai教程
熙胤3 小时前
springboot与springcloud对应版本
java·spring boot·spring cloud
J2虾虾3 小时前
SpringBoot 中给 @Autowired 搭配 @Lazy
java·spring boot·后端
智_永无止境4 小时前
Spring Boot 动态多数据源:核心思路与关键考量
spring boot
sheji34164 小时前
【开题答辩全过程】以 基于springboot的健身预约系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
Bruce_Liuxiaowei4 小时前
深入浅出:清理 OpenClaw 会话记录的完整操作解析
人工智能·大模型·智能体·openclaw
没有bug.的程序员4 小时前
500个微服务上云全线假死:Spring Boot 3.2 自动配置底层的生死狙击
java·spring boot·微服务·kubernetes·自动配置