保姆级JAVA对接ChatGPT教程,实现自己的AI对话助手

16,968 阅读8分钟

1.前言

大家好,我是王老狮,近期OpenAI开放了chatGPT的最新gpt-3.5-turbo模型,据介绍该模型是和当前官网使用的相同的模型,如果你还没体验过ChatGPT,那么今天就教大家如何打破网络壁垒,打造一个属于自己的智能助手把。本文包括API Key的申请以及网络代理的搭建,那么事不宜迟,我们现在开始。

2.对接流程

2.1.API-Key的获取

首先第一步要获取OpenAI接口的API Key,该Key是你用来调用接口的token,主要用于接口鉴权。获取该key首先要注册OpenAi的账号,具体可以见我的另外一篇文章,ChatGPT保姆级注册教程

  1. 打开platform.openai.com/网站,点击view API Key,

image.png

  1. 点击创建key

image.png

  1. 弹窗显示生成的key,记得把key复制,不然等会就找不到这个key了,只能重新创建。

image.png

将API Key保存好以备用

2.2.API用量的查看

这里可以查看API的使用情况,新账号注册默认有5美元的试用额度,之前都是18美元,API成本降了之后试用额度也狠狠地砍了一刀啊,哈哈。

image.png

2.3.核心代码实现

2.3.1.pom依赖

<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>
    <groupId>com.webtap</groupId>
    <artifactId>webtap</artifactId>
    <version>0.0.1</version>
    <packaging>jar</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.2.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>nz.net.ultraq.thymeleaf</groupId>
            <artifactId>thymeleaf-layout-dialect</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.4</version>
        </dependency>
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </dependency>
        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>1.9.2</version>
        </dependency>
        <!-- alibaba.fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.56</version>
        </dependency>
        <dependency>
            <groupId>net.sourceforge.nekohtml</groupId>
            <artifactId>nekohtml</artifactId>
            <version>1.9.22</version>
        </dependency>
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.4.1</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpasyncclient</artifactId>
            <version>4.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpcore-nio</artifactId>
            <version>4.3.2</version>
        </dependency>

        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.3.5</version>
            <exclusions>
                <exclusion>
                    <artifactId>commons-codec</artifactId>
                    <groupId>commons-codec</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>commons-httpclient</groupId>
            <artifactId>commons-httpclient</artifactId>
            <version>3.1</version>
            <exclusions>
                <exclusion>
                    <artifactId>commons-codec</artifactId>
                    <groupId>commons-codec</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.1</version>
        </dependency>
        <dependency>
            <groupId>com.github.ulisesbocchio</groupId>
            <artifactId>jasypt-spring-boot-starter</artifactId>
            <version>2.0.0</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

2.3.2.实体类ChatMessage.java

用于存放发送的消息信息,注解使用了lombok,如果没有使用lombok可以自动生成构造方法以及get和set方法

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessage {
    //消息角色
    String role;
    //消息内容
    String content;
}

2.3.3.实体类ChatCompletionRequest.java

用于发送的请求的参数实体类,参数释义如下:

model:选择使用的模型,如gpt-3.5-turbo

messages :发送的消息列表

temperature :温度,参数从0-2,越低表示越精准,越高表示越广发,回答的内容重复率越低

n :回复条数,一次对话回复的条数

stream :是否流式处理,就像ChatGPT一样的处理方式,会增量的发送信息。

max_tokens :生成的答案允许的最大token数

user :对话用户

@Data
@Builder
public class ChatCompletionRequest {

    String model;

    List<ChatMessage> messages;

    Double temperature;

    Integer n;

    Boolean stream;

    List<String> stop;

    Integer max_tokens;

    String user;
}

2.3.4.实体类ExecuteRet .java

用于接收请求返回的信息以及执行结果


/**
 * 调用返回
 */
public class ExecuteRet {

    /**
     * 操作是否成功
     */
    private final boolean success;

    /**
     * 返回的内容
     */
    private final String respStr;

    /**
     * 请求的地址
     */
    private final HttpMethod method;

    /**
     * statusCode
     */
    private final int statusCode;

