系列篇章💥

No. 文章
1 LangChain4j Java AI 应用开发实战(一):LangChain4j 快速入门指南
2 LangChain4j Java AI 应用开发实战(二):大模型参数调优实战:Temperature、TopP、MaxTokens 深度解析
3 LangChain4j Java AI 应用开发实战(三):多模态 AI 开发 - 图片理解与图像生成实战

目录


前言

大语言模型不仅能处理文本,还能"看懂"图片、"听懂"音频、"画出"图像。多模态能力让 AI 应用从单一的文本交互升级为全方位的感官体验。本文将带你掌握 LangChain4j 的多模态开发技术,包括 GPT-4V/GPT-5 的视觉理解、DALL·E 3 的图像生成、Whisper 的语音转文字。我们将通过真实代码示例,展示如何让 Java 应用识别图片内容、根据描述生成图像、转录音频文件,并分享多模态数据流设计、Base64 编码优化、国产模型多模态能力对比等实战技巧,开启 AI 应用的新维度。


一、什么是多模态 AI?

1.1 从文本到多感官

传统的大语言模型(LLM)只能处理文本输入和输出,就像一个只会读书写字的人。而多模态 AI(Multimodal AI)则具备多种"感官":

模态 能力 类比人类感官
文本 阅读、写作 语言能力
图像 看图、画图 视觉能力
音频 听音、说话 听觉/语言能力
视频 看动态画面 视觉+时间感知

1.2 多模态的应用场景

(1)视觉理解(Image Understanding)

  • OCR 增强:识别发票、合同、手写笔记
  • 商品识别:拍照识别商品、比价
  • 医疗影像:X 光片、CT 扫描辅助诊断
  • 工业质检:产品缺陷检测
  • 安防监控:异常行为识别

(2)图像生成(Image Generation)

  • 营销素材:自动生成广告图、海报
  • 游戏开发:角色设计、场景概念图
  • 教育内容:插图、示意图生成
  • 个性化定制:头像、壁纸生成

(3)音频处理(Audio Processing)

  • 会议记录:自动转录会议纪要
  • 客服录音分析:情感分析、关键词提取
  • 播客字幕:自动生成字幕
  • 语音助手:语音指令识别

1.3 LangChain4j 的多模态支持

LangChain4j 提供了统一的多模态接口:

┌─────────────────────────────────┐
│     LangChain4j 多模态 API       │
├──────────┬──────────┬───────────┤
│ 视觉理解  │ 图像生成  │ 音频处理   │
│ ChatModel│ImageModel│AudioModel │
│ + Image  │generate()│transcribe()│
└──────────┴──────────┴───────────┘
         ↓           ↓          ↓
    OpenAI     DALL·E 3    Whisper
    GPT-4V/5

二、视觉理解:让 AI "看懂"图片

2.1 实践代码解析

让我们从项目中的示例开始:

package com.langchain4j;

import dev.langchain4j.data.message.ImageContent;
import dev.langchain4j.data.message.TextContent;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.openai.OpenAiChatModel;

public class ChatModeWithImageExamples {

    public static void main(String[] args) {
        // 携带图片
        ChatModel chatModel = OpenAiChatModel.builder()
                .baseUrl("http://langchain4j.dev/demo/openai/v1")
                .modelName("gpt-4o-mini")
                .maxTokens(50)
                .build();

        UserMessage userMessage = UserMessage.from(
                TextContent.from("图片中有什么?"),
                ImageContent.from("https://cdn.pixabay.com/photo/2017/08/07/16/36/cat-2605502_1280.jpg")
        );

        ChatResponse chatResponse = chatModel.chat(userMessage);

        System.out.println(chatResponse.aiMessage().text());
        //输出:这是一只可爱的小猫,它正在休息或睡觉,身上有黑白相间的毛发,躺在绿色的表面上。
    }

}

2.2 核心概念解析

(1)UserMessage 支持多模态内容

传统的 UserMessage 只包含文本:

// 纯文本消息
UserMessage message = UserMessage.from("你好");

多模态版本的 UserMessage 可以包含多个内容块

UserMessage userMessage = UserMessage.from(
    TextContent.from("图片中有什么?"),      // 文本内容
    ImageContent.from("图片URL或Base64")     // 图片内容
);

关键点

  • TextContent:文本描述、问题、指令
  • ImageContent:图片数据(URL 或 Base64)
  • 可以混合多个内容块,如:文本 + 图片1 + 图片2

