阿里百炼&Spring AI Alibaba

发布于:2025-03-16 ⋅ 阅读:(15) ⋅ 点赞:(0)

学习链接

阿里AI-Spring Cloud Alibaba AI:快速搭建自己的通义千问

让你快速入门Spring Cloud Alibaba AI

SpringCloud + Spring AI Alibaba 整合阿里云百炼大模型

Spring AI Alibaba官方文档
spring-ai-alibaba 官方github
spring-ai-alibaba-examples - spring ai alibaba示例代码

spring-cloud-alibaba-examples/spring-cloud-ai-example(忽略这个)

Spring Cloud Alibaba官网
Spring Boot 单体应用升级 Spring Cloud 微服务最佳实践

阿里百炼

阿里百炼官网

创建api-key

在这里插入图片描述

查看api调用示例

在这里插入图片描述
在这里插入图片描述
jdk版本使用17(如果使用jdk8,okhttp版本和gson版本不兼容,要么这个类找不到,要么那个方法没有。更换了版本也总会有其它地方的问题。)

示例

pom.xml

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

    <!-- 用java8 总是 okhttp方法找不到,类找不到,有时这个版本能用,但另外的问题还存在,换了版本,方法又不存在。 -->
    <parent>
        <artifactId>spring-boot-dependencies</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>3.4.2</version>
    </parent>

    <groupId>org.example</groupId>
    <artifactId>demo-alibailian</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dashscope-sdk-java</artifactId>
            <version>2.18.2</version>
            <exclusions>
                <!-- 排除slf4j-simple依赖 -->
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-simple</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

    </dependencies>

</project>

AQuickStart

package com.zzhua;// dashscope SDK的版本 >= 2.18.2

import com.alibaba.dashscope.aigc.generation.Generation;
import com.alibaba.dashscope.aigc.generation.GenerationParam;
import com.alibaba.dashscope.aigc.generation.GenerationResult;
import com.alibaba.dashscope.common.Message;
import com.alibaba.dashscope.common.Role;
import com.zzhua.constant.Constants;

import java.util.Arrays;


/*
测试:
curl -X POST "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation" \
-H "Authorization: Bearer sk-~~~" \
-H "Content-Type: application/json" \
-d '{
    "model": "deepseek-r1",
    "input":{
        "messages":[
            {
                "role": "user",
                "content": "你是谁?"
            }
        ]
    },
    "parameters": {
        "result_format": "message"
    }
}'
*/
public class AQuickStart {

    public static GenerationResult callWithMessage() throws Exception {
        Generation gen = new Generation();
        Message userMsg = Message.builder()
                .role(Role.USER.getValue())
                .content("你是谁?")
                .build();
        GenerationParam param = GenerationParam.builder()
                // 若没有配置环境变量,请用百炼API Key将下行替换为:.apiKey("sk-xxx")
                .apiKey(Constants.API_KEY)
                .model("deepseek-r1")
                .prompt("你是谁?")
                .messages(Arrays.asList(userMsg))
                // 不可以设置为"text"
                .resultFormat(GenerationParam.ResultFormat.MESSAGE)
                .build();
        return gen.call(param);
    }
    public static void main(String[] args) {
        try {
            GenerationResult result = callWithMessage();
            System.out.println("思考过程:");
            System.out.println(result.getOutput().getChoices().get(0).getMessage().getReasoningContent());
            System.out.println("回复内容:");
            System.out.println(result.getOutput().getChoices().get(0).getMessage().getContent());
        } catch (Exception e) {
            // 使用日志框架记录异常信息
            System.err.println("An error occurred while calling the generation service: " + e.getMessage());
        }
        System.exit(0);
    }
}

MultiChat

package com.zzhua;// dashscope SDK的版本 >= 2.18.2

import java.util.Arrays;
import java.lang.System;

import com.alibaba.dashscope.aigc.generation.Generation;
import com.alibaba.dashscope.aigc.generation.GenerationParam;
import com.alibaba.dashscope.aigc.generation.GenerationResult;
import com.alibaba.dashscope.common.Message;
import com.alibaba.dashscope.common.Role;
import com.alibaba.dashscope.exception.ApiException;
import com.alibaba.dashscope.exception.InputRequiredException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.utils.JsonUtils;
import com.zzhua.constant.Constants;

public class MultiChat {

