探索Spring生态下的AI接口编程,来看看它能做什么

Spring AI项目旨在让开发含AI功能的应用变得更简单,去除不必要的复杂环节。它从知名的Python项目(如LangChain和LlamaIndex)中学习了经验,但并非简单复制,而是有自己独特之处。我们相信,未来的AI应用不会仅限于Python开发者,而是会广泛支持多种编程语言。 Spring AI的核心目标是解决AI集成的关键问题——顺畅地将企业数据、API与AI模型连接起来,让这一切变得更加容易实现。

刚接触springAI的时候,它还不支持国内大模型,只能文字生成,既没有chatMemory也没有向量支持,现如今稳定版即将发布,该有的都有了,文生图,读图,文生语音,语音转文字,function等,比起langchain4j,api用起来舒服些,关键它是spring出品的。

官网文档:https://docs.spring.io/spring-ai/reference/getting-started.html
在这里插入图片描述

1、引入相关包

1.1 指定版本

听说这个月会发布稳定的1.0版本,敬请期待,目前最新的是快照版,1.0.0-M2好多功能还没有。jdk版本需要17+

<properties>
        <java.version>17</java.version>
        <spring-ai.version>1.0.0-SNAPSHOT</spring-ai.version>
<!--        <spring-ai.version>1.0.0-M2</spring-ai.version>-->
    </properties>

1.2 引入bom

Spring AI材料清单(BOM)声明了给定Spring AI发行版所使用的所有依赖项的推荐版本。使用来自应用程序构建脚本的BOM避免了您自己指定和维护依赖版本的需要。

