你在做电商后台用户上传一张商品照片就要自动提取名称、型号、颜色你在做客服系统用户随手发了张订单截图就让机器人看懂已发货几个字在哪行。纯文字大模型做不到这些。本文用 Spring AI 2.0 的 GPT-4o / Claude 视觉模型教你用UserMessageMedia把图片一并丢给大模型从输入图片到返回结构化 JSON四个可复制代码示例走完全流程并对比本地 OCR LLM方案讲清该不该用它。一、这个问题到底是什么传统大模型是文本进、文本出你让它分析图片它只会说我看不到图片。多模态Multimodal模型则同时接受文字 图片 音频作为输入把图片像素编码成特征向量和文字一起交给 Transformer 处理最后照样输出文字或结构化数据。视觉Vision就是多模态里最常用的能力——让模型看懂图里的内容。实际业务里看懂图片的需求到处都是电商要自动识别商品图提取属性、客服要读懂用户上传的截图、财务要识别发票、风控要识别验证码。如果全靠人力转文字再交给大模型费时费力还容易出错。Spring AI 2.0 把多模态封装成了ChatModel的一个能力你只要把图片作为Media加到UserMessage里剩下的base64 编码、MIME 类型、请求构造、图像预处理全部由框架处理和调用普通文本对话的代码几乎一模一样。本文解决的正是这件事如何用 Spring AI 2.0 让大模型看懂一张图并把结果解析成可用的 JSON。你会看到完整的工程化写法——从配置模型、封装图片消息、到用结构化输出拿到可靠结果每一步都有可直接运行的代码。二、底层原理到底怎么回事多模态模型相比纯文本模型最大的差别在输入端。普通模型把文字拆成 token词元查词表得到每段的向量表示。多模态模型多了一个视觉编码器Vision Encoder图片先被缩放、切块成固定大小的 patch每个 patch 通过视觉编码器比如 ViTVision Transformer转成向量。这些图像向量和文字 token 的向量拼在一起一起喂进 Transformer 主网络。所以对模型来说图片不再是一堆像素而是和文字同一种语言的向量序列——这就是它能同时理解图和你问的话的原因。Spring AI 2.0 在底层替你做了三件事编码与 MIME 识别你传一个字节数组 MIME 类型如image/png框架知道这是 PNG 图片而不是文字。请求格式转换对 OpenAI 系模型图片会被转成 base64 字符串放进消息的content_part里对应 OpenAI 的图片消息协议对 Claude、Gemini协议又不一样。框架把这些差异全屏蔽了你写的代码和文本对话几乎一样。模型能力路由能不能传图片取决于模型。框架不拦你但你选了不支持的模型请求会失败或模型瞎编。关键的抽象就是两个类Media封装一段多媒体内容核心是MimeType告诉模型这是什么类型data图片的字节数组或 URI。UserMessage人类用户的消息。它是MediaContent接口的实现所以除了text()还能挂media(...)——一个用户消息可以同时带文字和一张或多张图片。为什么不直接走 OCROCR光学字符识别从图片里识别文字的技术只能提取图片里的字理解不了布局、语义和这张图想表达什么。比如同一张订单截图OCR 能吐出所有文字但分不清哪行是金额哪行是地址。多模态模型看整张图结合你的问题做推理能直接给出金额是 299 元、状态已发货这种结构化结论。当然 OCR 速度快、成本低、不出幻觉两者各有适用场景第六节专门对比。三、实战手把手写代码环境JDK 21 Spring Boot 4.1.1 Spring AI 2.0.1版本均为写文章时最新 GA已用 Maven Central 实查确认。POM 依赖pom.xmlparentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion4.1.1/versionrelativePath//parentpropertiesjava.version21/java.versionspring-ai.version2.0.1/spring-ai.version/propertiesdependencyManagementdependenciesdependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-bom/artifactIdversion${spring-ai.version}/versiontypepom/typescopeimport/scope/dependency/dependencies/dependencyManagementdependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency!-- Spring AI 2.0.x 的 OpenAI starter --dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-starter-model-openai/artifactId/dependency/dependencies注意Spring AI 2.0.x 的 starter 名字是spring-ai-starter-model-openai不是旧版的spring-ai-openai-spring-boot-starter。社区里大量老文章用的旧名字在 2.0 里会直接编译不过。starter 引入后框架会自动注入一个OpenAiChatModelBean我们拿来即用。配置application.ymlspring:ai:openai:api-key:${OPENAI_API_KEY}# 你的 API Key放环境变量别写死chat:options:model:gpt-4o-mini# 支持视觉的模型temperature:0.2# 低温度识别类任务要稳定输出只配了api-key和model。temperature设低因为识别图片是客观提取任务不需要创造性。图片不能走流式接口所以每个请求走完整的call()同步调用。示例 1基础——把一张图片交给大模型描述这段代码演示最核心的用法读本地图片 → 包成Media→ 加进UserMessage→ 调chatModel.call()。看懂它其余示例都是它的变体。packagecom.example.vision;importorg.springframework.ai.chat.messages.UserMessage;importorg.springframework.ai.chat.model.ChatModel;importorg.springframework.ai.chat.model.ChatResponse;importorg.springframework.ai.chat.prompt.Prompt;importorg.springframework.ai.content.Media;importorg.springframework.core.io.ClassPathResource;importorg.springframework.stereotype.Service;importorg.springframework.util.MimeType;ServicepublicclassBasicVisionService{// 框架自动注入的视觉模型privatefinalChatModelchatModel;publicBasicVisionService(ChatModelchatModel){this.chatModelchatModel;}publicStringdescribeImage(StringclasspathPath)throwsException{// 1. 读取 classpath 下的图片文件成字节数组byte[]imageBytesnewClassPathResource(classpathPath).getInputStream().readAllBytes();// 2. 用 MimeType 告诉框架这是 PNG 图片MediamedianewMedia(newMimeType(image,png),imageBytes);// 3. 构造一条既带文字又问图片的用户消息UserMessagemessageUserMessage.builder().text(请用一句话描述这张图片的主要内容。).media(media).build();// 4. 和普通文本对话一样调用ChatResponseresponsechatModel.call(newPrompt(message));returnresponse.getResult().getOutput().getText();}}关键点第 2 行new MimeType(image, png)声明图片格式框架据此选择正确的编码方式PNG 用 base64也可能转换成模型需要的格式。第 3 步的UserMessage.builder()同时设置文字和图片这就是多模态在代码里的样子——一条消息图话一起给。第 4 步chatModel.call()和普通文本调用完全相同说明多模态对上层代码几乎零侵入。写一个Controller测试packagecom.example.vision;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RequestParam;importorg.springframework.web.bind.annotation.RestController;RestControllerpublicclassVisionController{privatefinalBasicVisionServicebasicVisionService;publicVisionController(BasicVisionServicebasicVisionService){this.basicVisionServicebasicVisionService;}GetMapping(/vision/describe)publicStringdescribe(RequestParamStringimagePath)throwsException{returnbasicVisionService.describeImage(imagePath);}}把图片放到src/main/resources启动后访问http://localhost:8080/vision/describe?imagePathcat.png就能看到模型描述的图片内容。示例 2结构化输出——从商品图提取属性成 JSON示例 1 返回的是自由文本落到系统里不好处理。这里结合 Spring AI 2.0 的结构化输出BeanOutputConverter让模型直接返回一个ProductInfo对象。packagecom.example.vision;importorg.springframework.ai.chat.messages.UserMessage;importorg.springframework.ai.chat.model.ChatModel;importorg.springframework.ai.chat.model.ChatResponse;importorg.springframework.ai.chat.prompt.Prompt;importorg.springframework.ai.content.Media;importorg.springframework.ai.converter.BeanOutputConverter;importorg.springframework.core.io.ClassPathResource;importorg.springframework.stereotype.Service;importorg.springframework.util.MimeType;importjava.util.List;ServicepublicclassProductVisionService{privatefinalChatModelchatModel;publicProductVisionService(ChatModelchatModel){this.chatModelchatModel;}// POJO字段名决定了模型要提取哪些属性publicrecordProductInfo(Stringname,Stringcolor,Stringprice){}publicProductInfoextractProduct(StringclasspathPath)throwsException{byte[]imageBytesnewClassPathResource(classpathPath).getInputStream().readAllBytes();MediamedianewMedia(newMimeType(image,jpeg),imageBytes);// 用 BeanOutputConverter 声明期望的输出类型BeanOutputConverterProductInfoconverternewBeanOutputConverter(ProductInfo.class);UserMessagemessageUserMessage.builder().text(你是电商商品录入助手。请仔细查看这张商品图提取商品的名称、颜色和价格。converter.getFormat()).media(media).build();ChatResponseresponsechatModel.call(newPrompt(message));// 把模型返回的文本解析成 ProductInfo 对象returnconverter.convert(response.getResult().getOutput().getText());}}关键点ProductInfo是 Java 21 的 record字段名name/color/price就是模型要输出哪些字段的约定。converter.getFormat()会在提示词里追加一段 JSON 格式说明告诉模型必须返回符合这个类型的 JSON。最后converter.convert(...)把模型的文本 JSON 反序列化成ProductInfo。这样下游代码拿到的是强类型对象不是一堆字符串。示例 3多图对比——让模型对比几张图的差异多模态不止能处理一张图media(...)支持多个Media。这个示例演示让模型对比两张图比如用户的订单截图 vs 系统截图。packagecom.example.vision;importorg.springframework.ai.chat.messages.UserMessage;importorg.springframework.ai.chat.model.ChatModel;importorg.springframework.ai.chat.model.ChatResponse;importorg.springframework.ai.chat.prompt.Prompt;importorg.springframework.ai.content.Media;importorg.springframework.core.io.ClassPathResource;importorg.springframework.stereotype.Service;importorg.springframework.util.MimeType;importjava.util.List;ServicepublicclassMultiImageService{privatefinalChatModelchatModel;publicMultiImageService(ChatModelchatModel){this.chatModelchatModel;}publicStringcompare(StringpathA,StringpathB)throwsException{MediaimageAnewMedia(newMimeType(image,png),newClassPathResource(pathA).getInputStream().readAllBytes());MediaimageBnewMedia(newMimeType(image,png),newClassPathResource(pathB).getInputStream().readAllBytes());UserMessagemessageUserMessage.builder().text(第一张是用户上传的订单截图第二张是系统记录。请对比两张图找出金额、商品、状态不一致的地方逐条列出。).media(imageA,imageB)// 一次传多张图.build();ChatResponseresponsechatModel.call(newPrompt(message));returnresponse.getResult().getOutput().getText();}}关键点.media(imageA, imageB)说明UserMessage能挂多张图。注意提示词里要编号“第一张……第二张……”否则模型分不清哪张是哪张这是多图场景最容易踩的坑。示例 4图片 URL——不用下载直接传远程图很多时候图片不在本地而在 URL。Spring AI 也支持直接传 URL 格式的Media框架替你去拉取。packagecom.example.vision;importorg.springframework.ai.chat.messages.UserMessage;importorg.springframework.ai.chat.model.ChatModel;importorg.springframework.ai.chat.model.ChatResponse;importorg.springframework.ai.chat.prompt.Prompt;importorg.springframework.ai.content.Media;importorg.springframework.stereotype.Service;importorg.springframework.util.MimeType;importjava.net.URI;ServicepublicclassRemoteImageService{privatefinalChatModelchatModel;publicRemoteImageService(ChatModelchatModel){this.chatModelchatModel;}publicStringdescribeRemote(StringimageUrl){// data 传 URI而不是字节数组MediamedianewMedia(newMimeType(image,webp),URI.create(imageUrl));UserMessagemessageUserMessage.builder().text(这张图是网站页面截图请提取页面标题和导航菜单项。).media(media).build();ChatResponseresponsechatModel.call(newPrompt(message));returnresponse.getResult().getOutput().getText();}}关键点Media的data既可以是byte[]也可以是URI。传 URL 时框架自己下载。但要注意如果图片 URL 需要鉴权带 token 的内网图传 URL 会失败此时应该先下载成字节数组再传用示例 1 的方式。四、踩坑经验和最佳实践模型必须支持视觉。gpt-4o-mini、gpt-4o、claude-sonnet支持gpt-3.5不支持传图会报错或瞎编。配好后先用最简单的描述图片接口测通再写业务。图片太大先压缩。多模态模型按图片的 token 计费大图既贵又慢。上图前先压缩到合理尺寸比如最长边 1024px、转成 JPEG。一个 2000px 大图和 800px 小图识别效果差不多成本能差好几倍。图片类型别写错。new MimeType(image, png)里png、jpeg、webp要和你真实图片匹配。传错类型模型可能把字节当垃圾处理。程序里最好根据文件名后缀或文件头判断别硬编码。多图必须编号。示例 3 里强调过多张图一起传时提示词不用第一张第二张标注模型很容易跑偏。这是多图场景最高频的失败原因。结构化输出失败要有兜底。converter.convert()遇到模型输出不合 JSON 语法会抛异常。生产环境要 try-catch 并重试一次重试时把上次失败的输出塞进提示词让模型改正或退化为返回原始文本让人工处理。别假设模型 100% 遵守格式。API Key 绝不写死。application.yml里用${OPENAI_API_KEY}占位Key 放环境变量或配置中心。日志里也不要打印完整 Key。远程图鉴权问题。示例 4 提到需要带鉴权 header 才能下载的图框架拉不到。要么提前下载好传字节数组要么用一个轻量 HTTP 客户端带上 token 自己拉。识别类任务把 temperature 调低。识别/提取是客观任务temperature: 0~0.2能显著减少模型发挥导致的乱编。描述性/开放性任务才需要高 temperature。五、性能对比和技术选型多模态 LLM vs 传统 OCR 文本 LLM维度多模态 LLMGPT-4o 等OCR 文本 LLM理解能力看懂布局、语义、图里非文字信息只能读出纯文字不理解语义开发成本高一套代码全图通用高OCR 工具 文本 LLM 两套集成单次成本中按图片 token 计费低OCR 便宜 文本 token 便宜幻觉风险有可能编造图中没有的信息低OCR 只输出图上真实文字速度中等秒级快毫秒级适用场景复杂截图、商品图理解、多图对比纯文字提取、大批量、低预算怎么选只要读出图里的文字发票号码、证件号→ 用 OCR便宜快别上大模型。要看懂图的内容并推理这是什么商品、金额在哪、两个图差异→ 用多模态 LLM省心且理解力强。大批量场景可混合先用 OCR 快速过滤只对 OCR 拿不准的图调多模态平衡成本和准确率。实测提示商品图属性提取用gpt-4o-mini性价比很高识别准、成本低适合做 MVP量大再考虑蒸馏或换更便宜的小模型。六、总结Spring AI 2.0 把多模态封装得和普通文本对话几乎一样核心就是UserMessageMedia两个类Media负责包装一张图MIME 类型 字节或 URLUserMessage.builder().text(...).media(...)负责文字和图片一起发给模型最后chatModel.call()照旧返回结果。再配合BeanOutputConverter能把模型输出直接变成强类型对象适合商品属性提取、订单截图识别这类工程化需求。上手就记住四件事模型要选支持视觉的gpt-4o 系大图先压缩省钱且快多图要编号否则模型分不清识别任务 temperature 调低减少乱编。至于是不是该用多模态先判断需求是只读文字用 OCR还是要理解图的意思用多模态——后一种才是它的主场。从一张图到结构化 JSON一条消息、两行核心代码剩下的交给一个看得懂图的模型。