    public ExecuteRet(booleansuccess, StringrespStr, HttpMethodmethod, intstatusCode) {
        this.success =success;
        this.respStr =respStr;
        this.method =method;
        this.statusCode =statusCode;
    }

    @Override
    public String toString() {
        return String.format("[success:%s,respStr:%s,statusCode:%s]", success, respStr, statusCode);
    }

    /**
     *@returnthe isSuccess
     */
    public boolean isSuccess() {
        return success;
    }

    /**
     *@returnthe !isSuccess
     */
    public boolean isNotSuccess() {
        return !success;
    }

    /**
     *@returnthe respStr
     */
    public String getRespStr() {
        return respStr;
    }

    /**
     *@returnthe statusCode
     */
    public int getStatusCode() {
        return statusCode;
    }

    /**
     *@returnthe method
     */
    public HttpMethod getMethod() {
        return method;
    }
}

2.3.5.实体类ChatCompletionChoice .java

用于接收ChatGPT返回的数据

@Data
public class ChatCompletionChoice {

    Integer index;

    ChatMessage message;

    String finishReason;
}

2.3.6.接口调用核心类OpenAiApi .java

使用httpclient用于进行api接口的调用,支持post和get方法请求。

url为配置文件open.ai.url的值,表示调用api的地址:https://api.openai.com/ ,token为获取的api-key。 执行post或者get方法时增加头部信息headers.put("Authorization", "Bearer " + token); 用于通过接口鉴权。


@Slf4j
@Component
public class OpenAiApi {

    @Value("${open.ai.url}")
    private String url;
    @Value("${open.ai.token}")
    private String token;

    private static final MultiThreadedHttpConnectionManagerCONNECTION_MANAGER= new MultiThreadedHttpConnectionManager();

    static {
        // 默认单个host最大链接数
CONNECTION_MANAGER.getParams().setDefaultMaxConnectionsPerHost(
                Integer.valueOf(20));
        // 最大总连接数,默认20
CONNECTION_MANAGER.getParams()
                .setMaxTotalConnections(20);
        // 连接超时时间
CONNECTION_MANAGER.getParams()
                .setConnectionTimeout(60000);
        // 读取超时时间
CONNECTION_MANAGER.getParams().setSoTimeout(60000);
    }

    public ExecuteRet get(Stringpath, Map<String, String> headers) {
        GetMethod method = new GetMethod(url +path);
        if (headers== null) {
            headers = new HashMap<>();
        }
        headers.put("Authorization", "Bearer " + token);
        for (Map.Entry<String, String> h : headers.entrySet()) {
            method.setRequestHeader(h.getKey(), h.getValue());
        }
        return execute(method);
    }

    public ExecuteRet post(Stringpath, Stringjson, Map<String, String> headers) {
        try {
            PostMethod method = new PostMethod(url +path);
            //log.info("POST Url is {} ", url + path);
            // 输出传入参数
log.info(String.format("POST JSON HttpMethod's Params = %s",json));
            StringRequestEntity entity = new StringRequestEntity(json, "application/json", "UTF-8");
            method.setRequestEntity(entity);
            if (headers== null) {
                headers = new HashMap<>();
            }
            headers.put("Authorization", "Bearer " + token);
            for (Map.Entry<String, String> h : headers.entrySet()) {
                method.setRequestHeader(h.getKey(), h.getValue());
            }
            return execute(method);
        } catch (UnsupportedEncodingExceptionex) {
log.error(ex.getMessage(),ex);
        }
        return new ExecuteRet(false, "", null, -1);
    }

