ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

【Spring AI 入门与实战】07-多模态入门-图像理解与生成

【Spring AI 入门与实战】07-多模态入门-图像理解与生成 多模态入门图像理解与生成本文是专栏《Spring AI 入门与实战》的第 7 篇上一篇我们聊了《结构化输出让 AI 稳定返回 Java 对象》让模型输出变成可靠的 Java 数据。这一篇把输入从纯文本扩展到图片怎么把一张发票图片直接丢给模型抽出结构化字段以及怎么反过来让模型画图。一、场景引入财务同事提了个需求每月几千张报销发票要人工录入OCR 厂商按调用收费识别出原始文本之后发票抬头、金额、税号这些关键字段还得再写一堆规则去抠。团队想试试视觉大模型VLM一张图进去结构化字段出来一个接口把识别和抽取一起做完。这就是多模态输入的价值。顺带还有个反向需求运营活动页要配图希望用一句话生成海报底图。Spring AI 1.1 对这两件事都有直接支持——前者是 ChatModel 的多模态消息后者是独立的 ImageModel 抽象。本篇把两件事都讲清楚。二、核心讲解2.1 多模态消息的构造Media MimeType依赖和第 6 篇一样还是spring-ai-starter-model-openai。但配置要注意DeepSeek 的 chat 模型不支持图像输入图像理解任务我默认用通义的 qwen-vl-maxOpenAI 兼容端点spring:ai:openai:api-key:${DASHSCOPE_API_KEY}base-url:https://dashscope.aliyuncs.com/compatible-mode/v1chat:options:model:qwen-vl-maxtemperature:0.1多模态消息的核心类是org.springframework.ai.content.Media它承载两部分信息MIME 类型MimeTypeUtils.IMAGE_PNG、IMAGE_JPEG等和数据来源。来源支持两种本地 Resourceclasspath、文件、内存字节和远程 URI// 本地 classpath 图片MedialocalPngnewMedia(MimeTypeUtils.IMAGE_PNG,newClassPathResource(docs/invoice.png));// 远程 URL 图片MediaremoteJpgnewMedia(MimeTypeUtils.IMAGE_JPEG,URI.create(https://example.com/receipt.jpg));有了 Media用UserMessage.builder()把文字和图片组装成一条用户消息UserMessagemessageUserMessage.builder().text(请描述这张图片的内容。).media(localPng).build();2.2 实战票据信息抽取替代传统 OCR这个场景是上一篇entity()的天然搭档图片进、对象出。定义返回类型publicrecordInvoiceInfo(Stringtitle,// 发票抬头StringinvoiceNo,// 发票号码BigDecimalamount,// 价税合计StringissueDate,// 开票日期yyyy-MM-ddStringsellerTaxId// 销方税号){}服务类ServicepublicclassInvoiceOcrService{privatefinalChatClientchatClient;publicInvoiceOcrService(ChatModelchatModel){this.chatClientChatClient.builder(chatModel).build();}publicInvoiceInfoextract(Resourceimage){UserMessagemessageUserMessage.builder().text( 请从这张发票图片中抽取字段发票抬头、发票号码、\ 价税合计金额、开票日期、销方税号。 金额保留两位小数日期用 yyyy-MM-dd 格式。 金额以票面大写人民币为准。看不清的字段填null不要猜。).media(newMedia(MimeTypeUtils.IMAGE_PNG,image)).build();returnchatClient.prompt(newPrompt(message)).call().entity(InvoiceInfo.class);}}一个最简的文件上传接口RestControllerpublicclassInvoiceController{privatefinalInvoiceOcrServiceservice;publicInvoiceController(InvoiceOcrServiceservice){this.serviceservice;}PostMapping(/invoice)publicInvoiceInfoupload(RequestParam(file)MultipartFilefile)throwsIOException{returnservice.extract(newByteArrayResource(file.getBytes()));}}说清楚替代传统 OCR的思路与代价。思路传统方案是两段式——先检测加识别出文本行再用规则或小模型抽取字段VLM 是端到端图文一起理解字段抽取直接在推理里完成规则代码几乎清零。代价有四个一是精度不到 100%金额这类关键字段必须有校验手段双通道比对或人工抽检二是单价远高于传统 OCR调用一次 VLM 的费用可能是 OCR 的几十倍三是时延是秒级批量回填任务要想清楚吞吐四是幻觉模型看不清时可能编一个合理值——所以提示词里看不清填 null不要猜这句话是保命的没有它错误会以高置信度的样子流进财务系统。2.3 图像生成ImageModel生成是另一个方向的抽象ImageModel。spring-ai-starter-model-openai里自带 OpenAI 的实现dall-e-3国内模型可以用智谱的 starterCogView 系列API 形态一致。以 OpenAI 为例spring:ai:openai:api-key:${OPENAI_API_KEY}image:options:size:1024x1024注意image模块可以单独配 base-url 和 api-keyspring.ai.openai.image.base-url等也就是说聊天走 DeepSeek、画图走 OpenAI 可以共存于同一个应用。ServicepublicclassPosterService{privatefinalImageModelimageModel;publicPosterService(ImageModelimageModel){this.imageModelimageModel;}publicStringgenerate(Stringscene){ImageResponseresponseimageModel.call(newImagePrompt(扁平插画风scene暖色调构图留白适合做活动海报底图,OpenAiImageOptions.builder().withModel(dall-e-3).withWidth(1024).withHeight(1024).withResponseFormat(url)// 返回图片 URL改 b64_json 可直接拿字节.build()));returnresponse.getResult().getOutput().getUrl();}}一个容易被忽略的细节url格式返回的链接是有时效的OpenAI 大约一小时后失效生产上拿到链接要立刻下载并转存到自己的对象存储别把临时 URL 直接写进数据库。三、生产视角图像 token 成本要估算。主流 VLM 按分辨率折算 token比如 OpenAI 的 vision 系列大致按图片面积切 tile 计费一张 2048×1536 的照片可能折算出几千 token比整个文本 prompt 还贵。通义、智谱各有各的折算规则。上线前拿真实图片压测一次把单图 token 数算出来再乘 QPS别等账单出来才看。大图压缩是第一优化项。送到模型前把图片缩到性价比最优的档位多数 VLM 在 768~2048px 之间表现稳定票据抽取这种任务 1500px 长边完全够用JPEG 质量 80 肉眼无损。Java 侧用 Thumbnailator 两行搞定Thumbnails.of(file).size(1600, 1600).outputQuality(0.8).toFile(...)。压缩做在服务端入口收益同时体现在成本、时延和成功率三个维度。超限报错要处理。图片过大时厂商直接拒绝典型报错如Invalid input image - content size too large各家的字节上限不同base64 编码还会再膨胀约三分之一。上传接口要前置校验尺寸和大小给用户可读的提示而不是把 400 原样透传。安全与合规。发票、身份证、银行卡属于敏感个人信息上传到第三方云服务前要过合规评审尽量走企业协议端点、明确数据不留存条款、日志里只记图片哈希不记原图必要时考虑本地化部署的 VLM。缓存与幂等。对同一张图内容哈希相同别重复调用结果按哈希缓存批量回填任务要支持断点重跑。幻觉兜底。金额、税号这类强校验字段抽取结果必须过格式校验位数、校验位失败转人工。多模态模型的幻觉比文本更隐蔽因为它看起来很确定。四、踩坑记录坑一给 DeepSeek 发图直接被 400 拒绝。项目最初统一用 deepseek-chat发图后报org.springframework.web.client.HttpClientErrorException$BadRequest: 400 Bad Request on POST request for https://api.deepseek.com/chat/completions: {error:{message:Invalid request: content type is not supported, type:invalid_request_error,param:null,code:invalid_request_error}}原因很直接DeepSeek 的 chat 模型不支持图像输入消息里的图片部分整个被拒。解决方案是图像任务单独走一个指向通义 compatible-mode 端点的配置model 用 qwen-vl-max。换成视觉模型后又暴露一个软性问题中文发票上壹仟贰佰叁拾元整的大写金额识别偶发出错在提示词里加上金额以票面大写人民币为准之后正确率明显上来。多模态的坑一半在框架一半在提示词。坑二手机直拍原图触发大小限制。测试同事拿手机直拍原图上传报org.springframework.web.client.HttpClientErrorException$BadRequest: 400 Bad Request: {error:{message:Invalid input image - content size too large, type:invalid_request_error}}原图 4032×3024、4.8MBbase64 之后膨胀到 6MB 以上超出接口限制。解决服务端入口统一压缩长边 1600、质量 0.8单图压到 500KB 以内。顺手统计了一下压缩后单图 token 成本降了六成多——这个坑踩得值它逼着我们把压缩逻辑做成了标配。五、小结与练习这一篇的要点多模态输入等于UserMessage加Mediaorg.springframework.ai.content.Media数据来源支持本地 Resource 和远程 URI票据抽取场景和entity()是天作之合但看不清填 null的提示词和业务校验缺一不可图像生成走ImageModel抽象OpenAI 和智谱都开箱即用返回 URL 要及时转存图像按分辨率计费压缩是性价比最高的优化。练习找一张你手头的截图或单据照片写一个接口返回record ImageReport(String description, ListString textsInImage)输出图片描述和图里出现的全部文字。然后分别在原图和压缩后的图上调用对比响应里的 token 消耗与识别结果差异你会对压缩到什么程度开始丢信息有手感。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表