鱼跃项目多模态能力深度解析

项目源码: https://github.com/pengyuyanITYU/YU-AI-CODE.git

本文档深度总结了项目中实现的多模态能力,重点涵盖核心架构、关键组件、数据流转以及在应用创建和对话历史加载中的具体实现细节。

介绍

LangChain4j 提供了强大的多模态(Multimodal)支持,允许 Java 开发者与具备视觉能力的 LLM(如 GPT-4o, Gemini, Qwen-VL 等)进行无缝交互。通过统一的 API,开发者不再局限于纯文本对话,而是可以将 图片 与 文本 组合发送给大模型,轻松实现图像理解、OCR(文字识别)、视觉问答等复杂场景。

快速上手

关键依赖

<!-- LangChain4j 核心模块版本 (已发布正式版) -->  
<langchain4j.version>1.11.0</langchain4j.version>  
<!-- LangChain4j Starters 和社区模块版本 (目前仍处于 beta 阶段) -->  
<langchain4j-beta.version>1.11.0-beta19</langchain4j-beta.version>

<dependencies>
		<!-- Langchain4j 核心 -->  
		<dependency>  
			<groupId>dev.langchain4j</groupId>  
			<artifactId>langchain4j</artifactId>  
			<version>${langchain4j.version}</version>  
		</dependency>  
		
		<!-- OpenAI Spring Boot Starter  -->  
		<dependency>  
			<groupId>dev.langchain4j</groupId>  
			<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>  
			<version>${langchain4j-beta.version}</version>  
		</dependency>  
		
		<!-- Reactor 支持 (通常跟随核心或 Starter 版本,建议使用 beta 以防兼容性问题) -->  
		<dependency>  
			<groupId>dev.langchain4j</groupId>  
			<artifactId>langchain4j-reactor</artifactId>  
			<version>${langchain4j-beta.version}</version>  
		</dependency>
</dependencies>

代码示例

package com.yu.yuaicodemother;

import dev.langchain4j.data.image.Image;
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 dev.langchain4j.service.SystemMessage;
import java.time.Duration;

/**
 * LangChain4j 多模态能力修复演示类
 * <p>
 * 功能描述:
 * 演示如何使用 LangChain4j 的底层 API (Low-Level API) 与支持视觉能力的模型进行交互。
 * 本示例特指连接阿里云百炼平台 (DashScope) 的 Qwen-VL 系列模型。
 * </p>
 *
 * @version LangChain4j 0.32.0+
 */
public class MultimodalFix {

    /**
     * 定义 AI 服务接口 (High-Level API)
     * 注意:本示例的 main 方法中主要演示了底层 API 调用,此接口展示了声明式调用的写法。
     */
    public interface MultimodalAssistant {
        /**
         * 定义系统提示词 (System Prompt)
         * @param initPrompt 用户输入的混合消息(包含图片和文本)
         * @return AI 的回答
         */
        @SystemMessage("你是一个全能的视觉助手。")
        String chat(UserMessage initPrompt);
    }

