国外的Spring出AI了?阿里:没关系,我会出手

1,295 阅读6分钟

Spring AI 是 Spring 官方社区项目,旨在简化 Java AI 应用程序开发,让 Java 开发者像使用 Spring 开发普通应用一样开发 AI 应用。

Spring Cloud Alibaba AI 以 Spring AI 为基础,并在此基础上提供阿里云通义系列大模型全面适配,让用户在 5 分钟内开发基于通义大模型的 Java AI 应用。

Spring AI 主要对接的是国外的OpenAI,Microsoft,Amazon,Google和Huggingface 。所以阿里在 Spring AI 的基础上面提供了对国内的模型的适配,例如完成通义系列大模型的接入。

Spring Cloud Alibaba AI 现在已经做了这些应用开发的接入,如:

所以接下来一起入手体验一下Spring Cloud Alibaba AI

环境准备:JDK23

第一步,新建一个springboot项目

SpringBoot入门:如何新建SpringBoot项目(保姆级教程)

我们的pom文件为:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">


    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.12</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <modelVersion>4.0.0</modelVersion>
    <groupId>com.masiyi</groupId>
    <artifactId>spring-ai</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-ai</name>
    <description>spring-ai</description>
    <properties>
        <java.version>23</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.7.12</spring-boot.version>
    </properties>
    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-ai</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2023.0.1.2</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <configuration>
                    <mainClass>com.masiyi.springai.SpringAiApplication</mainClass>
                    <skip>true</skip>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

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


如果不配置仓库地址,则会出现包拉不下来的情况,如果jdk版本不对也会出现编译不了的情况,这个pom里面我们使用的jdk编译版本为17版本,但是spring-cloud-starter-alibaba-ai的jdk编译版本为17,所以我们使用比17高的jdk版本就行了

第二步,申请api-key

阿里的apikey是需要付费的,所以我们通过bailian.console.aliyun.com/ 购买对应的服务之后就可以申请我们的api-key

image-20240920102559875

第三步,编写yml配置文件

server:
  port: 8999

spring:
  application:
    name: tongyi-example
  cloud:
    ai:
      tongyi:
        connection:
          api-key:  sk-6670e214d0414bdbad173b7xxxx

我们按照上面配置好,如果是2023.0.1.2版本就按照上面写,官网的文档估计没有更新到最新版本,这部分还是作者看源码才知道要这么配置的。

配置好之后我们就可以来上手spring-cloud-starter-alibaba-ai了

第四步,新建一个web控制层

package com.masiyi.springai.controller;/*
 * Copyright 2023-2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


import com.masiyi.springai.service.TongYiService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.CrossOrigin;
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.Map;

/**
 * TongYi models Spring Cloud Alibaba Controller.
 *
 * @author yuluo
 * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
 * @since 2023.0.0.0
 */

@RestController
@RequestMapping("/ai")
@CrossOrigin
public class TongYiController {

    @Autowired
    @Qualifier("tongYiSimpleServiceImpl")
    private TongYiService tongYiSimpleService;

    @GetMapping("/example")
    public String completion(
            @RequestParam(value = "message", defaultValue = "Tell me a joke")
            String message
    ) {

        return tongYiSimpleService.completion(message);
    }

    @GetMapping("/stream")
    public Map<String, String> streamCompletion(
            @RequestParam(value = "message", defaultValue = "请告诉我西红柿炖牛腩怎么做?")
            String message
    ) {

        return tongYiSimpleService.streamCompletion(message);
    }

}

这里面的有两个方法,一个是返回一个字符串,一个是返回一个steam流,但是官网的写法后面还需要改造一下,这里留个坑,就不做研究。

第五步,新建service

/*
 * Copyright 2023-2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.masiyi.springai.service;

import java.util.Map;

/**
 * @author yuluo
 * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
 * @since 2023.0.0.0
 */

public interface TongYiService {

	/**
	 * Hello World example.
	 *
	 * @param message conversation content question.
	 * @return AI answer.
	 */
	String completion(String message);

	/**
	 * Stream call.
	 *
	 * @param message conversation content question.
	 * @return AI answer.
	 */
	Map<String, String> streamCompletion(String message);

}