    public static GenerationResult callWithMessage() throws ApiException, NoApiKeyException, InputRequiredException {
        Generation gen = new Generation();
        Message userMsg1 = Message.builder()
                .role(Role.USER.getValue())
                .content("你好")
                .build();
        Message AssistantMsg = Message.builder()
                .role(Role.ASSISTANT.getValue())
                .content("你好!很高兴见到你,有什么我可以帮忙的吗?")
                .build();
        Message UserMsg2 = Message.builder()
                .role(Role.USER.getValue())
                .content("你是谁")
                .build();
        GenerationParam param = GenerationParam.builder()
                // 若没有配置环境变量,请用百炼API Key将下行替换为:.apiKey("sk-xxx")
                .apiKey(Constants.API_KEY)
                .model("deepseek-r1")
                .messages(Arrays.asList(userMsg1, AssistantMsg, UserMsg2))
                // 不可以设置为"text"
                .resultFormat(GenerationParam.ResultFormat.MESSAGE)
                .build();
        return gen.call(param);
    }

    public static void main(String[] args) {
        try {
            GenerationResult result = callWithMessage();
            System.out.println("思考过程:");
            System.out.println(result.getOutput().getChoices().get(0).getMessage().getReasoningContent());
            System.out.println("回复内容:");
            System.out.println(result.getOutput().getChoices().get(0).getMessage().getContent());
        } catch (ApiException | NoApiKeyException | InputRequiredException e) {
            // 使用日志框架记录异常信息
            System.err.println("An error occurred while calling the generation service: " + e.getMessage());
        }
        System.exit(0);
    }
}

StreamChat

package com.zzhua;// dashscope SDK的版本 >= 2.18.2

import java.util.Arrays;

import com.zzhua.constant.Constants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.dashscope.aigc.generation.Generation;
import com.alibaba.dashscope.aigc.generation.GenerationParam;
import com.alibaba.dashscope.aigc.generation.GenerationResult;
import com.alibaba.dashscope.common.Message;
import com.alibaba.dashscope.common.Role;
import com.alibaba.dashscope.exception.ApiException;
import com.alibaba.dashscope.exception.InputRequiredException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import io.reactivex.Flowable;

import java.lang.System;

public class StreamChat {

    private static final Logger logger = LoggerFactory.getLogger(StreamChat.class);
    private static StringBuilder reasoningContent = new StringBuilder();
    private static StringBuilder finalContent = new StringBuilder();
    private static boolean isFirstPrint = true;

    private static void handleGenerationResult(GenerationResult message) {
        String reasoning = message.getOutput().getChoices().get(0).getMessage().getReasoningContent();
        String content = message.getOutput().getChoices().get(0).getMessage().getContent();

        if (!reasoning.isEmpty()) {
            reasoningContent.append(reasoning);
            if (isFirstPrint) {
                System.out.println("====================思考过程====================");
                isFirstPrint = false;
            }
            System.out.print(reasoning);
        }

        if (!content.isEmpty()) {
            finalContent.append(content);
            if (!isFirstPrint) {
                System.out.println("\n====================完整回复====================");
                isFirstPrint = true;
            }
            System.out.print(content);
        }
    }

    private static GenerationParam buildGenerationParam(Message userMsg) {
        return GenerationParam.builder()
                // 若没有配置环境变量,请用百炼API Key将下行替换为:.apiKey("sk-xxx")
                .apiKey(Constants.API_KEY)
                .model("deepseek-r1")
                .messages(Arrays.asList(userMsg))
                // 不可以设置为"text"
                .resultFormat(GenerationParam.ResultFormat.MESSAGE)
                .incrementalOutput(true)
                .build();
    }

    public static void streamCallWithMessage(Generation gen, Message userMsg)
            throws NoApiKeyException, ApiException, InputRequiredException {
        GenerationParam param = buildGenerationParam(userMsg);
        Flowable<GenerationResult> result = gen.streamCall(param);
        result.blockingForEach(message -> handleGenerationResult(message));
    }

    public static void main(String[] args) {
        try {
            Generation gen = new Generation();
            Message userMsg = Message.builder().role(Role.USER.getValue()).content("你是谁?").build();
            streamCallWithMessage(gen, userMsg);
//             打印最终结果
//            if (reasoningContent.length() > 0) {
//                System.out.println("\n====================完整回复====================");
//                System.out.println(finalContent.toString());
//            }
        } catch (ApiException | NoApiKeyException | InputRequiredException e) {
            logger.error("An exception occurred: {}", e.getMessage());
        }
        System.exit(0);
    }
}

Spring AI Alibaba

1、注意:因为 Spring AI Alibaba 基于 Spring Boot 3.x 开发,因此本地 JDK 版本要求为 17 及以上。

