详细介绍:大模型通义千问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的引入,方便我们使用。



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        3.5.9-SNAPSHOT
         
    
    gzj.spring
    ai
    0.0.1-SNAPSHOT
    ai
    ai
    
    
        
    
    
        
    
    
        
        
        
        
    
    
        17
        2.0.32
    
    
        
            org.springframework.boot
            spring-boot-starter-web
        
        
        
            com.alibaba.cloud.ai
            spring-ai-alibaba-agent-framework
            1.1.0.0-M5
        
        
        
            com.alibaba.cloud.ai
            spring-ai-alibaba-starter-dashscope
            1.1.0.0-M5
        
        
        
            org.springframework.boot
            spring-boot-starter-data-redis
        
        
        
            org.apache.commons
            commons-pool2
        
        
            com.alibaba
            dashscope-sdk-java
            2.22.2
        
        
            com.alibaba
            fastjson
            ${java.json}
        
        
            org.springframework.ai
            spring-ai-model
            1.1.0-M4
        
        
        
            io.reactivex.rxjava3
            rxjava
            3.1.8
        
        
            commons-io
            commons-io
            2.11.0
        
    
    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    
    
        
            spring-snapshots
            Spring Snapshots
            https://repo.spring.io/snapshot
            
                false
            
        
    
    
        
            spring-snapshots
            Spring Snapshots
            https://repo.spring.io/snapshot
            
                false
            
        
    

2. 配置文件(application.yml)

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)

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

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

4. 核心服务类()

4.1 MultimodalService

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 推送给前端;
  • 异常 / 结束处理:推送错误事件、完成事件,确保连接正常关闭。
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> 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 resultFlow = conv.streamCall(param);
                resultFlow.blockingForEach(item -> {
                    try {
                        List> 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)

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 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,可以关注我看后续的文章。

posted on 2026-01-07 13:23  ljbguanli  阅读(225)  评论(0)    收藏  举报