跳到主要内容
版本:1.1.3

Multimodal Chat

多模态聊天让模型能够"看懂"图片——在对话中携带图像等媒体内容,模型基于视觉信息理解场景并给出回答。Spring AI 通过 Media 类和 UserMessage 的媒体支持实现这一能力,兼容 OpenAI 视觉模型和 Ollama 多模态模型。


1. 架构定位

多模态聊天允许 AI 模型理解图片、视频等媒体内容。Spring AI 通过 Media 类统一包装媒体数据,ChatClient 自动处理不同厂商的编码差异。


2. 媒体数据封装

Media 是多媒体内容的统一载体,位于 spring-ai-commons 模块,封装 MIME 类型和原始数据。

2.1 构造方式

// 从 Resource 创建(推荐:类路径文件、上传文件)
Media image = Media.builder()
.mimeType(Media.Format.IMAGE_PNG)
.data(new ClassPathResource("images/screenshot.png"))
.build();

// 从 URL 创建
Media imageFromUrl = Media.builder()
.mimeType(Media.Format.IMAGE_JPEG)
.data(new URL("https://example.com/photo.jpg"))
.build();

// 从 URI 创建
Media imageFromUri = Media.builder()
.mimeType(Media.Format.IMAGE_PNG)
.data(URI.create("file:///tmp/screenshot.png"))
.build();

// 设置自定义 ID 和名称
Media imageWithId = Media.builder()
.mimeType(Media.Format.IMAGE_PNG)
.data(resource)
.id("img-001")
.name("screenshot.png")
.build();

Media 核心方法:

方法返回说明
getMimeType()MimeTypeMIME 类型
getData()Object原始数据(URL 时为 String,Resource 时为 byte[])
getDataAsByteArray()byte[]强制转换为 byte[]
getId()StringAI 模型分配的媒体引用 ID
getName()String自动生成或手动指定的名称

2.2 媒体格式常量

类别常量MIME 类型
图片IMAGE_PNGimage/png
图片IMAGE_JPEGimage/jpeg
图片IMAGE_GIFimage/gif
图片IMAGE_WEBPimage/webp
文档DOC_PDFapplication/pdf
文档DOC_CSVtext/csv
文档DOC_DOCapplication/msword
文档DOC_DOCXapplication/vnd.openxmlformats-officedocument.wordprocessingml.document
文档DOC_XLSapplication/vnd.ms-excel
文档DOC_XLSXapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet
文档DOC_HTMLtext/html
文档DOC_TXTtext/plain
文档DOC_MDtext/markdown
视频VIDEO_MKVvideo/x-matroska
视频VIDEO_MOVvideo/quicktime
视频VIDEO_MP4video/mp4
视频VIDEO_WEBMvideo/webm
视频VIDEO_FLVvideo/x-flv
视频VIDEO_MPEGvideo/mpeg
视频VIDEO_MPGvideo/mpg
视频VIDEO_WMVvideo/x-ms-wmv
视频VIDEO_THREE_GPvideo/3gpp

2.3 媒体内容处理

UserMessageAssistantMessage 实现了 MediaContent,这意味着模型回复也可以包含媒体内容

// 通过 builder 将 Media 附加到 UserMessage
UserMessage userMsg = UserMessage.builder()
.text("分析这张图片")
.media(image)
.build();
List<Media> mediaList = userMsg.getMedia();

// AssistantMessage 也可能包含媒体(如图片生成结果)
AssistantMessage reply = response.getResult().getOutput();
if (reply instanceof MediaContent mc && !mc.getMedia().isEmpty()) {
// 处理模型返回的图片
}

3. 在用户消息中附加媒体

3.1 Builder API

UserMessage.Builder 提供灵活的媒体构造方式:

// 纯文本
UserMessage.builder().text("Hello").build();

// 从 Resource 加载文本
UserMessage.builder().text(new ClassPathResource("prompts/query.txt")).build();

// 文本 + 单张图片
UserMessage message = UserMessage.builder()
.text("请分析这张图片")
.media(Media.builder()
.mimeType(Media.Format.IMAGE_PNG)
.data(new ClassPathResource("images/screenshot.png"))
.build())
.metadata(Map.of("source", "upload"))
.build();

// 文本 + 多张图片(varargs)
UserMessage multiImage = UserMessage.builder()
.text("对比这两张架构图")
.media(
Media.builder().mimeType(Media.Format.IMAGE_PNG)
.data(new ClassPathResource("v1.png")).build(),
Media.builder().mimeType(Media.Format.IMAGE_PNG)
.data(new ClassPathResource("v2.png")).build()
)
.build();

3.2 copy() 与 mutate()

UserMessage original = UserMessage.builder()
.text("原始问题")
.media(image1)
.build();

// 复制并追加媒体
UserMessage extended = original.mutate()
.media(image2)
.build();

4. 通过 ChatClient 使用多模态

ChatClient 的 PromptUserSpec 提供了三种 media() 重载,无需手动构造 UserMessage

重载说明
media(Media... media)传入已构建的 Media 对象
media(MimeType mimeType, URL url)从 URL 加载,自动构建 Media
media(MimeType mimeType, Resource resource)从 Resource 加载,自动构建 Media

