用户发了张图片,求指派怎么自动切到视觉模型?
本文目录 · 39 节
- 01、多模态到底是什么?
- 02、Spring AI 的多模态 API 怎么用?
- 03、求职派怎么处理用户发来的图片?
- 04、模型怎么知道该用视觉模型?
- 05、多供应商怎么适配?
- 06、用户偏好怎么隔离?
- 07、图片识别实战
- 08、求职派的岗位采集是怎么用多模态的?
- ending
- 01、多模态到底是什么?
- 02、Spring AI 的多模态 API 怎么用?
- 03、求职派怎么处理用户发来的图片?
- 04、模型怎么知道该用视觉模型?
- 05、多供应商怎么适配?
- 06、用户偏好怎么隔离?
- 07、图片识别实战
- 08、求职派的岗位采集是怎么用多模态的?
- ending
- 01、多模态到底是什么?
- 02、Spring AI 的多模态 API 怎么用?
- 03、求职派怎么处理用户发来的图片?
- 04、模型怎么知道该用视觉模型?
- 05、多供应商怎么适配?
- 06、用户偏好怎么隔离?
- 07、图片识别实战
- 08、求职派的岗位采集是怎么用多模态的?
- ending
星球:Java进阶之路&二哥编程星球 | 专栏:求职派 OpenClaw 架构 | 作者:沉默王二 | 发布:2026-07-28
用户发了张图片,求指派怎么自动切到视觉模型?
用户发了张图片,求指派怎么自动切到视觉模型?
Spring AI 对多模态的支持,API 层面其实很简单——构建一个 Media 对象,塞进 UserMessage,就完事了。
但到了生产环境,就有新的问题了。
用户发了一张图片过来,系统怎么知道该用视觉模型而不是文本模型?多个供应商都支持视觉能力,模型怎么选?不同用户想用不同的视觉模型,配置怎么隔离?
这篇内容就从 Spring AI 的多模态 API 讲起,再拆解求职派在它基础上做的三层设计——Media 构建、模型自动检测、用户偏好路由。读完你就能搞清楚,一个对接了微信、钉钉、飞书三个渠道的 Agent 系统,到底怎么处理用户发来的图片。技术派: https://paicoding.com/jobclaw-springai-multimodal
系好安全带,我们粗粗发~
01、多模态到底是什么?
先把概念理清楚。
多模态(Multimodal)指的是大模型能够同时处理多种类型的输入——文本、图片、音频、视频,然后生成自然语言的输出。

举个例子:用户在微信上给求职派发了一张招聘海报的截图,然后问“这个岗位适合我吗?”。大模型需要同时理解图片里的岗位信息和用户的文字提问,才能给出有意义的回答。
这就是多模态的典型场景,输入不是纯文本,而是文本 + 图片的组合。
目前支持多模态输入的主流模型包括智谱的 GLM-5V 系列、通义千问的 qwen3.7 系列等。求职派实际对接的多模态供应商是智谱和千问,所以后面的例子以这两家为主。
在 Spring AI 之前,要对接不同供应商的多模态能力,需要分别处理各家的 API 差异。
有的用 base64 编码传图,有的用 URL 引用,参数格式也各不相同。Spring AI 把这些差异屏蔽掉了,提供了统一的 Media 抽象,一套代码就能对接所有支持多模态的供应商。
02、Spring AI 的多模态 API 怎么用?
Spring AI 把多模态的接入抽象成了三个步骤,构建 Media、组装 UserMessage、发起调用。

第一步,构建 Media
Media 是 Spring AI 对媒体资源的统一封装,支持 builder 模式。
Java
Media media = Media.builder()
.mimeType(MimeTypeUtils.IMAGE_PNG)
.data(imageBytes)
.build();
mimeType 指定媒体类型(PNG、JPEG、PDF 等), data 传入字节数组。如果图片来自本地文件,也可以用 FileSystemResource。
Java
new Media(mimeType, new FileSystemResource(path.toFile()));
两种构造方式的区别在于数据来源。
字节数组适合网络下载的图片,文件资源适合本地存储的图片。
builder 模式下还有两个可选字段, name 用于标识媒体资源(批量处理时方便日志追踪), id 用于唯一标识(多轮对话中引用特定图片时使用)。
第二步,组装 UserMessage
把 Media 和文本提示词一起塞进 UserMessage。
Java
UserMessage msg = UserMessage.builder()
.text("识别这张图片中的食物,计算卡路里")
.media(media)
.build();
.media() 方法既接受单个 Media 对象,也接受 List。如果用户一次发了多张图片,传列表就行了。
第三步,发起调用
后面的流程和纯文本调用完全一样。
Java
Prompt prompt = new Prompt(msg);
String result = chatClient.prompt(prompt)
.call().content();
如果需要结构化返回,把 .content() 换成 .entity(FoodDetail.class) 即可。
Spring AI 会自动把模型的输出解析成 Java 对象。对于泛型类型(比如返回一个列表),需要用 ParameterizedTypeReference。
Java
.call().entity(
new ParameterizedTypeReference>() {});

