目录
[第一步:新增请求实体类 QvqReasoningRequest](#第一步:新增请求实体类 QvqReasoningRequest)
[第二步:新增 QVQ 推理服务接口 QvqReasoningService](#第二步:新增 QVQ 推理服务接口 QvqReasoningService)
[第三步:新增 QVQ 推理服务实现类 QvqReasoningServiceImpl](#第三步:新增 QVQ 推理服务实现类 QvqReasoningServiceImpl)
[第四步 VideoController](#第四步 VideoController)
[QVQ 视觉推理模型服务实现类(QvqReasoningServiceImpl)实现过程详细总结](#QVQ 视觉推理模型服务实现类(QvqReasoningServiceImpl)实现过程详细总结)
[二、前置参数构建:贴合 SDK 规范封装输入](#二、前置参数构建:贴合 SDK 规范封装输入)
[三、流式 API 调用:对接 SDK 的流式能力](#三、流式 API 调用:对接 SDK 的流式能力)
[四、流式结果解析与 SSE 推送:拆分 "思考过程 + 最终回复"](#四、流式结果解析与 SSE 推送:拆分 “思考过程 + 最终回复”)
一、概论
视觉推理模型能够先输出思考过程,再输出回答内容,适用于处理复杂的视觉分析任务,如解读数学题、分析图表数据或复杂视频理解等任务。
简单来说,视觉推理模型的核心特点是 **"先拆解逻辑,再给出结论"**------ 它不仅能完成复杂视觉分析任务,还能像人一样暴露背后的思考逻辑,彻底区别于普通视觉模型 "直接输出结果" 的模式,尤其适配需要深度逻辑推导的场景:
比如处理数学几何题时,它不会直接给出答案,而是先输出思考过程:"首先观察图片中的图形结构,识别出三角形的类型(等腰直角三角形)→ 提取已知条件(直角边长度为 5cm)→ 回忆勾股定理公式(a²+b²=c²)→ 代入数值计算斜边长度→ 验证计算结果是否符合图形比例",之后再给出明确的解题答案和最终结果;
分析图表数据(如柱状图、折线图)时,思考过程会是:"先确定图表类型为年度销售额折线图→ 解读横轴(年份 2020-2024)和纵轴(销售额单位万元)→ 提取各年份关键数据(2020 年 800 万、2021 年 1200 万...)→ 计算年度增长率(2021 年同比增长 50%)→ 分析增长趋势(2022-2023 年增速放缓)",再输出整合后的数据分析结论;
理解复杂视频(如事件类短视频、监控画面)时,思考过程会围绕 "事件顺序、因果关系" 展开:"先梳理视频帧中的关键场景(第 1 帧:车辆正常行驶,第 3 帧:行人横穿马路,第 5 帧:车辆刹车避让)→ 还原事件时间线(行人未走斑马线→ 司机发现后紧急刹车→ 未发生碰撞)→ 提炼核心事件(车辆避让违规横穿马路的行人)",最终给出完整的视频内容总结。
这种 "思考过程 + 最终答案" 的输出模式,让模型的决策逻辑可追溯、可解释,不仅能应对复杂视觉任务的深度分析需求,还能帮助用户理解结论的由来,尤其适合对逻辑严谨性要求高的场景(如教育解题、专业数据分析、事件溯源等)。
二、代码实现
以下是基于官方qvq-max模型(支持思考过程输出)的前后端分离后端接口实现,包含请求实体、服务接口、实现类、控制器,适配流式返回「思考过程 + 最终回复」
第一步:新增请求实体类 QvqReasoningRequest
java
package gzj.spring.ai.Request;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* QVQ模型推理请求参数(支持思考过程+最终回复流式输出)
* @author DELL
*/
@Data
public class QvqReasoningRequest {
/** 图片URL(支持HTTPS) */
@NotBlank(message = "图片URL不能为空")
private String imageUrl;
/** 提问文本(如解题、分析图片等) */
@NotBlank(message = "提问文本不能为空")
private String question;
/** 模型名称(默认qvq-max,需SDK≥2.19.0) */
private String modelName = "qvq-max";
}
第二步:新增 QVQ 推理服务接口 QvqReasoningService
java
package gzj.spring.ai.Service;
import com.alibaba.dashscope.exception.ApiException;
import com.alibaba.dashscope.exception.InputRequiredException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.exception.UploadFileException;
import gzj.spring.ai.Request.QvqReasoningRequest;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
/**
* QVQ模型推理服务接口(支持思考过程流式输出)
* @author DELL
*/
public interface QvqReasoningService {
/**
* QVQ模型流式推理(返回思考过程+最终回复)
* @param request 推理请求参数
* @return SseEmitter 用于前端接收流式结果(区分思考过程/最终回复)
*/
SseEmitter streamReasoningCall(QvqReasoningRequest request);
}
第三步:新增 QVQ 推理服务实现类 QvqReasoningServiceImpl
java
package gzj.spring.ai.Service.ServiceImpl;
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.InputRequiredException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.exception.UploadFileException;
import gzj.spring.ai.Request.QvqReasoningRequest;
import gzj.spring.ai.Service.QvqReasoningService;
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.*;
/**
* QVQ模型推理服务实现(支持思考过程+最终回复流式输出)
* 要求:dashscope SDK版本 ≥ 2.19.0
* @author DELL
*/
@Slf4j
@Service
public class QvqReasoningServiceImpl implements QvqReasoningService {
@Value("${dashscope.api-key}")
private String apiKey;
/**
* 构建多模态消息(图片+文本)
*/
private MultiModalMessage buildUserMessage(QvqReasoningRequest request) {
return MultiModalMessage.builder()
.role(Role.USER.getValue())
.content(Arrays.asList(
Collections.singletonMap("image", request.getImageUrl()),
Collections.singletonMap("text", request.getQuestion())
)).build();
}
/**
* 构建请求参数(开启增量输出)
*/
private MultiModalConversationParam buildConversationParam(QvqReasoningRequest request, MultiModalMessage userMsg) {
return MultiModalConversationParam.builder()
.apiKey(apiKey)
.model(request.getModelName())
.messages(Arrays.asList(userMsg))
.incrementalOutput(true) // 增量输出(流式核心)
.build();
}
/**
* 处理流式结果,拆分思考过程和最终回复,推送到前端
*/
private void handleStreamResult(MultiModalConversationResult result, SseEmitter emitter) {
try {
// 1. 处理思考过程(reasoningContent)
String reasoning = Optional.ofNullable(result.getOutput().getChoices().get(0).getMessage().getReasoningContent()).orElse("");
if (!reasoning.isEmpty()) {
// 推送思考过程(事件名:reasoning)
emitter.send(SseEmitter.event().name("reasoning").data(reasoning));
log.debug("推送思考过程片段:{}", reasoning);
}
// 2. 处理最终回复(text)
List<Map<String, Object>> content = result.getOutput().getChoices().get(0).getMessage().getContent();
if (content != null && !content.isEmpty()) {
String text = Optional.ofNullable(content.get(0).get("text")).orElse("").toString();
if (!text.isEmpty()) {
// 推送最终回复(事件名:answer)
emitter.send(SseEmitter.event().name("answer").data(text));
log.debug("推送回复片段:{}", text);
}
}
} catch (Exception e) {
log.error("流式结果处理失败", e);
handleEmitterError(emitter, "结果推送失败:" + e.getMessage());
}
}
/**
* 统一处理SSE发射器异常
*/
private void handleEmitterError(SseEmitter emitter, String errorMsg) {
try {
emitter.send(SseEmitter.event().name("error").data(errorMsg));
emitter.completeWithError(new RuntimeException(errorMsg));
} catch (Exception e) {
log.error("发射器异常处理失败", e);
}
}
/**
* QVQ模型流式推理(核心方法)
*/
@Override
public SseEmitter streamReasoningCall(QvqReasoningRequest request) {
// 设置超时时间60秒(推理类任务耗时更长)
SseEmitter emitter = new SseEmitter(60000L);
new Thread(() -> {
MultiModalConversation conv = new MultiModalConversation();
try {
// 1. 构建用户消息和请求参数
MultiModalMessage userMsg = buildUserMessage(request);
MultiModalConversationParam param = buildConversationParam(request, userMsg);
// 2. 流式调用API
Flowable<MultiModalConversationResult> resultFlow = conv.streamCall(param);
resultFlow.blockingForEach(result -> handleStreamResult(result, emitter));
// 3. 流式结束标记
emitter.send(SseEmitter.event().name("complete").data("推理完成"));
emitter.complete();
} catch (ApiException | NoApiKeyException | UploadFileException | InputRequiredException e) {
log.error("QVQ模型API调用失败", e);
handleEmitterError(emitter, "API调用失败:" + e.getMessage());
} catch (Exception e) {
log.error("QVQ模型流式推理未知异常", e);
handleEmitterError(emitter, "系统异常:" + e.getMessage());
}
}).start();
return emitter;
}
}
第四步 VideoController
java
package gzj.spring.ai.Controller;
import com.alibaba.dashscope.exception.ApiException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.exception.UploadFileException;
import gzj.spring.ai.Request.QvqReasoningRequest;
import gzj.spring.ai.Request.VideoFrameListRequest;
import gzj.spring.ai.Request.VideoRequest;
import gzj.spring.ai.Service.QvqReasoningService;
import gzj.spring.ai.Service.VideoFrameListService;
import gzj.spring.ai.Service.VideoService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
/**
* @author DELL
*/
@RestController
@RequestMapping("/api/multimodal/video")
@RequiredArgsConstructor
@CrossOrigin // 跨域支持(生产环境建议限定域名)
public class VideoController {
private final VideoService videoService;
private final VideoFrameListService videoFrameListService;
private final QvqReasoningService qvqReasoningService;
@RequestMapping("/simple")
public String simpleVideoCall(@RequestBody VideoRequest request) throws ApiException, NoApiKeyException, UploadFileException {
return videoService.simpleVideoCall(request);
}
/**
* 视频帧列表理解-普通调用(非流式)
*/
@PostMapping("/simpleFrame")
public String simpleFrameListCall(@Validated @RequestBody VideoFrameListRequest request)
throws ApiException, NoApiKeyException, UploadFileException {
return videoFrameListService.simpleFrameListCall(request);
}
/**
* 视频帧列表理解-流式调用(SSE推送)
*/
@PostMapping("/streamFrame")
public SseEmitter streamFrameListCall(@Validated @RequestBody VideoFrameListRequest request) {
return videoFrameListService.streamFrameListCall(request);
}
/**
* QVQ模型流式推理接口
* @param request 推理请求参数(图片URL+提问文本)
* @return SseEmitter 流式返回结果
*/
@PostMapping("/stream-reasoning")
public SseEmitter streamReasoning(@Validated @RequestBody QvqReasoningRequest request) {
return qvqReasoningService.streamReasoningCall(request);
}
}
三、演示
参postman参数:
java
{
"imageUrl": "https://img.alicdn.com/imgextra/i1/O1CN01gDEY8M1W114Hi3XcN_!!6000000002727-0-tps-1024-406.jpg",
"question": "请解答这道题",
"modelName": "qvq-max"
}
结果:

一大串因为是流式的,后续我们更新前端的页面就可以看到了,总之测试结果没有问题。
五、视觉推理模型核心总结
QVQ 视觉推理模型服务实现类(QvqReasoningServiceImpl)实现过程详细总结
该实现类是视觉推理模型(QVQ 系列)工程化落地的核心层,基于 DashScope SDK(≥2.19.0)将官方控制台示例转化为适配前后端分离的 SSE 流式服务,完整实现 "思考过程 + 最终回复" 的拆分推送,其实现过程可拆解为初始化准备、参数构建、流式调用、结果解析推送、收尾与异常处理五大核心阶段,具体如下:
一、初始化准备:基础配置与线程模型搭建
- 依赖注入与基础配置 :通过
@Value注入 DashScope 的 API Key,作为调用模型的身份凭证;定义 SSE 发射器(SseEmitter)超时时间为 60 秒(适配视觉推理(如数学题解读)的长耗时特性,避免过早断开连接)。 - 线程隔离设计:创建独立线程处理流式调用逻辑,避免 SSE 推送阻塞 Web 容器的请求线程,保证接口响应性(符合 Spring Web 的线程模型规范)。
二、前置参数构建:贴合 SDK 规范封装输入
核心是将前端请求参数转化为 DashScope SDK 要求的格式,分为两步:
- 构建用户多模态消息 :通过
buildUserMessage方法,将请求中的imageUrl(图片 HTTPS 链接)和question(提问文本)封装为MultiModalMessage,指定角色为USER,内容按 "图片 + 文本" 的列表格式组织,完全对齐 SDK 的消息结构要求。 - 构建请求参数 :通过
buildConversationParam方法,基于MultiModalConversationParam构建器配置核心参数 ------API Key、模型名(默认qvq-max,支持请求自定义)、消息列表,并开启incrementalOutput(true)(增量输出),确保模型返回流式结果而非一次性返回。
三、流式 API 调用:对接 SDK 的流式能力
- 实例化
MultiModalConversation客户端,调用streamCall方法传入构建好的参数,获取Flowable<MultiModalConversationResult>类型的流式结果(RxJava 流,适配 SDK 的响应式设计); - 通过
blockingForEach遍历流式结果,逐段处理模型返回的每一批数据(增量输出的核心载体)。
四、流式结果解析与 SSE 推送:拆分 "思考过程 + 最终回复"
这是视觉推理模型 "先思考、后回答" 核心特性的工程化落地,通过handleStreamResult方法实现精细化解析:
- 思考过程解析推送 :提取结果中的
ReasoningContent字段(QVQ 模型专属,需 SDK≥2.19.0),通过Optional.ofNullable做空值保护,非空时以 SSE 事件名reasoning推送(前端可单独监听思考过程); - 最终回复解析推送 :解析结果中
content列表的text字段,同样做空值保护,非空时以 SSE 事件名answer推送(与思考过程拆分,便于前端差异化渲染); - 日志埋点:为每段推送的思考过程 / 回复片段添加调试日志,便于问题排查。
五、流式收尾与异常处理:保证服务健壮性
- 正常收尾 :所有流式结果处理完成后,推送
complete事件(内容为 "推理完成"),调用emitter.complete()关闭 SSE 连接,释放资源; - 异常分层处理 :
- 捕获 SDK 专属异常(
ApiException/NoApiKeyException/UploadFileException等):定位模型调用层面的问题(如 API Key 错误、图片无法读取); - 捕获通用异常(
Exception):兜底处理未知错误; - 异常标准化推送:通过抽离的
handleEmitterError工具方法,统一推送error事件(携带错误信息),调用emitter.completeWithError()标记异常结束,保证前端能感知错误类型;
- 捕获 SDK 专属异常(
- 工具方法复用:将 SSE 异常处理逻辑抽离为独立方法,减少代码冗余,保证异常处理逻辑的一致性。
核心设计细节与适配点
- 事件拆分设计 :按
reasoning/answer/error/complete四类 SSE 事件拆分输出,精准贴合视觉推理模型 "先输出思考过程、再输出回答" 的核心特性,前端可按需监听不同事件实现差异化渲染; - 健壮性保障 :全链路空值保护(
Optional)、分层异常捕获,避免空指针或单一异常导致服务崩溃; - 扩展性设计:模型名通过请求参数自定义,无需修改代码即可切换 QVQ 系列其他模型;
- 工程化适配:完全贴合 Spring Boot 服务层规范,可直接集成到现有多模态服务体系中,与此前的图片 / 视频理解服务保持一致的代码风格和异常处理逻辑。
综上,该实现类的核心价值是将 QVQ 视觉推理模型的 "思考 + 回答" 能力从官方示例的控制台输出,转化为工程化、可复用、适配前后端分离的 SSE 流式服务,既保留了模型的核心特性,又符合企业级后端开发的规范与健壮性要求。