4.1 基础图片分析

完整示例:ChatClientImageDemo.java
ChatClientImageDemo.java
@Component
public class ChatClientImageDemo implements CommandLineRunner {

private final ChatClient chatClient;

public ChatClientImageDemo(ChatClient.Builder builder) {
this.chatClient = builder.build();
}

@Override
public void run(String... args) {
String response = chatClient.prompt()
.user(user -> user
.text("这张图片里有什么?请详细描述")
.media(Media.builder()
.mimeType(Media.Format.IMAGE_JPEG)
.data(new ClassPathResource("images/landscape.jpg"))
.build())
)
.call()
.content();

System.out.println("模型描述: " + response);
}
}

4.2 从 URL 加载

完整示例:ChatClientUrlDemo.java
ChatClientUrlDemo.java
@Component
public class ChatClientUrlDemo implements CommandLineRunner {

private final ChatClient chatClient;

public ChatClientUrlDemo(ChatClient.Builder builder) {
this.chatClient = builder.build();
}

@Override
public void run(String... args) throws MalformedURLException {
String response = chatClient.prompt()
.user(user -> user
.text("这张 UI 设计稿有哪些可以改进的地方?")
.media(MimeTypeUtils.IMAGE_PNG,
new URL("https://example.com/design.png"))
)
.call()
.content();

System.out.println(response);
}
}

4.3 从 Resource 简捷加载

完整示例:ChatClientResourceDemo.java
ChatClientResourceDemo.java
@Component
public class ChatClientResourceDemo implements CommandLineRunner {

private final ChatClient chatClient;

public ChatClientResourceDemo(ChatClient.Builder builder) {
this.chatClient = builder.build();
}

@Override
public void run(String... args) {
String response = chatClient.prompt()
.user(user -> user
.text("提取这张架构图中的所有组件名称和关系")
.media(Media.Format.IMAGE_PNG,
new ClassPathResource("images/architecture.png"))
)
.call()
.content();

System.out.println(response);
}
}

4.4 多图对比

完整示例:MultiImageDemo.java
MultiImageDemo.java
@Component
public class MultiImageDemo implements CommandLineRunner {

private final ChatClient chatClient;

public MultiImageDemo(ChatClient.Builder builder) {
this.chatClient = builder.build();
}

@Override
public void run(String... args) {
String response = chatClient.prompt()
.system("你是一个 UI/UX 设计评审专家,请从用户体验角度给出专业评审")
.user(user -> user
.text("""
请对比两张 UI 设计稿:
1. 分别描述每张的布局风格
2. 指出差异点
3. 哪张更适合电商场景?为什么?
""")
.media(Media.builder()
.mimeType(Media.Format.IMAGE_PNG)
.data(new ClassPathResource("images/ui-v1.png"))
.build())
.media(Media.builder()
.mimeType(Media.Format.IMAGE_PNG)
.data(new ClassPathResource("images/ui-v2.png"))
.build())
)
.call()
.content();

System.out.println(response);
}
}

6. 模型与媒体格式支持

厂商推荐模型图片格式文档格式视频本地部署
OpenAIgpt-4o, gpt-4o-miniPNG, JPEG, GIF, WebPPDF(读取)部分支持
Ollamallava, llava-phi3, bakllava, minicpm-vPNG, JPEG支持

OpenAI 模型视觉能力对比

模型视觉精度多图高清细节适用场景
gpt-4o最高支持优秀专业视觉分析、医学影像、复杂图表
gpt-4o-mini支持良好通用图片理解、成本敏感场景
gpt-4-turbo支持良好前代视觉模型,兼容过渡

7. 应用场景

7.1 图片内容理解

完整示例:ImageAnalysisDemo.java
ImageAnalysisDemo.java
@Component
public class ImageAnalysisDemo implements CommandLineRunner {

private final ChatClient chatClient;

public ImageAnalysisDemo(ChatClient.Builder builder) {
this.chatClient = builder.build();
}

@Override
public void run(String... args) {
record Analysis(String subject, String mood, List<String> objects) {}

Analysis result = chatClient.prompt()
.user(user -> user
.text("分析这张图片,提取主体、氛围和可见物体")
.media(Media.builder()
.mimeType(Media.Format.IMAGE_JPEG)
.data(new ClassPathResource("images/scene.jpg"))
.build())
)
.call()
.entity(Analysis.class);

System.out.println("主体: " + result.subject());
System.out.println("氛围: " + result.mood());
System.out.println("物体: " + result.objects());
}
}

7.2 代码截图转文本

完整示例:CodeScreenshotDemo.java
CodeScreenshotDemo.java
@Component
public class CodeScreenshotDemo implements CommandLineRunner {

private final ChatClient chatClient;

public CodeScreenshotDemo(ChatClient.Builder builder) {
this.chatClient = builder.build();
}

@Override
public void run(String... args) {
String code = chatClient.prompt()
.user(user -> user
.text("将截图中的代码完整地转录为文本,不要添加任何解释")
.media(Media.builder()
.mimeType(Media.Format.IMAGE_PNG)
.data(new ClassPathResource("images/code-snippet.png"))
.build())
)
.call()
.content();

System.out.println("转录代码:\n" + code);
}
}