API 层面就这么多,三步走完就能让大模型“看懂”图片了。
03、求职派怎么处理用户发来的图片?
API 简单不代表生产落地简单。
求职派同时对接了微信、钉钉、飞书三个 IM 渠道,用户可能通过任何一个渠道发图片过来。不同渠道传图片的方式不一样——微信给的是字节流,文件系统里存的是本地路径。
业务 Agent 在处理消息的时候,并不知道图片是从哪个渠道来的,也不应该关心。
求职派的做法是在 LLM 调用层统一处理媒体构建。核心逻辑在 BizAgentLlmCaller 的 buildUserMessage() 方法里。
Java
for (var image : message.getMedias()) {
Media media = createImageMedia(image);
if (media != null) {
mediaList.add(media);
}
}
这段代码遍历消息中携带的所有媒体资源,逐个转换成 Spring AI 的 Media 对象。
(源码位置: core/.../agent/llm/BizAgentLlmCaller.java,第 169-210 行)
转换的逻辑分两种情况。
Java
if (image.getData() != null && image.getData().length > 0) {
MimeType mimeType = MimeType.valueOf(image.getMimeType());
return new Media(mimeType, new ByteArrayResource(image.getData()));
} else if (image.getFilePath() != null) {
Path path = image.getFilePath();
MimeType mimeType = MimeType.valueOf(image.getMimeType());
return new Media(mimeType, new FileSystemResource(path.toFile()));
}
有字节数据的直接包装成 ByteArrayResource,有文件路径的包装成 FileSystemResource。不管图片从哪个渠道来,最终都变成了统一的 Media 对象。

这个设计的好处是业务 Agent 完全不需要关心媒体资源的来源。它拿到的永远是一个包含了文本和媒体的 UserMessage,剩下的事情交给 LLM 调用层处理。
04、模型怎么知道该用视觉模型?
这是多模态落地最关键的一环。
用户发纯文本消息时,系统应该用文本模型(比如 GLM-4.7-Flash);用户发了图片时,系统应该自动切换到视觉模型(比如 GLM-4V-Flash)。如果每个业务 Agent 都要自己判断、自己切模型,代码会非常冗余。
求职派的解法是在 LLM 调用器的 getClient() 方法里做自动检测。
Java
ModelConfig.ModelType model = ModelConfig.ModelType.TEXT;
if (prompt.getUserMessages().stream()
.anyMatch(m -> !CollectionUtils.isEmpty(m.getMedia()))) {
model = ModelConfig.ModelType.VISION;
}
var chatModel = (ChatModel) modelProviders.getModel(
user.jobClawUserId(), model);
(源码位置: core/.../agent/llm/SimpleLlmCaller.java,第 82-87 行)
逻辑简单,扫一遍 Prompt 里的所有用户消息,只要有任何一条消息携带了 Media,就把模型类型从 TEXT 切换到 VISION。

这意味着业务 Agent 在构建 Prompt 的时候完全不需要指定模型类型。消息里有图片,系统自动用视觉模型;没有图片,自动用文本模型。模型切换这件事,对业务层是透明的。
这个设计避免了一个很常见的问题——开发者忘记切模型。
如果模型切换由业务 Agent 手动控制,早晚有人会忘记处理图片场景,导致纯文本模型收到了图片输入,直接报错。自动检测机制从根本上消除了这类低级错误。
05、多供应商怎么适配?
模型类型确定了之后,还有一个问题。不同供应商对视觉模型的支持方式不一样,系统怎么适配?
求职派定义了 7 种模型类型,覆盖了目前主流的 AI 能力。
-
TEXT — 纯文本对话
-
VISION — 视觉理解(图片识别)
-
IMAGE — 图像生成
-
VIDEO — 视频处理
-
EMBEDDING — 向量嵌入
-
ASR — 语音识别
-
TTS — 语音合成
(源码位置: core/.../providers/ModelConfig.java,第 80-110 行)
每个供应商模块实现 ModelProvider 接口,通过 model() 方法处理不同的模型类型。以智谱供应商为例。
Java
return switch (info.getType()) {
case TEXT -> buildChatModel(info);
case VISION -> buildChatModel(info);
case IMAGE -> buildImageModel(info);
case EMBEDDING -> buildEmbeddingModel(info);
default -> throw new IllegalArgumentException(...);
};
(源码位置: providers/zhipu/.../ZhiPuModelProvider.java,第 66-73 行)
这里有个细节,智谱的 TEXT 和 VISION 走的是同一个 buildChatModel() 方法,因为智谱的视觉模型和文本模型使用同一套 API 协议,区别只在于模型名称( GLM-4.7-Flash vs GLM-4V-Flash)。

新增一个供应商的时候,只需要实现对应的模型类型支持就行了,不需要修改任何业务代码。
这就是 ModelProvider 接口作为扩展点的价值——供应商之间的 API 差异被封装在各自的模块内部,对外暴露的是统一的 Model 接口。
06、用户偏好怎么隔离?
最后一层设计是用户级别的模型偏好。
求职派是一个多用户系统,不同用户可能配置了不同的模型供应商和 API Key。
用户 A 用智谱的 glm-4.6v-flash,用户 B 用通义千问的 qwen-omni-turbo,系统需要按用户维度隔离模型选择。
这个能力由 ModelProviders.getModel() 方法实现。
Java
var preferModel = switch (modelType) {
case TEXT -> preference.getModels().getText();
case VISION -> preference.getModels().getVision();
case IMAGE -> preference.getModels().getImage();
// ...
};
(源码位置: core/.../providers/ModelProviders.java,第 103-111 行)
每个用户的偏好配置中都有一个 vision 字段,格式是 provider#modelName(例如 zhipu#GLM-4V-Flash)。系统先查用户自己的配置,查不到就回退到全局默认配置。

