From ae1ff68a75d8e08f9e223913d5a1bcdf8c2f2c62 Mon Sep 17 00:00:00 2001 From: zouzhiwen <409053122@qq.com> Date: Fri, 20 Mar 2026 19:15:09 +0800 Subject: [PATCH] init --- pom.xml | 67 ++++++ src/main/java/com/baoshi/ApiListTool.java | 28 +++ .../java/com/baoshi/BusinessRouteTool.java | 24 +++ src/main/java/com/baoshi/Main.java | 17 ++ src/main/java/com/baoshi/conf/AiConfig.java | 30 +++ .../controller/MultiModelsController.java | 41 ++++ src/main/resources/application.yml | 13 ++ src/test/java/com/baoshi/ChatModelTest.java | 13 ++ src/test/java/com/baoshi/DemoTest.java | 203 ++++++++++++++++++ 9 files changed, 436 insertions(+) create mode 100644 pom.xml create mode 100644 src/main/java/com/baoshi/ApiListTool.java create mode 100644 src/main/java/com/baoshi/BusinessRouteTool.java create mode 100644 src/main/java/com/baoshi/Main.java create mode 100644 src/main/java/com/baoshi/conf/AiConfig.java create mode 100644 src/main/java/com/baoshi/controller/MultiModelsController.java create mode 100644 src/main/resources/application.yml create mode 100644 src/test/java/com/baoshi/ChatModelTest.java create mode 100644 src/test/java/com/baoshi/DemoTest.java diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..0264b5e --- /dev/null +++ b/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.5.8 + + + + com.baoshi + spring-ai-alibaba-demo + 1.0-SNAPSHOT + + + 17 + 17 + UTF-8 + + + + + + com.fasterxml.jackson.core + jackson-annotations + 2.19.4 + + + + + com.alibaba.cloud.ai + spring-ai-alibaba-agent-framework + 1.1.2.2 + + + + + org.springframework.ai + spring-ai-starter-model-ollama + 1.1.2 + + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + 1.1.2.2 + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + \ No newline at end of file diff --git a/src/main/java/com/baoshi/ApiListTool.java b/src/main/java/com/baoshi/ApiListTool.java new file mode 100644 index 0000000..1f82ebb --- /dev/null +++ b/src/main/java/com/baoshi/ApiListTool.java @@ -0,0 +1,28 @@ +package com.baoshi; + +import org.springframework.ai.chat.model.ToolContext; + +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; + +/** + * 业务 + */ +public class ApiListTool implements BiFunction { + + //Map>.of("仓库",) + private final Map> map = Map.of( + "出库", List.of("/api/outbound"), + "存储", List.of("/api/Inventory") + ); + + @Override + public String apply(DomainRequest request, ToolContext toolContext) { + return String.join(",", map.get(request.domain())); + } + + /** 与 LLM tool call 返回的 JSON 结构对应 */ + public record DomainRequest(String domain) { + } +} \ No newline at end of file diff --git a/src/main/java/com/baoshi/BusinessRouteTool.java b/src/main/java/com/baoshi/BusinessRouteTool.java new file mode 100644 index 0000000..3799bd8 --- /dev/null +++ b/src/main/java/com/baoshi/BusinessRouteTool.java @@ -0,0 +1,24 @@ +package com.baoshi; + +import org.springframework.ai.chat.model.ToolContext; + +import java.util.function.BiFunction; + +/** + * 业务 + */ +public class BusinessRouteTool implements BiFunction { + + @Override + public String apply(RouteRequest request, ToolContext toolContext) { + + return """ + 订单相关业务域:出库, + 移库、盘点、上架相关业务域:存储 + """; + } + + /** 与 LLM tool call 返回的 JSON 结构对应 */ + public record RouteRequest() { + } +} \ No newline at end of file diff --git a/src/main/java/com/baoshi/Main.java b/src/main/java/com/baoshi/Main.java new file mode 100644 index 0000000..4e79494 --- /dev/null +++ b/src/main/java/com/baoshi/Main.java @@ -0,0 +1,17 @@ +package com.baoshi; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Wen + * @date 2026/3/12 18:38 + */ +//TIP To Run code, press or +// click the icon in the gutter. +@SpringBootApplication +public class Main { + public static void main(String[] args) { + SpringApplication.run(Main.class, args); + } +} \ No newline at end of file diff --git a/src/main/java/com/baoshi/conf/AiConfig.java b/src/main/java/com/baoshi/conf/AiConfig.java new file mode 100644 index 0000000..ca0abba --- /dev/null +++ b/src/main/java/com/baoshi/conf/AiConfig.java @@ -0,0 +1,30 @@ +package com.baoshi.conf; + +import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeChatProperties; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.model.ollama.autoconfigure.OllamaChatProperties; +import org.springframework.ai.ollama.OllamaChatModel; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AiConfig { + + @Bean + public ChatClient dashScope(@Autowired DashScopeChatModel dashScopeChatModel, + @Autowired DashScopeChatProperties properties) { + return ChatClient.builder(dashScopeChatModel) + //.defaultOptions(DashScopeChatOptions.builder().build()) + .build(); + } + + @Bean + public ChatClient ollama(@Autowired OllamaChatModel ollamaChatModel, + @Autowired OllamaChatProperties properties) { + return ChatClient.builder(ollamaChatModel).defaultOptions(properties.getOptions()).build(); + } + +} diff --git a/src/main/java/com/baoshi/controller/MultiModelsController.java b/src/main/java/com/baoshi/controller/MultiModelsController.java new file mode 100644 index 0000000..d76ee84 --- /dev/null +++ b/src/main/java/com/baoshi/controller/MultiModelsController.java @@ -0,0 +1,41 @@ +package com.baoshi.controller; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import java.util.Map; + +@RestController +public class MultiModelsController { + + @Autowired + private Map chatClientMap; + + @GetMapping("/chat") + Mono generation(@RequestParam String message, + @RequestParam String model) { + ChatClient chatClient = chatClientMap.get(model); + if (chatClient == null) { + return Mono.just("Unknown model: " + model); + } + return Mono.fromCallable(() -> chatClient.prompt().user(message).call().content()) + .subscribeOn(Schedulers.boundedElastic()); + } + + @GetMapping(value = "/chatStream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + Flux chatStream(@RequestParam String message, + @RequestParam String model) { + ChatClient chatClient = chatClientMap.get(model); + if (chatClient == null) { + return Flux.just("Unknown model: " + model); + } + return chatClient.prompt().user(message).stream().content(); + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..689bcea --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,13 @@ +# 解决 Spring AI 1.0.0-M6 与 1.1.x 并存时的 chatClientBuilderConfigurer bean 冲突 +spring: + ai: + ollama: + base-url: http://100.121.13.117:11434 + chat: + model: qwen2.5:7b + dashscope: + api-key: sk-36a57382b9ae4db696ecd07eb7150a88 + chat: + api-key: sk-36a57382b9ae4db696ecd07eb7150a88 +server: + port: 8080 diff --git a/src/test/java/com/baoshi/ChatModelTest.java b/src/test/java/com/baoshi/ChatModelTest.java new file mode 100644 index 0000000..36718a1 --- /dev/null +++ b/src/test/java/com/baoshi/ChatModelTest.java @@ -0,0 +1,13 @@ +package com.baoshi; + +import org.springframework.boot.test.context.SpringBootTest; + +/** + * @author Wen + * @date 2026/3/20 18:26 + */ +@SpringBootTest(classes = Main.class) +public class ChatModelTest { + + +} diff --git a/src/test/java/com/baoshi/DemoTest.java b/src/test/java/com/baoshi/DemoTest.java new file mode 100644 index 0000000..e831575 --- /dev/null +++ b/src/test/java/com/baoshi/DemoTest.java @@ -0,0 +1,203 @@ +package com.baoshi; + +import com.alibaba.cloud.ai.dashscope.audio.tts.DashScopeAudioSpeechModel; +import com.alibaba.cloud.ai.dashscope.audio.tts.DashScopeAudioSpeechOptions; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; +import com.alibaba.cloud.ai.dashscope.chat.MessageFormat; +import com.alibaba.cloud.ai.dashscope.common.DashScopeApiConstants; +import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel; +import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions; +import com.alibaba.cloud.ai.dashscope.spec.DashScopeModel; +import com.alibaba.cloud.ai.dashscope.video.DashScopeVideoModel; +import com.alibaba.cloud.ai.dashscope.video.DashScopeVideoOptions; +import com.alibaba.cloud.ai.dashscope.video.VideoMessage; +import com.alibaba.cloud.ai.dashscope.video.VideoPrompt; +import com.alibaba.cloud.ai.dashscope.video.VideoResponse; +import com.alibaba.cloud.ai.graph.agent.ReactAgent; +import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver; +import com.alibaba.cloud.ai.graph.exception.GraphRunnerException; +import com.baoshi.BusinessRouteTool.RouteRequest; +import org.junit.jupiter.api.Test; +import org.springframework.ai.audio.tts.Speech; +import org.springframework.ai.audio.tts.TextToSpeechPrompt; +import org.springframework.ai.audio.tts.TextToSpeechResponse; +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.content.Media; +import org.springframework.ai.image.ImagePrompt; +import org.springframework.ai.image.ImageResponse; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.function.FunctionToolCallback; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.io.FileSystemResource; +import org.springframework.util.MimeTypeUtils; +import reactor.core.publisher.Flux; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.Collections; + +/** + * @author Wen + * @date 2026/3/12 21:45 + */ +@SpringBootTest(classes = Main.class) +class DemoTest { + @Autowired + @Qualifier("ollamaChatModel") + // @Qualifier("dashScopeChatModel") + private ChatModel chatModel; + + @Test + void contextLoads() throws GraphRunnerException { + ToolCallback businessTool = FunctionToolCallback + .builder("business_route", new BusinessRouteTool()) + .description("根据用户描述确定业务域") + .inputType(RouteRequest.class) + .build(); + + ToolCallback apiListTool = FunctionToolCallback + .builder("api_list", new ApiListTool()) + .description("根据业务域名获取对应接口") + .inputType(ApiListTool.DomainRequest.class) + .build(); + + // 创建 agent + ReactAgent agent = ReactAgent.builder() + .name("warehouse_agent") + .model(chatModel) + .tools(apiListTool, businessTool) + .systemPrompt("你是一个业务知识助手,可以根据用户描述提供相关业务领域知识,在对话期间最先应该获取到当前用户想要讨论的业务域,再进行其他工具的调用.") + .saver(new MemorySaver()) + .build(); + + // 运行 agent + AssistantMessage response = agent.call("请告诉我订单相关接口"); + System.out.println(response); + } + + /** + * 文生图示例 + * 模型:wanx2.1-t2i-turbo(快速)、wan2.2-t2i-plus(增强)、qwen-image 等 + * 可选:style(photography/anime/watercolor...)、size、n(数量)、negativePrompt + */ + @Test + void text2Img(@Autowired DashScopeImageModel imageModel) { + DashScopeImageOptions options = DashScopeImageOptions.builder() + .model("wanx2.1-t2i-turbo") + // .style("anime") // 风格:photography/portrait/3d cartoon/anime/oil painting/watercolor/sketch/chinese painting/flat illustration/auto + // .n(1) // 生成数量 1-4 + // .size("1024*1024") // 尺寸:1024*1024、720*1280、1280*720 + // .negativePrompt("模糊") // 负面提示词 + .build(); + + ImageResponse response = imageModel.call(new ImagePrompt("程序员徐庶", options)); + String imageUrl = response.getResult().getOutput().getUrl(); + + System.out.println("文生图 URL: " + imageUrl); + // 或获取 base64: response.getResult().getOutput().getB64Json(); + } + + /** + * 文生视频示例(异步,通常需 1-5 分钟) + * 模型:wanx2.1-t2v-turbo(快速)、wanx2.1-t2v-plus(增强)、wanx2.5-t2v-preview 等 + * 可选:size(1280*720)、duration、imageUrl(图生视频)、negativePrompt + * + * 已知问题:spring-ai-alibaba 1.1.2.0 存在 DashScopeVideoOptions Jackson 反序列化 bug, + * 调用会抛出 InvalidDefinitionException,待上游修复。可关注: + * https://github.com/alibaba/spring-ai-alibaba/issues + */ + @Test + void text2Video(@Autowired DashScopeVideoModel videoModel) { + DashScopeVideoOptions options = DashScopeVideoOptions.builder() + .model(DashScopeModel.VideoModel.WANX21_T2V_TURBO.getName()) // 文生视频用 T2V,I2V 需提供 imageUrl + .input(DashScopeVideoOptions.InputOptions.builder() + .prompt("一只阳光下奔跑的猫") + .build()) + .build(); + + + VideoPrompt prompt = new VideoPrompt(Collections.singletonList(new VideoMessage("一只阳光下奔跑的猫")), options); + VideoResponse response = videoModel.call(prompt); + + String videoUrl = response.getResult().getOutput().videoUrl(); + String taskStatus = response.getResult().getOutput().taskStatus(); + + System.out.println("文生视频 状态: " + taskStatus + ", URL: " + videoUrl); + } + + /** + * 文生语音 + * + * @param audioSpeechModel + * @throws IOException + */ + @Test + void testText2Audio(@Autowired DashScopeAudioSpeechModel audioSpeechModel) throws IOException { + DashScopeAudioSpeechOptions options = DashScopeAudioSpeechOptions.builder() + .model(DashScopeModel.AudioModel.COSYVOICE_V1.getValue()) + .voice("longlaotie") + .format("mp3") + .build(); + + TextToSpeechPrompt prompt = new TextToSpeechPrompt("大家好,我是人帅活好的徐庶。", options); + + // 使用 stream() 收集所有音频块,call() 内部聚合可能有问题 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Flux stream = audioSpeechModel.stream(prompt); + stream.doOnNext(r -> { + for (Speech speech : r.getResults()) { + byte[] bytes = speech.getOutput(); + if (bytes.length > 0) { + try { + baos.write(bytes); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + }).blockLast(); + + byte[] audioBytes = baos.toByteArray(); + if (audioBytes.length == 0) { + throw new IllegalStateException("未收到任何音频数据"); + } + + File file = new File("./output.mp3"); + try (FileOutputStream fos = new FileOutputStream(file)) { + fos.write(audioBytes); + } + } + + /** + * 多模态 + */ + @Test + public void testMultimodal(@Autowired DashScopeChatModel dashScopeChatModel) throws MalformedURLException { + // flac、mp3、mp4、mpeg、mpga、m4a、ogg、wav 或 webm。需先运行 testText2Audio 生成 output.mp3 + var audioFile = new FileSystemResource("output.mp3"); + + Media media = new Media(MimeTypeUtils.parseMimeType("audio/mpeg"), audioFile); + UserMessage userMessage = UserMessage.builder().media(media).text("识别音频内容").build(); + userMessage.getMetadata().put(DashScopeApiConstants.MESSAGE_FORMAT, MessageFormat.AUDIO); + + DashScopeChatOptions options = DashScopeChatOptions.builder() + .multiModel(true) + .model("qwen-audio-turbo-latest") // 音频识别用 qwen-audio,qwen-vl 仅支持图片 + .build(); + + Prompt prompt = Prompt.builder().chatOptions(options).messages(userMessage).build(); + ChatResponse response = dashScopeChatModel.call(prompt); + + System.out.println(response.getResult().getOutput().getText()); + } +}