7.3 图表数据提取

完整示例:ChartExtractionDemo.java
ChartExtractionDemo.java
@Component
public class ChartExtractionDemo implements CommandLineRunner {

private final ChatClient chatClient;

public ChartExtractionDemo(ChatClient.Builder builder) {
this.chatClient = builder.build();
}

@Override
public void run(String... args) {
String csvData = chatClient.prompt()
.user(user -> user
.text("提取这张柱状图中的数据,以 CSV 格式返回(列名: 月份,销售额)")
.media(Media.builder()
.mimeType(Media.Format.IMAGE_PNG)
.data(new ClassPathResource("images/sales-chart.png"))
.build())
)
.call()
.content();

System.out.println("提取的数据:\n" + csvData);
}
}

7.4 PDF 内容读取

完整示例:PdfReadDemo.java
PdfReadDemo.java
@Component
public class PdfReadDemo implements CommandLineRunner {

private final ChatClient chatClient;

public PdfReadDemo(ChatClient.Builder builder) {
this.chatClient = builder.build();
}

@Override
public void run(String... args) {
String summary = chatClient.prompt()
.system("你是一个文档分析专家,请提取关键信息并以结构化方式呈现")
.user(user -> user
.text("请阅读这份 PDF 文档,总结其核心内容")
.media(Media.builder()
.mimeType(Media.Format.DOC_PDF)
.data(new ClassPathResource("docs/report.pdf"))
.build())
)
.call()
.content();

System.out.println("文档摘要:\n" + summary);
}
}

7.5 多模态对话记忆

多模态消息同样可以存入 ChatMemory,会话历史中保留图片引用(Media.id)。

完整示例:MultimodalMemoryDemo.java
MultimodalMemoryDemo.java
@Component
public class MultimodalMemoryDemo implements CommandLineRunner {

private final ChatClient chatClient;

public MultimodalMemoryDemo(ChatClient.Builder builder) {
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.maxMessages(20)
.build();

this.chatClient = builder
.defaultAdvisors(
PromptChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
}

@Override
public void run(String... args) {
String r1 = chatClient.prompt()
.user(user -> user
.text("这是我们的系统架构图,请记住关键组件的位置")
.media(Media.builder()
.mimeType(Media.Format.IMAGE_PNG)
.data(new ClassPathResource("images/arch.png"))
.build())
)
.call()
.content();

System.out.println("第一轮: " + r1);

String r2 = chatClient.prompt()
.user("基于之前的架构图,建议增加哪些组件来提升系统可靠性?")
.call()
.content();

System.out.println("第二轮: " + r2);
}
}

8. 注意事项

8.1 图片大小限制

  • OpenAI: 每张图片最大 20MB,分辨率过高会被自动缩放到 2048px(短边)或 768px(短边,低分辨率模式)。可通过 detail 参数控制(API 层面)。
  • Ollama: 取决于模型实现,通常建议图片控制在 5MB 以内,分辨率不超过 2048px。大图片会被重新缩放。

8.2 多图数量限制

  • OpenAI: 取决于模型,gpt-4o 支持对话中携带多张图片(受上下文限制)。
  • Ollama: 取决于具体模型,llava 支持单张图片的完整理解。

8.3 模型兼容性

非视觉模型接收 Media 时会报错或忽略图片。确保使用视觉模型:

// 正确的做法
OpenAiChatOptions.builder()
.model("gpt-4o")
.build();

// Ollama 需指定多模态模型
OllamaOptions.builder()
.model("llava-phi3")
.build();

8.4 数据格式差异

  • Media 通过 Resource 传入时,数据以 byte[] 存储。
  • 通过 URLURI 传入时,数据以 String(URL 字符串)存储。
  • Ollama 的 fromMediaData()byte[] 执行 Base64 编码,对 String 直接透传。

9. 完整综合示例

完整示例:MultimodalChatCompleteExample.java
MultimodalChatCompleteExample.java
@Component
public class MultimodalChatCompleteExample implements CommandLineRunner {

private final ChatClient chatClient;

public MultimodalChatCompleteExample(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("你是一个视觉分析专家,请基于图片内容给出专业、准确的回答")
.build();
}

@Override
public void run(String... args) {
record ImageAnalysis(String description, String category,
List<String> tags, String suggestion) {}

ImageAnalysis result = chatClient.prompt()
.user(user -> user
.text("""
分析这张 UI 设计稿:
1. 描述整体布局
2. 判断设计类别
3. 提取关键设计标签(3-5个)
4. 给出 1-2 条改进建议
""")
.media(Media.builder()
.mimeType(Media.Format.IMAGE_PNG)
.data(new ClassPathResource("images/ui-design.png"))
.build())
)
.call()
.entity(ImageAnalysis.class);

System.out.println("描述: " + result.description());
System.out.println("类别: " + result.category());
System.out.println("标签: " + String.join(", ", result.tags()));
System.out.println("建议: " + result.suggestion());
}
}