整条流程串起来就是这样。
用户发了一张图片 → LLM 调用器检测到 Media,模型类型设为 VISION → 查用户偏好找到视觉模型配置 zhipu#GLM-4V-Flash → 解析出供应商和模型名 → 智谱供应商构建对应的 ChatModel 实例 → 发起调用。
业务 Agent 全程只管一件事,把用户的消息和图片组装成 Prompt,然后调用 call() 方法。模型选择、供应商路由、偏好降级,全部在底层自动完成。
07、图片识别实战
理解了底层机制,我们来看一个完整的实战例子。
接收一张食物图片的 URL,调用视觉模型识别图片内容,返回食物名称和卡路里估算。
先定义结构化输出的数据模型。
Java
public record FoodDetail(
@JsonPropertyDescription("整张图片的描述")
String desc,
@JsonPropertyDescription("总的卡路里")
Double totalCalorie,
@JsonPropertyDescription("图片中的食材列表")
List itemList) {}
@JsonPropertyDescription 注解会被 Spring AI 自动提取,生成 JSON Schema 传给大模型,引导模型按指定格式返回。
然后写一个接口,完成从下载图片到调用模型的全流程。
Java
@RequestMapping(path = "recognitionAndOutput")
public FoodDetail recognitionAndOutput(
@RequestParam(name = "imgUrl") String imgUrl,
@RequestParam(name = "msg") String msg) {
byte[] imgs = HttpUtil.downloadBytes(imgUrl);
Media media = Media.builder()
.mimeType(MimeTypeUtils.IMAGE_PNG)
.data(imgs).build();
Message userMsg = UserMessage.builder()
.text(msg).media(media).build();
Prompt prompt = new Prompt(userMsg);
return chatClient.prompt(prompt)
.call().entity(FoodDetail.class);
}

从代码可以看出,多模态调用和普通的文本调用几乎没有区别。唯一的差异就是在构建 UserMessage 的时候多了一个 .media(media) 调用。
不过有一点需要注意,调用之前要确认使用的模型确实支持多模态输入。如果用一个纯文本模型去处理图片,调用会直接报错。在求职派的架构下,这个问题由模型自动检测机制解决了——只要消息里有 Media,系统会自动切换到视觉模型,不需要开发者操心。
08、求职派的岗位采集是怎么用多模态的?
除了用户侧的图片识别,求职派的管理后台也重度依赖了多模态能力。
岗位数据采集场景下,运营人员经常需要从招聘截图中批量提取岗位信息。这个功能由岗位采集代理实现:
Java
Media media = Media.builder()
.mimeType(type)
.data(bytes)
.name("图片-" + type.getSubtype() + "-" + rid)
.build();
UserMessage msg = UserMessage.builder()
.media(media)
.text("提取图片中的表格信息,按照指定要求返回")
.build();
(源码位置: app/.../gather/service/GatherAiAgent.java,第 119-127 行)
这里有个细节, Media.builder() 除了 mimeType 和 data,还设置了一个 name 字段。在批量处理多张图片的场景下, name 帮助在日志和调试时区分不同的图片。

更有意思的是大图片分批处理的设计。
当一张图片内容太多(比如一整页的招聘表格),单次调用可能无法提取完所有信息。采集代理会自动发起多轮对话,第一轮携带图片和提示词,后续轮次只发“继续返回”,直到模型返回完整的结果。
这个设计利用了 ChatMemory 的上下文保持能力。模型在第一轮已经“看过”了图片,后续轮次不需要重复发送图片,只需要告诉模型继续输出就行了。
ending
【Spring AI 的多模态 API 就三步,构建 Media、组装 UserMessage、发起调用。但从“能用”到“好用”,中间隔着模型自动检测、供应商适配、用户偏好路由这三层设计。】
求职派把这三层全部沉到了 LLM 调用层,业务 Agent 只管组装消息,不管模型怎么选。
这大概就是分层设计该有的样子——用的时候感觉不到它的存在。
如何把求职派写到简历上?
项目名称:求职派(JobClaw)—— 多 Agent 智能求职系统
项目简介:基于 Spring AI + LangGraph4J 的多 Agent 求职系统,对接微信/钉钉/飞书三个 IM 渠道,通过意图识别将用户消息路由到身份采集、岗位检索、岗位推荐等业务 Agent,支持多模态输入处理和多供应商模型切换。
技术栈:Java 21、Spring Boot 4.0.5、Spring AI 2.0.0-M4、LangGraph4J、JPA/Hibernate、React 19 / Next.js 15
核心职责:
-
基于 Spring AI 的 Media API 实现多模态输入处理,支持图片、文件等多种媒体类型的统一接入,业务层无需关心媒体来源和格式差异
-
设计并实现 LLM 调用层的模型自动检测机制,通过扫描 Prompt 中的 Media 内容自动切换文本/视觉模型,消除业务 Agent 的模型选择负担
-
实现多供应商(智谱、通义千问)视觉模型的统一适配层,新增供应商只需实现模型类型映射,无需修改业务代码
-
设计用户级别的模型偏好隔离方案,通过
provider#modelName格式支持按用户维度配置不同的视觉模型,并实现全局默认降级策略 -
基于多模态能力实现岗位数据智能采集,支持从招聘截图中批量提取结构化岗位信息,通过多轮对话机制处理大图片的分页识别
Spring AI 对多模态的支持,API 层面其实很简单——构建一个 Media 对象,塞进 UserMessage,就完事了。
但到了生产环境,就有新的问题了。
用户发了一张图片过来,系统怎么知道该用视觉模型而不是文本模型?多个供应商都支持视觉能力,模型怎么选?不同用户想用不同的视觉模型,配置怎么隔离?
这篇内容就从 Spring AI 的多模态 API 讲起,再拆解求职派在它基础上做的三层设计——Media 构建、模型自动检测、用户偏好路由。读完你就能搞清楚,一个对接了微信、钉钉、飞书三个渠道的 Agent 系统,到底怎么处理用户发来的图片。技术派: https://paicoding.com/jobclaw-springai-multimodal
系好安全带,我们粗粗发~
01、多模态到底是什么?
先把概念理清楚。
多模态(Multimodal)指的是大模型能够同时处理多种类型的输入——文本、图片、音频、视频,然后生成自然语言的输出。

