鱼跃项目多模态能力深度解析
鱼跃项目多模态能力深度解析
本文档深度总结了项目中实现的多模态能力,重点涵盖核心架构、关键组件、数据流转以及在应用创建和对话历史加载中的具体实现细节。
介绍
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):
- Apache POI (Primary): 针对
.docx,解析文档结构。亮点功能是实现了将 Word 表格 (XWPFTable) 自动转换为 Markdown 表格,保留结构化数据。 - Apache Tika (Fallback): 如果 POI 提取失败或内容为空,自动降级使用 Tika 进行通用文本提取,确保高可用性。
- Apache POI (Primary): 针对
- 限制: 最大字符数限制为 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);
设计考量与原因:
- LangChain4j 标准支持:
Content是 LangChain4j 框架的抽象基类,TextContent代表文本,ImageContent代表图片。使用List<Content>是框架对接多模态大模型的标准方式。 - 混合内容 (Mixed Content): 多模态交互的核心在于图文混排。用户的一条消息可能包含
[文本说明] + [UI设计图] + [补充文本]。单一的String无法结构化表达这种组合,而List<Content>可以完美映射。 - 模型原生映射: 现代多模态大模型(如 Gemini 1.5 Pro, GPT-4o)的 API 原生支持“内容块列表”的输入格式。该设计实现了底层模型能力的直通。
- 扩展性: 未来若需支持视频或音频,只需添加对应的
VideoContent实现,接口签名无需修改。
2.2 消息构建流程
MultiModalMessageBuilder 将业务层的 FileProcessResult 转化为 AI 层的 List<Content>:
- 图片处理: 提取
FileProcessResult中的 Base64 数据,封装为ImageContent。 - PDF 处理: 优先使用视觉化渲染产生的图片列表 (
imageBase64s),封装为多个ImageContent(模拟人类逐页阅读)。 - 文本/失败兜底:
- 如果是纯文本文件,或图片处理失败(如格式不支持),将其内容封装为
TextContent。 - 使用
<file_content>标签包裹,明确告知 AI 这是文件内容而非用户指令。
- 如果是纯文本文件,或图片处理失败(如格式不支持),将其内容封装为
- 熔断保护:
- 对于超大文件(触发
MAX_FILE_SIZE熔断),不传输内容。 - 仅添加包含 URL 的
System Note,避免 OOM 或 Token 溢出。
- 对于超大文件(触发
3. 场景应用深度解析
3.1 应用创建
在 AppServiceImpl.createApp 流程中,多模态能力用于智能决策:
- 输入: 用户提交
initPrompt(文本) 和fileList(设计图/需求文档 URL)。 - 处理: 调用
FileService下载并处理文件,生成List<FileProcessResult>。 - 路由:
MultiModalMessageBuilder构建多模态消息,传递给AiCodeGenTypeRoutingService。- AI 决策: AI 分析图片(如网页截图)和文本,智能判断是生成 HTML(前端单页)、Vue 项目(复杂前端)还是 Java 后端代码。
- 结果: 决策结果 (
codeGenType) 被存入App表,决定后续的代码生成策略。
3.2 对话代码生成
在 AppServiceImpl.chatToGenCode 流程中,多模态能力用于上下文理解:
- 实时处理: 类似于创建流程,实时下载并处理用户在对话中上传的新文件。
- 流式调用: 构建
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)
}
}
关键操作:
- 显式置空: 在入库前,代码强制将附件的
content字段(即 Base64 数据或大段文本)设为null。 - 序列化: 仅将包含 URL、文件名和类型的
MultiModalContent对象序列化为 JSON 字符串。 - 入库: 将该 JSON 字符串存入
chat_history表的message字段。
设计收益:
- 数据库轻量化: 彻底避免了将动辄几 MB 的 Base64 图片数据存入关系型数据库,防止
message字段爆炸,确保数据库读写性能。 - 元数据完整性: 保留了 URL 和文件类型,为后续的“懒加载”提供了必要索引。
5.2 懒加载与缓存优化
由于数据库中不存内容,系统在加载历史对话时(loadChatHistoryToMemory)需要实时重建上下文。为了解决这一过程带来的性能开销,引入了 Caffeine 本地缓存。
5.2.1 懒加载流程
- 反序列化: 从数据库读取 JSON,还原
MultiModalContent。 - 遍历附件: 对每个附件 URL,调用
fileService.processFile(url)。 - 重建上下文: 获取处理后的 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 数据,实现了毫秒级的上下文重建。
- 问题: 用户在同一个 Session 中多次对话,或者刷新页面,都会触发
- 降低带宽压力: 避免了频繁从对象存储(COS/OSS)重复下载同一个文件。
6. 总结
本项目通过精细的架构设计,成功实现了对多模态能力的高效整合:
- 前端: 支持多文件上传。
- 传输: 使用 URL 传递文件引用,减少带宽占用。
- 处理: 后端
Processor层负责“视觉化”和“标准化”。 - 交互: 通过
List<Content>与 LangChain4j 及大模型进行原生多模态交互。 - 存储与性能:
- Lean Storage: 仅存 URL,数据库减负。
- Lazy Loading + Cache: 内存懒加载 + Caffeine 缓存,实现了高性能、低延迟的历史回显。
更多推荐



所有评论(0)