抓住 AI 人工智能的风口之第 5 章 —— 使用视觉大模型(Vision-Language Model)支持图片识别,完善电商智能客服项目

1、导语

本篇博客基于第 3 章博客基础上,引入视觉大模型 Vision-Language Model,支持图片文件上传和识别,第 3 章博客地址:https://blog.csdn.net/BiandanLoveyou/article/details/161004907

2、本篇博客代码地址

本篇博客详细拆解如何引入视觉大模型 VL 来进一步完善 AI 智能客服系统,代码仓库地址:

https://gitee.com/biandanLoveyou/customer-service/tree/05-vision-image/

3、视觉模型的基本理解

3.1 什么是视觉模型?

视觉模型是指能够理解和处理图像、视频等视觉信息的人工智能模型。简单来说,就是让计算机能够"看懂"图片和视频内容。

视觉模型与传统大语言模型的核心能力对比:

能力 传统大语言模型 视觉模型
输入类型 仅文本 图像、视频、文本
理解图片 ❌ 不能 ✅ 能识别物体、场景、文字
图文推理 ❌ 不能 ✅ 能根据图片回答问题
典型任务 对话、翻译 看图说话、图像识别

3.2 视觉模型能做什么?

1. 图像识别

输入:一张猫的照片

输出:"这是一只橘猫,正趴在沙发上"

2. 光学字符识别(OCR)

输入:含有"身份证"文字的图片

输出:"图片中的文字是:身份证号XXX"

3. 视觉问答

输入:图片 + 问题"图中有几个人?"

输出:"有3个人"

4. 商品识别

输入:商品照片

输出:"这是华为手机,铂金色"

5. 场景理解

输入:街景照片

输出:"这是一个繁忙的十字路口,有红绿灯和行人"

3.3 视觉模型选型

常见视觉模型对比:

模型 开发商 特点 价格
GPT-4V OpenAI 强大的图文推理能力 付费
Qwen-VL 阿里通义 开源免费,支持中文好 免费
Claude 3.5 Anthropic 视觉+长上下文 付费
Gemini Google 原生多模态 部分免费
LLaVA 学术界 轻量级开源 免费

本次使用本地部署的视觉模型(请先使用 Ollama 提前下载好):qwen3-vl:4b

3.4 视觉模型如何与业务系统一起工作解决实际业务问题

视觉模型与业务系统一起工作的核心思路如下:

视觉模型解析结果 → 提取结构化商品信息 → 大语言模型理解意图 → 调用搜索工具 → 返回推荐商品

原理流程图:

复制代码
用户上传图片 + 问题 "有图片中的商品售卖吗?"
                ↓
        视觉模型解析图片
                ↓
    {
      "商品1": "华为笔记本电脑", "价格": "8000起", "品牌": "华为"
      "商品2": "美的空调", "卖点": "AI制冷,省电40%", "品牌": "美的"
    }
                ↓
        构建增强Prompt
                ↓
        大语言模型分析
                ↓
    识别到用户意图 → 调用 getAllProduct 工具
                ↓
        getAllProduct(productName="华为笔记本电脑")
        getAllProduct(productName="美的空调")
                ↓
        返回搜索结果
    {
      "华为笔记本电脑": [MateBook D 16, MateBook 14],
      "美的空调": [1.5匹新一级空调, 酷省电系列]
    }
                ↓
        LLM 组织回复
    "是的,系统中有您想要的商品!
     1. 华为笔记本电脑 - ¥8000
     2. 美的空调 - ¥5000,AI制冷省电40%
     需要我帮您了解更多详情吗?"
                ↓
        流式返回给前端

4、系统里引入视觉模型

4.1 配置视觉模型客户端 ChatClient

新建配置类 ImageModelConfig:

java 复制代码
package com.customer.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.ai.ollama.api.OllamaChatOptions;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author CSDN流放深圳
 * @description 图像模型配置类
 * @create 2026-06-02 15:06
 * @since 1.0.0
 */
@Configuration
public class ImageModelConfig {

    /**
     * 模型名称
     */
    private final String IMAGE_MODEL_NAME = "qwen3-vl:4b";

    @Value("${spring.ai.ollama.base-url}")
    private String BASE_URL;

