LangChain4j实战三:图像模型

1. 准备工作

功能 模型 模型对象创建方式
创建图片 wan2.2-t2i-plus 阿里的dashscope库,提供通义万相2的build方法
修改图片 qwen-image-edit-plus 没有模型对象,阿里的dashscope库,提供调用API
理解图片 qwen3-vl-plus 阿里模型提供OpenAI兼容服务,所以用OpenAI模型对象
1.1. 构建项目,添加pom文件
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.0</version>
        <relativePath/>
    </parent>

    <groupId>cn.cjc</groupId>
    <artifactId>springboot-ai</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>


        <!-- https://mvnrepository.com/artifact/dev.langchain4j/langchain4j -->
        <!--  LangChain4j 核心库 -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j</artifactId>
            <version>1.9.1</version>
        </dependency>
        <!-- AiService依赖 -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-spring-boot-starter</artifactId>
            <version>1.0.1-beta6</version>
        </dependency>

        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-core</artifactId>
            <version>1.9.1</version>
        </dependency>
        <!-- LangChain4j OpenAI支持(可用于通义千问的OpenAI兼容接口) -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
            <version>1.9.1-beta17</version>
        </dependency>
        <!-- 集成原生阿里云通义千问 (DashScope) -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
            <version>1.9.1-beta17</version>
        </dependency>
        <!--  导入响应式编程依赖包-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-reactor</artifactId>
            <version>1.9.1-beta17</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </dependency>
    </dependencies>
</project>
1.2. 配置文件
yaml 复制代码
spring:
  application:
    name: springboot-ai
  main:
    allow-bean-definition-overriding: true

langchain4j:
  open-ai:
    # 图片生成模型的配置
    image-gen-model:
      api-key: ******
      model-name: wan2.2-t2i-plus
    # 图片编辑模型的配置
    image-edit-model:
      api-key: ******
      model-name: qwen-image-edit-plus
      base-url: https://dashscope.aliyuncs.com/api/v1
    # 视觉理解模型的配置
    image-vl-model:
      api-key: ******
      model-name: qwen3-vl-plus
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
1.3. 提示词请求类
java 复制代码
@Data
public class PromptRequest {
    private String prompt;
    private int imageNum;
    private String imageUrl;
    private List<String> imageUrls;
}

2. 数据结构

  • 没有任何功能,单纯的保存数据
java 复制代码
@Data
public class ImageEditModelParam {
    private String modelName;
    private String baseUrl;
    private String apiKey;
}

3. 图片处理工具类

java 复制代码
/**
 * 图片处理工具类,用于处理在线图片的加载和保存
 */
@Slf4j
public class ImageUtils {

    /**
     * 从URL创建Image对象
     * @param imageUrl 图片的URL地址
     * @return langchain4j的Image对象
     * @throws IOException 如果图片加载失败
     */
    public static Image createImageFromUrl(String imageUrl) throws IOException {
        log.info("从URL创建Image对象: {}", imageUrl);

        // 下载图片数据并转换为字节数组
        byte[] imageBytes = downloadImage(imageUrl);

        // 将字节数组转换为Base64编码
        String base64Data = Base64.getEncoder().encodeToString(imageBytes);

        log.info("图片下载成功,原始大小: {} 字节,Base64编码后: {} 字符",
                imageBytes.length, base64Data.length());

        // 使用Base64数据创建Image对象
        return Image.builder().base64Data(base64Data).build();
    }

    /**
     * 下载图片并返回字节数组
     * @param imageUrl 图片的URL地址
     * @return 图片的字节数组
     * @throws IOException 如果下载失败
     */
    private static byte[] downloadImage(String imageUrl) throws IOException {
        try (InputStream in = new URL(imageUrl).openStream();
             ByteArrayOutputStream out = new ByteArrayOutputStream()) {

            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytesRead);
            }

