我本来以为,今天这套 Java 多模态内容,最多就是照着文档把接口串一下:图片理解、图片生成、图文问答 API,两个小时差不多就能收工。
结果一上手就发现,事情根本没这么简单。
真正把链路跑通,不是"会调一下接口"这么轻松。中间我连续踩了 4 个坑:下载图片爆内存、Logback 版本打架、Spring Boot 启动直接崩、图文问答明明让它说中文却死活回英文。
但好消息是,这一轮坑踩完之后,我把 Java 多模态的 4 条核心链路都跑通了:LangChain4j 图片理解、Spring AI 多模态对比、SiliconFlow 图片生成、LangChain4j 图文对话 API。
如果你也准备在 Java 里做多模态,这篇文章能帮你少绕不少弯路。
一句话先说结论:多模态最难的从来不是"能不能调通",而是"链路跑通后结果靠不靠谱"。
01 先别急着写 API,先把多模态拆成 4 条链路
今天这套内容,我不是一上来就写一个"大而全"的图文接口,而是强行拆成 4 个 Step 去做。
为什么要拆?因为多模态这东西特别容易"看起来全通了,实际上哪儿坏了根本说不清"。
我最后拆成了这 4 条链路:
- LangChain4j 图片理解
- Spring AI 多模态对比
- SiliconFlow Qwen-Image 图片生成
- LangChain4j 图文对话 API
这四步不是形式主义,它们分别解决的是 4 个不同问题。
第一步解决"视觉模型到底怎么接图片"。第二步解决"Spring AI 和 LangChain4j 在抽象层面差在哪"。第三步解决"图片理解和图片生成根本不是一套接口"。第四步才是把前面几步真正收成一个能给业务用的 HTTP API。
很多人一开始就想直接做上传图片问答接口,这种做法最大的问题是:一旦出错,你根本不知道是框架问题、模型问题、HTTP 上传问题,还是 Prompt 约束问题。
我这次拆开之后,定位问题明显快很多。
比如 Step 1 我确认了 LangChain4j 1.15.0 里多模态的核心写法就是:
java
ImageContent imageContent = ImageContent.from(base64Image, contentType);
UserMessage userMessage = UserMessage.from(question, imageContent);
String answer = chatModel.chat(userMessage).aiMessage().text();
这一步重点不是把代码敲出来,而是确认图片输入在 LangChain4j 里本质上就是 ImageContent + UserMessage。
到了 Step 2,我再看 Spring AI,就会明显感觉两边抽象风格不一样:
- LangChain4j 更底层,消息对象更显式
- Spring AI 更像 Spring 生态风格,偏向
ChatClient.prompt().user(...)
这时候你就不会把两个框架混着理解。
金句:先拆链路,再谈封装,不然你调的不是多模态,是运气。
02 图片理解和图片生成,根本不是一回事
很多人一看到"多模态",脑子里就把图片理解、图片生成、图文问答全归成一类,觉得都是"图片相关能力"。这在概念上没问题,但到了代码层面,这种想法会直接把你带沟里。
因为图片理解和图片生成,真的不是一个接口。
我今天实际跑通的两条请求链路是这样的:
图片理解
text
POST /v1/chat/completions
消息体里放文本问题 + 图片内容,让视觉模型去理解图片。
图片生成
text
POST /v1/images/generations
请求体里放 prompt,让图像生成模型返回图片 URL。
这两个接口从职责、输入、输出,到后续处理方式都完全不同。
图片理解更像"给模型看一张图,再问它问题";图片生成更像"给模型一句画图指令,让它返回一张图"。
我这次 Step 3 用的是 SiliconFlow 的 Qwen/Qwen-Image。代码写完以后,本来以为已经稳了,结果一运行又被现实教育了一次。
接口本身返回成功了,但下载图片时直接炸了:
text
DataBufferLimitException: Exceeded limit on max bytes to buffer : 262144
这个错的本质不是图片生成失败,而是 WebFlux 默认只愿意帮你缓冲 256KB。而生成出来的 PNG 明显比这大得多。
我一开始是这样写的:
java
.bodyToMono(byte[].class)
这行代码的问题很典型:它会把整个图片一次性聚合到内存里。对于普通 JSON 没问题,但对图片这种二进制文件,这种写法就是雷。
后来我改成了流式写文件:
java
.bodyToFlux(DataBuffer.class)
.as(dataBuffers -> DataBufferUtils.write(dataBuffers, outputPath))
.then()
.block(REQUEST_TIMEOUT);
问题立刻解决。
这个坑给我的提醒很直接:多模态里一旦牵涉到文件,别老想着 byte[] 一把梭,流式处理才是正路。
03 真正烦人的,不是接口,而是工程环境
如果今天只是"写 Demo",那多模态其实不算太难。
真正烦人的,是你以为自己在调模型,结果半天时间都死在工程环境上。
我今天就连续撞上了两个特别典型的工程坑。
坑 1:Logback 版本冲突
启动 MultimodalChatApiDemo 的时候,程序一上来就直接崩:
text
NoSuchMethodError: 'void ch.qos.logback.classic.LoggerContext.initCollisionMaps()'
这个错看着像 Logback 内部炸了,但本质其实很简单:
logback-classic是1.5.12logback-core是1.5.32
两个版本不一致,Spring Boot 启动阶段直接挂。
这类问题最恶心的点在于:它跟你的业务代码一毛钱关系都没有,但会让你误以为整个项目都坏了。
最后的修复也不复杂------把手动指定的旧版 logback-classic 去掉,让 Spring Boot 4.0.5 自己接管版本管理。
坑 2:Spring Boot 父版本和插件版本不一致
另一个坑更隐蔽。
项目父 POM 是:
text
Spring Boot 4.0.5
但 spring-boot-maven-plugin 居然写的是:
text
3.2.5
这玩意儿你平时不盯着看,很容易漏掉。但一旦运行起来,插件行为和运行时能力不一致,排查起来就特别恶心。
修复方法也很直接:插件版本跟父版本保持一致。
坑 3:Spring AI 自动配置把纯 LangChain4j Demo 搅乱了
我后来把 MultimodalChatApiDemo 改成了纯 LangChain4j,理论上已经不需要 Spring AI 参与了。
但应用启动时,Spring Boot 还是把同包里的 Spring AI Demo 和一堆 OpenAI 自动配置全扫进来了,结果又因为缺 credential 报错。
这个坑说明一件事:你以为自己只改了一份 Demo,实际上整个 Spring 容器都在参与这件事。
最后我是通过两步处理掉的:
- 排除 Spring AI 的相关自动配置
- 把
MultimodalChatApiDemo的组件扫描范围隔离开,避免扫描到别的 Demo
工程层面这几个坑踩完之后,我反而更确定一件事:
做多模态,真正拖你时间的,往往不是模型本身,而是工程环境的隐性耦合。
金句:模型报错不可怕,最怕的是你以为在调模型,其实在给依赖地雷排爆。
04 图文问答 API 跑通,不代表结果可信
今天最后真正让我觉得"这事有意思了"的,不是接口返回成功,而是接口返回成功之后,结果居然还是不靠谱。
我把 MultimodalChatApiDemo 跑通之后,用一张 langgraph4j-cover.png 去测试,问题也很普通:
text
请描述这张图片的主题,并提取图片中的主要文字
接口确实返回了 JSON,说明整个链路通了:
text
上传图片 → Spring Boot 接口 → LangChain4j → SiliconFlow Qwen3-VL → 返回答案
但是结果质量有明显问题:
- 中文问题,模型却回英文
LangGraph4j被识别成了PingGraph- 还有一处被识别成了
Jaava - 它不是 OCR 式提取,而是在脑补技术词
这一刻我才真正觉得,多模态这东西最核心的坑不在"能不能调通",而在"调通之后你敢不敢信它"。
后来我对 MultimodalChatApiDemo 做了两轮修正。
第一轮,是把原来直接塞用户问题:
java
.text(question)
改成明确的中文约束 + OCR 约束。
第二轮,我发现普通用户消息的约束优先级不够,模型还是可能阳奉阴违,所以又把规则提升到了 SystemMessage:
java
SystemMessage.from("""
你是一个中文视觉信息提取助手。
你的输出必须全部使用简体中文,禁止使用英文。
不要猜测图片中看不清的文字;无法确认时必须写 [无法确认]。
识别技术名词时必须保留图片中的原始拼写。
""")
再把用户问题和图片一起作为 UserMessage 丢进去。
这一步给我的感受特别深:多模态里,Prompt 不是锦上添花,而是结果质量的硬约束。
如果你只是让它"描述图片",那它大概率会按自己最擅长的方式自由发挥;但如果你的目标其实是 OCR、术语提取、结构化理解,那 Prompt 不写死规则,结果一定飘。
所以我现在对多模态接口的判断标准已经变了。
以前是:
返回了答案,就算成功。
现在是:
返回了答案,而且答案可控、可复查、可约束,才算真正成功。
05 今天这套多模态实战,最后沉淀下来的到底是什么
如果只看表面,今天我完成的是 4 个 Demo。
但如果往下挖一层,真正沉淀下来的其实是 4 个工程认知。
第一,链路必须拆开做
图片理解、图片生成、图文问答 API,不要一上来就糊成一个大接口。拆开做,定位才快。
第二,文件处理别贪方便
尤其是图片下载、上传这种二进制链路,byte[] 很容易把自己坑死。流式处理才稳。
第三,框架自动配置要有边界感
Spring Boot 这种生态爽是爽,但自动配置一旦越界,就特别容易把一个小 Demo 搞成全项目联动故障。
第四,多模态结果一定要做约束
图片问答不是天然可信的,尤其牵涉到术语识别、文字提取、OCR 场景时,必须用 System Prompt、结构化输出、甚至后处理去兜底。
说白了,今天最大的收获不是"我会调视觉模型了",而是我终于把这件事从"接口调用"提升到了"工程实现"。
这是两码事。
会调接口的人很多,但能把接口变成稳定业务能力的人并不多。
金句:多模态真正值钱的,不是模型会看图,而是你能把它驯成一个可靠组件。
06 最后给准备上手 Java 多模态的人 3 条建议
如果你也准备补这一块,我建议你别走"先做大一统 Demo"的路线,直接按这 3 条来。
1. 先做最小链路验证
先确认图片理解能通,再确认图片生成能通,最后再合并成图文接口。不要一开始就全堆进去。
2. 每跑通一步,就顺手验证一次真实输出
不是只看 HTTP 200,也不是只看有没有 JSON,而是看结果是不是你真正想要的东西。
3. Prompt 设计从第一天就别偷懒
尤其是 OCR、术语识别、结构化提取,越早把规则写死,后面返工越少。
今天这轮跑下来,我对 Java 多模态的感觉反而更踏实了。
它没有想象中那么神秘,也没有文档里写得那么丝滑。真正落地时,坑照样一大堆。
但正因为坑多,你一旦踩完并且把它们写进自己的工程模板里,后面再做类似能力,速度会快很多。
说到底,这类能力不怕难,怕的是你每次都从头踩一遍。
把重复的坑踩成标准件,后面就省事了。
如果你最近也在折腾 LangChain4j、Spring AI 或者 Java 多模态。
下一篇我准备接着写:多模型切换这件事,为什么不是 if-else 选模型这么简单。
就这样,有问题随时来掰扯。