<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>${spring-ai.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
</dependencyManagement>

1.3 引入自动配置模块

引入对应大模型的依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Spring AI Open AI 自动配置模块 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
        </dependency>
</dependencies>

如果是百度千帆

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-qianfan-spring-boot-starter</artifactId>
</dependency>

如果是智普

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-zhipuai-spring-boot-starter</artifactId>
</dependency>

1.4 配置仓库

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

1.5 在application.properties配置key

server.port=8080

#Open AI的配置
spring.ai.openai.base-url=your model url
spring.ai.openai.api-key=your key
spring.ai.openai.chat.options.model=gpt-3.5-turbo

# 调用api接口失败后的重试次数
spring.ai.retry.max-attempts=1

#spring.ai.openai.image.options.model=dall-e-3

# 打印详细日志
logging.level.org.springframework.ai.chat.client.advisor=DEBUG

2、hello world

ChatModel是一个接口,每个大模型都有它的实现类,open AI对应的是OpenAiChatModel。spring boot会帮我们自动实例化它,直接使用即可。

//    @Autowired
//    private OpenAiChatModel chatModel;

    @Autowired
    private ChatModel chatModel;
    
    @GetMapping("/hello")
    public String helloWorld(@RequestParam("message") String message) {
        return chatModel.call(message);
    }

在浏览器输入:http://localhost:8080/hello?message=你是谁

浏览器显示如下

我是一个AI智能助手,可以帮助您回答问题和提供信息。您有什么需要我帮忙的吗?

3、chatClient

直接使用chatModle来编写复杂的业务是繁琐的, ChatClient提供了一个流畅的API来与AI模型进行通信 。它封装了chatModel,chatClient实例使用ChatClient.Builder来获取,我们使用chatClient来改写上面的代码

@RestController
public class HelloWorld {

    private final ChatClient chatClient1;

    public HelloWorld(ChatClient.Builder builder) {
        this.chatClient1 = builder.build();
    }

    @GetMapping("/hello")
    public String helloWorld(@RequestParam("message") String message) {
        return chatClient1.prompt().user(message).call().content();
    }
}

好像还变复杂了,对吧

流式输出,就是文字一个一个的输出,提升等待体验

    @GetMapping("/test3")
    public Flux<String> test3(@RequestParam("message") String message) {
        return chatClient1.prompt().user(message).stream().content();
    }

3.1 设置系统提示词

chatModel有4类消息,系统消息(配置模型能力),用户消息,AI回复消息,工具扩展消息(调用本地方法)

关于这些基础知识参考另一篇文章:https://blog.csdn.net/dan_seek/article/details/140755726

或者官网:https://docs.spring.io/spring-ai/reference/concepts.html

使用bean方式来实例化chatClient

@Configuration
public class Config {
	@Bean
    ChatClient chatClient2(ChatClient.Builder builder){
        return builder
                // 指定系统提示词
                .defaultSystem("你是位资深诗人").build();
    }
}

控制器

    private final ChatClient chatClient2;    
	public HelloWorld(ChatClient chatClient2) {
        this.chatClient2 = chatClient2;
    }	
	@GetMapping("/test1")
    public String test1(@RequestParam("message") String message) {

        return chatClient2.prompt().user(message).call().content();
    }

浏览器输入:http://localhost:8080/test1?message=写首歌颂小草的短诗

浏览器显示如下

小小的草儿绿叶茂,生长在田野间小道。 风吹过,摇曳舞,自由自在展翅飞。 细腻的叶片婀娜姿,静静地守护大地。 无声无息默默劳,润泽万物生生不息。 小草啊,你是大自然的守护神, 你的美丽与坚强,让人心生敬仰。

使用占位符传参

如果系统提示词是动态的,可以通过传参的方式

    @Bean
    ChatClient chatClient3(ChatClient.Builder builder){
        return builder.defaultSystem("请模仿{name}的口吻回答问题,并告诉用户你的名字").build();
    }

控制器

	@Autowired
    private ChatClient chatClient3;

	@GetMapping("/test2")
    public String test2(@RequestParam("message") String message) {
        String name = "庄子";

        return chatClient3.prompt()
                // 设置系统提示词参数
                .system(sp -> sp.param("name", name))
                .user(message).call().content();
    }

浏览器输入:http://localhost:8080/test2?message=你的梦想是什么

浏览器显示如下

吾乃庄周也。梦境乃人生之一部分,吾之梦想乃与天地相融,如风如云自在游荡,无拘无束,无忧无虑,与万物共舞共鸣。愿世人亦能放下烦恼,追寻内心最深处的宁静与自由。

3.2 记住历史会话

上面的对话中,你告诉大模型你的名字,然后马上问大模型我是谁,大模型是不知道的。因为大模型是无状态的,想让大模型知道上下文,你需要把历史记录一并发给大模型,借助chatMemory很容易实现。

chatMemory有一个默认的实现类,也可以自己实现一个保存至数据库的实现

	@Bean
    public ChatMemory chatMemory() {
        return new InMemoryChatMemory();
    }
    @Bean
    ChatClient chatClient4(ChatClient.Builder builder, ChatMemory chatMemory){
        return builder.defaultSystem("你是一位友好的助手")
                .defaultAdvisors(
//                        new PromptChatMemoryAdvisor(chatMemory) // 历史记录添加到系统文本中
                         new MessageChatMemoryAdvisor(chatMemory) // 历史记录添加到消息列表中
                         )
                .build();
    }

控制器

    @Autowired
    private ChatClient chatClient4;

    @GetMapping("/test4")
    public String test4(@RequestParam("message") String message) {
        // memory id 用于区分会话
        String chatId = "123456";

        return chatClient4.prompt()
                .user(message)
                .advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)
                        // 设置记忆的会话长度 10条会话
                        .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10))
                .call().content();
    }

3.3 输出详细日志

如果想要查看响应体里的数据结构,需要如下配置,稍作修改上面的chatClient

    @Bean
    ChatClient chatClient4(ChatClient.Builder builder, ChatMemory chatMemory){
        return builder.defaultSystem("你是一位作家")
                .defaultAdvisors(
                         new MessageChatMemoryAdvisor(chatMemory) // 历史记录添加到消息列表中
                        // 启用日志,需要在配置文件配置 logging.level.org.springframework.ai.chat.client.advisor=DEBUG
                        ,new SimpleLoggerAdvisor(
                                // 自定义日志输出
//                                request -> "Custom request: " + request.userText(),
//                                response -> "Custom response: " + response.getResult()
                        )
                         )
                .build();
    }

控制台日志如下