    /**
     * 初始化图像模型 ChatClient
     *
     * @return
     */
    @Bean(name = "imageChatClient") //调用者需要增加注解 @Qualifier("imageChatClient")
    public ChatClient initImageChatClient() {
        // 构建 Ollama API 客户端
        OllamaApi ollamaApi = OllamaApi.builder()
                .baseUrl(BASE_URL)
                .build();

        // 构建配置选项
        OllamaChatOptions options = OllamaChatOptions.builder()
                .model(IMAGE_MODEL_NAME)//模型名称
                .keepAlive("12h")//模型驻留内存时间12小时
                .truncate(true)//超长文本截断
                .temperature(0.5)
                .build();

        // 创建 OllamaChatModel
        OllamaChatModel ollamaChatModel = OllamaChatModel.builder()
                .ollamaApi(ollamaApi)
                .defaultOptions(options)
                .build();

        return ChatClient.builder(ollamaChatModel).build();
    }
}

重要说明:因为系统里配置了两个 ChatClient,需要指定一个默认的 ChatClient 否则启动报错。我们修改:ChatClientConfig 增加默认使用的 Client,增加注解:@Primary

4.2 视觉模型实现类

考虑到用户会上传多张照片进行识别,这里做了一个批量处理:

java 复制代码
package com.customer.service.impl;

import com.customer.entity.constant.CommonKeys;
import com.customer.holder.UserContextHolder;
import com.customer.service.ImageAnalyzeService;
import com.customer.util.CustomerFileUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.content.Media;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import org.springframework.util.MimeType;

import java.io.File;
import java.util.List;
import java.util.Map;

/**
 * @author CSDN流放深圳
 * @description 图片处理服务接口实现类
 * @create 2026-05-16 12:05
 * @since 1.0.0
 */
@Service
@Slf4j
public class ImageServiceImpl implements ImageAnalyzeService {

    @Autowired
    @Qualifier("imageChatClient")//使用视觉模型客户端
    private ChatClient imageChatClient;


    /**
     * 分析图片内容接口
     *
     * @param imageFileList 图片文件集合
     * @return 返回图片解析内容
     */
    @Override
    public String analyzeImage(List<File> imageFileList) {
        // 从上下文获取用户ID
        String userId = UserContextHolder.customerUserInfo().getUserId();

        // 创建用户消息
        UserMessage userMessage = new UserMessage("请仔细分析图片。" +
                "如果有商品信息,请提取商品名称、品牌信息,请用简短的文字描述返回;" +
                "如果没有商品信息,直接返回【无商品信息】。");
        for (File imageFile : imageFileList) {
            //动态获取 MIME 类型
            String mimeType = CustomerFileUtil.getImageMimeType(imageFile);
            //创建资源对象
            Resource imageResource = new FileSystemResource(imageFile);
            userMessage.getMedia().add(new Media(MimeType.valueOf(mimeType), imageResource));
        }

        return imageChatClient.prompt()
                .messages(userMessage)
                .toolContext(Map.of(CommonKeys.USER_ID, userId))
                .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, userId))
                .call().content();
    }

}

4.3 业务调用类

当识别用户上传的图片后,直接把识别后的商品信息经过结构化输出,再传递给大语言模型,让大语言模型调用工具帮我们查询系统里的商品信息。修改 ChatServiceImpl 类如下:

java 复制代码
package com.customer.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.customer.entity.constant.CommonKeys;
import com.customer.entity.dto.ChatDTO;
import com.customer.holder.UserContextHolder;
import com.customer.service.ChatService;
import com.customer.service.ImageAnalyzeService;
import com.customer.util.CustomerFileUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import reactor.core.publisher.Flux;

import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

/**
 * @author CSDN流放深圳
 * @description 聊天服务实现类
 * @create 2026-05-16 12:05
 * @since 1.0.0
 */
@Service
@Slf4j
public class ChatServiceImpl implements ChatService {

    @Autowired
    private ChatClient chatClient;

    @Autowired
    private ImageAnalyzeService imageAnalyzeService;