这个是可以不用的,可以直接实现上面的接口,但是官方用了,这里就按照官方的写法来

/*
 * Copyright 2023-2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.masiyi.springai.service;

import java.util.Map;


/**
 * @author yuluo
 * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
 * @since 2023.0.0.0
 */

public abstract class AbstractTongYiServiceImpl implements TongYiService {

    private static final String INFO_PREFIX = "please implement ";
    private static final String INFO_SUFFIX = "() method.";

    @Override
    public String completion(String message) {

        throw new RuntimeException(INFO_PREFIX + Thread.currentThread().getStackTrace()[2].getMethodName());
    }

    @Override
    public Map<String, String> streamCompletion(String message) {

        throw new RuntimeException(INFO_PREFIX + Thread.currentThread()
                .getStackTrace()[2].getMethodName() + INFO_SUFFIX);
    }


}

核心的实现类:这里面我们注入了ChatModel和StreamingChatModel两个对话类

package com.masiyi.springai.service.impl;/*
 * Copyright 2023-2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


import com.masiyi.springai.service.AbstractTongYiServiceImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.StreamingChatModel;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;

import java.util.Map;

/**
 * The Chat simple example service implementation.
 * There is optional message parameter whose default value is "Tell me a joke".
 * pl The response to the request is from the TongYi models Service.
 *
 * @author yuluo
 * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
 * @since 2023.0.0.0
 */

@Service
public class TongYiSimpleServiceImpl extends AbstractTongYiServiceImpl {

	private static final Logger logger = LoggerFactory.getLogger(TongYiSimpleServiceImpl.class);

	private final ChatModel chatModel;

	private final StreamingChatModel streamingChatModel;

	@Autowired
	public TongYiSimpleServiceImpl(ChatModel chatModel, StreamingChatModel streamingChatModel) {

		this.chatModel = chatModel;
		this.streamingChatModel = streamingChatModel;
	}

	@Override
	public String completion(String message) {

		Prompt prompt = new Prompt(new UserMessage(message));

		return chatModel.call(prompt).getResult().getOutput().getContent();
	}

	@Override
	public Map<String, String> streamCompletion(String message) {

		StringBuilder fullContent = new StringBuilder();

		streamingChatModel.stream(new Prompt(message))
				.flatMap(chatResponse -> Flux.fromIterable(chatResponse.getResults()))
				.map(content -> content.getOutput().getContent())
				.doOnNext(fullContent::append)
				.last()
				.map(lastContent -> Map.of(message, fullContent.toString()))
				.block();

		logger.info(fullContent.toString());

		return Map.of(message, fullContent.toString());
	}

}

第六步,调用接口

启动我们的项目之后,我们可以调用这两个接口试一下效果

ai/example接口

image-20240920105741908

ai/strem接口

image-20240920113211205

也调通了,但是这个吐字有重叠是有问题的。这里我们查看源码发现线上的版本是注释掉了这个参数,所以吐字会返回原来已经回答过的内容,这里我们知道就好,不做详细研究

image-20240920113547714

至此,对话chat的示例我们就上手完成了

还有很多官方的示例,例如prompt模板(引导语言模型生成特定内容)、Function Call(使模型能调用外部函数)、文生图(使用给出的文字生成对应的图片)、向量数据库(高效存储和检索高维向量)等,但是这里就不一一介绍了,大家可以去官方的示例demo里面clone下来去看:官方示例

除此之外,demo里面也提供了一个前端文件,就在resources/static 目录下,使用浏览器打开 index.html 文件,输入问题,即可获得输出响应

image-20240920134853173

但是确保对得上你的后端端口:

image-20240920135312832

至此,Spring Cloud Alibaba AI 我们就体验完了,其实任何模型只要暴露api,我们就可以使用原生的java代码去调用他,但是往往很麻烦,因为我们需要考虑很多东西,而阿里则把他集成到了springboot中,也继承了springboot的优良特性,引入即使用。但是值得注意的是,这些都不是免费的,而是需要按量收费的。

在这里插入图片描述