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]";
}
}
}
- 生成图片时,用的是WanxImageModel实例,这是通义万相2的专用模型对象
- 编辑图片时,使用了MultiModalConversation的call方法,并未使用模型对象
- 理解图片时,使用的模型对象是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);
}
}