大模型通义千问3-VL-Plus - 视觉理解

一、概论

Qwen3系列视觉理解模型,实现思考模式和非思考模式的有效融合,视觉智能体能力在OS World等公开测试集上达到世界顶尖水平。此版本在视觉coding、空间感知、多模态思考等方向全面升级;视觉感知与识别能力大幅提升,支持超长视频理解。

简单来说,阿里 Qwen3 系列视觉理解模型核心亮点如下:

  1. 模式灵活:能同时用 "思考模式"(解复杂问题,比如数学推理)和 "非思考模式"(快速响应,比如简单识图),不用单独切换模型;
  2. 会 "动手":能操作电脑 / 手机界面(比如点按钮、填表单),在 OS World 这类权威测试里拿了世界顶尖成绩;
  3. 能力升级:视觉 coding(图片转网页代码)、空间感知(判断物体位置 / 3D 关系)、多模态思考(结合图文推理)都比之前强;
  4. 看得更准:能认的物体更多,从日常东西到专业领域(比如动植物、产品型号)都能识别;
  5. 能 "追长视频":原生支持看几小时长视频,还能精准找到里面的细节(比如某句话在几分几秒)。

二、准备

如果之前已经有申请过 API-key 的同学,我们只需要复制一下视觉模型的model就可以了

如果没有的同学,可以看我这篇文章:Vue3+Springboot3+千问plus流式(前后端分离)_博客 springboot+vue-CSDN博客

完成第一步的 API 获取,然后将 key 设置到环境变量或者配置文件中就可以啦。

三、代码实现

1. 依赖

后续设计到电脑操作的依赖我们再另外添加,下面这些是一些基本的依赖,其中最重要的是SDK的引入,方便我们使用。

java 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.9-SNAPSHOT</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>gzj.spring</groupId>
    <artifactId>ai</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>ai</name>
    <description>ai</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>17</java.version>
        <java.json>2.0.32</java.json>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Spring AI Alibaba Agent Framework -->
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-agent-framework</artifactId>
            <version>1.1.0.0-M5</version>
        </dependency>

        <!-- DashScope ChatModel 支持(如果使用其他模型,请参考文档选择对应的 starter) -->
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
            <version>1.1.0.0-M5</version>
        </dependency>

        <!-- 新增Redis依赖(Spring Boot整合版) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- 可选:Redis连接池(提升性能) -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dashscope-sdk-java</artifactId>
            <version>2.22.2</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${java.json}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-model</artifactId>
            <version>1.1.0-M4</version>
        </dependency>

        <!-- RxJava(流式调用) -->
        <dependency>
            <groupId>io.reactivex.rxjava3</groupId>
            <artifactId>rxjava</artifactId>
            <version>3.1.8</version>
        </dependency>

        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.11.0</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
    <repositories>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <releases>
                <enabled>false</enabled>
            </releases>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <releases>
                <enabled>false</enabled>
            </releases>
        </pluginRepository>
    </pluginRepositories>

</project>

2. 配置文件(application.yml)

java 复制代码
server:
  port: 8080
spring:
  servlet:
    multipart:
      enabled: true
      max-file-size: 100MB
      max-request-size: 100MB
  application:
    name: ai
  ai:
    dashscope:
      model: qwen-plus
      modelV1: qwen3-vl-plus
      api-key: ${DASHSCOPE_API_KEY}
      agent:
        options:
          app-id: 0bc97a579fdb4c2880acc

其中 ,max-file-size 设置的是当个文件上传的最大值,max-request-size 设置的是整个HTTP请求的最大值(包括文件以及各类请求体)

3. 请求参数 DTO(MultimodalRequest.java)

java 复制代码
import lombok.Data;

@Data
public class MultimodalRequest {
    /** 图片URL */
    private String imageUrl;
    /** 提问文本 */
    private String question;
}

由于后续我们要添加更多的新功能,所以目前我们就先搞个 req 类型的数据了(res 是出去的数据,req 是进来的数据)

4. 核心服务类()

4.1 MultimodalService

java 复制代码
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.exception.UploadFileException;
import gzj.spring.ai.Request.MultimodalRequest;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

/**
 * @author DELL
 */
public interface MultimodalService {

    public SseEmitter streamCall(MultimodalRequest request);
    public String simpleCall(MultimodalRequest request) throws NoApiKeyException, UploadFileException;
    
}

4.2 MultimodalServiceImpl