举个例子:用户在微信上给求职派发了一张招聘海报的截图,然后问“这个岗位适合我吗?”。大模型需要同时理解图片里的岗位信息和用户的文字提问,才能给出有意义的回答。
这就是多模态的典型场景,输入不是纯文本,而是文本 + 图片的组合。
目前支持多模态输入的主流模型包括智谱的 GLM-5V 系列、通义千问的 qwen3.7 系列等。求职派实际对接的多模态供应商是智谱和千问,所以后面的例子以这两家为主。
在 Spring AI 之前,要对接不同供应商的多模态能力,需要分别处理各家的 API 差异。
有的用 base64 编码传图,有的用 URL 引用,参数格式也各不相同。Spring AI 把这些差异屏蔽掉了,提供了统一的 Media 抽象,一套代码就能对接所有支持多模态的供应商。
02、Spring AI 的多模态 API 怎么用?
Spring AI 把多模态的接入抽象成了三个步骤,构建 Media、组装 UserMessage、发起调用。

第一步,构建 Media
Media 是 Spring AI 对媒体资源的统一封装,支持 builder 模式。
Java
Media media = Media.builder()
.mimeType(MimeTypeUtils.IMAGE_PNG)
.data(imageBytes)
.build();
mimeType 指定媒体类型(PNG、JPEG、PDF 等), data 传入字节数组。如果图片来自本地文件,也可以用 FileSystemResource。
Java
new Media(mimeType, new FileSystemResource(path.toFile()));
两种构造方式的区别在于数据来源。
字节数组适合网络下载的图片,文件资源适合本地存储的图片。
builder 模式下还有两个可选字段, name 用于标识媒体资源(批量处理时方便日志追踪), id 用于唯一标识(多轮对话中引用特定图片时使用)。
第二步,组装 UserMessage
把 Media 和文本提示词一起塞进 UserMessage。
Java
UserMessage msg = UserMessage.builder()
.text("识别这张图片中的食物,计算卡路里")
.media(media)
.build();
.media() 方法既接受单个 Media 对象,也接受 List。如果用户一次发了多张图片,传列表就行了。
第三步,发起调用
后面的流程和纯文本调用完全一样。
Java
Prompt prompt = new Prompt(msg);
String result = chatClient.prompt(prompt)
.call().content();
如果需要结构化返回,把 .content() 换成 .entity(FoodDetail.class) 即可。
Spring AI 会自动把模型的输出解析成 Java 对象。对于泛型类型(比如返回一个列表),需要用 ParameterizedTypeReference。
Java
.call().entity(
new ParameterizedTypeReference>() {});

API 层面就这么多,三步走完就能让大模型“看懂”图片了。
03、求职派怎么处理用户发来的图片?
API 简单不代表生产落地简单。
求职派同时对接了微信、钉钉、飞书三个 IM 渠道,用户可能通过任何一个渠道发图片过来。不同渠道传图片的方式不一样——微信给的是字节流,文件系统里存的是本地路径。
业务 Agent 在处理消息的时候,并不知道图片是从哪个渠道来的,也不应该关心。
求职派的做法是在 LLM 调用层统一处理媒体构建。核心逻辑在 BizAgentLlmCaller 的 buildUserMessage() 方法里。
Java
for (var image : message.getMedias()) {
Media media = createImageMedia(image);
if (media != null) {
mediaList.add(media);
}
}
这段代码遍历消息中携带的所有媒体资源,逐个转换成 Spring AI 的 Media 对象。
(源码位置: core/.../agent/llm/BizAgentLlmCaller.java,第 169-210 行)
转换的逻辑分两种情况。
Java
if (image.getData() != null && image.getData().length > 0) {
MimeType mimeType = MimeType.valueOf(image.getMimeType());
return new Media(mimeType, new ByteArrayResource(image.getData()));
} else if (image.getFilePath() != null) {
Path path = image.getFilePath();
MimeType mimeType = MimeType.valueOf(image.getMimeType());
return new Media(mimeType, new FileSystemResource(path.toFile()));
}
有字节数据的直接包装成 ByteArrayResource,有文件路径的包装成 FileSystemResource。不管图片从哪个渠道来,最终都变成了统一的 Media 对象。

这个设计的好处是业务 Agent 完全不需要关心媒体资源的来源。它拿到的永远是一个包含了文本和媒体的 UserMessage,剩下的事情交给 LLM 调用层处理。
04、模型怎么知道该用视觉模型?
这是多模态落地最关键的一环。
用户发纯文本消息时,系统应该用文本模型(比如 GLM-4.7-Flash);用户发了图片时,系统应该自动切换到视觉模型(比如 GLM-4V-Flash)。如果每个业务 Agent 都要自己判断、自己切模型,代码会非常冗余。
求职派的解法是在 LLM 调用器的 getClient() 方法里做自动检测。
Java
ModelConfig.ModelType model = ModelConfig.ModelType.TEXT;
if (prompt.getUserMessages().stream()
.anyMatch(m -> !CollectionUtils.isEmpty(m.getMedia()))) {
model = ModelConfig.ModelType.VISION;
}
var chatModel = (ChatModel) modelProviders.getModel(
user.jobClawUserId(), model);
(源码位置: core/.../agent/llm/SimpleLlmCaller.java,第 82-87 行)
逻辑简单,扫一遍 Prompt 里的所有用户消息,只要有任何一条消息携带了 Media,就把模型类型从 TEXT 切换到 VISION。

