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());
+ }
+}