2、spring-ai-alibaba-examples:此仓库中包含许多 Example 来介绍 Spring AI Alibaba 从基础到高级的各种用法和 AI 项目的最佳实践。 更详细的介绍介绍请参阅每个子项目中的 README.md 和 Spring AI Alibaba 官网

3、 阿里云百炼平台 获取 API-KEY

4、由于 spring-ai 相关依赖包还没有发布到中央仓库,如出现 spring-ai-core 等相关依赖解析问题,请在您项目的 pom.xml 依赖中加入指定的仓库配置。

简单示例

pom.xml

<?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>
        <artifactId>spring-boot-dependencies</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>3.4.2</version>
    </parent>

    <groupId>org.example</groupId>
    <artifactId>demo-alibailian</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

		<!-- spring-ai-alibaba-starter 传递依赖了dashscope-sdk-java, 但是可能不是需要的版本(不是最新的版本) -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dashscope-sdk-java</artifactId>
            <version>2.18.2</version>
            <exclusions>
                <!-- 排除slf4j-simple依赖 -->
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-simple</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-starter</artifactId>
            <version>1.0.0-M5.1</version>
        </dependency>

    </dependencies>

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

</project>

application.yml

spring:
  ai:
    dashscope:
      api-key: sk-~~~ # 阿里百炼的key

HelloworldController

@RestController
@RequestMapping("/helloworld")
public class HelloworldController {

    private static final String DEFAULT_PROMPT = "你是一个博学的智能聊天助手,请根据用户提问回答!";

    private final ChatClient dashScopeChatClient;

    // ChatClientAutoConfiguration中注册了scope为prototype的ChatClient.Builder,
    // 同时该ChatClient.Builder会依赖1个 ChatModel, 此ChatModel在DashScopeAutoConfiguration中定义了DashScopeChatModel这个bean,
    // 所以,当需要ChatClient.Builder时,就会触发多例bean的获取,从而会寻找ChatModel的实现bean,如果有不止1个ChatModel的bean定义了,那么就会报错
    public HelloworldController(ChatClient.Builder chatClientBuilder) {
        this.dashScopeChatClient = chatClientBuilder
                .defaultSystem(DEFAULT_PROMPT)
                // 实现 Chat Memory 的 Advisor
                // 在使用 Chat Memory 时,需要指定对话 ID,以便 Spring AI 处理上下文。
                .defaultAdvisors(
                        new MessageChatMemoryAdvisor(new InMemoryChatMemory())
                )
                // 实现 Logger 的 Advisor
                .defaultAdvisors(
                        new SimpleLoggerAdvisor()
                )
                // 设置 ChatClient 中 ChatModel 的 Options 参数
                .defaultOptions(
                        DashScopeChatOptions.builder()
                                .withTopP(0.7)
                                .build()
                )
                .build();
    }

    // 也可以如下这样创建,与上面是1个意思
	/*public HelloworldController(ChatModel chatModel) {

		// 构造时,可以设置 ChatClient 的参数
		// {@link org.springframework.ai.chat.client.ChatClient};
		this.dashScopeChatClient = ChatClient.builder(chatModel)
				// 实现 Chat Memory 的 Advisor
				// 在使用 Chat Memory 时,需要指定对话 ID,以便 Spring AI 处理上下文。
				.defaultAdvisors(
						new MessageChatMemoryAdvisor(new InMemoryChatMemory())
				)
				// 实现 Logger 的 Advisor
				.defaultAdvisors(
						new SimpleLoggerAdvisor()
				)
				// 设置 ChatClient 中 ChatModel 的 Options 参数
				.defaultOptions(
						DashScopeChatOptions.builder()
								.withTopP(0.7)
								.build()
				)
				.build();
	}*/

    // http://localhost:8080/helloworld/simple/chat?query=你是谁