    /**
     * 与大模型AI聊天
     *
     * @param dto
     * @return 流式输出
     */
    @Override
    public Flux<String> chat(ChatDTO dto) {
        //从上下文获取用户ID
        String userId = UserContextHolder.customerUserInfo().getUserId();
        //判断用户文字输入或者图片是否都为空
        String userMessage = dto.getUserMessage();
        List<MultipartFile> images = dto.getImages();
        if (StrUtil.isEmpty(userMessage) && CollUtil.isEmpty(images)) {
            return Flux.just("请输入文字或者上传图片");
        }
        if(CollUtil.isNotEmpty(images) && images.size() > 3){
            return Flux.just("最多支持上传3张图片");
        }

        //如果有图片,先调用视觉模型处理
        String analyzeResult = null;
        if(CollUtil.isNotEmpty(images)){
            //把图片上传到根目录的临时文件夹
            List<File> fileList = CustomerFileUtil.uploadImages(images);
            //调用视觉模型分析图片
            log.info("--------------  开始调用视觉模型分析图片   --------------");
            analyzeResult = imageAnalyzeService.analyzeImage(fileList);
            //使用视觉模型解析后的图片说明
            log.info("视觉模型解析后的商品信息: {}", analyzeResult);
            //删除临时文件(使用异步非阻塞)
            CompletableFuture.runAsync(() -> CustomerFileUtil.deleteTempFiles(fileList));
        }

        // 构建增强的系统提示词,引导LLM调用商品搜索工具
        String enhancedMessage = buildShoppingPrompt(analyzeResult, userMessage);
        log.info("增强的用户提示词enhancedMessage: {}", enhancedMessage);

        return chatClient.prompt()
                .user(enhancedMessage)
                //设置用户ID到大模型的上下文中,还可以设置其他业务参数,放在 Map 集合中
                .toolContext(Map.of(CommonKeys.USER_ID, userId))
                // 增加"顾问",允许通过注入检索数据(Retrieval Context)和对话历史(Chat Memory)来修改传入的 Prompt
                .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, userId)) //固定值 chat_memory_conversation_id
                .stream().content();
    }

    /**
     * 构建增强的系统提示词,引导LLM调用商品搜索工具
     * @param analyzeResult
     * @param userMessage
     * @return
     */
    private String buildShoppingPrompt(String analyzeResult, String userMessage) {
        String result = "";
        if (StrUtil.isNotBlank(userMessage)) {
            result = "用户提问:" + userMessage;
        }
        if (StrUtil.isNotBlank(analyzeResult)) {
            result += " 查询商品信息:" + analyzeResult;
        }
        return result;
    }


}

4.4 优化商品搜索

之前我们直接通过 like 关键字查询数据库的商品信息,在大型系统显然是不合理的。一般会把商品信息放入 ES(Elasticsearch)等搜索引擎当中。我们这里优化一下,当数据库查询不到商品时,可以使用向量数据库进行查询:

java 复制代码
package com.customer.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.customer.dao.ProductInfoDao;
import com.customer.entity.po.ProductInfoEntity;
import com.customer.service.ProductInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @author CSDN流放深圳
 * @description 商品信息服务实现类
 * @create 2026-05-19 14:42
 * @since 1.0.0
 */
@Service
@Slf4j
public class ProductInfoServiceImpl extends ServiceImpl<ProductInfoDao, ProductInfoEntity> implements ProductInfoService {

    @Autowired
    private VectorStore vectorStore;

    /**
     * 根据ID查询商品
     *
     * @param productId
     */
    @Override
    public ProductInfoEntity getProductById(String productId) {
        ProductInfoEntity productInfoEntity = this.baseMapper.selectById(productId);
        return productInfoEntity;
    }

