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