    /**
     * ChatClient 简单调用
     */
    @GetMapping("/simple/chat")
    public String simpleChat(@RequestParam("query") String query) {
        // 1、DefaultChatClientRequestSpec 构造方法中添加的 CallAroundAdvisor 触发了请求的执行
        //    调用 chatModel.call(Prompt),即 DashScopeChatModel#call(Prompt)
        // 2、然后调用 DashScopeApi#chatCompletionEntity(ChatCompletionRequest)
        //    可以看到 最终默认调用的模型是 qwen-plus
        //    (是因为 DashScopeAutoConfiguration 定义了 DashScopeChatModel 这个bean,
        //      而 DashScopeChatModel 这个bean又引用了 DashScopeChatProperties 这个bean,
        //      而 DashScopeChatProperties 中直接初始化的 DashScopeChatOptions 使用了 qwen-plus。
        //      所以,如果需要改掉模型,方法就是 1. 通过spring.ai.datascope.chat.options.model设置 来改掉默认的模型
        //                                  2. 或者这样改 dashScopeChatClient.prompt(new Prompt(query, ChatOptions.builder().model("deepseek-r1").build())).call().content()
        // 3、注意几个类: DashScopeAutoConfiguration、
        //              DashScopeConnectionProperties(配置前缀是 spring.ai.datascope)、
        //              DashScopeChatProperties(配置前缀是 spring.ai.datascope.chat)
        // return dashScopeChatClient.prompt(query).call().content();
        return dashScopeChatClient.prompt(new Prompt(query, ChatOptions.builder().model("deepseek-r1").build())).call().content();
    }

    // http://localhost:8080/helloworld/stream/chat?query=你是谁
    /**
     * ChatClient 流式调用
     */
    @GetMapping("/stream/chat")
    public Flux<String> streamChat(@RequestParam(value = "query", defaultValue = "你好,很高兴认识你,能简单介绍一下自己吗?") String query, HttpServletResponse response) {

        response.setCharacterEncoding("UTF-8");
        return dashScopeChatClient.prompt(query).stream().content();
    }

    // http://127.0.0.1:8080/helloworld/advisor/chat/123?query=你好,我叫牧生,之后的会话中都带上我的名字
    // http://127.0.0.1:8080/helloworld/advisor/chat/123?query=我叫什么名字?

    /**
     * ChatClient 使用自定义的 Advisor 实现功能增强.
     * eg:
     * http://127.0.0.1:8080/helloworld/advisor/chat/123?query=你好,我叫牧生,之后的会话中都带上我的名字
     * 你好,牧生!很高兴认识你。在接下来的对话中,我会记得带上你的名字。有什么想聊的吗?
     * http://127.0.0.1:8080/helloworld/advisor/chat/123?query=我叫什么名字?
     * 你叫牧生呀。有什么事情想要分享或者讨论吗,牧生?
     */
    @GetMapping("/advisor/chat/{id}")
    public Flux<String> advisorChat(
            HttpServletResponse response,
            @PathVariable(value = "id") String id,
            @RequestParam(value = "query") String query) {

        response.setCharacterEncoding("UTF-8");

        return this.dashScopeChatClient.prompt(query)
                // 因为前面使用Client.Builder构建Client时, 添加了MessageChatMemoryAdvisor切面
                // 所以, 这里可以添加切面参数
                .advisors(
                        a -> a
                                .param(CHAT_MEMORY_CONVERSATION_ID_KEY, id)
                                .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100)
                ).stream().content();
    }

}

DashScopeChatModelController

import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import jakarta.servlet.http.HttpServletResponse;
import reactor.core.publisher.Flux;

import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/dashscope/chat-model")
public class DashScopeChatModelController {

	private static final String DEFAULT_PROMPT = "你好,介绍下你自己吧。";

	private final ChatModel dashScopeChatModel;

	public DashScopeChatModelController(ChatModel chatModel) {
		this.dashScopeChatModel = chatModel;
	}

	// http://localhost:8080/dashscope/chat-model/simple/chat
	/**
	 * 最简单的使用方式,没有任何 LLMs 参数注入。
	 * @return String types.
	 */
	@GetMapping("/simple/chat")
	public String simpleChat() {

		return dashScopeChatModel.call(new Prompt(DEFAULT_PROMPT, DashScopeChatOptions
				.builder()
				// .withModel("qwen2.5-vl-72b-instruct") // 这个会报错, 这应该是模型的问题
				.withModel("qwen-plus") // 这个正常回答: 你好!我是通义千问,阿里巴巴集团旗下...
				// .withModel("deepseek-r1")  // 这个正常回答: 您好!我是由中国的深度求索(DeepSeek)公司开发的智能助手DeepSeek-R1。如您有任...
				// .withModel("deepseek-r1-distill-qwen-1.5b")  // 这个正常回答: 我是DeepSeek-R1,一个由深度求索公司开发的智能助手,我...
				.build())).getResult().getOutput().getContent();
	}