(2)ImageContent 的两种形式

方式一:URL(推荐)

ImageContent.from("https://example.com/image.jpg")

优点

  • 节省带宽(不传输图片二进制数据)
  • 速度快
  • 适合公开可访问的图片

缺点

  • 需要图片可通过 URL 访问
  • 隐私图片不适合

方式二:Base64 编码

// 读取本地图片并转换为 Base64
byte[] imageBytes = Files.readAllBytes(Path.of("cat.jpg"));
String base64Image = Base64.getEncoder().encodeToString(imageBytes);

ImageContent.from("data:image/jpeg;base64," + base64Image)

优点

  • 支持本地图片
  • 适合隐私图片

缺点

  • 数据传输量大(Base64 比原文件大约 33%)
  • 可能超过 API 限制

(3)支持的模型

模型 多模态支持 说明
GPT-4V OpenAI 首个多模态模型
GPT-4o 原生多模态,速度更快
GPT-4o-mini 性价比最高,推荐首选
GPT-5 最强多模态能力(2026 年最新)
通义千问 VL 阿里多模态模型
DeepSeek 暂不支持图片

2.3 实战案例 1:智能 OCR 系统

(1)需求

用户上传发票图片,AI 自动提取关键信息(金额、日期、商家)。

(2)完整代码

import dev.langchain4j.data.message.ImageContent;
import dev.langchain4j.data.message.TextContent;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;

import java.util.Base64;
import java.nio.file.Files;
import java.nio.file.Path;

public class SmartOCR {

    public static void main(String[] args) throws Exception {
        
        // 1. 配置模型
        ChatModel chatModel = OpenAiChatModel.builder()
            .apiKey(System.getenv("OPENAI_API_KEY"))
            .modelName("gpt-4o")  // 使用高质量模型提升识别准确率
            .build();

        // 2. 读取发票图片
        byte[] imageBytes = Files.readAllBytes(Path.of("invoice.jpg"));
        String base64Image = "data:image/jpeg;base64," + 
            Base64.getEncoder().encodeToString(imageBytes);

        // 3. 构建多模态消息
        UserMessage userMessage = UserMessage.from(
            TextContent.from("""
                请识别这张发票图片中的以下信息,并以 JSON 格式返回:
                - invoice_number: 发票号码
                - date: 开票日期(YYYY-MM-DD 格式)
                - merchant: 商家名称
                - total_amount: 总金额(数字)
                - items: 商品列表(数组,每项包含 name 和 price)
                
                只返回 JSON,不要其他文字。
                """),
            ImageContent.from(base64Image)
        );

        // 4. 调用模型
        String response = chatModel.chat(userMessage);
        
        // 5. 解析结果
        System.out.println(response);
        // 输出示例:
        // {
        //   "invoice_number": "INV-2024-001234",
        //   "date": "2024-03-15",
        //   "merchant": "星巴克咖啡",
        //   "total_amount": 45.00,
        //   "items": [
        //     {"name": "拿铁咖啡", "price": 32.00},
        //     {"name": "蓝莓 muffin", "price": 13.00}
        //   ]
        // }
    }
}

(3)关键技术点

结构化输出:要求模型返回 JSON,便于程序解析
详细指令:明确指定需要提取的字段和格式
高质量模型:OCR 任务建议使用 GPT-4o 或 GPT-5 等模型


2.4 实战案例 2:商品识别与比价

(1)需求

用户拍摄商品照片,AI 识别商品名称并提供购买建议。

(2)完整代码

public class ProductRecognizer {

    public static void main(String[] args) throws Exception {
        
        ChatModel chatModel = OpenAiChatModel.builder()
            .apiKey(System.getenv("OPENAI_API_KEY"))
            .modelName("gpt-4o-mini")  // 性价比模型
            .build();

        // 读取商品图片
        byte[] imageBytes = Files.readAllBytes(Path.of("product.jpg"));
        String base64Image = "data:image/jpeg;base64," + 
            Base64.getEncoder().encodeToString(imageBytes);

        UserMessage userMessage = UserMessage.from(
            TextContent.from("""
                请识别这张图片中的商品,并提供以下信息:
                1. 商品名称和品牌
                2. 商品类别
                3. 预估价格范围
                4. 购买建议(是否值得购买,注意事项)
                
                以简洁的列表形式回答。
                """),
            ImageContent.from(base64Image)
        );

        String response = chatModel.chat(userMessage);
        System.out.println(response);
        
        // 输出示例:
        // 1. 商品名称:Nike Air Max 270 运动鞋
        // 2. 商品类别:运动鞋/休闲鞋
        // 3. 预估价格:¥800-1200
        // 4. 购买建议:
        //    - 适合日常穿着和轻度运动
        //    - 建议在官方旗舰店购买,避免假货
        //    - 当前市场价格稳定,可立即购买
    }
}