核心逻辑

  • SseEmitter:Spring 提供的 SSE(Server-Sent Events)工具,用于服务端向前端单向推送数据(无需前端轮询);
  • 新线程处理:流式调用是阻塞的,开启独立线程避免占用 Spring MVC 的请求线程池;
  • incrementalOutput(true):百炼 SDK 的流式开关,开启后模型会分段返回回答(比如一句话拆成多次返回);
  • resultFlow.blockingForEach:遍历流式结果,每收到一段就通过 emitter.send 推送给前端;
  • 异常 / 结束处理:推送错误事件、完成事件,确保连接正常关闭。
java 复制代码
import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversation;
import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationParam;
import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationResult;
import com.alibaba.dashscope.common.MultiModalMessage;
import com.alibaba.dashscope.common.Role;
import com.alibaba.dashscope.exception.ApiException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.exception.UploadFileException;
import com.alibaba.dashscope.utils.Constants;
import gzj.spring.ai.Request.MultimodalRequest;
import gzj.spring.ai.Service.MultimodalService;
import io.reactivex.Flowable;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
 * 多模态服务封装
 * @author DELL
 */
@Slf4j
@Service
public class MultimodalServiceImpl implements MultimodalService {

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



    /**
     * 普通调用(非流式)
     */
    @Override
    public String simpleCall(MultimodalRequest request) throws ApiException, IllegalAccessError, NoApiKeyException, UploadFileException {
        MultiModalConversation conv = new MultiModalConversation();

        // 构建用户消息(图片+文本)
        MultiModalMessage userMessage = MultiModalMessage.builder()
                .role(Role.USER.getValue())
                .content(Arrays.asList(
                        Collections.singletonMap("image", request.getImageUrl()),
                        Collections.singletonMap("text", request.getQuestion())
                )).build();

        // 构建请求参数
        MultiModalConversationParam param = MultiModalConversationParam.builder()
                .apiKey(apiKey)
                .model("qwen3-vl-plus")
                .messages(Arrays.asList(userMessage))
                .build();

        // 同步调用
        MultiModalConversationResult result = conv.call(param);

        // 解析返回结果
        List<Map<String, Object>> content = result.getOutput().getChoices().get(0).getMessage().getContent();
        if (content != null && !content.isEmpty()) {
            return content.get(0).get("text").toString();
        }
        return "未获取到有效结果";
    }