	// http://localhost:8080/dashscope/chat-model/stream/chat
	/**
	 * Stream 流式调用。可以使大模型的输出信息实现打字机效果。
	 * @return Flux<String> types.
	 */
	@GetMapping("/stream/chat")
	public Flux<String> streamChat(HttpServletResponse response) {

		// 避免返回乱码
		response.setCharacterEncoding("UTF-8");

		Flux<ChatResponse> stream = dashScopeChatModel.stream(new Prompt(DEFAULT_PROMPT, DashScopeChatOptions
				.builder()
				// .withModel("qwen-plus") // 这个正常回答: 你好!我是通义千问,阿里巴巴集团旗下...
				// .withModel("deepseek-r1")  // 这个正常回答: 您好!我是由中国的深度求索(DeepSeek)公司开发的智能助手DeepSeek-R1。如您有任...
				.withModel("deepseek-r1-distill-qwen-1.5b")  // /这个正常回答: 我是DeepSeek-R1,...
				.build()));
		return stream.map(resp -> resp.getResult().getOutput().getContent());
	}

	// http://localhost:8080/dashscope/chat-model/custom/chat
	/**
	 * 使用编程方式自定义 LLMs ChatOptions 参数, {@link com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions}
	 * 优先级高于在 application.yml 中配置的 LLMs 参数!
	 */
	@GetMapping("/custom/chat")
	public String customChat() {

		// 不指定模型, 将会使用默认的  spring.ai.dashscope.chat.options.model 配置的模型(默认是qwen-plus)
		DashScopeChatOptions customOptions = DashScopeChatOptions.builder()
				.withTopP(0.7)
				.withTopK(50)
				.withTemperature(0.8)
				.build();

		return dashScopeChatModel.call(new Prompt(DEFAULT_PROMPT, customOptions)).getResult().getOutput().getContent();
	}

}

图解spring AI的结构

在这里插入图片描述

deepseek

这节示例是使用OpenAi对接deepseek,跟dashscope没关系(所以这个依赖可以不用引入),需要引入spring-ai-openai-spring-boot-starter。如果同时引入spring-ai-alibaba-starter就需要注意 注入的ChatModel的具体实现。

pom.xml

<?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>
        <artifactId>spring-boot-dependencies</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>3.4.2</version>
    </parent>

    <groupId>org.example</groupId>
    <artifactId>demo-alibailian</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>17</java.version>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

		<dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dashscope-sdk-java</artifactId>
            <version>2.18.2</version>
            <exclusions>
                &lt;!&ndash; 排除slf4j-simple依赖 &ndash;&gt;
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-simple</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-starter</artifactId>
            <version>1.0.0-M5.1</version>
        </dependency>

        <!-- 引入spring-ai openai的启动依赖 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
            <version>1.0.0-M5</version>
        </dependency>

    </dependencies>

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

</project>

application.yml

spring:
  ai:
    # 百炼的配置
    dashscope:
      api-key: sk-~~~# 百炼的ApiKey
      chat:
        options:
          model: qwen-plus
    # 可兼容openai的配置
    openai:
      api-key: sk-~~~# deepseek的ApiKey
      base-url: https://api.deepseek.com
      chat:
        options:
          model: deepseek-chat
      embedding:
        enabled: false

DeepSeekChatClientController

@RestController
public class DeepSeekChatClientController {

    private static final String DEFAULT_PROMPT = "你好,介绍下你自己!";

    private final ChatModel chatModel;

    private final ChatClient deepSeekChatClient;

    // 【注入的是 OpenAiChatModel, 而这里配置的是与openai兼容的deepseek。
    //  OpenAiAutoConfiguration 中定义了 OpenAiChatModel。
    //  ChatModel(这个由各个spring-ai模块包自己实现,并通过自动配置类提供出来)
    //  -> ChatClient.Builder(这个在 ClientAutoConfiguration 中定义为多例bean,它需要1个 ChatModel)
    //  -> ChatClient(使用 ChatClient.Builder 构建,其实现为 DefaultChatClient)】
    public DeepSeekChatClientController (OpenAiChatModel chatModel) {

        this.chatModel = chatModel;

        // 这里实际上就是在模拟 ClientAutoConfiguration 中定义 ChatClient.Builder多例bean 类似
        this.deepSeekChatClient = ChatClient.builder(chatModel)
                .defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
                // 实现 Logger 的 Advisor
                .defaultAdvisors(new SimpleLoggerAdvisor())
                // 设置 ChatClient 中 ChatModel 的 Options 参数
                .defaultOptions(OpenAiChatOptions.builder().temperature(0.7d).build()).build();
    }