    /**
     * 查询所有商品(可根据商品名称模糊查询匹配)
     *
     * @param productName
     */
    @Override
    public List<ProductInfoEntity> getAllProduct(String productName) {
        log.info("=== getAllProduct 被调用,参数: " + productName + " ===");
        // 查询商品,一般有几种查询策略:1、最常用的 ES(Elasticsearch),2、向量数据库查询(语义搜索),3、缓存查询,4、数据库模糊查询
        List<ProductInfoEntity> list = this.baseMapper.selectList(Wrappers.<ProductInfoEntity>lambdaQuery()
                .like(StrUtil.isNotBlank(productName), ProductInfoEntity::getProductName, productName));
        //2026年6月3日优化:如果数据库中没有数据,则从向量数据库中查询
        if (CollUtil.isEmpty(list)) {
            System.out.println("数据库中没有数据,从向量数据库中查询");
            SearchRequest request = SearchRequest.builder()
                    .query(productName)//查询条件
                    .topK(5)//返回数量
                    .build();
            List<Document> documentList = vectorStore.similaritySearch(request);
            for (Document document : documentList) {
                //获取向量存储中的内容
                String text = document.getText();
                //转为商品信息
                ProductInfoEntity productInfoEntity = JSON.parseObject(text, ProductInfoEntity.class);
                list.add(productInfoEntity);
            }
        }
        log.info("返回商品列表:{}", JSON.toJSONString(list));
        return list;
    }

}

4.5 系统启动时,把商品信息初始化到向量数据库

修改 VectorDatabaseConfig 类,增加系统启动时把商品信息初始化到向量数据库的逻辑:

java 复制代码
package com.customer.config;

import cn.hutool.crypto.SecureUtil;
import com.alibaba.fastjson.JSON;
import com.customer.entity.constant.CommonRedisKeys;
import com.customer.entity.po.ProductInfoEntity;
import com.customer.service.ProductInfoService;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.TextReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.data.redis.core.RedisTemplate;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @author CSDN流放深圳
 * @description 文件内容存入到向量数据库配置类
 * @create 2026-05-22 10:20
 * @since 1.0.0
 */
@Configuration
@Slf4j
public class VectorDatabaseConfig {

    @Autowired
    private VectorStore vectorStore;

    /**
     * 读取resources目录下的文件【电商系统服务条款】
     */
    @Value("classpath:terms-of-service.txt")
    private Resource resource;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private ProductInfoService productInfoService;

    /**
     * 初始化向量数据库。程序启动时就执行
     */
    @PostConstruct
    public void init() {
        //1。读取文件内容
        TextReader textReader = new TextReader(resource);
        //设置编码格式,防止乱码
        textReader.setCharset(Charset.defaultCharset());

        //获取文件路径等元数据
        //拿到文件路径后,常见用途是做去重校验------用文件路径生成哈希值,存入 Redis,避免同一文件被重复加载到向量数据库中。但需要注意,这种方式只能判断"是否同一个文件",无法感知文件内容的变化。如果文件内容更新但路径不变,你可以考虑用文件内容的哈希值来做判断。
        String fileName = (String) textReader.getCustomMetadata().get(TextReader.SOURCE_METADATA);
        //对文件路径进行md5加密
        String securityFileName = SecureUtil.md5(fileName);
        String redisKey = CommonRedisKeys.CUSTOMER_TERMS_OF_SERVICE + securityFileName;
        //如果文档存在,先删掉,再存储
        deleteVectorStoreIfExists(redisKey);
        //写入向量数据库
        List<Document> documentList = new TokenTextSplitter().transform(textReader.read());
        vectorStore.add(documentList);

        //保存文档Id集合
        List<String> documentIds = documentList.stream().map(Document::getId).collect(Collectors.toList());
        //保存到 Redis 中
        redisTemplate.opsForValue().setIfAbsent(redisKey, JSON.toJSONString(documentIds));
        log.info("电商系统服务条款初始化完成,本次共初始化 " + documentIds.size() + " 条数据到向量数据库");
    }

    /**
     * 程序启动,初始化商品信息到向量数据库
     */
    @PostConstruct
    public void initProductInfo() {
        log.info("程序启动,初始化商品信息到向量数据库");
        List<Document> documentList = new ArrayList<>();
        List<ProductInfoEntity> list = productInfoService.getAllProduct(null);
        for (ProductInfoEntity productInfoEntity : list) {
            //转化成文档集合
            documentList.add(new Document(JSON.toJSONString(productInfoEntity)));
        }
        String redisKey = CommonRedisKeys.CUSTOMER_PRODUCT_INFO_KEY;
        //如果文档存在,先删掉,再存储
        deleteVectorStoreIfExists(redisKey);
        //添加到向量存储中
        vectorStore.add(documentList);

        //保存文档Id集合
        List<String> documentIds = documentList.stream().map(Document::getId).collect(Collectors.toList());
        //保存到 Redis 中
        redisTemplate.opsForValue().setIfAbsent(redisKey, JSON.toJSONString(documentIds));
        log.info("商品信息初始化完成,本次共初始化 " + documentIds.size() + " 条数据到向量数据库");
    }