            return out.toByteArray();
        }
    }

    /**
     * 从URL下载图片并保存到本地文件
     * @param imageUrl 图片的URL地址
     * @param targetPath 保存到本地的文件路径
     * @return 保存的文件路径
     * @throws IOException 如果下载或保存失败
     */
    public static Path saveImageFromUrl(String imageUrl, Path targetPath) throws IOException {
        log.info("从URL下载图片到本地: {} -> {}", imageUrl, targetPath);

        // 确保目标目录存在
        Path parentDir = targetPath.getParent();
        if (parentDir != null) {
            Files.createDirectories(parentDir);
        }

        // 使用Java标准库下载图片
        try (InputStream in = new URL(imageUrl).openStream()) {
            Files.copy(in, targetPath, StandardCopyOption.REPLACE_EXISTING);
        }

        log.info("图片保存成功: {}", targetPath);
        return targetPath;
    }

    /**
     * 获取图片的Base64编码数据
     * @param image langchain4j的Image对象
     * @return Base64编码的字符串
     */
    public static String getImageBase64(Image image) {
        String base64Data = image.base64Data();
        if (base64Data == null) {
            log.error("Image对象的base64Data为null");
            throw new IllegalStateException("图片数据未正确加载");
        }
        return base64Data;
    }

    /**
     * 将Base64编码的图片数据保存为文件
     * @param base64Data Base64编码的图片数据
     * @param targetPath 保存到本地的文件路径
     * @return 保存的文件路径
     * @throws IOException 如果保存失败
     */
    public static Path saveBase64Image(String base64Data, Path targetPath) throws IOException {
        log.info("保存Base64编码的图片到: {}", targetPath);

        // 确保目标目录存在
        Path parentDir = targetPath.getParent();
        if (parentDir != null) {
            Files.createDirectories(parentDir);
        }

        // 解码Base64数据并保存
        byte[] imageBytes = Base64.getDecoder().decode(base64Data);
        Files.write(targetPath, imageBytes);

        log.info("Base64图片保存成功: {}, 大小: {} 字节", targetPath, imageBytes.length);
        return targetPath;
    }
}

4. 配置类

java 复制代码
import cn.cjc.ai.entity.ImageEditModelParam;
import dev.langchain4j.community.model.dashscope.WanxImageModel;
import dev.langchain4j.community.model.dashscope.WanxImageSize;
import dev.langchain4j.model.openai.OpenAiChatModel;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * LangChain4j配置类
 */
@Configuration
public class ImageLangChain4jConfig {

    // 图片生成模型的配置
    @Value("${langchain4j.open-ai.image-gen-model.api-key}")
    private String imageGenModelApiKey;

    @Value("${langchain4j.open-ai.image-gen-model.model-name}")
    private String imageGenModelName;

    // 图片编辑模型的配置
    @Value("${langchain4j.open-ai.image-edit-model.api-key}")
    private String imageEditModelApiKey;

    @Value("${langchain4j.open-ai.image-edit-model.model-name}")
    private String imageEditModelName;

    @Value("${langchain4j.open-ai.image-edit-model.base-url}")
    private String imageEditModelBaseUrl;

    // 视觉理解模型的配置
    @Value("${langchain4j.open-ai.image-vl-model.api-key}")
    private String imageVLModelApiKey;

    @Value("${langchain4j.open-ai.image-vl-model.model-name}")
    private String imageVLModelName;

    @Value("${langchain4j.open-ai.image-vl-model.base-url}")
    private String imageVLModelBaseUrl;

    /**
     * 创建并配置用于图像生成的OpenAiChatModel实例
     */
    @Bean("imageGenModel")
    public WanxImageModel imageGenModel() {
        return WanxImageModel.builder()
                .apiKey(imageGenModelApiKey)
                .modelName(imageGenModelName)
                .size(WanxImageSize.SIZE_1024_1024)
                .build();
    }

    /**
     * 创建数据结构实例,这只是个保管数据的对象,里面包含了图像编辑模型的配置参数
     */
    @Bean("imageEditModelParam")
    public ImageEditModelParam imageEditModelParam() {
        ImageEditModelParam param = new ImageEditModelParam();
        param.setModelName(imageEditModelName);
        param.setBaseUrl(imageEditModelBaseUrl);
        param.setApiKey(imageEditModelApiKey);
        return param;
    }