    public ExecuteRet execute(HttpMethodmethod) {
        HttpClient client = new HttpClient(CONNECTION_MANAGER);
        int statusCode = -1;
        String respStr = null;
        boolean isSuccess = false;
        try {
            client.getParams().setParameter(HttpMethodParams.HTTP_CONTENT_CHARSET, "UTF8");
            statusCode = client.executeMethod(method);
method.getRequestHeaders();

            // log.info("执行结果statusCode = " + statusCode);
            InputStreamReader inputStreamReader = new InputStreamReader(method.getResponseBodyAsStream(), "UTF-8");
            BufferedReader reader = new BufferedReader(inputStreamReader);
            StringBuilder stringBuffer = new StringBuilder(100);
            String str;
            while ((str = reader.readLine()) != null) {
log.debug("逐行读取String = " + str);
                stringBuffer.append(str.trim());
            }
            respStr = stringBuffer.toString();
            if (respStr != null) {
             log.info(String.format("执行结果String = %s, Length = %d", respStr, respStr.length()));
                          } 
            inputStreamReader.close();
            reader.close();
            // 返回200,接口调用成功
            isSuccess = (statusCode == HttpStatus.SC_OK);
        } catch (IOExceptionex) {
        } finally {
method.releaseConnection();
        }
        return new ExecuteRet(isSuccess, respStr,method, statusCode);
    }

}

2.3.7.定义接口常量类PathConstant.class

用于维护支持的api接口列表

public class PathConstant {
    public static class MODEL {
				//获取模型列表
        public static String MODEL_LIST = "/v1/models";
    }

    public static class COMPLETIONS {
        public static String CREATE_COMPLETION = "/v1/completions";
				//创建对话
        public static String CREATE_CHAT_COMPLETION = "/v1/chat/completions";
      
    }
}

2.3.8.接口调用调试单元测试类OpenAiApplicationTests.class

核心代码都已经准备完毕,接下来写个单元测试测试下接口调用情况。


@SpringBootTest
@RunWith(SpringRunner.class)
public class OpenAiApplicationTests {

    @Autowired
    private OpenAiApi openAiApi;
    @Test
    public void createChatCompletion2() {
        Scanner in = new Scanner(System.in);
        String input = in.next();
        ChatMessage systemMessage = new ChatMessage('user', input);
        messages.add(systemMessage);
        ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
                .model("gpt-3.5-turbo-0301")
                .messages(messages)
                .user("testing")
                .max_tokens(500)
                .temperature(1.0)
                .build();
        ExecuteRet executeRet = openAiApi.post(PathConstant.COMPLETIONS.CREATE_CHAT_COMPLETION, JSONObject.toJSONString(chatCompletionRequest),
                null);
        JSONObject result = JSONObject.parseObject(executeRet.getRespStr());
        List<ChatCompletionChoice> choices = result.getJSONArray("choices").toJavaList(ChatCompletionChoice.class);
        System.out.println(choices.get(0).getMessage().getContent());
        ChatMessage context = new ChatMessage(choices.get(0).getMessage().getRole(), choices.get(0).getMessage().getContent());
        System.out.println(context.getContent());
    }

}
  • 使用Scanner 用于控制台输入信息,如果单元测试时控制台不能输入,那么进入IDEA的安装目录,修改以下文件。增加最后一行增加-Deditable.java.test.console=true即可。

image.png image.png

  • 创建ChatMessage对象,用于存放参数,role有user,system,assistant,一般接口返回的响应为assistant角色,我们一般使用user就好。

  • 定义请求参数ChatCompletionRequest,这里我们使用3.1日发布的最新模型gpt-3.5-turbo-0301。具体都有哪些模型大家可以调用v1/model接口查看支持的模型。

  • 之后调用openAiApi.post进行接口的请求,并将请求结果转为JSON对象。取其中的choices字段转为ChatCompletionChoice对象,该对象是存放api返回的具体信息。

    接口返回信息格式如下:

    {
        "id": "chatcmpl-6rNPw1hqm5xMVMsyf6PXClRHtNQAI",
        "object": "chat.completion",
        "created": 1678179420,
        "model": "gpt-3.5-turbo-0301",
        "usage": {
            "prompt_tokens": 16,
            "completion_tokens": 339,
            "total_tokens": 355
        },
        "choices": [{
            "message": {
                "role": "assistant",
                "content": "\n\nI. 介绍数字孪生的概念和背景\n    A. 数字孪生的定义和意义\n    B. 数字孪生的发展历程\n    C. 数字孪生在现代工业的应用\n\nII. 数字孪生的构建方法\n    A. 数字孪生的数据采集和处理\n    B. 数字孪生的建模和仿真\n    C. 数字孪生的验证和测试\n\nIII. 数字孪生的应用领域和案例分析\n    A. 制造业领域中的数字孪生应用\n    B. 建筑和城市领域中的数字孪生应用\n    C. 医疗和健康领域中的数字孪生应用\n\nIV. 数字孪生的挑战和发展趋势\n    A. 数字孪生的技术挑战\n    B. 数字孪生的实践难点\n    C. 数字孪生的未来发展趋势\n\nV. 结论和展望\n    A. 总结数字孪生的意义和价值\n    B. 展望数字孪生的未来发展趋势和研究方向"
            },
            "finish_reason": "stop",
            "index": 0
        }]
    }
    
  • 输出对应的信息。