    /**
     * 根据 key 删除Redis 中缓存的向量数据
     * @param redisKey
     */
    private void deleteVectorStoreIfExists(String redisKey){
        //判断文件是否已经存在
        Object obj = redisTemplate.opsForValue().get(redisKey);
        //如果文档存在,先删掉
        if (null != obj) {
            List<String> documentIds = JSON.parseArray((String) redisTemplate.opsForValue().get(redisKey), String.class);
            vectorStore.delete(documentIds);
            redisTemplate.delete(redisKey);
        }
    }
}

4.6 增加文件上传处理工具类

新建工具类:CustomerFileUtil

java 复制代码
package com.customer.util;

import com.customer.entity.enums.SysExceptionEnum;
import com.customer.exception.CustomerRuntimeException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.MimeTypeUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * @author CSDN流放深圳
 * @description 文件处理工具类
 * @create 2026-06-04 15:05
 * @since 1.0.0
 */
@Slf4j
public class CustomerFileUtil {

    /**
     * 上传图片到临时文件夹
     * @param images
     * @return
     */
    public static List<File> uploadImages(List<MultipartFile> images) {
        List<File> tempFileList = new ArrayList<>();
        for (MultipartFile image : images) {
            // 校验文件类型
            String contentType = image.getContentType();
            if (contentType == null || !contentType.startsWith("image/")) {
                throw new CustomerRuntimeException(SysExceptionEnum.IMAGE_FORMAT_ERROR);
            }
            File tempFile = null;
            try {
                // 1. 获取项目根目录
                String projectDir = System.getProperty("user.dir");
                // 2. 创建临时目录
                File tempDir = new File(projectDir, "tempFile");
                if (!tempDir.exists()) {
                    tempDir.mkdirs();
                }
                // 3. 创建临时文件(使用时间戳保证唯一性)
                String extension = getFileExtension(image);
                String fileName = "temp_" + System.currentTimeMillis() + extension;
                tempFile = new File(tempDir, fileName);
                // 4. 保存文件
                image.transferTo(tempFile);
                // 5. 添加到临时文件列表
                tempFileList.add(tempFile);
            } catch (IOException e) {
                log.error("文件IO处理失败: {}", e.getMessage(), e);
                throw new CustomerRuntimeException(SysExceptionEnum.IMAGE_DAMAGED_ERROR);
            } catch (Exception e) {
                log.error("文件处理失败: {}", e.getMessage(), e);
                throw new CustomerRuntimeException(SysExceptionEnum.IMAGE_RECOGNITION_ERROR);
            }
        }
        return tempFileList;
    }

    /**
     * 获取文件扩展名
     * @param file 文件对象
     * @return 文件扩展名
     */
    public static String getFileExtension(MultipartFile file) {
        // 优先从文件名获取
        String originalFilename = file.getOriginalFilename();
        if (originalFilename != null && originalFilename.contains(".")) {
            String ext = originalFilename.substring(originalFilename.lastIndexOf("."));
            // 校验扩展名是否合法
            if (ext.matches("\\.(png|jpg|jpeg|webp|gif|bmp)$")) {
                return ext;
            }
        }

        // 降级:根据 Content-Type 返回默认扩展名
        String contentType = file.getContentType();
        if (contentType != null) {
            if (contentType.contains("png")) return ".png";
            if (contentType.contains("jpeg")) return ".jpg";
            if (contentType.contains("webp")) return ".webp";
            if (contentType.contains("gif")) return ".gif";
            if (contentType.contains("bmp")) return ".bmp";
        }
        // 最终兜底
        return ".png";
    }

    /**
     * 删除临时文件
     * @param fileList
     */
    public static void deleteTempFiles(List<File> fileList) {
        fileList.forEach(file -> {
            if (file.exists()) {
                file.delete();
            }
        });
    }