这意味着业务 Agent 在构建 Prompt 的时候完全不需要指定模型类型。消息里有图片,系统自动用视觉模型;没有图片,自动用文本模型。模型切换这件事,对业务层是透明的。
这个设计避免了一个很常见的问题——开发者忘记切模型。
如果模型切换由业务 Agent 手动控制,早晚有人会忘记处理图片场景,导致纯文本模型收到了图片输入,直接报错。自动检测机制从根本上消除了这类低级错误。
05、多供应商怎么适配?
模型类型确定了之后,还有一个问题。不同供应商对视觉模型的支持方式不一样,系统怎么适配?
求职派定义了 7 种模型类型,覆盖了目前主流的 AI 能力。
-
TEXT — 纯文本对话
-
VISION — 视觉理解(图片识别)
-
IMAGE — 图像生成
-
VIDEO — 视频处理
-
EMBEDDING — 向量嵌入
-
ASR — 语音识别
-
TTS — 语音合成
(源码位置: core/.../providers/ModelConfig.java,第 80-110 行)
每个供应商模块实现 ModelProvider 接口,通过 model() 方法处理不同的模型类型。以智谱供应商为例。
Java
return switch (info.getType()) {
case TEXT -> buildChatModel(info);
case VISION -> buildChatModel(info);
case IMAGE -> buildImageModel(info);
case EMBEDDING -> buildEmbeddingModel(info);
default -> throw new IllegalArgumentException(...);
};
(源码位置: providers/zhipu/.../ZhiPuModelProvider.java,第 66-73 行)
这里有个细节,智谱的 TEXT 和 VISION 走的是同一个 buildChatModel() 方法,因为智谱的视觉模型和文本模型使用同一套 API 协议,区别只在于模型名称( GLM-4.7-Flash vs GLM-4V-Flash)。

新增一个供应商的时候,只需要实现对应的模型类型支持就行了,不需要修改任何业务代码。
这就是 ModelProvider 接口作为扩展点的价值——供应商之间的 API 差异被封装在各自的模块内部,对外暴露的是统一的 Model 接口。
06、用户偏好怎么隔离?
最后一层设计是用户级别的模型偏好。
求职派是一个多用户系统,不同用户可能配置了不同的模型供应商和 API Key。
用户 A 用智谱的 glm-4.6v-flash,用户 B 用通义千问的 qwen-omni-turbo,系统需要按用户维度隔离模型选择。
这个能力由 ModelProviders.getModel() 方法实现。
Java
var preferModel = switch (modelType) {
case TEXT -> preference.getModels().getText();
case VISION -> preference.getModels().getVision();
case IMAGE -> preference.getModels().getImage();
// ...
};
(源码位置: core/.../providers/ModelProviders.java,第 103-111 行)
每个用户的偏好配置中都有一个 vision 字段,格式是 provider#modelName(例如 zhipu#GLM-4V-Flash)。系统先查用户自己的配置,查不到就回退到全局默认配置。

整条流程串起来就是这样。
用户发了一张图片 → LLM 调用器检测到 Media,模型类型设为 VISION → 查用户偏好找到视觉模型配置 zhipu#GLM-4V-Flash → 解析出供应商和模型名 → 智谱供应商构建对应的 ChatModel 实例 → 发起调用。
业务 Agent 全程只管一件事,把用户的消息和图片组装成 Prompt,然后调用 call() 方法。模型选择、供应商路由、偏好降级,全部在底层自动完成。
07、图片识别实战
理解了底层机制,我们来看一个完整的实战例子。
接收一张食物图片的 URL,调用视觉模型识别图片内容,返回食物名称和卡路里估算。
先定义结构化输出的数据模型。
Java
public record FoodDetail(
@JsonPropertyDescription("整张图片的描述")
String desc,
@JsonPropertyDescription("总的卡路里")
Double totalCalorie,
@JsonPropertyDescription("图片中的食材列表")
List itemList) {}
@JsonPropertyDescription 注解会被 Spring AI 自动提取,生成 JSON Schema 传给大模型,引导模型按指定格式返回。
然后写一个接口,完成从下载图片到调用模型的全流程。
Java
@RequestMapping(path = "recognitionAndOutput")
public FoodDetail recognitionAndOutput(
@RequestParam(name = "imgUrl") String imgUrl,
@RequestParam(name = "msg") String msg) {
byte[] imgs = HttpUtil.downloadBytes(imgUrl);
Media media = Media.builder()
.mimeType(MimeTypeUtils.IMAGE_PNG)
.data(imgs).build();
Message userMsg = UserMessage.builder()
.text(msg).media(media).build();
Prompt prompt = new Prompt(userMsg);
return chatClient.prompt(prompt)
.call().entity(FoodDetail.class);
}

从代码可以看出,多模态调用和普通的文本调用几乎没有区别。唯一的差异就是在构建 UserMessage 的时候多了一个 .media(media) 调用。
不过有一点需要注意,调用之前要确认使用的模型确实支持多模态输入。如果用一个纯文本模型去处理图片,调用会直接报错。在求职派的架构下,这个问题由模型自动检测机制解决了——只要消息里有 Media,系统会自动切换到视觉模型,不需要开发者操心。
08、求职派的岗位采集是怎么用多模态的?
除了用户侧的图片识别,求职派的管理后台也重度依赖了多模态能力。
岗位数据采集场景下,运营人员经常需要从招聘截图中批量提取岗位信息。这个功能由岗位采集代理实现:
Java
Media media = Media.builder()
.mimeType(type)
.data(bytes)
.name("图片-" + type.getSubtype() + "-" + rid)
.build();
UserMessage msg = UserMessage.builder()
.media(media)
.text("提取图片中的表格信息,按照指定要求返回")
.build();
(源码位置: app/.../gather/service/GatherAiAgent.java,第 119-127 行)
这里有个细节, Media.builder() 除了 mimeType 和 data,还设置了一个 name 字段。在批量处理多张图片的场景下, name 帮助在日志和调试时区分不同的图片。