2.3.9.结果演示

image.png

2.4.连续对话实现

2.4.1连续对话的功能实现

基本接口调通之后,发现一次会话之后,没有返回完,输入继续又重新发起了新的会话。那么那么我们该如何实现联系上下文呢?其实只要做一些简单地改动,将每次对话的信息都保存到一个消息列表中,这样问答就支持上下文了,代码如下:

List<ChatMessage> messages = new ArrayList<>();
@Test
public void createChatCompletion() {
    Scanner in = new Scanner(System.in);
    String input = in.next();
    while (!"exit".equals(input)) {
        ChatMessage systemMessage = new ChatMessage(ChatMessageRole.USER.value(), input);
        messages.add(systemMessage);
        ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
                .model("gpt-3.5-turbo-0301")
                .messages(messages)
                .user("testing")
                .max_tokens(500)
                .temperature(1.0)
                .build();
        ExecuteRet executeRet = openAiApi.post(PathConstant.COMPLETIONS.CREATE_CHAT_COMPLETION, JSONObject.toJSONString(chatCompletionRequest),
                null);
        JSONObject result = JSONObject.parseObject(executeRet.getRespStr());
        List<ChatCompletionChoice> choices = result.getJSONArray("choices").toJavaList(ChatCompletionChoice.class);
        System.out.println(choices.get(0).getMessage().getContent());
        ChatMessage context = new ChatMessage(choices.get(0).getMessage().getRole(), choices.get(0).getMessage().getContent());
        messages.add(context);
        in = new Scanner(System.in);
        input = in.next();
    }
}

因为OpenAi的/v1/chat/completions接口消息参数是个list,这个是用来保存我们的上下文的,因此我们只要将每次对话的内容用list进行保存即可。

2.4.2结果如下:

image.png

image.png

4.常见问题

4.1.OpenAi接口调用不通

因为https://api.openai.com/地址也被限制了,但是接口没有对地区做校验,因此可以自己搭建一个香港代理,也可以走科学上网。

我采用的是香港代理的模式,一劳永逸,具体代理配置流程如下:

  1. 购买一台香港的虚拟机,反正以后都会用得到,作为开发者建议搞一个。搞活动的时候新人很便宜,基本3年的才200块钱。
  2. 访问nginx.org/download/ng… 下载最新版nginx
  3. 部署nginx并修改/nginx/config/nginx.conf文件,配置接口代理路径如下
server {
        listen       19999;
        server_name  ai;

         ssl_certificate      /usr/local/nginx/ssl/server.crt;
        ssl_certificate_key  /usr/local/nginx/ssl/server.key;

        ssl_session_cache    shared:SSL:1m;
        ssl_session_timeout  5m;

        ssl_ciphers  HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers  on;

        #charset koi8-r;

        location /v1/ {
                proxy_pass <https://api.openai.com>;
        }
    }
  1. 启动nginx
  2. 将接口访问地址改为nginx的机器出口IP+端口即可

如果代理配置大家还不了解,可以留下评论我单独出一期教程。

4.2.接口返回401

检查请求方法是否增加token字段以及key是否正确

5.总结

至此JAVA对OpenAI对接就已经完成了,并且也支持连续对话,大家可以在此基础上不断地完善和桥接到web服务,定制自己的ChatGPT助手了。我自己也搭建了个平台,不断地在完善中,具体可见下图,后续会开源出来,想要体验的可以私信我获取地址和账号哈

image.png

本文正在参加「金石计划」