    /**
     * 创建并配置用于视觉理解的OpenAiChatModel实例
     */
    @Bean("imageVLModel")
    public OpenAiChatModel imageVLModel() {
        return OpenAiChatModel.builder()
                .apiKey(imageVLModelApiKey)
                .modelName(imageVLModelName)
                .baseUrl(imageVLModelBaseUrl)
                .build();
    }
}

5. 服务类

java 复制代码
import cn.cjc.ai.entity.ImageEditModelParam;
import cn.cjc.ai.utils.ImageUtils;
import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversation;
import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationParam;
import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationResult;
import com.alibaba.dashscope.common.MultiModalMessage;
import com.alibaba.dashscope.common.Role;
import dev.langchain4j.community.model.dashscope.WanxImageModel;
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.openai.OpenAiChatModel;
import dev.langchain4j.model.output.Response;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

import java.util.*;

/**
 * 通义千问服务类,用于与通义千问模型进行交互
 */
@Service
@Slf4j
public class QWenImageService {

    // 用于图像理解任务
    @Autowired
    @Qualifier("imageVLModel")
    private OpenAiChatModel imageVLModel;

    // 用于图像生成任务
    @Autowired
    @Qualifier("imageGenModel")
    private WanxImageModel imageGenModel;

    // 用于图像编辑任务
    @Autowired
    @Qualifier("imageEditModelParam")
    private ImageEditModelParam imageEditModelParam;

    /**
     * @param prompt 图片生成提示词
     * @return 生成的图片URL或相关信息
     */
    public String generateImage(String prompt, int imageNum) {
        try {
            log.info("开始生成图片,提示词: {},{}", imageNum,prompt);
            // 使用imageGenModel生成图片
            Response<List<Image>> result = imageGenModel.generate(prompt, imageNum);

            log.info("图片生成成功,结果: {}", result);
            return result + "[from generateImage]";
        } catch (Exception e) {
            log.error("生成图片时发生错误: {}", e.getMessage(), e);
            return "生成图片时发生错误: " + e.getMessage() + "[from generateImage]";
        }
    }

    public String editImage(List<String> imageUrls, String prompt) {
        MultiModalConversation conv = new MultiModalConversation();
        log.info("开始编辑图片,提示词: {}, 图片:{}", prompt, imageUrls);
        var contents = new ArrayList<Map<String, Object>>();
        for (String imageUrl : imageUrls) {
            contents.add(Collections.singletonMap("image", imageUrl));
        }
        contents.add(Collections.singletonMap("text", prompt));

        MultiModalMessage userMessage = MultiModalMessage.builder().role(Role.USER.getValue())
                .content(contents)
                .build();

        // qwen-image-edit-plus支持输出1-6张图片,此处以两张为例
        Map<String, Object> parameters = new HashMap<>();
        parameters.put("watermark", false);
        parameters.put("negative_prompt", " ");
        parameters.put("n", 2);
        parameters.put("prompt_extend", true);
        // 仅当输出图像数量n=1时支持设置size参数,否则会报错
        // parameters.put("size", "1024*2048");

        MultiModalConversationParam param = MultiModalConversationParam.builder()
                .apiKey(imageEditModelParam.getApiKey())
                .model(imageEditModelParam.getModelName())
                .messages(Collections.singletonList(userMessage))
                .parameters(parameters)
                .build();

        try {
            MultiModalConversationResult result = conv.call(param);
            log.info("图片编辑成功,结果: {}", result);
            return result + "[from editImage]";
        } catch (Exception e) {
            log.error("编辑图片时发生错误: {}", e.getMessage(), e);
            return "编辑图片时发生错误: " + e.getMessage() + "[from editImage]";
        }
    }

