feat(ai): 新增多模态与结构化输出功能支持
- 引入 Cassandra作为聊天记忆存储后端 - 配置 DashScope 多模态模型支持图文输入- 新增结构化输出控制器,支持 Bean、Map、List 等格式转换 - 添加文生图接口,集成阿里百炼图像生成能力 - 更新应用配置以支持多模态及持久化聊天记录 - 升级依赖项,引入 DashScope SDK 和 Cassandra 支持库 - 创建 ActorFilmography 和 Book 数据模型用于结构化响应 - 调整 ChatClient 配置以适配新的多模态与记忆逻辑
This commit is contained in:
20
pom.xml
20
pom.xml
@@ -55,14 +55,28 @@
|
|||||||
<groupId>com.alibaba.cloud.ai</groupId>
|
<groupId>com.alibaba.cloud.ai</groupId>
|
||||||
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
|
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Cassandra -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.ai</groupId>
|
||||||
|
<artifactId>spring-ai-starter-model-chat-memory-repository-cassandra</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- 阿里百炼 SDK -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba</groupId>
|
||||||
|
<artifactId>dashscope-sdk-java</artifactId>
|
||||||
|
<version>2.21.13</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-simple</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.hanserwei.airobot.config;
|
package com.hanserwei.airobot.config;
|
||||||
|
|
||||||
|
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
|
||||||
import com.hanserwei.airobot.advisor.MyLoggerAdvisor;
|
import com.hanserwei.airobot.advisor.MyLoggerAdvisor;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.ai.chat.client.ChatClient;
|
import org.springframework.ai.chat.client.ChatClient;
|
||||||
@@ -22,11 +23,13 @@ public class ChatClientConfig {
|
|||||||
* @return ChatClient
|
* @return ChatClient
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public ChatClient chatClient(DeepSeekChatModel chatModel) {
|
public ChatClient chatClient(DashScopeChatModel chatModel) {
|
||||||
return ChatClient.builder(chatModel)
|
return ChatClient.builder(chatModel)
|
||||||
.defaultSystem("请你扮演一名犬小哈 Java 项目实战专栏的客服人员")
|
// .defaultSystem("请你扮演一名犬小哈 Java 项目实战专栏的客服人员")
|
||||||
.defaultAdvisors(new SimpleLoggerAdvisor(),
|
.defaultAdvisors(
|
||||||
new MyLoggerAdvisor())
|
new SimpleLoggerAdvisor(),
|
||||||
|
new MyLoggerAdvisor(),
|
||||||
|
MessageChatMemoryAdvisor.builder(chatMemory).build())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import jakarta.annotation.Resource;
|
|||||||
import org.springframework.ai.chat.memory.ChatMemory;
|
import org.springframework.ai.chat.memory.ChatMemory;
|
||||||
import org.springframework.ai.chat.memory.ChatMemoryRepository;
|
import org.springframework.ai.chat.memory.ChatMemoryRepository;
|
||||||
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
|
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
|
||||||
|
import org.springframework.ai.chat.memory.repository.cassandra.CassandraChatMemoryRepository;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ public class ChatMemoryConfig {
|
|||||||
* 记忆存储
|
* 记忆存储
|
||||||
*/
|
*/
|
||||||
@Resource
|
@Resource
|
||||||
private ChatMemoryRepository chatMemoryRepository;
|
private CassandraChatMemoryRepository chatMemoryRepository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化一个 ChatMemory 实例,并注入到 Spring 容器中
|
* 初始化一个 ChatMemory 实例,并注入到 Spring 容器中
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package com.hanserwei.airobot.controller;
|
||||||
|
|
||||||
|
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.ai.chat.messages.UserMessage;
|
||||||
|
import org.springframework.ai.chat.model.Generation;
|
||||||
|
import org.springframework.ai.chat.prompt.Prompt;
|
||||||
|
import org.springframework.ai.content.Media;
|
||||||
|
import org.springframework.ai.openai.OpenAiChatModel;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.util.MimeTypeUtils;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/v9/ai")
|
||||||
|
public class MultimodalityController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private DashScopeChatModel chatModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流式对话
|
||||||
|
* @param message
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/generateStream", produces = "text/html;charset=utf-8")
|
||||||
|
public Flux<String> generateStream(@RequestParam(value = "message") String message) {
|
||||||
|
// 1. 创建媒体资源
|
||||||
|
Media image = new Media(
|
||||||
|
MimeTypeUtils.IMAGE_PNG,
|
||||||
|
new ClassPathResource("/images/img.png")
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. 附加选项(可选),如温度值等等
|
||||||
|
Map<String, Object> metadata = new HashMap<>();
|
||||||
|
metadata.put("temperature", 0.7);
|
||||||
|
|
||||||
|
// 3. 构建多模态消息
|
||||||
|
UserMessage userMessage = UserMessage.builder()
|
||||||
|
.text(message)
|
||||||
|
.media(image)
|
||||||
|
.metadata(metadata)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 4. 构建提示词
|
||||||
|
Prompt prompt = new Prompt(List.of(userMessage));
|
||||||
|
// 5. 流式调用
|
||||||
|
return chatModel.stream(prompt)
|
||||||
|
.mapNotNull(chatResponse -> {
|
||||||
|
Generation generation = chatResponse.getResult();
|
||||||
|
return generation.getOutput().getText();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@ import org.springframework.ai.chat.model.Generation;
|
|||||||
import org.springframework.ai.chat.prompt.Prompt;
|
import org.springframework.ai.chat.prompt.Prompt;
|
||||||
import org.springframework.ai.chat.prompt.PromptTemplate;
|
import org.springframework.ai.chat.prompt.PromptTemplate;
|
||||||
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
|
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
|
||||||
import org.springframework.ai.openai.OpenAiChatModel;
|
|
||||||
import org.springframework.ai.template.st.StTemplateRenderer;
|
import org.springframework.ai.template.st.StTemplateRenderer;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package com.hanserwei.airobot.controller;
|
||||||
|
|
||||||
|
import com.hanserwei.airobot.model.ActorFilmography;
|
||||||
|
import com.hanserwei.airobot.model.Book;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.ai.chat.client.ChatClient;
|
||||||
|
import org.springframework.ai.chat.prompt.Prompt;
|
||||||
|
import org.springframework.ai.chat.prompt.PromptTemplate;
|
||||||
|
import org.springframework.ai.converter.BeanOutputConverter;
|
||||||
|
import org.springframework.ai.converter.ListOutputConverter;
|
||||||
|
import org.springframework.ai.converter.MapOutputConverter;
|
||||||
|
import org.springframework.core.convert.support.DefaultConversionService;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/v8/ai")
|
||||||
|
public class StructuredOutputController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ChatClient chatClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 示例1: BeanOutputConverter - 获取演员电影作品集
|
||||||
|
*
|
||||||
|
* @param name
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@GetMapping("/actor/films")
|
||||||
|
public ActorFilmography generate(@RequestParam(value = "name") String name) {
|
||||||
|
// 一次性返回结果
|
||||||
|
return chatClient.prompt()
|
||||||
|
.user(u -> u.text("""
|
||||||
|
请为演员 {actor} 生成包含5部代表作的电影作品集,
|
||||||
|
只包含 {actor} 担任主演的电影,不要包含任何解释说明。
|
||||||
|
""")
|
||||||
|
.param("actor", name))
|
||||||
|
.call()
|
||||||
|
.entity(ActorFilmography.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 示例2: MapOutputConverter - 获取编程语言信息
|
||||||
|
* @param language
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@GetMapping("/language-info")
|
||||||
|
public Map<String, Object> getLanguageInfo(@RequestParam(value = "lang") String language) {
|
||||||
|
|
||||||
|
String userText = """
|
||||||
|
请提供关于编程语言 {language} 的结构化信息,包含以下字段:"
|
||||||
|
name (语言名称), "
|
||||||
|
popularity (流行度排名,整数), "
|
||||||
|
features (主要特性,字符串数组), "
|
||||||
|
releaseYear (首次发布年份). "
|
||||||
|
不要包含任何解释说明,直接输出 JSON 格式数据。
|
||||||
|
""";
|
||||||
|
|
||||||
|
return chatClient.prompt()
|
||||||
|
.user(u -> u.text(userText).param("language", language))
|
||||||
|
.call()
|
||||||
|
.entity(new MapOutputConverter());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 示例3: ListOutputConverter - 获取城市列表
|
||||||
|
* @param country
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@GetMapping("/city-list")
|
||||||
|
public List<String> getCityList(@RequestParam(value = "country") String country) {
|
||||||
|
|
||||||
|
return chatClient.prompt()
|
||||||
|
.user(u -> u.text(
|
||||||
|
"""
|
||||||
|
列出 {country} 的8个主要城市名称。
|
||||||
|
不要包含任何编号、解释或其他文本,直接输出城市名称列表。
|
||||||
|
""")
|
||||||
|
.param("country", country))
|
||||||
|
.call()
|
||||||
|
.entity(new ListOutputConverter(new DefaultConversionService()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用低级 API 的 BeanOutputConverter - 获取书籍信息
|
||||||
|
* @param bookTitle
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@GetMapping("/book-info")
|
||||||
|
public Book getBookInfo(@RequestParam(value = "name") String bookTitle) {
|
||||||
|
|
||||||
|
// 使用 BeanOutputConverter 定义输出格式
|
||||||
|
BeanOutputConverter<Book> converter = new BeanOutputConverter<>(Book.class);
|
||||||
|
|
||||||
|
// 提示词模板
|
||||||
|
String template = """
|
||||||
|
请提供关于书籍《{bookTitle}》的详细信息:
|
||||||
|
1. 作者姓名
|
||||||
|
2. 出版年份
|
||||||
|
3. 主要类型(数组)
|
||||||
|
4. 书籍描述(不少于50字)
|
||||||
|
|
||||||
|
不要包含任何解释说明,直接按指定格式输出。
|
||||||
|
{format}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// 创建 Prompt
|
||||||
|
PromptTemplate promptTemplate = new PromptTemplate(template);
|
||||||
|
Prompt prompt = promptTemplate.create(Map.of(
|
||||||
|
"bookTitle", bookTitle,
|
||||||
|
"format", converter.getFormat()
|
||||||
|
));
|
||||||
|
|
||||||
|
// 调用模型并转换结果
|
||||||
|
String result = chatClient.prompt(prompt)
|
||||||
|
.call()
|
||||||
|
.content();
|
||||||
|
|
||||||
|
// 结构化转换
|
||||||
|
return converter.convert(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.hanserwei.airobot.controller;
|
||||||
|
|
||||||
|
import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis;
|
||||||
|
import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisParam;
|
||||||
|
import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisResult;
|
||||||
|
import com.alibaba.dashscope.exception.ApiException;
|
||||||
|
import com.alibaba.dashscope.exception.NoApiKeyException;
|
||||||
|
import com.alibaba.dashscope.utils.JsonUtils;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/v10/ai")
|
||||||
|
@Slf4j
|
||||||
|
public class Text2ImgController {
|
||||||
|
|
||||||
|
@Value("${spring.ai.dashscope.api-key}")
|
||||||
|
private String apiKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用阿里百炼图生文大模型
|
||||||
|
* @param prompt 提示词
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@GetMapping("/text2img")
|
||||||
|
public String text2Image(@RequestParam(value = "prompt") String prompt) {
|
||||||
|
// 构建文生图参数
|
||||||
|
ImageSynthesisParam param = ImageSynthesisParam.builder()
|
||||||
|
.apiKey(apiKey) // 阿里百炼 API Key
|
||||||
|
.model("wanx2.1-t2i-plus") // 模型名称
|
||||||
|
.prompt(prompt) // 提示词
|
||||||
|
.n(1) // 生成图片的数量,这里指定为一张
|
||||||
|
.size("1024*1024") // 输出图像的分辨率
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 同步调用 AI 大模型,生成图片
|
||||||
|
ImageSynthesis imageSynthesis = new ImageSynthesis();
|
||||||
|
ImageSynthesisResult result = null;
|
||||||
|
try {
|
||||||
|
log.info("## 同步调用,请稍等一会...");
|
||||||
|
result = imageSynthesis.call(param);
|
||||||
|
} catch (ApiException | NoApiKeyException e){
|
||||||
|
log.error("", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回生成的结果(包含图片的 URL 链接)
|
||||||
|
return JsonUtils.toJson(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.hanserwei.airobot.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@JsonPropertyOrder({"actor", "movies"})
|
||||||
|
public record ActorFilmography(String actor, List<String> movies) {
|
||||||
|
}
|
||||||
37
src/main/java/com/hanserwei/airobot/model/Book.java
Normal file
37
src/main/java/com/hanserwei/airobot/model/Book.java
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package com.hanserwei.airobot.model;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class Book {
|
||||||
|
/**
|
||||||
|
* 书名
|
||||||
|
*/
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 作者
|
||||||
|
*/
|
||||||
|
private String author;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布年份
|
||||||
|
*/
|
||||||
|
private Integer publishYear;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 类型
|
||||||
|
*/
|
||||||
|
private List<String> genres;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简介
|
||||||
|
*/
|
||||||
|
private String description;
|
||||||
|
}
|
||||||
@@ -2,6 +2,10 @@
|
|||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: han-ai-robot-springboot
|
name: han-ai-robot-springboot
|
||||||
|
cassandra:
|
||||||
|
contact-points: 127.0.0.1 # Cassandra 集群节点地址(可配置多个,用逗号分隔)
|
||||||
|
port: 9042 # 端口号
|
||||||
|
local-datacenter: datacenter1 # 必须与集群配置的数据中心名称一致(大小写敏感)
|
||||||
ai:
|
ai:
|
||||||
deepseek:
|
deepseek:
|
||||||
api-key: ENC(MROXdiEHmWk08koE63bTzFqW52MaXLpMkM9Cyl40Ubj+Lw1yKeZuHLEcs6jTFY8ditY4gJ1365LMAY8Z9G1uwfYFYaYdb3NyijplX7GuDZA=) # 填写 DeepSeek Api Key, 改成你自己的
|
api-key: ENC(MROXdiEHmWk08koE63bTzFqW52MaXLpMkM9Cyl40Ubj+Lw1yKeZuHLEcs6jTFY8ditY4gJ1365LMAY8Z9G1uwfYFYaYdb3NyijplX7GuDZA=) # 填写 DeepSeek Api Key, 改成你自己的
|
||||||
@@ -34,8 +38,17 @@ spring:
|
|||||||
api-key: ENC(cMgcKZkFllyE88DIbGwLKot9Vg02co+gsmY8L8o4/o3UjhcmqO4lJzFU35Sx0n+qFG8pDL0wBjoWrT8X6BuRw9vNlQhY1LgRWHaF9S1zzyM=)
|
api-key: ENC(cMgcKZkFllyE88DIbGwLKot9Vg02co+gsmY8L8o4/o3UjhcmqO4lJzFU35Sx0n+qFG8pDL0wBjoWrT8X6BuRw9vNlQhY1LgRWHaF9S1zzyM=)
|
||||||
chat:
|
chat:
|
||||||
options:
|
options:
|
||||||
model: qwen-plus
|
model: qwen-omni-turbo
|
||||||
temperature: 0.7
|
temperature: 0.7
|
||||||
|
multi-model: true
|
||||||
|
chat:
|
||||||
|
memory:
|
||||||
|
repository:
|
||||||
|
cassandra:
|
||||||
|
keyspace: han_ai_robot
|
||||||
|
table: t_ai_chat_memory
|
||||||
|
time-to-live: 1095d
|
||||||
|
initialize-schema: true
|
||||||
|
|
||||||
jasypt:
|
jasypt:
|
||||||
encryptor:
|
encryptor:
|
||||||
|
|||||||
BIN
src/main/resources/images/img.png
Normal file
BIN
src/main/resources/images/img.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 510 KiB |
Reference in New Issue
Block a user