2.5 性能优化技巧

(1)图片尺寸压缩

大图片会消耗更多 Token 和处理时间。建议在上传前压缩图片

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;

public class ImageCompressor {
    
    public static byte[] compressImage(byte[] originalBytes, int maxWidth, int maxHeight) 
            throws Exception {
        
        // 读取原始图片
        BufferedImage originalImage = ImageIO.read(
            new ByteArrayInputStream(originalBytes)
        );
        
        // 计算缩放比例
        double scale = Math.min(
            (double) maxWidth / originalImage.getWidth(),
            (double) maxHeight / originalImage.getHeight()
        );
        
        if (scale >= 1.0) {
            return originalBytes;  // 无需压缩
        }
        
        // 缩放图片
        int newWidth = (int) (originalImage.getWidth() * scale);
        int newHeight = (int) (originalImage.getHeight() * scale);
        
        BufferedImage scaledImage = new BufferedImage(
            newWidth, newHeight, BufferedImage.TYPE_INT_RGB
        );
        scaledImage.createGraphics().drawImage(
            originalImage, 0, 0, newWidth, newHeight, null
        );
        
        // 转换为 JPEG(质量 80%)
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ImageIO.write(scaledImage, "jpg", baos);
        
        return baos.toByteArray();
    }
}

// 使用
byte[] compressed = ImageCompressor.compressImage(imageBytes, 1024, 1024);

效果

  • 原始图片:5MB → 压缩后:200KB
  • Token 消耗减少 80%
  • 响应速度提升 50%

(2)缓存识别结果

对于重复的图片(如同一张发票多次上传),使用哈希缓存

import java.security.MessageDigest;
import java.util.concurrent.ConcurrentHashMap;

public class OCRService {
    
    private final ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();
    private final ChatModel chatModel;
    
    public String recognizeInvoice(byte[] imageBytes) {
        // 计算图片哈希
        String hash = calculateHash(imageBytes);
        
        // 检查缓存
        if (cache.containsKey(hash)) {
            return cache.get(hash);  // 直接返回缓存结果
        }
        
        // 调用 AI 识别
        String result = callAI(imageBytes);
        
        // 存入缓存(设置过期时间)
        cache.put(hash, result);
        
        return result;
    }
    
    private String calculateHash(byte[] data) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] hashBytes = md.digest(data);
            return Base64.getEncoder().encodeToString(hashBytes);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

三、图像生成:让 AI "画出"图片

3.1 实践代码解析

package com.langchain4j;

import dev.langchain4j.data.image.Image;
import dev.langchain4j.model.image.ImageModel;
import dev.langchain4j.model.openai.OpenAiImageModel;
import dev.langchain4j.model.output.Response;

import static dev.langchain4j.model.openai.OpenAiImageModelName.DALL_E_3;

public class T10_OpenAiImageModelExamples {

    public static void main(String[] args) {

        // ==================== 1. 配置图像生成模型 ====================

        ImageModel model = OpenAiImageModel.builder()
                .apiKey("demo")
                .modelName(DALL_E_3)
                .build();

        // ==================== 2. 生成图像 ====================

        Response<Image> response = model.generate("Donald Duck in New York, cartoon style");

        // ==================== 3. 获取生成结果 ====================

        System.out.println(response.content().url()); 
        // 输出:https://oaidalleapiprodscus.blob.core.windows.net/private/...
    }
}

3.2 核心概念解析

(1)ImageModel 接口

ChatModel 不同,图像生成使用专门的 ImageModel 接口:

public interface ImageModel {
    Response<Image> generate(String prompt);
    Response<List<Image>> generate(String prompt, int n);  // 生成多张
}

(2)DALL·E 3 vs DALL·E 2

特性 DALL·E 3 DALL·E 2
图像质量 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
Prompt 理解 极强,能理解复杂描述 一般
生成数量 每次 1 张 每次 1-10 张
成本 $0.04-0.08/张 $0.016-0.02/张
适用场景 高质量需求 批量生成

