This commit is contained in:
zouzhiwen 2026-03-20 19:06:49 +08:00
commit 7b84f63629
40 changed files with 2287 additions and 0 deletions

10
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ApifoxUploaderProjectSetting">
<option name="apiAccessToken" value="APS-fqVD1Y7hESdEfyuf3WkBVHlqPP6WIsBG" />
<option name="yapiTokens" value="" />
</component>
</project>

18
.idea/compiler.xml Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="spring-ai-alibaba-demo" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="spring-ai-alibaba-demo" options="-parameters" />
</option>
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Ask2AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="com.github.wanniwa.editorjumper.settings.EditorJumperProjectSettings">
<option name="projectEditorType" value="Cursor" />
</component>
</project>

7
.idea/encodings.xml Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

View File

@ -0,0 +1,8 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="JavadocDeclaration" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ADDITIONAL_TAGS" value="date" />
</inspection_tool>
</profile>
</component>

20
.idea/jarRepositories.xml Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://maven.aliyun.com/nexus/content/repositories/central/" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
</component>
</project>

12
.idea/misc.xml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="zulu-17" project-jdk-type="JavaSDK" />
</project>

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"maven.view": "hierarchical"
}

View File

@ -0,0 +1,33 @@
autoDeleteData: false
autoDetectedPackages:
- com.baoshi
autoKeepDataNum: 10
enableAutoDetect: true
entryDisplayConfig:
excludedPathPatterns: []
skipJsCss: true
funcDisplayConfig:
skipConstructors: false
skipFieldAccess: true
skipFieldChange: true
skipGetters: false
skipNonProjectPackages: false
skipPrivateMethods: false
skipSetters: false
ignoreSameClassCall: null
ignoreSamePackageCall: null
includedPackagePrefixes: null
includedParentClasses: null
maxColSize: 32
maxNumFirst: 12
maxNumFirstImportant: 1024
maxNumHash: 3
maxNumHashImportant: 256
maxObjectDepth: 4
maxStrSize: 4096
name: xcodemap-filter
openMainWindow: true
recordMode: manual
sourceDisplayConfig:
color: blue
startOnDebug: false

BIN
output.mp3 Normal file

Binary file not shown.

67
pom.xml Normal file
View File

@ -0,0 +1,67 @@
<?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 http://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.8</version>
<relativePath/>
</parent>
<groupId>com.baoshi</groupId>
<artifactId>spring-ai-alibaba-demo</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>
<!-- 显式指定 jackson-annotations 版本,解决 mcp-core 引入的 2.17.0 与 jackson-databind 2.19.4 不兼容导致的 NoSuchMethodError: JsonProperty.isRequired() -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.19.4</version>
</dependency>
<!-- Spring AI Alibaba Agent Framework -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-agent-framework</artifactId>
<version>1.1.2.2</version>
</dependency>
<!-- Spring AI Ollama - 本地运行大模型(需与 spring-ai-alibaba 1.1.2 同版本) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
<version>1.1.2</version>
</dependency>
<!-- DashScope ChatModel 支持(如果使用其他模型,请跳转 Spring AI 文档选择对应的 starter -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
<version>1.1.2.2</version>
</dependency>
<!-- WebFluxSpring AI 依赖引入 spring-webflux 后Spring Boot 会以响应式模式启动,需显式添加此 starter 提供 ReactiveWebServerFactory -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- 测试依赖(含 JUnit 5支持方法参数 @Autowired 注入) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$" dumb="true">
<sourceFolder url="file://$MODULE_DIR$/src/main/java/com/baoshi" isTestSource="false" />
</content>
</component>
</module>

View File

@ -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<ApiListTool.DomainRequest, ToolContext, String> {
//Map<String, List<String>>.of("仓库",)
private final Map<String, List<String>> 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) {
}
}

View File

@ -0,0 +1,24 @@
package com.baoshi;
import org.springframework.ai.chat.model.ToolContext;
import java.util.function.BiFunction;
/**
* 业务
*/
public class BusinessRouteTool implements BiFunction<BusinessRouteTool.RouteRequest, ToolContext, String> {
@Override
public String apply(RouteRequest request, ToolContext toolContext) {
return """
订单相关业务域:出库,
移库盘点上架相关业务域:存储
""";
}
/** 与 LLM tool call 返回的 JSON 结构对应 */
public record RouteRequest() {
}
}

View File

@ -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 <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
@SpringBootApplication
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
}

View File

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

View File

@ -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<String, ChatClient> chatClientMap;
@GetMapping("/chat")
Mono<String> 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<String> 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();
}
}

View File

@ -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

View File

@ -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 {
}

View File

@ -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...)sizen(数量)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*1024720*12801280*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)durationimageUrl(图生视频)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()) // 文生视频用 T2VI2V 需提供 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<TextToSpeechResponse> 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 {
// flacmp3mp4mpegmpgam4aoggwav 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-audioqwen-vl 仅支持图片
.build();
Prompt prompt = Prompt.builder().chatOptions(options).messages(userMessage).build();
ChatResponse response = dashScopeChatModel.call(prompt);
System.out.println(response.getResult().getOutput().getText());
}
}

View File

@ -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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,3 @@
artifactId=spring-ai-alibaba-demo
groupId=com.baoshi
version=1.0-SNAPSHOT

View File

@ -0,0 +1,4 @@
com/baoshi/BusinessRouteTool.class
com/baoshi/ApiListTool.class
com/baoshi/ApiListTool$DomainRequest.class
com/baoshi/BusinessRouteTool$RouteRequest.class

View File

@ -0,0 +1,5 @@
/Users/kazusa/IdeaProjects/spring-ai/spring-ai-alibaba-demo/src/main/java/com/baoshi/ApiListTool.java
/Users/kazusa/IdeaProjects/spring-ai/spring-ai-alibaba-demo/src/main/java/com/baoshi/BusinessRouteTool.java
/Users/kazusa/IdeaProjects/spring-ai/spring-ai-alibaba-demo/src/main/java/com/baoshi/Main.java
/Users/kazusa/IdeaProjects/spring-ai/spring-ai-alibaba-demo/src/main/java/com/baoshi/conf/AiConfig.java
/Users/kazusa/IdeaProjects/spring-ai/spring-ai-alibaba-demo/src/main/java/com/baoshi/controller/MultiModelsController.java

View File

@ -0,0 +1 @@
com/baoshi/DemoTest.class

View File

@ -0,0 +1 @@
/Users/kazusa/IdeaProjects/spring-ai/spring-ai-alibaba-demo/src/test/java/com/baoshi/DemoTest.java

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,4 @@
-------------------------------------------------------------------------------
Test set: com.baoshi.DemoTest
-------------------------------------------------------------------------------
Tests run: 4, Failures: 0, Errors: 0, Skipped: 1, Time elapsed: 15336 s -- in com.baoshi.DemoTest

Binary file not shown.