request: AdvisedRequest[chatModel=OpenAiChatModel [defaultOptions=OpenAiChatOptions: {“streamUsage”:false,“model”:“gpt-3.5-turbo”,“temperature”:0.7}], userText=我是小飞, systemText=你是一位作家, chatOptions=OpenAiChatOptions: {“streamUsage”:false,“model”:“gpt-3.5-turbo”,“temperature”:0.7}, media=[], functionNames=[], functionCallbacks=[], messages=[], userParams={}, systemParams={}, advisors=[org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor@27d1c903, SimpleLoggerAdvisor], advisorParams={chat_memory_response_size=2, chat_memory_conversation_id=123456}]
2024-09-12T00:26:33.525+08:00 DEBUG 560 — [ai-spring] [nio-8080-exec-1] o.s.a.c.c.advisor.SimpleLoggerAdvisor : response: {“result”:{“output”:{“messageType”:“ASSISTANT”,“metadata”:{“refusal”:“”,“finishReason”:“STOP”,“index”:0,“id”:“chatcmpl-A6KSmRO8aLFYHBemBI5Am78dfg7oG”,“role”:“ASSISTANT”,“messageType”:“ASSISTANT”},“toolCalls”:[],“content”:“你好,小飞!很高兴认识你。你喜欢写作吗?”},“metadata”:{“finishReason”:“STOP”,“contentFilterMetadata”:null}},“results”:[{“output”:{“messageType”:“ASSISTANT”,“metadata”:{“refusal”:“”,“finishReason”:“STOP”,“index”:0,“id”:“chatcmpl-A6KSmRO8aLFYHBemBI5Am78dfg7oG”,“role”:“ASSISTANT”,“messageType”:“ASSISTANT”},“toolCalls”:[],“content”:“你好,小飞!很高兴认识你。你喜欢写作吗?”},“metadata”:{“finishReason”:“STOP”,“contentFilterMetadata”:null}}],“metadata”:{“id”:“chatcmpl-A6KSmRO8aLFYHBemBI5Am78dfg7oG”,“model”:“gpt-3.5-turbo-0125”,“rateLimit”:{“requestsLimit”:5000,“requestsRemaining”:4999,“tokensLimit”:2000000,“tokensRemaining”:1999973,“requestsReset”:0.012000000,“tokensReset”:0.0},“usage”:{“generationTokens”:27,“promptTokens”:22,“totalTokens”:49},“promptMetadata”:[],“empty”:false}}

4、生成图片

可以在application.properties全局配置模型的相关参数,也可以在调用的时候指定

	@Autowired
    ImageModel imageModel;

//    @Autowired
//    OpenAiImageModel imageModel2;

    @GetMapping("/testImage")
    public String test1() {

        ImageResponse response = imageModel.call(
                new ImagePrompt("给spring ai框架生成一个涂鸦" )
        );

        // 自定义图片参数
//        ImageOptions options = OpenAiImageOptions.builder()
//                .withModel("dall-e-3")
//                .withQuality("hd")
//                // 图片数量
//                .withN(1)
//                // 图片高度 Must be one of 256, 512, or 1024
//                .withHeight(512)
//                .withWidth(512)
//                .build();
//
//        ImageResponse response = imageModel.call(
//                new ImagePrompt("给spring ai框架生成一个涂鸦", options )
//        );

        String url =response.getResult().getOutput().getUrl();
        System.out.println(url);
        return url;

    }

5、生成向量

创建知识库时需要把文字转为向量存储到向量数据库中

    @Autowired
    EmbeddingModel embeddingModel;

    @GetMapping("/testEmbed")
    public String test1() {
        float[] f = embeddingModel.embed("hello world");
        System.out.println(Arrays.toString(f));

        return Arrays.toString(f);
    }

浏览器显示如下

[-0.014903838, 0.0013317588, -0.018488139, -0.031072723, -0.024273094, 0.0075046294, -0.023034403, -0.0010262037, -0.012795426, -0.022441411, 0.025801694, 0.010884678, -0.033075716, -0.0037193708, 0.0058178995, 0.013849632, 0.01941057, -0.022863094, 0.01836954, 0.011326127, -0.0061605168, …

6、大模型调用私有API

通过Function Calling API可以让大模型调用本地的接口,比如问大模型苹果的库存是多少,这个时候就需要大模型调用企业内部接口查询。目前这个功能还不完善,这个API还不支持智普和百度千帆

6.1 定义给大模型调用的服务

固定写法,照着写即可,Request和Response的参数根据实际情况增减。@JsonClassDescription(“获取天气信息”) 注解是给大模型看的,需要描述清楚这个方法是做什么的;@JsonPropertyDescription是描述参数的用法,有时不写也可以,大模型根据上下文能猜对,但是下面的例子如果不写,request.location的值有时是中文有时是拼音

import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;

import java.util.function.Function;

public class MockWeatherService implements Function<MockWeatherService.Request, MockWeatherService.Response> {

    /**
     * 温度
     */
    public enum Unit {

        /**
         *  摄氏度
         */
        C("摄氏度"),
        /**
         * 华氏度
         */
        F("华氏度");

        /**
         * 人类可读的单位名称
         */
        public final String unitName;

        private Unit(String text) {
            this.unitName = text;
        }

    }
    @JsonClassDescription("获取天气信息") // 工具描述
    public record Request(@JsonProperty(required = true,
            value = "location") @JsonPropertyDescription("城市名,如北京") String location
                          ) {}
    public record Response(double temp, Unit unit) {}

    public Response apply(Request request) {
        System.out.println("---request.location:" + request.location);
        // 调用天气API,只是模拟
        if (request.location.contains("北京")){
            return new Response(32.0, Unit.C);
        } else if (request.location.contains("长沙")) {
            return new Response(38.0, Unit.F);
        }
        return new Response(25.0, Unit.C);
    }
}

6.2 定义function

使用bean注入,bean的名字(weatherFunction1)就是函数名

    // 天气工具类
    @Bean
//    @Description("获取天气信息") // 工具的描述,与MockWeatherService.Request的描述二选一
    public Function<MockWeatherService.Request, MockWeatherService.Response> weatherFunction1() { // bean的名字就是function的名字
        return new MockWeatherService();
    }

也可以使用包装类定义function

// 包裹的天气工具类
    @Bean
    public FunctionCallback weatherFunctionInfo() {

        return FunctionCallbackWrapper.builder(new MockWeatherService())
                .withName("currentWeatherByWrap") // (1) function 名字,区分大小写
                .withDescription("获取天气信息") // (2) function 描述
                .build();
    }
}