    /**
     * 使用图片理解模型根据提示词处理图片
     */
    public String useImage(String imageUrl, String prompt) {
        try {
            log.info("开始处理图片: {}", imageUrl);

            // 使用ImageUtils类来创建Image对象,这样可以确保图片数据被正确加载
            Image image = ImageUtils.createImageFromUrl(imageUrl);

            // 验证图片是否成功加载(通过检查base64数据是否存在且有一定长度)
            String base64Data = ImageUtils.getImageBase64(image);
            if (base64Data == null || base64Data.isEmpty() || base64Data.length() < 10) {
                log.error("图片加载失败:Base64数据无效或为空");
                return "图片加载失败,请检查URL或网络连接[from useImage]";
            }

            log.info("图片成功加载,Base64数据长度: {} 字符", base64Data.length());

            // 创建图片内容
            ImageContent imageContent = new ImageContent(image, ImageContent.DetailLevel.HIGH);

            // 用户提问
            UserMessage messages = UserMessage.from(List.of(
                    TextContent.from(prompt),
                    imageContent));

            // 调用模型进行处理
            log.info("将图片内容发送给模型处理...");
            String result = imageVLModel.chat(messages).aiMessage().text();

            log.info("模型返回结果: {}", result);
            return result + "[from useImage]";
        } catch (Exception e) {
            log.error("处理图片时发生错误: {}", e.getMessage(), e);
            return "处理图片时发生错误: " + e.getMessage() + "[from useImage]";
        }
    }
}
  1. 生成图片时,用的是WanxImageModel实例,这是通义万相2的专用模型对象
  2. 编辑图片时,使用了MultiModalConversation的call方法,并未使用模型对象
  3. 理解图片时,使用的模型对象是OpenAI兼容的OpenAiChatModel对象,因为该模型提供了OpenAI兼容的接口,另外就是要先把图片下载好再转为base64传给模型

6. controller

java 复制代码
@RestController
@RequestMapping("/api/handleImage")
public class QWenImageController {

    @Autowired
    private QWenImageService QWenImageService;

    /**
     * {
     *   "prompt": "生成一张图片,内容是三国演义中的威震逍遥津",
     *   "imageNum": 2
     * }
     * 响应中的revisedPrompt字段含义:模型在内部对原始prompt做"智能改写"后,
     * 最终真正用于扩散生成的提示词文本,有了这个文本,
     * 我们可以拿来继续修改,以生成更符合要求的图片
     */
    @PostMapping("/generateImage")
    public ResponseEntity<String> generateImage(@RequestBody PromptRequest request) {
        String response = QWenImageService.generateImage(request.getPrompt(), request.getImageNum());
        return ResponseEntity.ok(response);
    }

    /**
     * {
     *   "prompt": "图1中叠加到图2中",
     *   "imageUrls": [
     *     "https://picsum.photos/200/300",
     *     "https://picsum.photos/200/500"
     *   ]
     * }
     */ 
    @PostMapping("/editImage")
    public ResponseEntity<String> editImage(@RequestBody PromptRequest request) {
        String response = QWenImageService.editImage(request.getImageUrls(), request.getPrompt());
        return ResponseEntity.ok(response);
    }

    /**
     * {
     *   "imageUrl": "https://picsum.photos/200/300",
     *   "prompt": "图片中有什么?"
     * }
     */
    @PostMapping("/useImage")
    public ResponseEntity<String> useImage(@RequestBody PromptRequest request) {
        String response = QWenImageService.useImage(request.getImageUrl(), request.getPrompt());
        return ResponseEntity.ok(response);
    }
}
相关推荐
无尽的沉默1 小时前
使用Thymeleaf配置国际化页面(语言切换)
前端·spring boot
SimonKing1 小时前
跨越数据孤岛!SpringBoot使用JDBC调用Calcite联邦查询实战
java·后端·程序员
Java编程爱好者1 小时前
金融级数据库架构实战:MySQL Router + MGR 深度指南
后端
好家伙VCC2 小时前
# 发散创新:基于Python的TTS语音合成实战与优化策略 在人工智能加速落地的今天,**文本转
java·开发语言·人工智能·python
Java编程爱好者2 小时前
Java后端开发面试题总结(全网最全、最细、附答案)
后端
等D春C夏X2 小时前
最终版C++11/14/17学习大纲(精准核对42条条款)
java·开发语言
ZaneAI2 小时前
🚀 Claude Agent SDK 使用指南:会话管理(Session )
langchain·agent·claude
Cg136269159742 小时前
HTML标题标签
java
Java水解2 小时前
Spring应用事件机制实践
后端·spring