】07-多模態(tài)入門-圖像理解與生成)
多模態(tài)入門圖像理解與生成本文是專欄《Spring AI 入門與實戰(zhàn)》的第 7 篇上一篇我們聊了《結構化輸出讓 AI 穩(wěn)定返回 Java 對象》讓模型輸出變成可靠的 Java 數(shù)據(jù)。這一篇把輸入從純文本擴展到圖片怎么把一張發(fā)票圖片直接丟給模型抽出結構化字段以及怎么反過來讓模型畫圖。一、場景引入財務同事提了個需求每月幾千張報銷發(fā)票要人工錄入OCR 廠商按調用收費識別出原始文本之后發(fā)票抬頭、金額、稅號這些關鍵字段還得再寫一堆規(guī)則去摳。團隊想試試視覺大模型VLM一張圖進去結構化字段出來一個接口把識別和抽取一起做完。這就是多模態(tài)輸入的價值。順帶還有個反向需求運營活動頁要配圖希望用一句話生成海報底圖。Spring AI 1.1 對這兩件事都有直接支持——前者是 ChatModel 的多模態(tài)消息后者是獨立的 ImageModel 抽象。本篇把兩件事都講清楚。二、核心講解2.1 多模態(tài)消息的構造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多模態(tài)消息的核心類是org.springframework.ai.content.Media它承載兩部分信息MIME 類型MimeTypeUtils.IMAGE_PNG、IMAGE_JPEG等和數(shù)據(jù)來源。來源支持兩種本地 Resourceclasspath、文件、內存字節(jié)和遠程 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 實戰(zhàn)票據(jù)信息抽取替代傳統(tǒng) OCR這個場景是上一篇entity()的天然搭檔圖片進、對象出。定義返回類型publicrecordInvoiceInfo(Stringtitle,// 發(fā)票抬頭StringinvoiceNo,// 發(fā)票號碼BigDecimalamount,// 價稅合計StringissueDate,// 開票日期yyyy-MM-ddStringsellerTaxId// 銷方稅號){}服務類ServicepublicclassInvoiceOcrService{privatefinalChatClientchatClient;publicInvoiceOcrService(ChatModelchatModel){this.chatClientChatClient.builder(chatModel).build();}publicInvoiceInfoextract(Resourceimage){UserMessagemessageUserMessage.builder().text( 請從這張發(fā)票圖片中抽取字段發(fā)票抬頭、發(fā)票號碼、\ 價稅合計金額、開票日期、銷方稅號。 金額保留兩位小數(shù)日期用 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()));}}說清楚替代傳統(tǒng) OCR的思路與代價。思路傳統(tǒng)方案是兩段式——先檢測加識別出文本行再用規(guī)則或小模型抽取字段VLM 是端到端圖文一起理解字段抽取直接在推理里完成規(guī)則代碼幾乎清零。代價有四個一是精度不到 100%金額這類關鍵字段必須有校驗手段雙通道比對或人工抽檢二是單價遠高于傳統(tǒng) OCR調用一次 VLM 的費用可能是 OCR 的幾十倍三是時延是秒級批量回填任務要想清楚吞吐四是幻覺模型看不清時可能編一個合理值——所以提示詞里看不清填 null不要猜這句話是保命的沒有它錯誤會以高置信度的樣子流進財務系統(tǒng)。2.3 圖像生成ImageModel生成是另一個方向的抽象ImageModel。spring-ai-starter-model-openai里自帶 OpenAI 的實現(xiàn)dall-e-3國內模型可以用智譜的 starterCogView 系列API 形態(tài)一致。以 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 可直接拿字節(jié).build()));returnresponse.getResult().getOutput().getUrl();}}一個容易被忽略的細節(jié)url格式返回的鏈接是有時效的OpenAI 大約一小時后失效生產上拿到鏈接要立刻下載并轉存到自己的對象存儲別把臨時 URL 直接寫進數(shù)據(jù)庫。三、生產視角圖像 token 成本要估算。主流 VLM 按分辨率折算 token比如 OpenAI 的 vision 系列大致按圖片面積切 tile 計費一張 2048×1536 的照片可能折算出幾千 token比整個文本 prompt 還貴。通義、智譜各有各的折算規(guī)則。上線前拿真實圖片壓測一次把單圖 token 數(shù)算出來再乘 QPS別等賬單出來才看。大圖壓縮是第一優(yōu)化項。送到模型前把圖片縮到性價比最優(yōu)的檔位多數(shù) VLM 在 768~2048px 之間表現(xiàn)穩(wěn)定票據(jù)抽取這種任務 1500px 長邊完全夠用JPEG 質量 80 肉眼無損。Java 側用 Thumbnailator 兩行搞定Thumbnails.of(file).size(1600, 1600).outputQuality(0.8).toFile(...)。壓縮做在服務端入口收益同時體現(xiàn)在成本、時延和成功率三個維度。超限報錯要處理。圖片過大時廠商直接拒絕典型報錯如Invalid input image - content size too large各家的字節(jié)上限不同base64 編碼還會再膨脹約三分之一。上傳接口要前置校驗尺寸和大小給用戶可讀的提示而不是把 400 原樣透傳。安全與合規(guī)。發(fā)票、身份證、銀行卡屬于敏感個人信息上傳到第三方云服務前要過合規(guī)評審盡量走企業(yè)協(xié)議端點、明確數(shù)據(jù)不留存條款、日志里只記圖片哈希不記原圖必要時考慮本地化部署的 VLM。緩存與冪等。對同一張圖內容哈希相同別重復調用結果按哈希緩存批量回填任務要支持斷點重跑?;糜X兜底。金額、稅號這類強校驗字段抽取結果必須過格式校驗位數(shù)、校驗位失敗轉人工。多模態(tài)模型的幻覺比文本更隱蔽因為它看起來很確定。四、踩坑記錄坑一給 DeepSeek 發(fā)圖直接被 400 拒絕。項目最初統(tǒng)一用 deepseek-chat發(fā)圖后報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。換成視覺模型后又暴露一個軟性問題中文發(fā)票上壹仟貳佰叁拾元整的大寫金額識別偶發(fā)出錯在提示詞里加上金額以票面大寫人民幣為準之后正確率明顯上來。多模態(tài)的坑一半在框架一半在提示詞??佣謾C直拍原圖觸發(fā)大小限制。測試同事拿手機直拍原圖上傳報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 以上超出接口限制。解決服務端入口統(tǒng)一壓縮長邊 1600、質量 0.8單圖壓到 500KB 以內。順手統(tǒng)計了一下壓縮后單圖 token 成本降了六成多——這個坑踩得值它逼著我們把壓縮邏輯做成了標配。五、小結與練習這一篇的要點多模態(tài)輸入等于UserMessage加Mediaorg.springframework.ai.content.Media數(shù)據(jù)來源支持本地 Resource 和遠程 URI票據(jù)抽取場景和entity()是天作之合但看不清填 null的提示詞和業(yè)務校驗缺一不可圖像生成走ImageModel抽象OpenAI 和智譜都開箱即用返回 URL 要及時轉存圖像按分辨率計費壓縮是性價比最高的優(yōu)化。練習找一張你手頭的截圖或單據(jù)照片寫一個接口返回record ImageReport(String description, ListString textsInImage)輸出圖片描述和圖里出現(xiàn)的全部文字。然后分別在原圖和壓縮后的圖上調用對比響應里的 token 消耗與識別結果差異你會對壓縮到什么程度開始丟信息有手感。