    // http://localhost:8080/ai/customOptions
    /**
     * 使用自定义参数调用DeepSeek模型
     *
     * @return ChatResponse 包含模型响应结果的封装对象
     * @apiNote 当前硬编码指定模型为deepseek-chat,温度参数0.7以平衡生成结果的创造性和稳定性
     */
    @GetMapping(value = "/ai/customOptions")
    public ChatResponse testDeepSeekCustomOptions () {
        return this.deepSeekChatClient.prompt(DEFAULT_PROMPT).call().chatResponse();
    }

    // http://localhost:8080/ai/generate
    /**
     * 执行默认提示语的AI生成请求
     *
     * @return Map 包含生成结果的键值对,格式为{ "generation": 响应内容 }
     */
    @GetMapping("/ai/generate")
    public Map<String, Object> testEasyChat () {
        return Map.of("generation", this.deepSeekChatClient.prompt(DEFAULT_PROMPT).call().content());
    }

    // http://localhost:8080/ai/stream
    /**
     * 流式生成接口 - 支持实时获取生成过程的分块响应
     *
     * @return Flux<ChatResponse> 响应式流对象,包含分块的模型响应数据
     * @see Flux 基于Project Reactor的响应式流对象
     */
    @GetMapping("/ai/stream")
    public Flux<String> testDeepSeekGenerateWithStream (HttpServletResponse response) {
        response.setCharacterEncoding("UTF-8");
        // 1、DefaultChatClientRequestSpec 构造方法中添加的 StreamAroundAdvisor 触发了请求的执行
        //    调用 chatModel.stream(Prompt),即 chatModel#stream(Prompt),这里的chatModel就是OpenAiChatModel的实现
        // 2、所以具体怎么解析响应的 需要到 OpenAiChatModel#stream(Prompt) 方法中查看
        // 3、所以,其实就发现 chatClient还绕了一圈,然后再去调用chatModel,绕一圈的目的是为了增强功能
        return deepSeekChatClient.prompt(DEFAULT_PROMPT).stream().chatResponse()
                .map(chatResponse -> chatResponse.getResult().getOutput().getContent());
    }

}

DeepSeekChatModelController

@RestController
public class DeepSeekChatModelController {

    private static final String DEFAULT_PROMPT = "你好,介绍下你自己吧。";

    private final ChatModel deepSeekChatModel;

    // 注入的是 OpenAiChatModel
    public DeepSeekChatModelController (OpenAiChatModel chatModel) {
        this.deepSeekChatModel = chatModel;
    }

    // http://localhost:8080/simple/chat
    /**
     * 最简单的使用方式,没有任何 LLMs 参数注入。
     *
     * @return String types.
     */
    @GetMapping("/simple/chat")
    public String simpleChat () {
        return deepSeekChatModel.call(new Prompt(DEFAULT_PROMPT)).getResult().getOutput().getContent();
    }

    // http://localhost:8080/stream/chat
    /**
     * Stream 流式调用。可以使大模型的输出信息实现打字机效果。
     *
     * @return Flux<String> types.
     */
    @GetMapping("/stream/chat")
    public Flux<String> streamChat (HttpServletResponse response) {
        response.setCharacterEncoding("UTF-8");
        Flux<ChatResponse> stream = deepSeekChatModel.stream(new Prompt(DEFAULT_PROMPT));
        return stream.map(resp -> resp.getResult().getOutput().getContent());
    }

    // http://localhost:8080/custom/chat
    /**
     * 使用编程方式自定义 LLMs ChatOptions 参数, {OpenAIChatOption}
     * 优先级高于在 application.yml 中配置的 LLMs 参数!
     */
    @GetMapping("/custom/chat")
    public String customChat () {
        // 指定的模型 将覆盖 chatModel 中配置的模型
        // 模型名优先级顺序 Prompt中的options > client配置的options > chatModel中的options(通过配置文件配置的)
        OpenAiChatOptions customOptions = OpenAiChatOptions.builder().model("deepseek-reasoner").temperature(0.8d).build();
        return deepSeekChatModel.call(new Prompt(DEFAULT_PROMPT, customOptions)).getResult().getOutput().getContent();
    }
}

dashscope文生图

pom.xml

<?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>
        <artifactId>spring-boot-dependencies</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>3.4.2</version>
    </parent>

    <groupId>org.example</groupId>
    <artifactId>demo-alibailian</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>17</java.version>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dashscope-sdk-java</artifactId>
            <version>2.18.2</version>
            <exclusions>
                <!-- 排除slf4j-simple依赖 -->
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-simple</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-starter</artifactId>
            <version>1.0.0-M5.1</version>
        </dependency>


    </dependencies>

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

</project>

application.yml