更有意思的是大图片分批处理的设计。
当一张图片内容太多(比如一整页的招聘表格),单次调用可能无法提取完所有信息。采集代理会自动发起多轮对话,第一轮携带图片和提示词,后续轮次只发“继续返回”,直到模型返回完整的结果。
这个设计利用了 ChatMemory 的上下文保持能力。模型在第一轮已经“看过”了图片,后续轮次不需要重复发送图片,只需要告诉模型继续输出就行了。
ending
【Spring AI 的多模态 API 就三步,构建 Media、组装 UserMessage、发起调用。但从“能用”到“好用”,中间隔着模型自动检测、供应商适配、用户偏好路由这三层设计。】
求职派把这三层全部沉到了 LLM 调用层,业务 Agent 只管组装消息,不管模型怎么选。
这大概就是分层设计该有的样子——用的时候感觉不到它的存在。
如何把求职派写到简历上?
项目名称:求职派(JobClaw)—— 多 Agent 智能求职系统
项目简介:基于 Spring AI + LangGraph4J 的多 Agent 求职系统,对接微信/钉钉/飞书三个 IM 渠道,通过意图识别将用户消息路由到身份采集、岗位检索、岗位推荐等业务 Agent,支持多模态输入处理和多供应商模型切换。
技术栈:Java 21、Spring Boot 4.0.5、Spring AI 2.0.0-M4、LangGraph4J、JPA/Hibernate、React 19 / Next.js 15
核心职责:
-
基于 Spring AI 的 Media API 实现多模态输入处理,支持图片、文件等多种媒体类型的统一接入,业务层无需关心媒体来源和格式差异
-
设计并实现 LLM 调用层的模型自动检测机制,通过扫描 Prompt 中的 Media 内容自动切换文本/视觉模型,消除业务 Agent 的模型选择负担
-
实现多供应商(智谱、通义千问)视觉模型的统一适配层,新增供应商只需实现模型类型映射,无需修改业务代码
-
设计用户级别的模型偏好隔离方案,通过
provider#modelName格式支持按用户维度配置不同的视觉模型,并实现全局默认降级策略 -
基于多模态能力实现岗位数据智能采集,支持从招聘截图中批量提取结构化岗位信息,通过多轮对话机制处理大图片的分页识别
Spring AI 对多模态的支持,API 层面其实很简单——构建一个 Media 对象,塞进 UserMessage,就完事了。
但到了生产环境,就有新的问题了。
用户发了一张图片过来,系统怎么知道该用视觉模型而不是文本模型?多个供应商都支持视觉能力,模型怎么选?不同用户想用不同的视觉模型,配置怎么隔离?
这篇内容就从 Spring AI 的多模态 API 讲起,再拆解求职派在它基础上做的三层设计——Media 构建、模型自动检测、用户偏好路由。读完你就能搞清楚,一个对接了微信、钉钉、飞书三个渠道的 Agent 系统,到底怎么处理用户发来的图片。技术派: https://paicoding.com/jobclaw-springai-multimodal
系好安全带,我们粗粗发~
01、多模态到底是什么?
先把概念理清楚。
多模态(Multimodal)指的是大模型能够同时处理多种类型的输入——文本、图片、音频、视频,然后生成自然语言的输出。

举个例子:用户在微信上给求职派发了一张招聘海报的截图,然后问“这个岗位适合我吗?”。大模型需要同时理解图片里的岗位信息和用户的文字提问,才能给出有意义的回答。
这就是多模态的典型场景,输入不是纯文本,而是文本 + 图片的组合。
目前支持多模态输入的主流模型包括智谱的 GLM-5V 系列、通义千问的 qwen3.7 系列等。求职派实际对接的多模态供应商是智谱和千问,所以后面的例子以这两家为主。
在 Spring AI 之前,要对接不同供应商的多模态能力,需要分别处理各家的 API 差异。
有的用 base64 编码传图,有的用 URL 引用,参数格式也各不相同。Spring AI 把这些差异屏蔽掉了,提供了统一的 Media 抽象,一套代码就能对接所有支持多模态的供应商。
02、Spring AI 的多模态 API 怎么用?
Spring AI 把多模态的接入抽象成了三个步骤,构建 Media、组装 UserMessage、发起调用。

第一步,构建 Media
Media 是 Spring AI 对媒体资源的统一封装,支持 builder 模式。
Java
Media media = Media.builder()
.mimeType(MimeTypeUtils.IMAGE_PNG)
.data(imageBytes)
.build();
mimeType 指定媒体类型(PNG、JPEG、PDF 等), data 传入字节数组。如果图片来自本地文件,也可以用 FileSystemResource。
Java
new Media(mimeType, new FileSystemResource(path.toFile()));
两种构造方式的区别在于数据来源。
字节数组适合网络下载的图片,文件资源适合本地存储的图片。
builder 模式下还有两个可选字段, name 用于标识媒体资源(批量处理时方便日志追踪), id 用于唯一标识(多轮对话中引用特定图片时使用)。
第二步,组装 UserMessage
把 Media 和文本提示词一起塞进 UserMessage。
Java
UserMessage msg = UserMessage.builder()
.text("识别这张图片中的食物,计算卡路里")
.media(media)
.build();
.media() 方法既接受单个 Media 对象,也接受 List。如果用户一次发了多张图片,传列表就行了。
第三步,发起调用
后面的流程和纯文本调用完全一样。
Java
Prompt prompt = new Prompt(msg);
String result = chatClient.prompt(prompt)
.call().content();
如果需要结构化返回,把 .content() 换成 .entity(FoodDetail.class) 即可。
Spring AI 会自动把模型的输出解析成 Java 对象。对于泛型类型(比如返回一个列表),需要用 ParameterizedTypeReference。
Java
.call().entity(
new ParameterizedTypeReference>() {});