    /**
     * 流式调用(SSE推送)
     */
    public SseEmitter streamCall(MultimodalRequest request) {
        // 设置超时时间30秒
        SseEmitter emitter = new SseEmitter(30000L);

        new Thread(() -> {
            MultiModalConversation conv = new MultiModalConversation();

            // 构建用户消息
            MultiModalMessage userMessage = MultiModalMessage.builder()
                    .role(Role.USER.getValue())
                    .content(Arrays.asList(
                            Collections.singletonMap("image", request.getImageUrl()),
                            Collections.singletonMap("text", request.getQuestion())
                    )).build();

            // 构建请求参数
            MultiModalConversationParam param = MultiModalConversationParam.builder()
                    .apiKey(apiKey)
                    .model("qwen3-vl-plus")
                    .messages(Arrays.asList(userMessage))
                    .incrementalOutput(true) // 增量输出(流式)
                    .build();

            try {
                // 流式调用
                Flowable<MultiModalConversationResult> resultFlow = conv.streamCall(param);
                resultFlow.blockingForEach(item -> {
                    try {
                        List<Map<String, Object>> content = item.getOutput().getChoices().get(0).getMessage().getContent();
                        if (content != null && !content.isEmpty()) {
                            String text = content.get(0).get("text").toString();
                            // 推送流式数据到前端
                            emitter.send(SseEmitter.event().data(text));
                        }
                    } catch (Exception e) {
                        log.error("流式推送失败", e);
                        try {
                            emitter.send(SseEmitter.event().name("error").data(e.getMessage()));
                        } catch (Exception ex) {
                            ex.printStackTrace();
                        }
                    }
                });
                // 流式结束标记
                emitter.send(SseEmitter.event().name("complete").data("流结束"));
                emitter.complete();
            } catch (ApiException | NoApiKeyException | UploadFileException e) {
                log.error("流式调用失败", e);
                try {
                    emitter.send(SseEmitter.event().name("error").data(e.getMessage()));
                    emitter.completeWithError(e);
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            } catch (Exception e) {
                log.error("未知异常", e);
                try {
                    emitter.completeWithError(e);
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }).start();

        return emitter;
    }
}

5. 接口层(MultimodalController)

java 复制代码
import gzj.spring.ai.Request.MultimodalRequest;
import gzj.spring.ai.Service.MultimodalService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

/**
 * 多模态接口控制器
 */
@RestController
@RequestMapping("/api/multimodal")
@RequiredArgsConstructor
@CrossOrigin // 跨域支持(生产环境建议限定域名)
public class MultimodalController {

    private final MultimodalService multimodalService;

    /**
     * 普通调用接口(非流式)
     */
    @PostMapping("/simple")
    public ResponseEntity<String> simpleCall(@RequestBody MultimodalRequest request) {
        try {
            String result = multimodalService.simpleCall(request);
            return ResponseEntity.ok(result);
        } catch (Exception e) {
            return ResponseEntity.badRequest().body("调用失败:" + e.getMessage());
        }
    }

    /**
     * 流式调用接口(SSE)
     */
    @PostMapping("/stream")
    public SseEmitter streamCall(@RequestBody MultimodalRequest request) {
        return multimodalService.streamCall(request);
    }
}

四、关键事项

  1. API Key 安全:生产环境不要硬编码 API Key,建议通过环境变量、配置中心管理。
  2. 跨域配置 :生产环境需限定允许跨域的域名,不要直接用@CrossOrigin(可配置全局 CORS)。
  3. 地域适配 :若使用新加坡地域模型,需在application.yml中配置dashscope.base-urlhttps://dashscope-intl.aliyuncs.com/api/v1
  4. 流式调用 SSE:前端 EventSource 默认是 GET 请求,若需 POST,可改用 axios 的流式响应(或使用 fetch API)。
  5. 异常处理:后端增加全局异常处理器,前端完善错误提示。
  6. 模型替换 :可将qwen3-vl-plus替换为其他多模态模型(如qwen-vl-max),需确认模型名称正确性。

五、总结

1.整个过程提供两种调用方式

  • 「普通同步调用」:一次性返回完整的模型回答;
  • 「流式 SSE 调用」:分段向前端推送模型回答(适合大文本、实时性要求高的场景);整体用于处理「图片 + 文本」的多模态问答请求(比如上传图片 URL,提问 "图里有什么?",返回模型的回答)。

2.核心逻辑

  • 组装「图片 URL + 文本问题」的用户消息,符合百炼多模态接口的参数格式;
  • 同步调用 conv.call(param):阻塞当前线程,直到模型返回完整回答;
  • 解析结果:百炼返回的结果是嵌套结构,逐层提取 content 中的文本内容,最终返回给调用方(如 Controller)

3.注意点

  • 方法抛出的 IllegalAccessError 是「Error 类型」(系统级错误,如 JVM 类加载异常),不是业务异常,后续我会优化;
  • 仅处理单轮对话(消息列表只有用户消息),多轮对话需补充历史消息(ASSISTANT 角色的回复),这个我们后续和电脑操控的功能结合起来。

六、演示

注意,这里的 URL 我们暂时调用的是网上的,如果要调用本地的图片的话需要另外的写法,我会在下一篇使用 本地图片 的方式。

如果你觉得我写的还ok,可以关注我看后续的文章。

相关推荐
渡我白衣2 小时前
AI应用层革命(六)——智能体的伦理边界与法律框架:当机器开始“做决定”
人工智能·深度学习·神经网络·机器学习·计算机视觉·自然语言处理·语音识别
裤裤兔2 小时前
已经安装了PyTorch,Jupyter Notebook仍然报错“No module named torch“
人工智能·pytorch·jupyter
正经教主2 小时前
【Trae+AI】和Trae学习搭建App_2.2.1:第4章·安卓APP调用Express后端实战1:前端调用后端
人工智能·学习·express
Le1Yu2 小时前
抢单和派单(超卖问题;ES、Mysql、Redis同步问题;责任链模式)
java
老蒋新思维2 小时前
创客匠人峰会深度解析:知识变现的 “IP 资产化” 革命 —— 从 “运营流量” 到 “沉淀资产” 的长期增长逻辑
大数据·人工智能·网络协议·tcp/ip·创始人ip·创客匠人·知识变现
瀚岳-诸葛弩2 小时前
对比tensorflow,从0开始学pytorch(三)--自定义层
人工智能·pytorch·tensorflow
测试人社区-小明2 小时前
AI在金融软件测试中的实践
人工智能·测试工具·金融·pycharm·机器人·github·量子计算
小哲慢慢来2 小时前
机器学习基本概念
人工智能·机器学习
CoderYanger2 小时前
C.滑动窗口-求子数组个数-越长越合法——2962. 统计最大元素出现至少 K 次的子数组
java·数据结构·算法·leetcode·职场和发展