spring:
  ai:
    # 百炼的配置
    dashscope:
      api-key: sk-~~~# 百炼的ApiKey

DashScopeImageController

可以看出来,spring ai alibaba其实就是实现了spring ai core中的接口

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;

import jakarta.servlet.http.HttpServletResponse;

import org.springframework.ai.image.ImageModel;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
import org.springframework.http.MediaType;
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("/example")
public class DashScopeImageController {

	private final ImageModel imageModel;

	private static final String DEFAULT_PROMPT = "为人工智能生成一张富有科技感的图片!";

	public DashScopeImageController(ImageModel imageModel) {
		this.imageModel = imageModel;
	}

	// http://localhost:8080/example/image?text=生成1张小怪兽打奥特曼的图片
	@GetMapping("/image")
	public void image(@RequestParam(value = "text", defaultValue = DEFAULT_PROMPT) String text, HttpServletResponse response) {

		ImageResponse imageResponse = imageModel.call(new ImagePrompt(text));
		String imageUrl = imageResponse.getResult().getOutput().getUrl();

		System.out.println(imageUrl);

		try {
			URL url = URI.create(imageUrl).toURL();
			InputStream in = url.openStream();

			response.setHeader("Content-Type", MediaType.IMAGE_PNG_VALUE);
			response.getOutputStream().write(in.readAllBytes());
			response.getOutputStream().flush();
		} catch (IOException e) {
			response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
		}
	}

}

dashscope语音

pom.xml

<?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>
        <artifactId>spring-boot-dependencies</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>3.4.2</version>
    </parent>

    <groupId>org.example</groupId>
    <artifactId>demo-alibailian</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>17</java.version>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dashscope-sdk-java</artifactId>
            <version>2.18.2</version>
            <exclusions>
                <!-- 排除slf4j-simple依赖 -->
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-simple</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-starter</artifactId>
            <version>1.0.0-M5.1</version>
        </dependency>


    </dependencies>

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

</project>

application.yml

spring:
  ai:
    # 百炼的配置
    dashscope:
      api-key: sk-~~~# 百炼的ApiKey

TTSController

@RestController
@RequestMapping("/ai/tts")
public class TTSController /*implements ApplicationRunner*/ {

	private final SpeechSynthesisModel speechSynthesisModel;

	private static final String TEXT = "白日依山尽,黄河入海流。";

	private static final String FILE_PATH = System.getProperty("user.dir") + "/file/";

	public TTSController(SpeechSynthesisModel speechSynthesisModel) {

		this.speechSynthesisModel = speechSynthesisModel;
	}

	// http://localhost:8080/ai/tts?text=白日依山尽,黄河入海流。
	@GetMapping
	public void tts(@RequestParam(value = "text", defaultValue = TEXT) String text) throws IOException {

		SpeechSynthesisResponse response = speechSynthesisModel.call(
				new SpeechSynthesisPrompt(text)
		);

		File file = new File(FILE_PATH + "/output.mp3");
		try (FileOutputStream fos = new FileOutputStream(file)) {
			ByteBuffer byteBuffer = response.getResult().getOutput().getAudio();
			fos.write(byteBuffer.array());
		}
		catch (IOException e) {
			throw new IOException(e.getMessage());
		}
	}

	@GetMapping("/stream")
	public void streamTTS(@RequestParam(value = "text", defaultValue = TEXT) String text) {

		Flux<SpeechSynthesisResponse> response = speechSynthesisModel.stream(
				new SpeechSynthesisPrompt(text)
		);

		CountDownLatch latch = new CountDownLatch(1);
		File file = new File(FILE_PATH + "/output-stream.mp3");
		try (FileOutputStream fos = new FileOutputStream(file)) {

			response.doFinally(
					signal -> latch.countDown()
			).subscribe(synthesisResponse -> {
				ByteBuffer byteBuffer = synthesisResponse.getResult().getOutput().getAudio();
				byte[] bytes = new byte[byteBuffer.remaining()];
				byteBuffer.get(bytes);
				try {
					fos.write(bytes);
				}
				catch (IOException e) {
					throw new RuntimeException(e);
				}
			});

			latch.await();
		}
		catch (IOException | InterruptedException e) {
			throw new RuntimeException(e);
		}
	}

	/*@Override
	public void run(ApplicationArguments args) {

		File file = new File(FILE_PATH);
		if (!file.exists()) {
			file.mkdirs();
		}
	}

	@PreDestroy
	public void destroy() throws IOException {

		FileUtils.deleteDirectory(new File(FILE_PATH));
	}
*/
}

STTController