API 层面就这么多,三步走完就能让大模型“看懂”图片了。
03、求职派怎么处理用户发来的图片?
API 简单不代表生产落地简单。
求职派同时对接了微信、钉钉、飞书三个 IM 渠道,用户可能通过任何一个渠道发图片过来。不同渠道传图片的方式不一样——微信给的是字节流,文件系统里存的是本地路径。
业务 Agent 在处理消息的时候,并不知道图片是从哪个渠道来的,也不应该关心。
求职派的做法是在 LLM 调用层统一处理媒体构建。核心逻辑在 BizAgentLlmCaller 的 buildUserMessage() 方法里。
Java
for (var image : message.getMedias()) {
Media media = createImageMedia(image);
if (media != null) {
mediaList.add(media);
}
}
这段代码遍历消息中携带的所有媒体资源,逐个转换成 Spring AI 的 Media 对象。
(源码位置: core/.../agent/llm/BizAgentLlmCaller.java,第 169-210 行)
转换的逻辑分两种情况。
Java
if (image.getData() != null && image.getData().length > 0) {
MimeType mimeType = MimeType.valueOf(image.getMimeType());
return new Media(mimeType, new ByteArrayResource(image.getData()));
} else if (image.getFilePath() != null) {
Path path = image.getFilePath();
MimeType mimeType = MimeType.valueOf(image.getMimeType());
return new Media(mimeType, new FileSystemResource(path.toFile()));
}
有字节数据的直接包装成 ByteArrayResource,有文件路径的包装成 FileSystemResource。不管图片从哪个渠道来,最终都变成了统一的 Media 对象。

这个设计的好处是业务 Agent 完全不需要关心媒体资源的来源。它拿到的永远是一个包含了文本和媒体的 UserMessage,剩下的事情交给 LLM 调用层处理。
04、模型怎么知道该用视觉模型?
这是多模态落地最关键的一环。
用户发纯文本消息时,系统应该用文本模型(比如 GLM-4.7-Flash);用户发了图片时,系统应该自动切换到视觉模型(比如 GLM-4V-Flash)。如果每个业务 Agent 都要自己判断、自己切模型,代码会非常冗余。
求职派的解法是在 LLM 调用器的 getClient() 方法里做自动检测。
Java
ModelConfig.ModelType model = ModelConfig.ModelType.TEXT;
if (prompt.getUserMessages().stream()
.anyMatch(m -> !CollectionUtils.isEmpty(m.getMedia()))) {
model = ModelConfig.ModelType.VISION;
}
var chatModel = (ChatModel) modelProviders.getModel(
user.jobClawUserId(), model);
(源码位置: core/.../agent/llm/SimpleLlmCaller.java,第 82-87 行)
逻辑简单,扫一遍 Prompt 里的所有用户消息,只要有任何一条消息携带了 Media,就把模型类型从 TEXT 切换到 VISION。

这意味着业务 Agent 在构建 Prompt 的时候完全不需要指定模型类型。消息里有图片,系统自动用视觉模型;没有图片,自动用文本模型。模型切换这件事,对业务层是透明的。
这个设计避免了一个很常见的问题——开发者忘记切模型。
如果模型切换由业务 Agent 手动控制,早晚有人会忘记处理图片场景,导致纯文本模型收到了图片输入,直接报错。自动检测机制从根本上消除了这类低级错误。
05、多供应商怎么适配?
模型类型确定了之后,还有一个问题。不同供应商对视觉模型的支持方式不一样,系统怎么适配?
求职派定义了 7 种模型类型,覆盖了目前主流的 AI 能力。
-
TEXT — 纯文本对话
-
VISION — 视觉理解(图片识别)
-
IMAGE — 图像生成
-
VIDEO — 视频处理
-
EMBEDDING — 向量嵌入
-
ASR — 语音识别
-
TTS — 语音合成
(源码位置: core/.../providers/ModelConfig.java,第 80-110 行)
每个供应商模块实现 ModelProvider 接口,通过 model() 方法处理不同的模型类型。以智谱供应商为例。
Java
return switch (info.getType()) {
case TEXT -> buildChatModel(info);
case VISION -> buildChatModel(info);
case IMAGE -> buildImageModel(info);
case EMBEDDING -> buildEmbeddingModel(info);
default -> throw new IllegalArgumentException(...);
};
(源码位置: providers/zhipu/.../ZhiPuModelProvider.java,第 66-73 行)
这里有个细节,智谱的 TEXT 和 VISION 走的是同一个 buildChatModel() 方法,因为智谱的视觉模型和文本模型使用同一套 API 协议,区别只在于模型名称( GLM-4.7-Flash vs GLM-4V-Flash)。

新增一个供应商的时候,只需要实现对应的模型类型支持就行了,不需要修改任何业务代码。
这就是 ModelProvider 接口作为扩展点的价值——供应商之间的 API 差异被封装在各自的模块内部,对外暴露的是统一的 Model 接口。
06、用户偏好怎么隔离?
最后一层设计是用户级别的模型偏好。
求职派是一个多用户系统,不同用户可能配置了不同的模型供应商和 API Key。
用户 A 用智谱的 glm-4.6v-flash,用户 B 用通义千问的 qwen-omni-turbo,系统需要按用户维度隔离模型选择。
这个能力由 ModelProviders.getModel() 方法实现。
Java
var preferModel = switch (modelType) {
case TEXT -> preference.getModels().getText();
case VISION -> preference.getModels().getVision();
case IMAGE -> preference.getModels().getImage();
// ...
};
(源码位置: core/.../providers/ModelProviders.java,第 103-111 行)
每个用户的偏好配置中都有一个 vision 字段,格式是 provider#modelName(例如 zhipu#GLM-4V-Flash)。系统先查用户自己的配置,查不到就回退到全局默认配置。