推荐:除非需要批量生成,否则优先使用 DALL·E 3。

(3)高级配置

ImageModel model = OpenAiImageModel.builder()
    .apiKey(System.getenv("OPENAI_API_KEY"))
    .modelName(DALL_E_3)
    .size("1792x1024")          // 图像尺寸
    .quality("hd")              // 高清质量
    .style("vivid")             // 鲜明风格
    .responseFormat("url")      // 返回格式
    .build();

参数说明

参数 可选值 说明
size 1024x1024(默认)
1792x1024(宽屏)
1024x1792(竖屏)
图像分辨率
quality standard(默认)
hd
图像质量,hd 更细腻但成本高 2 倍
style vivid(默认)
natural
vivid 更鲜艳,natural 更写实
responseFormat url(默认)
b64_json
url 返回临时链接,b64_json 返回 Base64

3.3 Prompt 工程:如何写出好的图像描述

(1)基础结构

一个好的图像 Prompt 应包含:

[主体] + [动作/状态] + [环境/背景] + [风格] + [细节]

示例

// ❌ 差的 Prompt
model.generate("一只猫");

// ✅ 好的 Prompt
model.generate(
    "一只橘色的波斯猫,蜷缩在阳光照射的窗台上," +
    "窗外是巴黎埃菲尔铁塔的远景," +
    "印象派油画风格,温暖的色调,柔和的光线," +
    "高细节,8k 分辨率"
);

(2)风格关键词库

风格类型 关键词
写实 photorealistic, realistic, hyperrealistic, 8k
卡通 cartoon style, anime, manga, Disney style
艺术 oil painting, watercolor, impressionist, abstract
科幻 cyberpunk, futuristic, sci-fi, neon lights
复古 vintage, retro, 1950s style, film grain
极简 minimalist, clean, simple, flat design

(3)实战案例:营销海报生成

public class MarketingPosterGenerator {

    public static void main(String[] args) {
        
        ImageModel model = OpenAiImageModel.builder()
            .apiKey(System.getenv("OPENAI_API_KEY"))
            .modelName(DALL_E_3)
            .size("1024x1792")  // 竖屏,适合手机海报
            .quality("hd")
            .style("vivid")
            .build();

        String prompt = """
            A vibrant summer sale poster for a coffee shop,
            featuring a large iced latte with condensation droplets,
            surrounded by fresh coffee beans and green leaves,
            bright yellow and orange color scheme,
            bold text space at the top for "SUMMER SALE 50% OFF",
            modern minimalist design, high contrast,
            professional commercial photography style, 8k
            """;

        Response<Image> response = model.generate(prompt);
        
        String imageUrl = response.content().url();
        System.out.println("海报生成成功:" + imageUrl);
        
        // ⚠️ 重要:立即下载保存,URL 1 小时后失效
        downloadAndSave(imageUrl, "summer_sale_poster.png");
    }
    
    private static void downloadAndSave(String url, String filename) {
        // 实现下载逻辑...
    }
}

3.4 图像保存与管理

⚠️ 重要提醒:URL 临时性

DALL·E 返回的 URL 有效期仅 1 小时,必须及时下载保存!

(1)下载到本地

import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;

public class ImageDownloader {
    
    public static void downloadImage(String imageUrl, String savePath) 
            throws Exception {
        
        URL url = new URL(imageUrl);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("GET");
        
        try (var inputStream = conn.getInputStream()) {
            Files.copy(inputStream, Path.of(savePath));
        }
        
        System.out.println("图片已保存到:" + savePath);
    }
}

// 使用
downloadImage(response.content().url(), "/images/generated/poster.png");

(2)上传到云存储(推荐生产环境)

import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

public class S3ImageUploader {
    
    private final S3Client s3Client;
    private final String bucketName;
    
    public String uploadToS3(byte[] imageBytes, String filename) {
        
        String key = "generated-images/" + UUID.randomUUID() + "-" + filename;
        
        PutObjectRequest request = PutObjectRequest.builder()
            .bucket(bucketName)
            .key(key)
            .contentType("image/png")
            .build();
        
        s3Client.putObject(request, 
            software.amazon.awssdk.core.sync.RequestBody.fromBytes(imageBytes)
        );
        
        // 返回永久 URL
        return String.format("https://%s.s3.amazonaws.com/%s", bucketName, key);
    }
}


四、音频处理:让 AI "听懂"声音