@RestController
@RequestMapping("/ai/stt")
public class STTController {

	private final AudioTranscriptionModel transcriptionModel;

	private static final Logger log = LoggerFactory.getLogger(STTController.class);

	private static final String DEFAULT_MODEL_1 = "sensevoice-v1";

	private static final String DEFAULT_MODEL_2 = "paraformer-realtime-v2";

	private static final String DEFAULT_MODEL_3 = "paraformer-v2";

	private static final String AUDIO_RESOURCES_URL = "https://dashscope.oss-cn-beijing.aliyuncs.com/samples/audio/paraformer/hello_world_female2.wav";

	private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

	public STTController(AudioTranscriptionModel transcriptionModel) {

		this.transcriptionModel = transcriptionModel;
	}

	@GetMapping
	public String stt() throws MalformedURLException {

		AudioTranscriptionResponse response = transcriptionModel.call(
				new AudioTranscriptionPrompt(
						new UrlResource(AUDIO_RESOURCES_URL),
						DashScopeAudioTranscriptionOptions.builder()
								.withModel(DEFAULT_MODEL_1)
								.build()
				)
		);

		return response.getResult().getOutput();
	}

	@GetMapping("/stream")
	public String streamSTT() {

		CountDownLatch latch = new CountDownLatch(1);
		StringBuilder stringBuilder = new StringBuilder();

		Flux<AudioTranscriptionResponse> response = transcriptionModel
				.stream(
						new AudioTranscriptionPrompt(
								new FileSystemResource("spring-ai-alibaba-audio-example/dashscope-audio/src/main/resources/stt/count.pcm"),
								DashScopeAudioTranscriptionOptions.builder()
										.withModel(DEFAULT_MODEL_2)
										.withSampleRate(16000)
										.withFormat(DashScopeAudioTranscriptionOptions.AudioFormat.PCM)
										.withDisfluencyRemovalEnabled(false)
										.build()
						)
				);

		response.doFinally(
				signal -> latch.countDown()
		).subscribe(
				resp -> stringBuilder.append(resp.getResult().getOutput())
		);

		try {
			latch.await();
		}
		catch (InterruptedException e) {
			throw new RuntimeException(e);
		}

		return stringBuilder.toString();
	}

	@GetMapping("/async")
	public String asyncSTT() {
		StringBuilder stringBuilder = new StringBuilder();
		CountDownLatch latch = new CountDownLatch(1);

		try {
			AudioTranscriptionResponse submitResponse = transcriptionModel.asyncCall(
					new AudioTranscriptionPrompt(
							new UrlResource(AUDIO_RESOURCES_URL),
							DashScopeAudioTranscriptionOptions.builder()
									.withModel(DEFAULT_MODEL_3)
									.build()
					)
			);

			DashScopeAudioTranscriptionApi.Response.Output submitOutput = Objects.requireNonNull(submitResponse.getMetadata()
					.get("output"));
			String taskId = submitOutput.taskId();

			scheduler.scheduleAtFixedRate(
					() -> checkTaskStatus(taskId, stringBuilder, latch), 0, 1, TimeUnit.SECONDS);
			latch.await();

		}
		catch (MalformedURLException e) {
			throw new DashScopeException("Error in URL format: " + e.getMessage());
		}
		catch (InterruptedException e) {
			Thread.currentThread().interrupt();
			throw new DashScopeException("Thread was interrupted: " + e.getMessage());
		}
		finally {
			scheduler.shutdown();
		}

		return stringBuilder.toString();
	}

	private void checkTaskStatus(String taskId, StringBuilder stringBuilder, CountDownLatch latch) {

		try {
			AudioTranscriptionResponse fetchResponse = transcriptionModel.fetch(taskId);
			DashScopeAudioTranscriptionApi.Response.Output fetchOutput =
					Objects.requireNonNull(fetchResponse.getMetadata().get("output"));
			DashScopeAudioTranscriptionApi.TaskStatus taskStatus = fetchOutput.taskStatus();

			if (taskStatus.equals(DashScopeAudioTranscriptionApi.TaskStatus.SUCCEEDED)) {
				stringBuilder.append(fetchResponse.getResult().getOutput());
				latch.countDown();
			}
			else if (taskStatus.equals(DashScopeAudioTranscriptionApi.TaskStatus.FAILED)) {
				log.warn("Transcription failed.");
				latch.countDown();
			}
		}
		catch (Exception e) {
			latch.countDown();
			throw new RuntimeException("Error occurred while checking task status: " + e.getMessage());
		}
	}

}