整条流程串起来就是这样。
用户发了一张图片 → LLM 调用器检测到 Media,模型类型设为 VISION → 查用户偏好找到视觉模型配置 zhipu#GLM-4V-Flash → 解析出供应商和模型名 → 智谱供应商构建对应的 ChatModel 实例 → 发起调用。
业务 Agent 全程只管一件事,把用户的消息和图片组装成 Prompt,然后调用 call() 方法。模型选择、供应商路由、偏好降级,全部在底层自动完成。
07、图片识别实战
理解了底层机制,我们来看一个完整的实战例子。
接收一张食物图片的 URL,调用视觉模型识别图片内容,返回食物名称和卡路里估算。
先定义结构化输出的数据模型。
Java
public record FoodDetail(
@JsonPropertyDescription("整张图片的描述")
String desc,
@JsonPropertyDescription("总的卡路里")
Double totalCalorie,
@JsonPropertyDescription("图片中的食材列表")
List itemList) {}
@JsonPropertyDescription 注解会被 Spring AI 自动提取,生成 JSON Schema 传给大模型,引导模型按指定格式返回。
然后写一个接口,完成从下载图片到调用模型的全流程。
Java
@RequestMapping(path = "recognitionAndOutput")
public FoodDetail recognitionAndOutput(
@RequestParam(name = "imgUrl") String imgUrl,
@RequestParam(name = "msg") String msg) {
byte[] imgs = HttpUtil.downloadBytes(imgUrl);
Media media = Media.builder()
.mimeType(MimeTypeUtils.IMAGE_PNG)
.data(imgs).build();
Message userMsg = UserMessage.builder()
.text(msg).media(media).build();
Prompt prompt = new Prompt(userMsg);
return chatClient.prompt(prompt)
.call().entity(FoodDetail.class);
}

从代码可以看出,多模态调用和普通的文本调用几乎没有区别。唯一的差异就是在构建 UserMessage 的时候多了一个 .media(media) 调用。
不过有一点需要注意,调用之前要确认使用的模型确实支持多模态输入。如果用一个纯文本模型去处理图片,调用会直接报错。在求职派的架构下,这个问题由模型自动检测机制解决了——只要消息里有 Media,系统会自动切换到视觉模型,不需要开发者操心。
08、求职派的岗位采集是怎么用多模态的?
除了用户侧的图片识别,求职派的管理后台也重度依赖了多模态能力。
岗位数据采集场景下,运营人员经常需要从招聘截图中批量提取岗位信息。这个功能由岗位采集代理实现:
Java
Media media = Media.builder()
.mimeType(type)
.data(bytes)
.name("图片-" + type.getSubtype() + "-" + rid)
.build();
UserMessage msg = UserMessage.builder()
.media(media)
.text("提取图片中的表格信息,按照指定要求返回")
.build();
(源码位置: app/.../gather/service/GatherAiAgent.java,第 119-127 行)
这里有个细节, Media.builder() 除了 mimeType 和 data,还设置了一个 name 字段。在批量处理多张图片的场景下, name 帮助在日志和调试时区分不同的图片。

更有意思的是大图片分批处理的设计。
当一张图片内容太多(比如一整页的招聘表格),单次调用可能无法提取完所有信息。采集代理会自动发起多轮对话,第一轮携带图片和提示词,后续轮次只发“继续返回”,直到模型返回完整的结果。
这个设计利用了 ChatMemory 的上下文保持能力。模型在第一轮已经“看过”了图片,后续轮次不需要重复发送图片,只需要告诉模型继续输出就行了。
ending
【Spring AI 的多模态 API 就三步,构建 Media、组装 UserMessage、发起调用。但从“能用”到“好用”,中间隔着模型自动检测、供应商适配、用户偏好路由这三层设计。】
求职派把这三层全部沉到了 LLM 调用层,业务 Agent 只管组装消息,不管模型怎么选。
这大概就是分层设计该有的样子——用的时候感觉不到它的存在。
如何把求职派写到简历上?
项目名称:求职派(JobClaw)—— 多 Agent 智能求职系统
项目简介:基于 Spring AI + LangGraph4J 的多 Agent 求职系统,对接微信/钉钉/飞书三个 IM 渠道,通过意图识别将用户消息路由到身份采集、岗位检索、岗位推荐等业务 Agent,支持多模态输入处理和多供应商模型切换。
技术栈:Java 21、Spring Boot 4.0.5、Spring AI 2.0.0-M4、LangGraph4J、JPA/Hibernate、React 19 / Next.js 15
核心职责:
-
基于 Spring AI 的 Media API 实现多模态输入处理,支持图片、文件等多种媒体类型的统一接入,业务层无需关心媒体来源和格式差异
-
设计并实现 LLM 调用层的模型自动检测机制,通过扫描 Prompt 中的 Media 内容自动切换文本/视觉模型,消除业务 Agent 的模型选择负担
-
实现多供应商(智谱、通义千问)视觉模型的统一适配层,新增供应商只需实现模型类型映射,无需修改业务代码
-
设计用户级别的模型偏好隔离方案,通过
provider#modelName格式支持按用户维度配置不同的视觉模型,并实现全局默认降级策略 -
基于多模态能力实现岗位数据智能采集,支持从招聘截图中批量提取结构化岗位信息,通过多轮对话机制处理大图片的分页识别