    /**
     * 获取图片的 MIME 类型
     * @param imageFile 图片文件
     * @return 标准的 MIME 类型
     */
    public static String getImageMimeType(File imageFile) {
        // 根据文件扩展名判断
        String fileName = imageFile.getName().toLowerCase();
        if (fileName.endsWith(".png")) {
            return MimeTypeUtils.IMAGE_PNG_VALUE;  // "image/png"
        } else if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) {
            return MimeTypeUtils.IMAGE_JPEG_VALUE; // "image/jpeg"
        } else if (fileName.endsWith(".gif")) { //"image/gif"
            return MimeTypeUtils.IMAGE_GIF_VALUE;
        } else if (fileName.endsWith(".webp")) {
            return "image/webp";
        } else if (fileName.endsWith(".bmp")) {
            return "image/bmp";
        } else {
            // 默认使用 PNG
            return MimeTypeUtils.IMAGE_PNG_VALUE;
        }
    }
}

4.7 修改聊天入参类

聊天入参增加多文件上传的参数支持,修改 ChatDTO:

java 复制代码
package com.customer.entity.dto;

import lombok.Data;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

/**
 * @author CSDN流放深圳
 * @description 聊天参数
 * @create 2026-05-16 11:35
 * @since 1.0.0
 */
@Data
public class ChatDTO {

    /**
     * 聊天内容
     */
    //@NotBlank(message = "【聊天内容】不允许为空[userMessage]", groups = {OtherGroup.class})
    private String userMessage;

    /**
     * 图片文件,MultipartFile 类型
     */
    //@Size(max = 3, message = "【图片文件】最多允许上传3张图片", groups = {OtherGroup.class})
    private List<MultipartFile> images;
}

4.8 修改前端 html

与第 3 章使用不同的前端页面,增加多图片上传功能,完整代码如下(请到代码仓库自行下载):

其它小改动请查看代码仓库,以分支(05-vision-image)最新代码为准。

5、系统演示

小技巧:可以使用命令 flushall 清空 Redis 的所有 key。注意,这个小技巧仅限于开发的时候使用,生产环境慎用!

5.1 系统启动

5.2 用户上传商品图片(不加提示词)

查看后台打印的日志:

说明:首先调用视觉大模型识别用户上传的图片,通过 UserMessage 限定大模型返回的内容(其实视觉大模型识别一张图片返回的内容会很多,这里我们仅需要商品信息,只对业务有作用的信息即可。)

调用本地大模型比较耗时,需要等待1、2分钟。

5.3 用户上传非商品图片(不加提示词)

查看后台日志(无商品信息,则查询全部商品呈现给用户):

5.4 用户上传图片并询问是否有商品在售

5.5 下单购物

其它订单流程测试可参考第 3 章自行测试。

(end)

相关推荐
imDwAaY1 小时前
从感知机到 Attention:我用 PyTorch 打穿 CS188 机器学习终章 CS188 Proj5 学习笔记
人工智能·pytorch·笔记·python·学习·机器学习
龙萱坤诺2 小时前
无限画布 + gpt-image-2:用智狐AI工作台把AI草图直接拖进排版区
人工智能·ai短剧·无限画布
马***41110 小时前
适配成人英语学习痛点,打造落地性强的学习辅助方式
人工智能·学习
夜焱辰10 小时前
浏览器端 Agent 的文件版本管理:不用 Git,基于 OPFS + SQLite 自己造了一个
前端·人工智能
Ricky055310 小时前
CTRL-WORLD:一种用于机器人操控的可控生成世界模型(中美2025年联合研究)
人工智能·机器人·世界模型
jeffer_liu10 小时前
Spring AI 生产级实战:工具调用
java·人工智能·后端·spring·ai编程
阿乔外贸日记10 小时前
2026尼日利亚五项清关政策更新,拉高能源装备进口综合成本
大数据·人工智能·搜索引擎·智能手机·云计算·能源
民乐团扒谱机10 小时前
【AI笔记】短时纯音时长对音高感知偏移效应研究综述
人工智能·笔记
侃谈科技圈11 小时前
破除数据中台落地困境:2026数据治理平台差异化能力与选型决策指南
大数据·人工智能