    public static void main(String[] args) {
        // ==========================================
        // 1. 配置连接参数
        // ==========================================
        
        // 阿里云百炼兼容 OpenAI 协议的 Base URL
        String baseUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1";
        
        // 【重要】请在此处填写您的实际 API Key,或从环境变量中获取
        String apiKey = "YOUR_API_KEY"; 

        // 简单的安全检查
        if (apiKey == null || apiKey.isEmpty() || "YOUR_API_KEY".equals(apiKey)) {
            System.err.println("错误:请先设置正确的 DASHSCOPE_API_KEY");
            return;
        }

        // ==========================================
        // 2. 构建 ChatModel 实例
        // ==========================================
        // 使用 OpenAiChatModel 是因为阿里云 Qwen 实现了 OpenAI 的 API 接口标准
        ChatModel model = OpenAiChatModel.builder()
                .baseUrl(baseUrl)
                .apiKey(apiKey)
                // 指定模型名称,必须是支持视觉的模型 (如 qwen-vl-max, qwen-omni 等)
                .modelName("qwen3-omni-flash-2025-12-01") 
                // 视觉任务处理时间较长,建议适当延长超时时间
                .timeout(Duration.ofSeconds(60))
                // 开启日志,方便在控制台查看请求和响应的 JSON 细节
                .logRequests(true)
                .logResponses(true)
                .build();

        // ==========================================
        // 3. 准备多模态数据
        // ==========================================
        
        // 测试图片 URL (请替换为真实可访问的公网图片链接)
        String imageUrl = "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg"; 
    

        // ==========================================
        // 4. 执行调用 (底层 API 方式)
        // ==========================================
        System.out.println("\n=== 尝试底层 API 方式 ===");
        try {
            // 手动构建 UserMessage,同时传入 TextContent 和 ImageContent
            UserMessage multimodalMessage = UserMessage.from(
                    TextContent.from("请详细描述这张图片的内容,关注天气情况。"), // 文本指令
                    ImageContent.from(imageUrl)                               // 图片内容
            );

            // 发起请求并获取响应
            // model.chat(...) 返回 ChatResponse
            // .aiMessage() 获取 AI 的消息体
            // .text() 获取具体的文本回复内容
            String response = model.chat(multimodalMessage).aiMessage().text();
            
            // 输出结果
            System.out.println("AI回复: " + response);
            
        } catch (Exception e) {
            System.err.println("底层 API 调用失败: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

流式输出代码示例

package com.yu.yuaicodemother;  
  
import dev.langchain4j.data.message.ChatMessage;  
import dev.langchain4j.data.message.ImageContent;  
import dev.langchain4j.data.message.TextContent;  
import dev.langchain4j.data.message.UserMessage;  
import dev.langchain4j.model.chat.StreamingChatModel;  
import dev.langchain4j.model.chat.response.ChatResponse;  
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;  
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;  
import reactor.core.publisher.Flux;  
  
import java.time.Duration;  
import java.util.Collections;  
import java.util.List;  
  
/**  
 * LangChain4j 多模态流式响应演示类
 * <p>  
 * 功能描述:  
 * 使用新的 chat() 方法和 StreamingChatResponseHandler 处理流式输出。  
 * 这种方式是 LangChain4j 当前推荐的响应式处理标准。  
 * </p>  
 */  
public class MultimodalStreamingFix {  
  
    public static void main(String[] args) {  
        // 1. 配置参数  
        String baseUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1";  
        String apiKey = "YOUR_API_KEY"; // 请替换为实际的 API Key  
        // 2. 构建流式模型实例 (StreamingChatModel)      
          StreamingChatModel streamingModel = OpenAiStreamingChatModel.builder()  
                .baseUrl(baseUrl)  
                .apiKey(apiKey)  
                .modelName("qwen3-omni-flash-2025-12-01") // 使用全模态模型  
                .timeout(Duration.ofSeconds(60))  
                .logRequests(false)  
                .logResponses(false)  
                .build();  
  
        // 3. 准备测试数据  
        String imageUrl = "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg";  
        UserMessage message = UserMessage.from(  
                TextContent.from("请简要描述这张图,并预测画面外可能发生了什么?"),  
                ImageContent.from(imageUrl)  
        );  
  
        // 4. 调用并订阅 Flux        
        System.out.println("\n=== 开始接收流式响应 ===");  
  
        getChatFlux(streamingModel, message)  
                .doOnNext(System.out::print) // 实时打印每个字符  
                .doOnComplete(() -> System.out.println("\n\n=== 传输完成 ==="))  
                .doOnError(e -> System.err.println("\n流异常: " + e.getMessage()))  
                .blockLast(); // 仅在演示中阻塞  
    }  
  
    /**  
     * 将 LangChain4j 的回调式流转为 Project Reactor 的 Flux<String>  
     * 适配 LangChain4j 0.35.0+ API  
     *     * @param model 异步模型  
     * @param userMessage 用户消息  
     * @return 字符流  
     */  
    public static Flux<String> getChatFlux(StreamingChatModel model, UserMessage userMessage) {  
        return Flux.create(sink -> {  
            List<ChatMessage> messages = Collections.singletonList(userMessage);  
  
            // 使用最新的 chat 方法  
            model.chat(messages, new StreamingChatResponseHandler() {  
  
                @Override  
                public void onPartialResponse(String partialResponse) {  
                    // 对应以前的 onNext,处理增量文本  
                    sink.next(partialResponse);  
                }  
  
                @Override  
                public void onCompleteResponse(ChatResponse response) {  
                    // 对应以前的 onComplete,传输完成  
                    sink.complete();  
                }  
  
                @Override  
                public void onError(Throwable error) {  
                    // 异常处理  
                    sink.error(error);  
                }  
            });  
        });  
    }  
}

项目实战

1. 核心架构与组件

项目采用模块化设计,将文件处理、消息构建与 AI 服务调用解耦,实现了灵活、可扩展的多模态支持。

1.1 关键类与职责

  • FileProcessorFactory:

    • 职责: 策略工厂,根据文件后缀(如 .pdf, .png, .java)将请求分发给具体的处理器。
  • PdfFileProcessor (视觉化渲染):

    • 策略: 摒弃传统的纯文本提取,采用 Visual Rendering 策略。
    • 实现: 使用 PDFBox 将 PDF 页面渲染为高保真图片。
    • 优势: 让 AI 能“看见”文档中的图表、表格、公式及排版结构,显著提升对非结构化文档的理解能力。
  • ImageFileProcessor (智能压缩):

    • 实现: 使用 Thumbnailator 进行智能压缩(限制长边 1024px,质量 0.8)。
    • 目的: 在保证 OCR/视觉识别精度的前提下,最小化 Base64 体积,节省 Token 并提升传输速度。
  • TextFileProcessor (代码与纯文本):

    • 支持格式: txt, md, html, css, vue, js, java, py, sql, json, yaml 等。
    • 核心逻辑:
      • 安全读取 (Safety First): 设置了 1MB 的硬性大小限制。超过此大小的纯文本可能是日志文件,直接读入内存有 OOM 风险,因此会被拦截并返回 URL。
      • Token 优化: 自动检测并压缩连续的空行(3行及以上 -> 2行),减少无效 Token 消耗。
      • 智能截断: 限制最大字符数为 20,000(约 5k-10k Token)。超出部分会被截断,并追加 [System Note: File content truncated...] 提示。
  • WordFileProcessor (Office 文档):

    • 支持格式: doc, docx
    • 双重提取策略 (Dual Strategy):
      1. Apache POI (Primary): 针对 .docx,解析文档结构。亮点功能是实现了将 Word 表格 (XWPFTable) 自动转换为 Markdown 表格,保留结构化数据。
      2. Apache Tika (Fallback): 如果 POI 提取失败或内容为空,自动降级使用 Tika 进行通用文本提取,确保高可用性。
    • 限制: 最大字符数限制为 30,000
  • DocumentTextNormalizer (文本清洗引擎):

    • 职责: 共享工具类,用于清洗所有从文档中提取的文本。
    • 功能:
      • 统一换行: 将 \r\n (Win), \r (Mac) 统一为 \n
      • 去噪: 移除不可见的控制字符(Control Characters)。
      • 压缩: 进一步压缩多余空行,确保输入给 AI 的数据是干净且紧凑的。
  • MultiModalMessageBuilder:

    • 职责: 核心构建器,汇总上述所有处理器的结果 (FileProcessResult),组装成 AI 可理解的 List<Content>

2. 核心实现细节

2.1 为什么 AiCodeGeneratorService 使用 List<Content>

AiCodeGeneratorService 接口中,核心生成方法定义如下:

Flux<String> generateHTMLCode(@dev.langchain4j.service.UserMessage List<Content> contents);

设计考量与原因:

  1. LangChain4j 标准支持: Content 是 LangChain4j 框架的抽象基类,TextContent 代表文本,ImageContent 代表图片。使用 List<Content> 是框架对接多模态大模型的标准方式。
  2. 混合内容 (Mixed Content): 多模态交互的核心在于图文混排。用户的一条消息可能包含 [文本说明] + [UI设计图] + [补充文本]。单一的 String 无法结构化表达这种组合,而 List<Content> 可以完美映射。
  3. 模型原生映射: 现代多模态大模型(如 Gemini 1.5 Pro, GPT-4o)的 API 原生支持“内容块列表”的输入格式。该设计实现了底层模型能力的直通。
  4. 扩展性: 未来若需支持视频或音频,只需添加对应的 VideoContent 实现,接口签名无需修改。

2.2 消息构建流程

MultiModalMessageBuilder 将业务层的 FileProcessResult 转化为 AI 层的 List<Content>

  1. 图片处理: 提取 FileProcessResult 中的 Base64 数据,封装为 ImageContent
  2. PDF 处理: 优先使用视觉化渲染产生的图片列表 (imageBase64s),封装为多个 ImageContent(模拟人类逐页阅读)。
  3. 文本/失败兜底:
    • 如果是纯文本文件,或图片处理失败(如格式不支持),将其内容封装为 TextContent
    • 使用 <file_content> 标签包裹,明确告知 AI 这是文件内容而非用户指令。
  4. 熔断保护:
    • 对于超大文件(触发 MAX_FILE_SIZE 熔断),不传输内容。
    • 仅添加包含 URL 的 System Note,避免 OOM 或 Token 溢出。

3. 场景应用深度解析

3.1 应用创建

AppServiceImpl.createApp 流程中,多模态能力用于智能决策

  1. 输入: 用户提交 initPrompt (文本) 和 fileList (设计图/需求文档 URL)。
  2. 处理: 调用 FileService 下载并处理文件,生成 List<FileProcessResult>
  3. 路由: MultiModalMessageBuilder 构建多模态消息,传递给 AiCodeGenTypeRoutingService
    • AI 决策: AI 分析图片(如网页截图)和文本,智能判断是生成 HTML(前端单页)、Vue 项目(复杂前端)还是 Java 后端代码。
  4. 结果: 决策结果 (codeGenType) 被存入 App 表,决定后续的代码生成策略。

3.2 对话代码生成

AppServiceImpl.chatToGenCode 流程中,多模态能力用于上下文理解

  1. 实时处理: 类似于创建流程,实时下载并处理用户在对话中上传的新文件。
  2. 流式调用: 构建 List<Content> 后,通过 AiCodeGeneratorFacade 调用流式接口,AI 边看图边生成代码。

4. 对话历史管理

为了平衡性能与功能,项目采用了 “Lean Storage, Lazy Loading” (轻量存储,懒加载) 策略。

4.1 总体策略

  • 存储: 数据库仅存储 “元数据索引” (Metadata Index),即 URL。
  • 计算: 内存负责 “实时渲染” (Real-time Rendering),即 Base64。

5. 对话历史的持久化机制

系统采用 “Lean Storage” (瘦身存储) 策略,将多模态消息序列化为 JSON 字符串存入数据库。

5.1 持久化内容

ChatHistoryServiceImpl.addChatMessage 中,系统构建 MultiModalContent 对象作为中间载体:

public class MultiModalContent {
    private String text; // 用户输入的文本提示词
    private List<AttachmentInfo> attachments; // 附件列表

    public static class AttachmentInfo {
        private String fileName;
        private String type; // IMAGE 或 DOCUMENT
        private String url;  // 文件的访问地址 (OSS/COS)
        private String content; // ⚠️ 注意:持久化时此字段被显式置空 (null)
    }
}

关键操作

  1. 显式置空: 在入库前,代码强制将附件的 content 字段(即 Base64 数据或大段文本)设为 null
  2. 序列化: 仅将包含 URL、文件名和类型的 MultiModalContent 对象序列化为 JSON 字符串。
  3. 入库: 将该 JSON 字符串存入 chat_history 表的 message 字段。

设计收益

  • 数据库轻量化: 彻底避免了将动辄几 MB 的 Base64 图片数据存入关系型数据库,防止 message 字段爆炸,确保数据库读写性能。
  • 元数据完整性: 保留了 URL 和文件类型,为后续的“懒加载”提供了必要索引。

5.2 懒加载与缓存优化

由于数据库中不存内容,系统在加载历史对话时(loadChatHistoryToMemory)需要实时重建上下文。为了解决这一过程带来的性能开销,引入了 Caffeine 本地缓存

5.2.1 懒加载流程
  1. 反序列化: 从数据库读取 JSON,还原 MultiModalContent
  2. 遍历附件: 对每个附件 URL,调用 fileService.processFile(url)
  3. 重建上下文: 获取处理后的 Base64/文本,组装为 ImageContent/TextContent 给 AI。
5.2.2 Caffeine 本地缓存代码实战

FileServiceImpl.java 中,我们定义了一个针对文件处理结果的内存缓存,具体实现如下:

// 1. 缓存定义:Key=文件URL, Value=处理结果(含Base64)
private final Cache<String, FileProcessResult> fileProcessCache = Caffeine.newBuilder()
        .expireAfterWrite(1, TimeUnit.HOURS) // 写入1小时后过期,防止内存泄漏
        .maximumSize(1000)                   // 限制最大条数,防止 OOM
        .build();

public FileProcessResult processFile(String fileUrl, String originalFileName) {
    // 2. 缓存查询 (Cache Hit)
    FileProcessResult cachedResult = fileProcessCache.getIfPresent(fileUrl);
    if (cachedResult != null && ProcessStatusEnum.SUCCESS.getValue().equals(cachedResult.getStatus())) {
        log.info("Hit cache for file: {}", originalFileName);
        return cachedResult; // 直接返回,跳过下载和渲染
    }

    // ... 下载文件 tempFile ...
    // ... 调用 processor.process(tempFile) 进行耗时处理 ...

    // 3. 缓存写入 (Cache Miss & Populate)
    if (ProcessStatusEnum.SUCCESS.getValue().equals(result.getStatus())) {
        fileProcessCache.put(fileUrl, result);
    }
    return result;
}

解决了什么核心问题?

  • 消除重复计算 (Zero Re-processing):
    • 问题: 用户在同一个 Session 中多次对话,或者刷新页面,都会触发 loadChatHistoryToMemory。如果没有缓存,系统必须对历史记录中的每一张图片、每一个 PDF 重新下载、重新渲染、重新压缩。这会导致极高的 CPU 占用(图像处理)和网络延迟。
    • 解决: 引入 Caffeine 后,首次处理的结果被缓存。后续加载历史记录时,直接从内存读取 Base64 数据,实现了毫秒级的上下文重建。
  • 降低带宽压力: 避免了频繁从对象存储(COS/OSS)重复下载同一个文件。

6. 总结

本项目通过精细的架构设计,成功实现了对多模态能力的高效整合:

  1. 前端: 支持多文件上传。
  2. 传输: 使用 URL 传递文件引用,减少带宽占用。
  3. 处理: 后端 Processor 层负责“视觉化”和“标准化”。
  4. 交互: 通过 List<Content> 与 LangChain4j 及大模型进行原生多模态交互。
  5. 存储与性能:
    • Lean Storage: 仅存 URL,数据库减负。
    • Lazy Loading + Cache: 内存懒加载 + Caffeine 缓存,实现了高性能、低延迟的历史回显。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