保姆級JAVA對接ChatGPT教程,實現自己的AI對話助手

語言: CN / TW / HK

1.前言

大家好,我是王老獅,近期OpenAI開放了chatGPT的最新gpt-3.5-turbo模型,據介紹該模型是和當前官網使用的相同的模型,如果你還沒體驗過ChatGPT,那麼今天就教大家如何打破網絡壁壘,打造一個屬於自己的智能助手把。本文包括API Key的申請以及網絡代理的搭建,那麼事不宜遲,我們現在開始。

2.對接流程

2.1.API-Key的獲取

首先第一步要獲取OpenAI接口的API Key,該Key是你用來調用接口的token,主要用於接口鑑權。獲取該key首先要註冊OpenAi的賬號,具體可以見我的另外一篇文章,ChatGPT保姆級註冊教程

  1. 打開http://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依賴

``` 4.0.0 com.webtap webtap 0.0.1 jar

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

```

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的地址:http://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 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接口調用不通

因為http://api.openai.com/地址也被限制了,但是接口沒有對地區做校驗,因此可以自己搭建一個香港代理,也可以走科學上網。

我採用的是香港代理的模式,一勞永逸,具體代理配置流程如下:

  1. 購買一台香港的虛擬機,反正以後都會用得到,作為開發者建議搞一個。搞活動的時候新人很便宜,基本3年的才200塊錢。
  2. 訪問http://nginx.org/download/nginx-1.23.3.tar.gz 下載最新版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 <http://api.openai.com>;
    }
}

```

  1. 啟動nginx
  2. 將接口訪問地址改為nginx的機器出口IP+端口即可

如果代理配置大家還不瞭解,可以留下評論我單獨出一期教程。

4.2.接口返回401

檢查請求方法是否增加token字段以及key是否正確

5.總結

至此JAVA對OpenAI對接就已經完成了,並且也支持連續對話,大家可以在此基礎上不斷地完善和橋接到web服務,定製自己的ChatGPT助手了。我自己也搭建了個平台,不斷地在完善中,具體可見下圖,後續會開源出來,想要體驗的可以私信我獲取地址和賬號哈

image.png

本文正在參加「金石計劃」