你在做电商后台,用户上传一张商品照片就要自动提取名称、型号、颜色;你在做客服系统,用户随手发了张订单截图就让机器人看懂"已发货"几个字在哪行。纯文字大模型做不到这些。本文用 Spring AI 2.0 的 GPT-4o / Claude 视觉模型,教你用
UserMessage+Media把图片一并丢给大模型,从输入图片到返回结构化 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 通过视觉编码器(比如 ViT,Vision 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(...)——一个用户消息可以同时带文字和一张或多张图片。
为什么不直接走 OCR?OCR(光学字符识别,从图片里识别文字的技术)只能提取图片里的字,理解不了布局、语义和"这张图想表达什么"。比如同一张订单截图,OCR 能吐出所有文字,但分不清哪行是金额哪行是地址。多模态模型看整张图,结合你的问题做推理,能直接给出"金额是 299 元、状态已发货"这种结构化结论。当然 OCR 速度快、成本低、不出幻觉,两者各有适用场景,第六节专门对比。
三、实战:手把手写代码
环境:JDK 21 + Spring Boot 4.1.1 + Spring AI 2.0.1(版本均为写文章时最新 GA,已用 Maven Central 实查确认)。
POM 依赖(pom.xml):
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>4.1.1</version><relativePath/></parent><properties><java.version>21</java.version><spring-ai.version>2.0.1</spring-ai.version></properties><dependencyManagement><dependencies><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-bom</artifactId><version>${spring-ai.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Spring AI 2.0.x 的 OpenAI starter --><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-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.yml):
spring: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.chatModel=chatModel;}publicStringdescribeImage(StringclasspathPath)throwsException{// 1. 读取 classpath 下的图片文件成字节数组byte[]imageBytes=newClassPathResource(classpathPath).getInputStream().readAllBytes();// 2. 用 MimeType 告诉框架这是 PNG 图片Mediamedia=newMedia(newMimeType("image","png"),imageBytes);// 3. 构造一条"既带文字又问图片"的用户消息UserMessagemessage=UserMessage.builder().text("请用一句话描述这张图片的主要内容。").media(media).build();// 4. 和普通文本对话一样调用ChatResponseresponse=chatModel.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.basicVisionService=basicVisionService;}@GetMapping("/vision/describe")publicStringdescribe(@RequestParamStringimagePath)throwsException{returnbasicVisionService.describeImage(imagePath);}}把图片放到src/main/resources,启动后访问http://localhost:8080/vision/describe?imagePath=cat.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.chatModel=chatModel;}// POJO:字段名决定了模型要提取哪些属性publicrecordProductInfo(Stringname,Stringcolor,Stringprice){}publicProductInfoextractProduct(StringclasspathPath)throwsException{byte[]imageBytes=newClassPathResource(classpathPath).getInputStream().readAllBytes();Mediamedia=newMedia(newMimeType("image","jpeg"),imageBytes);// 用 BeanOutputConverter 声明期望的输出类型BeanOutputConverter<ProductInfo>converter=newBeanOutputConverter<>(ProductInfo.class);UserMessagemessage=UserMessage.builder().text("你是电商商品录入助手。请仔细查看这张商品图,"+"提取商品的名称、颜色和价格。"+converter.getFormat()).media(media).build();ChatResponseresponse=chatModel.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.chatModel=chatModel;}publicStringcompare(StringpathA,StringpathB)throwsException{MediaimageA=newMedia(newMimeType("image","png"),newClassPathResource(pathA).getInputStream().readAllBytes());MediaimageB=newMedia(newMimeType("image","png"),newClassPathResource(pathB).getInputStream().readAllBytes());UserMessagemessage=UserMessage.builder().text("第一张是用户上传的订单截图,第二张是系统记录。"+"请对比两张图,找出金额、商品、状态不一致的地方,逐条列出。").media(imageA,imageB)// 一次传多张图.build();ChatResponseresponse=chatModel.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.chatModel=chatModel;}publicStringdescribeRemote(StringimageUrl){// data 传 URI,而不是字节数组Mediamedia=newMedia(newMimeType("image","webp"),URI.create(imageUrl));UserMessagemessage=UserMessage.builder().text("这张图是网站页面截图,请提取页面标题和导航菜单项。").media(media).build();ChatResponseresponse=chatModel.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:
| 维度 | 多模态 LLM(GPT-4o 等) | OCR + 文本 LLM |
|---|---|---|
| 理解能力 | 看懂布局、语义、图里非文字信息 | 只能读出纯文字,不理解语义 |
| 开发成本 | 高,一套代码全图通用 | 高,OCR 工具 + 文本 LLM 两套集成 |
| 单次成本 | 中,按图片 token 计费 | 低,OCR 便宜 + 文本 token 便宜 |
| 幻觉风险 | 有,可能编造图中没有的信息 | 低,OCR 只输出图上真实文字 |
| 速度 | 中等(秒级) | 快(毫秒级) |
| 适用场景 | 复杂截图、商品图理解、多图对比 | 纯文字提取、大批量、低预算 |
怎么选:
- 只要"读出图里的文字"(发票号码、证件号)→ 用 OCR,便宜快,别上大模型。
- 要"看懂图的内容并推理"(这是什么商品、金额在哪、两个图差异)→ 用多模态 LLM,省心且理解力强。
- 大批量场景可混合:先用 OCR 快速过滤,只对 OCR 拿不准的图调多模态,平衡成本和准确率。
实测提示:商品图属性提取用gpt-4o-mini性价比很高,识别准、成本低,适合做 MVP;量大再考虑蒸馏或换更便宜的小模型。
六、总结
Spring AI 2.0 把多模态封装得和普通文本对话几乎一样,核心就是UserMessage+Media两个类:Media负责"包装一张图"(MIME 类型 + 字节或 URL),UserMessage.builder().text(...).media(...)负责"文字和图片一起发给模型",最后chatModel.call()照旧返回结果。再配合BeanOutputConverter,能把模型输出直接变成强类型对象,适合商品属性提取、订单截图识别这类工程化需求。
上手就记住四件事:模型要选支持视觉的(gpt-4o 系);大图先压缩(省钱且快);多图要编号(否则模型分不清);识别任务 temperature 调低(减少乱编)。至于是不是该用多模态,先判断需求是"只读文字"(用 OCR)还是"要理解图的意思"(用多模态)——后一种才是它的主场。
从一张图到结构化 JSON,一条消息、两行核心代码,剩下的交给一个看得懂图的模型。