4.1 实践代码解析

package com.langchain4j;

import dev.langchain4j.data.audio.Audio;
import dev.langchain4j.model.audio.AudioTranscriptionModel;
import dev.langchain4j.model.audio.AudioTranscriptionRequest;
import dev.langchain4j.model.audio.AudioTranscriptionResponse;
import dev.langchain4j.model.openai.OpenAiAudioTranscriptionModel;

import static dev.langchain4j.model.openai.OpenAiAudioTranscriptionModelName.WHISPER_1;

public class T10_OpenAiAudioModelExamples {

    public static void main(String[] args) {

        // ==================== 1. 配置语音转录模型 ====================

        AudioTranscriptionModel model = OpenAiAudioTranscriptionModel.builder()
                .apiKey("demo")
                .modelName(WHISPER_1)
                .logRequests(true)
                .logResponses(true)
                .build();

        // ==================== 2. 构建转录请求 ====================

        AudioTranscriptionResponse response = model.transcribe(
            AudioTranscriptionRequest.builder()
                .audio(Audio.builder()
                    .mimeType("audio/wav")           // 音频格式
                    .binaryData(toBytes("audio.wav")) // 音频数据
                    .build())
                .build()
        );

        // ==================== 3. 获取转录结果 ====================

        System.out.println(response.text());
    }

