从零构建一个SpringBoot大模型调用应用——复盘

目录

[1. 创建OpenAiService](#1. 创建OpenAiService)

1.1客户端构造器

[1.1.1 baseUrl,apiKey,prefix](#1.1.1 baseUrl,apiKey,prefix)

[1.1.2 修改配置](#1.1.2 修改配置)

[1.2 创建客户端](#1.2 创建客户端)

[1.3 包装客户端](#1.3 包装客户端)

[1.4 创建代理](#1.4 创建代理)

[2. 调用OpenAiService](#2. 调用OpenAiService)

[2.1 包装request](#2.1 包装request)

[2.2 message](#2.2 message)

注意:

[1. Arrays.asList(message),为什么要封装成一个列表呢?](#1. Arrays.asList(message),为什么要封装成一个列表呢?)

[2. 缺陷与进阶:messages(Arrays.asList(message))](#2. 缺陷与进阶:messages(Arrays.asList(message)))

[2.3 调用以及接受响应](#2.3 调用以及接受响应)

[3. 技术架构](#3. 技术架构)


简单实现思路:

后端创建一个可以调用大模型的客户端,后端将请求数据给客户端,客户端发起请求,接收到响应之后,再将大模型的返回数据给后端处理

需要的工具:SDK(充当应用与 AI 服务之间的"集成桥接"层,能帮我们省去很多事情,如抽象化底层的 API 复杂性)

当我们选择好SDK后,就可以开始写了!


1. 创建OpenAiService

1.1客户端构造器

通过SDK中的OpenAiService的方法去创建一个客户端构造器

此处之所以要创建客户端构造器,也是为了方便我们去修改一些配置,能够构造出适配的满足个人应用需求以及解决一些请求问题等等的客户端,如果没有使用new Builder,则构造出来的就是默认Client

简而言之就是使获得改造后的能够满足需求的专属客户端

1.1.1 baseUrl,apiKey,prefix

prefix:/compatible-mode

引入原因

那么这里我们的问题是,我们采用的是千问的模型,要写调用API的请求的话,可以使用阿里的调用协议,但是各家有家的,使用起来都多少有差别,兼容性不好,有没有更好的办法呢?

有的有的,我们一般采用的都是OpenAI的协议,那么想使用OpenAI协议也能调用到千问的模型的话,阿里提供了**'** /compatible-mode' 前缀,++这个前缀的作用是让阿里云的服务器充当"翻译官",将请求从 OpenAI 格式转换为阿里内部能识别的格式++

baseUrlhttps://dashscope.aliyuncs.com/ ,这个是你++请求的AI厂家的地址++

apiKey :给AI服务发起请求的时候需要的一个++身份认证++

1.1.2 修改配置

通过

复制代码
OkHttpClient.Builder clientBuilder = OpenAiService
        .defaultClient(apiKey, Duration.ofSeconds(60))
        .newBuilder();

获得一个客户端构造器,Duration.ofSeconds(60)是等待时间,如果超过这个时间大模型还没有响应,后端就会断开,防止过长时间的等待

我们在客户端构造器中增加一个Interceptor,负责拦截请求,把prefix加到请求当中去

复制代码
clientBuilder.addInterceptor(new Interceptor() {
    @Override
    public Response intercept(Chain chain) throws IOException {
        //从拦截器链中拿到请求
        Request oringal = chain.request();
        //拿到url,再去修改
        HttpUrl oringalUrl = oringal.url();
        String newPath = prefix + oringalUrl.encodedPath();

        HttpUrl newUrl = oringalUrl.newBuilder()
                .encodedPath(newPath)
                .build();

        Request newRequest = oringal.newBuilder()
                .url(newUrl)
                .build();

        return chain.proceed(newRequest);

    }
});

其中chain是拦截器链,在请求发送的过程中会有一系列的拦截器对请求进行拦截操作

思考:为什么public Response intercept(Chain chain) 返回的类型是Response?

public Response intercept(Chain chain) 返回的类型是Response,即大模型返回的结果也会经过一个个拦截器返回回去,当请求到达下一个拦截器或发给大模型的时候,该程序会在return chain.proceed(newRequest);处进行等待,直到返回结果或断开连接,才算执行完这个拦截器的方法

1.2 创建客户端

这里就是很简单直接的用刚刚构建好的客户端构造器,直接构建客户端就行了

复制代码
OkHttpClient client = clientBuilder.build();

1.3 包装客户端

一个非常重要的工具出现了,retrofit

使用原因:

在没有 retrofit 之前,你要调用大模型,可能需要手动写一堆代码去拼接 URL、设置请求头、把 Java 对象转成 JSON 字符串、发请求,然后再把返回的 JSON 字符串手动解析成 Java 对象

client的主要功能是发送请求,接收响应,但是它并不知道里面有啥东西,retrofit相当于一个组装车间,将client 和 OpenAiService.defaultObjectMapper()给retrofit之后,就能实现++通过定义一个接口,就能在后台自动帮你完成所有的网络请求拼接、JSON 转换等工作++

复制代码
Retrofit retrofit = OpenAiService
        .defaultRetrofit(client, OpenAiService.defaultObjectMapper())
        .newBuilder()
        .baseUrl(url)
        .build();

1.4 创建代理

OpenAiApi是一个接口,接口无法创建实例,利用retrofit可以创建一个代理对象,然后将其包装成OpenAiService供后端使用

++当使用OpenAiService去请求AI服务的时候,代理就会自动触发retrofit去拦截请求,进行打包,再交给client去发送请求给AI服务++

复制代码
OpenAiApi api = retrofit.create(OpenAiApi.class);
return new OpenAiService(api);

以下为配置类完整代码

java 复制代码
import com.theokanning.openai.client.OpenAiApi;
import com.theokanning.openai.service.OpenAiService;
import okhttp3.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import retrofit2.Retrofit;

import java.io.IOException;
import java.time.Duration;

//客户端的相关配置
@Configuration
public class AIConfig {

    //从配置文件读取apikay,baseUrl,prefix

    @Value("${ai.api-key}")
    private String apiKey;

    @Value("${ai.base-url}")
    private String baseUrl;

    @Value("${ai.prefix}")
    private String pathPrefix;

    //创建客户端工厂
    @Bean
    public OpenAiService openAiService(){

        String url = baseUrl;
        if (baseUrl != null && !baseUrl.isEmpty()){
            url = baseUrl;
            if (!baseUrl.endsWith("/")){
                url = baseUrl + "/";
            }
        }

        //OpenAiService已经在内部底层帮我们实现类ApiKey认证拦截器等等,
        //如果使用new OkHttpClient.Builder(),则需要自己实现认证拦截器
        OkHttpClient.Builder clientBuilder = OpenAiService
                .defaultClient(apiKey, Duration.ofSeconds(60))
                .newBuilder();

        //拦截器
        final String prefix =(pathPrefix != null && !pathPrefix.isEmpty()
                ? pathPrefix : "");

        if(!prefix.isEmpty()){

            clientBuilder.addInterceptor(new Interceptor() {
                @Override
                public Response intercept(Chain chain) throws IOException {
                    //从拦截器链中拿到请求
                    Request oringal = chain.request();
                    //拿到url,再去修改
                    HttpUrl oringalUrl = oringal.url();
                    String newPath = prefix + oringalUrl.encodedPath();

                    HttpUrl newUrl = oringalUrl.newBuilder()
                            .encodedPath(newPath)
                            .build();

                    Request newRequest = oringal.newBuilder()
                            .url(newUrl)
                            .build();

                    return chain.proceed(newRequest);

                }
            });
        }
        //创建客户端
        OkHttpClient client = clientBuilder.build();

        //需要retrofit做json转换
        Retrofit retrofit = OpenAiService
                .defaultRetrofit(client, OpenAiService.defaultObjectMapper())
                .newBuilder()
                .baseUrl(url)
                .build();

        //创建代理
        OpenAiApi api = retrofit.create(OpenAiApi.class);
        return new OpenAiService(api);
    }
}

2. 调用OpenAiService

2.1 包装request

通过

复制代码
openAiService.createChatCompletion(request)

给大模型发送请求前,我们需要包装一下request 的内容,同样也是利用请求构造器,

其中要给你选择的模型(model

还有要发送给大模型的信息(message

**maxTokens(1024)**是请求和响应所能接受的最大token,

temperature(0.7) 是在想大模型提问时,希望大模型的创意度或者准确度占比多少的设定,++0~1之间,越小越追求数据准确,越大越会发挥创意++

复制代码
ChatCompletionRequest request = ChatCompletionRequest.builder()
        .model(model)
        .messages(Arrays.asList(message))
        .maxTokens(1024)
        .temperature(0.7)
        .build();

2.2 message

在包装请求过程中,涉及到message,

复制代码
ChatMessage message = new ChatMessage("user", prompt);

其中ChatMessage message = new ChatMessage("user", prompt);的 "user",指的是角色,不同角色可以让大模型有不同的回复方式

user:用户角色(真实使用用户提的问题)

system:系统角色(为大模型给设定)

assistant:AI助手(这个应该是可以辅助实现多轮对话的,因为初学还没实践验证过)

而prompt就是给大模型的提问。


注意:

复制代码
ChatCompletionRequest request = ChatCompletionRequest.builder()
        .model(model)
        .messages(Arrays.asList(message))
        .maxTokens(1024)
        .temperature(0.7)
        .build();
1. ++Arrays.asList(message),为什么要封装成一个列表呢?++

大模型本身其实就是要实现对轮对话的,所以肯定需要知道上下文的内容

虽然我们这是单独发一个message对象过去,但是规定下就是需要一个消息列表,方便存储上下文的信息,实现多轮对话。

如果在messages()中直接放进一个message,就会报错

2. 缺陷与进阶:++messages(Arrays.asList(message))++

现在的写法 Arrays.asList(userMessage) 在当前的场景下是完全没问题的,因为大模型 SDK 只需要"读取"这个列表,不需要往里面"增加"或"删除"消息。

问题 :Arrays.asList() 返回的是一个固定大小的列表 (不支持 addremove 操作)。如果未来业务逻辑中,需要在发送前动态往这个列表里追加消息(比如 list.add(anotherMessage)),直接用它就会报错。

我询问之后,目前给出可能的真实场景应用代码(这里可以理解一下,之后学深入会学到应该):

复制代码
// 1. 从数据库或内存中,把历史对话列表拿出来
List<ChatMessage> historyMessages = getHistoryFromDB(userId);

// 2. 把用户当前输入的新消息,追加到这个已有的列表中
historyMessages.add(new ChatMessage("user", "用户的新问题"));

// 3. 把这个包含了完整上下文的列表,传给大模型
ChatCompletionRequest request = ChatCompletionRequest.builder()
        .model("qwen-turbo")
        .messages(historyMessages)  // 传入的是积累了多轮的列表
        .build();

2.3 调用以及接受响应

复制代码
try {
    StringBuilder sb = new StringBuilder();
    openAiService.createChatCompletion(request)
            .getChoices()
            .forEach(choice -> sb.append(choice.getMessage().getContent()));

    log.info("AI回答返回成功");
    return sb.toString();
} catch (Exception e) {
    log.error("调用大模型失败", e);
    throw new RuntimeException("调用AI服务失败: " + e.getMessage(), e);
}

代码解释:

.getChoices()

在配置请求时,有一个++参数叫 n(代表让大模型生成几个回答),没有设置,默认是 1++

大模型 API 的设计规范总是返回一个列表**(List<Choice>)** ,所以我们需要通过 **getChoices()**把这个列表拿出来

.forEach(choice -> sb.append(choice.getMessage().getContent()));

之后遍历这个列表,把大模型生成的文字提取出来,并拼接到StringBuilder result 中

以下是完整Service代码

java 复制代码
import com.theokanning.openai.completion.chat.ChatCompletionRequest;
import com.theokanning.openai.completion.chat.ChatMessage;
import com.theokanning.openai.service.OpenAiService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.List;

@Slf4j
@Service
public class AIService {

    @Autowired
    private OpenAiService openAiService;

    @Value("${ai.model}")
    private String model;

    public String request() {

        String question = "介绍一下张元英";
        String prompt = "你是资深韩娱专家,请回答问题:" + question;
        ChatMessage message = new ChatMessage("user", prompt);

        ChatCompletionRequest request = ChatCompletionRequest.builder()
                .model(model)
                .messages(Arrays.asList(message))
                .maxTokens(1024)
                .temperature(0.7)
                .build();

        log.info("准备调用大模型, model={}, prompt={}", model, prompt);

        try {
            StringBuilder sb = new StringBuilder();
            openAiService.createChatCompletion(request)
                    .getChoices()
                    .forEach(choice -> sb.append(choice.getMessage().getContent()));

            log.info("AI回答返回成功");
            return sb.toString();
        } catch (Exception e) {
            log.error("调用大模型失败", e);
            throw new RuntimeException("调用AI服务失败: " + e.getMessage(), e);
        }
    }
}

3. 技术架构

整个基本调用AI服务的流程

浏览器 → Controller(/ai/request) → Service(构建prompt+发请求) → Config(OkHttp+Retrofit+拦截器) → DashScope API


小白努力中,谢谢阅读!如有错误欢迎指出