feat(ai): 新增多模态与结构化输出功能支持
- 引入 Cassandra作为聊天记忆存储后端 - 配置 DashScope 多模态模型支持图文输入- 新增结构化输出控制器,支持 Bean、Map、List 等格式转换 - 添加文生图接口,集成阿里百炼图像生成能力 - 更新应用配置以支持多模态及持久化聊天记录 - 升级依赖项,引入 DashScope SDK 和 Cassandra 支持库 - 创建 ActorFilmography 和 Book 数据模型用于结构化响应 - 调整 ChatClient 配置以适配新的多模态与记忆逻辑
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package com.hanserwei.airobot.config;
|
||||
|
||||
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
|
||||
import com.hanserwei.airobot.advisor.MyLoggerAdvisor;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
@@ -22,11 +23,13 @@ public class ChatClientConfig {
|
||||
* @return ChatClient
|
||||
*/
|
||||
@Bean
|
||||
public ChatClient chatClient(DeepSeekChatModel chatModel) {
|
||||
public ChatClient chatClient(DashScopeChatModel chatModel) {
|
||||
return ChatClient.builder(chatModel)
|
||||
.defaultSystem("请你扮演一名犬小哈 Java 项目实战专栏的客服人员")
|
||||
.defaultAdvisors(new SimpleLoggerAdvisor(),
|
||||
new MyLoggerAdvisor())
|
||||
// .defaultSystem("请你扮演一名犬小哈 Java 项目实战专栏的客服人员")
|
||||
.defaultAdvisors(
|
||||
new SimpleLoggerAdvisor(),
|
||||
new MyLoggerAdvisor(),
|
||||
MessageChatMemoryAdvisor.builder(chatMemory).build())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import jakarta.annotation.Resource;
|
||||
import org.springframework.ai.chat.memory.ChatMemory;
|
||||
import org.springframework.ai.chat.memory.ChatMemoryRepository;
|
||||
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.Configuration;
|
||||
|
||||
@@ -14,7 +15,7 @@ public class ChatMemoryConfig {
|
||||
* 记忆存储
|
||||
*/
|
||||
@Resource
|
||||
private ChatMemoryRepository chatMemoryRepository;
|
||||
private CassandraChatMemoryRepository chatMemoryRepository;
|
||||
|
||||
/**
|
||||
* 初始化一个 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.PromptTemplate;
|
||||
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.ai.template.st.StTemplateRenderer;
|
||||
import org.springframework.http.MediaType;
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user