    private static byte[] toBytes(String fileName) {
        try {
            URL fileUrl = T10_OpenAiAudioModelExamples.class.getResource(fileName);
            return Files.readAllBytes(Path.of(fileUrl.toURI()));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

4.2 核心概念解析

(1)Whisper 模型

Whisper 是 OpenAI 开发的开源语音识别模型,支持:

  • 99 种语言:包括中文、英文、日文、韩文等
  • 多种音频格式:mp3, wav, m4a, flac, ogg 等
  • 说话人分离:识别不同说话者(需额外处理)
  • 时间戳:精确到单词级别的时序信息

(2)AudioTranscriptionModel 接口

public interface AudioTranscriptionModel {
    AudioTranscriptionResponse transcribe(AudioTranscriptionRequest request);
}

(3)Audio 对象

Audio audio = Audio.builder()
    .mimeType("audio/wav")      // MIME 类型(必填)
    .binaryData(audioBytes)     // 音频二进制数据(必填)
    .build();

支持的 MIME 类型

  • audio/wav
  • audio/mpeg (mp3)
  • audio/mp4 (m4a)
  • audio/flac
  • audio/ogg
  • audio/webm

文件大小限制25MB,超过需切割。


4.3 高级配置

AudioTranscriptionResponse response = model.transcribe(
    AudioTranscriptionRequest.builder()
        .audio(Audio.builder()
            .mimeType("audio/mp3")
            .binaryData(audioBytes)
            .build())
        .language("zh")                    // 指定语言为中文
        .prompt("这是技术会议讨论内容")      // 提示词,引导术语风格
        .responseFormat("verbose_json")    // 详细 JSON 格式
        .temperature(0.2)                  // 采样温度
        .build()
);

参数说明

参数 说明 示例
language 指定音频语言,提升准确率 "zh", "en", "ja"
prompt 提示词,引导转录风格 "这是医学讲座"
responseFormat 输出格式 text(默认), json, srt, vtt, verbose_json
temperature 采样温度(0-1) 0(确定性), 0.5(平衡)

4.4 实战案例 1:会议记录自动生成

(1)需求

录制会议音频,自动转录为文字,并提取关键决策。

(2)完整代码

public class MeetingTranscriber {

    public static void main(String[] args) throws Exception {
        
        // 1. 配置 Whisper 模型
        AudioTranscriptionModel whisper = OpenAiAudioTranscriptionModel.builder()
            .apiKey(System.getenv("OPENAI_API_KEY"))
            .modelName(WHISPER_1)
            .build();

        // 2. 读取会议录音
        byte[] audioBytes = Files.readAllBytes(Path.of("meeting.mp3"));

        // 3. 转录音频
        AudioTranscriptionResponse response = whisper.transcribe(
            AudioTranscriptionRequest.builder()
                .audio(Audio.builder()
                    .mimeType("audio/mp3")
                    .binaryData(audioBytes)
                    .build())
                .language("zh")  // 中文会议
                .build()
        );

        String transcript = response.text();
        System.out.println("转录结果:\n" + transcript);

        // 4. 使用 ChatModel 提取关键决策
        ChatModel chatModel = OpenAiChatModel.builder()
            .apiKey(System.getenv("OPENAI_API_KEY"))
            .modelName("gpt-4o-mini")
            .build();

        String summaryPrompt = """
            以下是会议转录文本,请提取:
            1. 主要讨论议题
            2. 达成的决策
            3. 待办事项(负责人 + 截止时间)
            
            以 Markdown 格式输出。
            
            会议内容:
            %s
            """.formatted(transcript);

        String summary = chatModel.chat(summaryPrompt);
        System.out.println("\n会议纪要:\n" + summary);
        
        // 输出示例:
        // # 主要讨论议题
        // 1. Q2 产品发布计划
        // 2. 市场推广预算分配
        // 
        // # 达成的决策
        // - 确定 6 月 15 日发布新产品
        // - 市场预算增加 20%
        // 
        // # 待办事项
        // - 张三:完成产品测试报告(6 月 10 日前)
        // - 李四:制定市场推广方案(6 月 12 日前)
    }
}

4.5 实战案例 2:长音频分段处理

(1)问题

Whisper API 限制单次最多 25MB 音频(约 25 分钟)。如何处理 1 小时的录音?

(2)解决方案:音频切割 + 合并

import javax.sound.sampled.*;
import java.io.ByteArrayInputStream;

public class LongAudioTranscriber {

    private static final int MAX_SEGMENT_DURATION = 20 * 60; // 20 分钟/段

    public String transcribeLongAudio(byte[] audioBytes) throws Exception {
        
        // 1. 切割音频
        List<byte[]> segments = splitAudio(audioBytes, MAX_SEGMENT_DURATION);
        
        // 2. 逐段转录
        AudioTranscriptionModel whisper = OpenAiAudioTranscriptionModel.builder()
            .apiKey(System.getenv("OPENAI_API_KEY"))
            .modelName(WHISPER_1)
            .build();

        StringBuilder fullTranscript = new StringBuilder();
        
        for (int i = 0; i < segments.size(); i++) {
            System.out.println("处理第 " + (i + 1) + "/" + segments.size() + " 段...");
            
            AudioTranscriptionResponse response = whisper.transcribe(
                AudioTranscriptionRequest.builder()
                    .audio(Audio.builder()
                        .mimeType("audio/wav")
                        .binaryData(segments.get(i))
                        .build())
                    .language("zh")
                    .build()
            );
            
            fullTranscript.append(response.text()).append("\n\n");
        }
        
        return fullTranscript.toString();
    }

    private List<byte[]> splitAudio(byte[] audioBytes, int maxSeconds) 
            throws Exception {
        
        // 读取音频流
        AudioInputStream audioStream = AudioSystem.getAudioInputStream(
            new ByteArrayInputStream(audioBytes)
        );
        
        AudioFormat format = audioStream.getFormat();
        long totalFrames = audioStream.getFrameLength();
        float frameRate = format.getFrameRate();
        
        // 计算每段的帧数
        long framesPerSegment = (long) (frameRate * maxSeconds);
        
        List<byte[]> segments = new ArrayList<>();
        long currentFrame = 0;
        
        while (currentFrame < totalFrames) {
            long remainingFrames = totalFrames - currentFrame;
            long segmentFrames = Math.min(framesPerSegment, remainingFrames);
            
            // 读取当前段
            byte[] segmentBytes = readSegment(audioStream, segmentFrames, format);
            segments.add(segmentBytes);
            
            currentFrame += segmentFrames;
        }
        
        return segments;
    }

    private byte[] readSegment(AudioInputStream stream, long frames, 
                               AudioFormat format) throws Exception {
        // 实现音频段读取逻辑...
        // 简化版:实际需要使用 Java Sound API 精确切割
        return new byte[0];
    }
}

提示:音频切割较复杂,生产环境建议使用专业库如 FFmpeg(通过 ProcessBuilder 调用)。


五、多模态数据流设计

5.1 架构概览

┌──────────┐     ┌──────────────┐     ┌─────────────┐
│ 用户上传  │────▶│  预处理服务   │────▶│  AI 处理     │
│ 图片/音频 │     │ - 压缩       │     │ - 视觉理解   │
└──────────┘     │ - 格式转换   │     │ - 图像生成   │
                 │ - 鉴权       │     │ - 语音转录   │
                 └──────────────┘     └──────┬──────┘
                                            │
                                            ▼
                                     ┌─────────────┐
                                     │  结果存储    │
                                     │ - 图片保存   │
                                     │ - 文本索引   │
                                     │ - 元数据记录 │
                                     └─────────────┘

5.2 Base64 编码优化

(1)问题

Base64 编码会使数据量增加 33%,影响传输速度。

(2)解决方案

方案 1:使用 URL(优先)

// ✅ 推荐:直接传 URL
ImageContent.from("https://example.com/image.jpg")

方案 2:压缩后再编码

// 压缩图片
byte[] compressed = compressImage(originalBytes, 1024, 1024);

// Base64 编码
String base64 = Base64.getEncoder().encodeToString(compressed);

方案 3:异步上传 + URL 回调

// 1. 用户上传到 OSS
String ossUrl = ossService.upload(userFile);

// 2. 传 URL 给 AI
ImageContent.from(ossUrl);

5.3 错误处理与重试

public class MultimodalService {

    private static final int MAX_RETRIES = 3;

    public String processImageWithRetry(byte[] imageBytes) {
        
        for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
            try {
                return processImage(imageBytes);
            } catch (Exception e) {
                logger.warn("第 {} 次尝试失败: {}", attempt, e.getMessage());
                
                if (attempt == MAX_RETRIES) {
                    throw new ServiceException("图像处理失败,请稍后重试", e);
                }
                
                // 指数退避重试
                try {
                    Thread.sleep((long) Math.pow(2, attempt) * 1000);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                }
            }
        }
        
        throw new IllegalStateException("不应到达此处");
    }

    private String processImage(byte[] imageBytes) {
        // 实际处理逻辑...
        return chatModel.chat(userMessage);
    }
}

六、常见问题与避坑指南

6.1 图片无法识别

现象:AI 回复"我无法查看图片"或返回无关内容

原因

  1. 图片 URL 不可访问(403/404)
  2. Base64 格式错误
  3. 模型不支持多模态

解决方案

// 1. 验证 URL 可访问性
try {
    HttpURLConnection conn = (HttpURLConnection) new URL(imageUrl).openConnection();
    conn.setRequestMethod("HEAD");
    if (conn.getResponseCode() != 200) {
        throw new IllegalArgumentException("图片 URL 不可访问");
    }
} catch (Exception e) {
    // 降级:下载到本地再转 Base64
}

// 2. 确认使用多模态模型
OpenAiChatModel.builder()
    .modelName("gpt-4o")  // ✅ 支持多模态
    // .modelName("gpt-3.5-turbo")  // ❌ 不支持
    .build();

6.2 图像生成超时

现象SocketTimeoutException

原因:DALL·E 生成耗时 5-15 秒,默认超时太短

解决方案

OpenAiImageModel.builder()
    .apiKey(System.getenv("OPENAI_API_KEY"))
    .timeout(Duration.ofSeconds(30))  // 增加超时时间
    .build();

6.3 音频转录不准确

现象:中文识别成英文,或专有名词错误

原因:未指定语言或缺少上下文

解决方案

AudioTranscriptionRequest.builder()
    .audio(audio)
    .language("zh")  // 明确指定中文
    .prompt("这是人工智能技术研讨会,涉及 Transformer、BERT 等专业术语")
    .build();

结语

通过本文的学习,你已经掌握了 LangChain4j 的多模态开发核心技术。从让 AI "看懂"图片的视觉理解,到"画出"图像的 DALL·E 3 生成,再到"听懂"声音的 Whisper 转录,多模态能力为你的 Java 应用打开了全新的交互维度。记住,多模态不仅是技术的堆砌,更是用户体验的升级——让用户能用最自然的方式(拍照、说话)与 AI 交互。下一篇我们将探索提示词工程进阶,学习如何通过模板化和结构化的 Prompt 设计,让 AI 的输出更加精准可控,为构建企业级 AI 应用打下坚实基础!


在这里插入图片描述

🎯🔖更多专栏系列文章:AI大模型提示工程完全指南AI大模型探索之路(零基础入门)AI大模型预训练微调进阶AI大模型开源精选实践AI大模型Spring AI开发实战🔥🔥🔥 其他专栏可以查看博客主页

🔔 关于作者:资深程序老猿,10年+架构经验,现专注 AIGC 探索与实践。
👍 若文章对你有所触动,恳请点赞 ⭐ 关注 ⭐ 收藏!AI 浪潮已至,愿与你同行。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