先把结论放前面:Spring AI 的多模态能力,并不只是“给模型传一张图”那么简单。真正把它接入业务时,你会遇到消息结构、图片格式、上下文保留、结构化输出等一系列问题,这些问题单看文档容易踩坑,实际跑一遍才能体会到。
这篇写的是多模态系列里的第四种 API——视觉理解,也就是同时把文本和图片交给模型,让它看图回答问题。前几篇分别整理过图像生成、语音转文字和语音合成,这篇轮到视觉理解,正好把 Spring AI 里四类常见多模态能力补全。文章会从定位和原理讲起,再给出完整可跑的 Spring Boot 实现,最后是高频问题和排查方式。
适合谁看?已经用 Spring AI 跑通过普通文本对话,现在想把图片输入加进项目里的开发者。看完你应该能独立写出一个支持“文字+图片”问答的服务接口。
1. 先弄懂第四多模态API在Spring AI里的定位
1.1 多模态能力全景梳理
Spring AI 作为一套面向大模型应用的抽象层,把市面上主流模型的能力收敛成了几个统一入口。站在项目接入的角度看,常见多模态能力大致可以分成四个方向:
| 能力方向 | 输入 | 输出 | 典型场景 |
|---|---|---|---|
| 图像生成 | 文本提示词 | 图片 | 海报生成、配图生产 |
| 语音转文字 | 音频文件 | 文本 | 会议转写、客服质检 |
| 语音合成 | 文本 | 音频 | 语音播报、虚拟人 |
| 视觉理解 | 文本+图片 | 文本 | 图片问答、截图提取信息、审核辅助 |
前三种相对容易理解:输入和输出之间是“单向转换”。视觉理解看着也简单,但真正落地时它是四类里少有的“双向交叉”场景——图片内容影响回答,文本问题又限制回答方向,两者必须同时在一次请求里发给模型。
这也是我把它排到第四篇才写的原因。前三种多模态 API 只要处理好“把媒体数据塞进请求”就行,视觉理解还涉及消息的 content 结构拆分、提示词策略、上下文管理等更抽象的问题。
1.2 视觉理解解决的核心问题
视觉理解解决的核心问题只有一个:让模型读取图片里的非结构化信息,再输出结构化内容。
这句话听起来平淡,实际业务价值很大。比如:
- 用户上传一张商品图片,系统自动生成描述文案和标签
- 截图里包含表格数据,直接让模型提取成 JSON
- 设计稿自动转成页面代码时的 UI 描述生成
- 审核系统里识别图片中是否有违规元素
这些场景的共同点是,图片不是给人看的,而是给程序用的。程序要拿到图片里的信息,以前必须靠人工标注或者写一堆 OCR 和规则逻辑,现在直接问模型就行。
和纯文本对话相比,视觉理解的核心差异在于“模态融合”。文本消息里只有纯字符串,视觉理解的消息必须同时携带文字指令和图片数据。Spring AI 对这个过程的封装做得比较干净:你只要按它的消息模型组织好内容,底层会把图片转换并拼装成模型 API 认可的格式。
提示:视觉理解不是“把图片描述一遍”这个简单功能。它在工程上的用法,更多是作为一个“信息抽取器”,把图片变成下游程序能消费的结构化数据。
2. 依赖配置与消息结构:视觉理解前的必修课
2.1 依赖引入与版本选择
我建议直接基于 Spring Boot 3.2 / 3.3 工程来加依赖。Spring AI 的场景化 starter 已经做得比较完善,不需要手动拼底层客户端。
Maven 引入方式是在pom.xml里加 BOM 和对应 starter:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>然后在 dependencies 区域加入 model-chat 的通用 starter:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-chat</artifactId> </dependency>这个通用 starter 在 Spring AI 1.0 里是推荐的接入方式,它不绑定特定厂商,而是通过 base-url 和 api-key 指向你实际使用的模型网关或云端服务。好处很明显:以后从一家服务商切换到另一家,业务代码基本不用动。
2.2 关键配置项说明
接入视觉理解时,application.yml里核心配置如下:
spring: application: name: multimodal-vision-demo ai: chat: base-url: ${AI_BASE_URL:http://localhost:8080/v1} api-key: ${AI_API_KEY:sk-local-demo} options: model: ${AI_VISION_MODEL:multimodal-latest} temperature: 0.2 max-tokens: 1024几个参数背后的原理要理解清楚:
base-url:指向兼容大模型协议的服务地址。如果本地起了网关代理,填网关地址;如果直接用云端服务,填云端地址。这个地址必须支持多模态消息格式。api-key:身份凭证,建议用环境变量注入,不要提交到代码仓库。model:指定模型实例。注意,不是所有模型都支持图片输入,模型名字里通常能看到视觉、多模态这类标识。务必替换成你实际账号可用的视觉模型名。temperature:随机性控制。视觉理解任务大多数是信息提取场景,温度可以调低一些,建议 0.1 到 0.3 之间,减少模型自由发挥。max-tokens:限制返回最大长度,避免一次请求返回超长无效内容造成成本浪费。
2.3 理解多模态消息的内容结构
很多人的第一反应是把图片塞到 prompt 字符串里,这是不对的。视觉理解的消息必须按结构化对象组织,文本和图片各自作为 content 的一部分。
模型 API 看到的实际请求结构类似这样:
{ "model": "multimodal-latest", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "描述这张图片里有什么" }, { "type": "image_url", "image_url": { "url": "https://example.com/image.png" } } ] } ] }也就是说,user 角色的消息里,content 不再是单个字符串,而是一个数组,里面可以交替放文本块和图片块。图片块既可以放一个公网图片链接,也可以放 base64 编码后的图片数据。
Spring AI 给你封装好了对应的消息模型。你不需要自己拼 JSON,而是要会用它的UserMessage和Media类型。
注意:消息结构是视觉理解最常见的翻车点。如果你自己拼接字符串传图片,八成会收到模型服务端的格式错误;正确做法是让
UserMessage来承载文本与媒体列表。
3. 手把手实现图片问答服务与结构化输出
3.1 初始化 ChatClient
进入代码阶段。Spring AI 面向业务开发的主入口是ChatClient,它把 prompt、消息组装、调用模型、解析响应串成一条链。
先注册一个 Bean:
import org.springframework.ai.chat.client.ChatClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class AiConfig { @Bean ChatClient chatClient(ChatClient.Builder builder) { return builder.build(); } }如果你的项目里暂时不需要调用多个模型实例,用默认构建方式就够了。直接注入ChatClient到业务类即可。
3.2 URL 图片 + 文本的问答接口
场景一:图片已经放在公网 URL 上。你只需要构造Media对象并加进UserMessage。
代码如下:
import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.Media; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import java.net.URL; import java.util.List; @Service public class VisionService { private final ChatClient chatClient; public VisionService(ChatClient chatClient) { this.chatClient = chatClient; } public String describeImage(String instruction, String imageUrl) throws Exception { Media imageMedia = new Media(MediaType.IMAGE_PNG, new URL(imageUrl)); UserMessage userMessage = new UserMessage( instruction, List.of(imageMedia) ); return chatClient.prompt(userMessage) .call() .content(); } }这里要做三点说明:
第一,Media的构造函数在不同版本里可能有差异。早期版本接收MimeType和Object data,新一点版本可能提供 builder 风格。核心不变:mimeType 告诉模型图片格式,data 是图片资源来源。
第二,URL 图片适合模型服务端可以直接访问的场景。如果图片在本地网络,模型服务端拉不到,就不能用 URL,改成传图片字节。
第三,instruction要写成明确的指令,比如“用中文描述这张图片的内容,并列出主要物体”。不要只写“看看这张图”,那会让模型输出不可控。
3.3 本地图片转 Base64 传入
本地文件场景更常见。程序读文件、转字节、再交给Media:
import org.springframework.core.io.ClassPathResource; import org.springframework.http.MediaType; import java.io.InputStream; byte[] imageBytes = readImageBytes("sample.png"); Media imageMedia = new Media(MediaType.IMAGE_PNG, imageBytes); UserMessage userMessage = new UserMessage( "识别图中文字并输出为 JSON 列表", List.of(imageMedia) );readImageBytes的常规写法是读取资源文件或上传文件流,转成字节数组即可:
private byte[] readImageBytes(String path) throws Exception { ClassPathResource resource = new ClassPathResource(path); try (InputStream is = resource.getInputStream()) { return is.readAllBytes(); } }这里有一个细节:字节数组和 URL 字符串在底层会被 Spring AI 转成不同的请求格式。URL 字符串会作为图片地址下发,字节数组会转成 base64 内嵌进请求体。这个转换过程程序自动处理,但你心里要有概念,否则排查请求体过大问题时无从下手。
注意:本地图片转字节传入时,mimeType 必须和文件格式一致。png 图填
image/jpeg,一部分模型服务端不会主动纠正,会直接报格式错误。
3.4 多图输入与多轮对话
视觉理解也支持同时传多张图片。做法很简单,把所有图片塞进一个Media列表:
List<Media> mediaList = List.of( new Media(MediaType.IMAGE_JPEG, firstImageBytes), new Media(MediaType.IMAGE_JPEG, secondImageBytes) ); UserMessage userMessage = new UserMessage( "对比这两张图中的差异,输出差异点清单", mediaList );多个图片在消息里的顺序是敏感的。模型按消息序列感知图片先后,“图一”“图二”这种指代在提示词里要写清楚,否则模型容易混淆。
多轮对话方面,Spring AI 的ChatMemory可以保存历史消息。但建议注意一点:很多模型服务端在处理历史消息时,对图片历史的支持比文本历史弱。常见做法是只把用户文本问题和模型文本回答存入记忆,图片数据不带入历史。这样既能保持上下文,又避免请求体膨胀和部分平台对图片历史的兼容问题。
3.5 结构化输出:让模型返回 JSON 而不是散文
信息提取类场景里,直接调.content()会得到一段自然语言文本,这对接程序不友好。更推荐用.entity()方法做结构化映射。
先定义一个输出结构:
public record ImageAnalysisResult( String summary, java.util.List<String> objects, java.util.List<String> ocrText, boolean containsPeople ) { }调用方式:
ImageAnalysisResult result = chatClient.prompt(userMessage) .call() .entity(ImageAnalysisResult.class);底层逻辑是 Spring AI 在请求里附加了“以指定 JSON 结构返回”的约束,再把模型输出反序列化成对象。这样一来,下游程序可以直接用result.ocrText()拿到识别文字列表,而不是自己解析一段自然语言。
提示:结构化输出是视觉理解落地最关键的技巧。纯文本回答适合人看,不适合程序。加上
entity()之后,图片识别结果才能真正进入业务逻辑。
3.6 完整服务层参考
把上面几段组合成一个稍完整的服务类:
@Service public class VisionQaService { private final ChatClient chatClient; public VisionQaService(ChatClient chatClient) { this.chatClient = chatClient; } public ImageAnalysisResult analyzePng(byte[] imageBytes, String question) { Media media = new Media(MediaType.IMAGE_PNG, imageBytes); UserMessage message = new UserMessage(question, List.of(media)); return chatClient.prompt(message) .call() .entity(ImageAnalysisResult.class); } }配合一个简单的 Controller 暴露接口:
@RestController @RequestMapping("/api/vision") public class VisionController { private final VisionQaService visionQaService; public VisionController(VisionQaService visionQaService) { this.visionQaService = visionQaService; } @PostMapping(value = "/analyze", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ImageAnalysisResult analyze( @RequestPart("file") MultipartFile file, @RequestParam("question") String question) throws IOException { return visionQaService.analyzePng( file.getBytes(), question ); } }到这里,一个可以接受图片文件和文本问题、返回结构化分析结果的接口就完整了。
4. 踩坑实录:视觉理解高频问题排查清单
4.1 模型不支持图片输入
现象:调用返回的错误明确提示当前模型不支持 image 或 vision 输入。
原因:选错了模型名。并非所有文本模型都带视觉能力。
排查思路:先到你的模型网关或云端控制台查看可用模型列表,找带 vision / multimodal / omni 标识的型号。很多平台是同一系列分不同版本,比如基础语言模型和视觉模型是两个独立模型名。
注意:不要只在配置里改一个参数名,要确认 base-url 对应的服务端确实支持多模态消息传输。某些本地网关可能还没开启图片透传。
4.2 图片 URL 拉取失败
现象:请求报超时,或模型返回“image not found”“URL 无法访问”。
原因:模型服务端在请求时需要主动访问图片 URL,如果你的图片地址是内网地址或带鉴权的临时链接,服务端根本拉不到。
排查思路:用浏览器或无痕窗口直接打开图片 URL,确认公网可访问。需要鉴权的图片链接,要么改造为带过期签名的公开链接,要么直接改用字节数组传入。
我实际使用时更偏向字节数组方案。它不需要考虑服务端网络,也不用担心 URL 过期,只是会占一点请求体大小。
4.3 请求体过大导致失败
现象:本地图片传进去后请求报 413,或模型服务端返回 payload too large。
原因:图片字节数组转 base64 后体积会增加约三分之一。一张 10MB 的图片,转完 base64 接近 14MB,加上 JSON 包装,很容易突破请求体上限。
处理方式:
- 限制上传文件大小,建议单张图 5MB 以内
- 服务端先压缩再送模型
- 长边缩放到 1024 或 2048 像素以内
图片缩放不一定要引入复杂的图像处理库,Java 自带ImageIO就能做基础缩放。压缩后再转字节,质量和体积平衡更好。
4.4 模型回答与图片无关、出现幻觉
现象:请求成功返回,但模型没按图片内容回答,而是顺着提示词编了一段。
原因:多数时候是提示词缺少约束。模型在信息不足时倾向于用常识补全,这在视觉任务里特别明显。
处理技巧:
- 明确要求“只根据图片内容回答”
- 如果图片没有相关信息,要求模型回复“未识别到相关信息”
- 用结构化输出把回答范围框住
比如指令写成“你是图片信息抽取器,严格从图片中提取信息,不要推测图片外内容”,模型跑偏概率会降低很多。
4.5 中文内容乱码或编码问题
现象:图片里的中文识别出来是乱码,或者返回内容出现奇怪的转义符。
原因:有几层可能性。一是上传时传错了 charset,但多数现代框架会正确处理 UTF-8;二是图片本身分辨率太低,中文小字识别不全;三是模型对低质量图片中文识别能力不足。
处理方式:优先检查原图分辨率,其次在提示词里要求“按 UTF-8 输出所有文本”,最后再考虑是不是模型本身 OCR 能力偏弱。真遇到后者,建议先接独立的 OCR 服务,把识别文本作为上下文传给模型,而不是直接把图丢给视觉模型。
4.6 API Key 或路由权限问题
现象:401 Unauthorized 或 403 Forbidden。
原因:api-key 配置不对,或者当前 key 没有视觉模型调用权限。
排查思路:
- 用 curl 直接请求一次多模态接口,验证 key 和模型名是否可用
- 确认 base-url 路径完整,有的服务地址需要以
/v1结尾 - 查看服务商后台的模型权限控制,部分平台需要单独开通视觉模型权限
这一步最关键的是排除干扰。大多数时候不是代码问题,而是最简单不过的配置问题。
我在实际接入中的体会是,视觉理解 API 调试时最好先用一张内容简单、文字清晰的测试图跑通最小链路,确认消息结构和密钥没有问题,再叠加结构化输出和复杂业务逻辑。先小后大,能省下大量盲改时间。
这个系列到第四篇,Spring AI 里常见的四类多模态能力算梳理完了。如果你接下来想在项目里落地多模态应用,我建议从视觉理解入手,因为它和文本对话共享大部分代码结构,改造门槛最低。后面有时间的话,我打算再写一篇多模态能力和向量检索结合的内容,把“图片转化为可检索的文本描述”这条链路串起来。