6.3 模型调用函数

使用大模型调用内部定义的函数,有下面几种写法

6.3.1 chatModel
    @Autowired
    private ChatModel chatModel;

    @GetMapping("/tool1")
    public String tool1(@RequestParam("message") String message) {
        message = message.isEmpty() ? "北京和长沙的天气?" : message;
        UserMessage userMessage = new UserMessage(message);

        Prompt prompt = new Prompt(List.of(userMessage),
                OpenAiChatOptions.builder().withFunction("weatherFunction1").build());
        // chatMobile--function用法
        ChatResponse response = chatModel.call(prompt);

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

    }

在浏览器输入:http://localhost:8080/tool1?message=长沙的天气

浏览器显示如下

长沙现在的温度是38°F。


在浏览器输入:http://localhost:8080/tool1?message=长沙和北京的天气

浏览器显示如下

长沙的天气温度为38°F,北京的天气温度为32°C。

控制台会打印(说明大模型调用了两次MockWeatherService接口)

—request.location:长沙
—request.location:北京


在浏览器输入:http://localhost:8080/tool1?message=敦贝煌的天气

浏览器显示如下(故意写错地名,大模型也能识别)

敦煌目前的气温是25摄氏度。

6.3.2 chatClient

当业务复杂时,chatClient的优势就体现出来了

    private final ChatClient chatClient;

    public ToolController(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }   

	@GetMapping("/tool2")
    public String tool2(@RequestParam("message") String message) {

        // chatClient--function的用法
        String result = chatClient.prompt()
                .functions("weatherFunction1").user(message).call().content();

        log.info("result: {}", result);
        return result;

    }



    /**
     * FunctionCallbackWrapper.builder创建的function
     * @param message user message
     * @return
     */

    @GetMapping("/tool4")
    public String tool4(@RequestParam("message") String message) {

        // chatClient--function的用法
        String result = chatClient.prompt()
                // currentWeatherByWrap在Config.java中定义
                .functions("currentWeatherByWrap").user(message).call().content();

        log.info("result: {}", result);
        return result;

    }
6.3.3 使用包装类直接定义function

function不事先定义也可以,不过写法有点绕

/**
     * 使用包装类FunctionCallbackWrapper直接定义function
     * @param message user message
     * @return
     */
    @GetMapping("/tool3")
    public String tool3(@RequestParam("message") String message) {
        UserMessage userMessage = new UserMessage(message);

        var promptOptions = OpenAiChatOptions.builder()
//                .withModel(OpenAiApi.ChatModel.GPT_4_O.getValue()) // 指定模型
                .withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
                        .withName("getCurrentWeather") // function的名字,可随意取名
                        .withDescription("获取城市的天气") // function的描述
//                        .withResponseConverter((response) -> "" + response.temp() + response.unit())
                        .build()))
                .build();

        // chatModel--function用法
        ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions));
        return response.getResult().getOutput().getContent();

//        // chatClient--function的用法
//        return chatClient.prompt(new Prompt(userMessage, promptOptions)).call().content();

    }

参考代码:https://github.com/spring-projects/spring-ai/tree/main/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值