今天要來實作一下現在話題最高的科技項目-ChatGPT
我記得這股AI潮好像是從大概今年1月多開始的,但其實從2021年左右的時候,OpenAI這家公司就發表了這款AI軟件
而其實呢,我自己也都在思考。老實說我認識到這個ChatGPT也只是在今年1月的時候知道的,並沒有比社會大眾早多少
而也在這時,我的YouTube不知道是因為演算法的緣故還是真的太多人出片
我的YouTube也瞬間被OpenAI還有各種AI模型應用洗版....
然後人類繼續討論AI會不會取代人類,這屁話我在大學期間都不知道聽過幾次了= =
好啦,回歸正題。由於AI很夯的緣故,我也去搜尋了一下個人使用者能不能去串接他的API...答案很意外地是肯定的
OpenAI他們公司有開放所有個人使用者去有限度地使用他們的引擎,多虧他們佛心我們這種小小的開發者也可以秀一手AI串接技術(笑)
好,那廢話就別再繼續說了,讓我們開始吧
來看看功能
然後Github
->點我
1. 前言
首先,雖然說是串連ChatGPT的API,但其實說穿了這只是一篇很普通的okHttp3應用實作(笑)
也就是說,本質上我只是去串接OpenAI提供的API而已,其實並沒有什麼大不了的,跟「人工智慧」甚至沒啥屁關係(大笑
而這次的實作,基本上我不想做什麼聊天室、甚至上下文對話等等
我這次想做的就是很簡單的一問一答,就這樣
總之,廢話到此為止。開始實作囉!
2. 取得ChatGPT金鑰&使用Postman測試
首先,我們要先去ChatGPT官網取得金鑰
網址在這
->https://platform.openai.com/
首先,這是官網。進去後,沒有註冊的先註冊註冊
有註冊的應該就會是跟我一樣的登入狀態
再來請點選View API keys(補充,這個View API keys手機版沒有)
再來點選「API keys」,然後點「+Create new secrent key」
最後,這段碼就是你的金鑰囉!
而基本上這串金鑰只會出現這一次,之後就再也無法用任何方法在看到這串金鑰。請特別注意囉!
OK,取得金鑰後,接著要來在Postman上面做測試
Postman的部分我就先預設大家都有,就不再提如何安裝
不會的話問ChatGPT
首先,ChatGPT的基本URL為
->https://api.openai.com/v1/completions
總之先在URL欄裡將發送方式設為POST,然後填入
再來點選Headers,在key的地方打上Authorization,value的部分請這樣寫
Bearer key
假設你的金鑰是oooo08570857的話,那你就要這樣打
「Bearer oooo08570857」,Bearer跟oooo08570857之間記得要空白
最後在點選Body,輸入以下的JSON字串
{ "model": "text-davinci-003", "prompt": "如何使用okHttp3", "temperature": 0.5, "max_tokens": 1000, "top_p": 1, "frequency_penalty": 0, "presence_penalty": 0 }
最後點Send就可以成功發送囉!
另外,他回傳的時間並不是很即時,也需要大概2秒的時間去算。因此按下送出後也要稍微等一下哦!
回傳長這樣,供參考↓
最後,附上ChatGPT告訴我的每個參數的意義(笑)
-
"model": 指定使用的模型,這裡是 "text-davinci-003",它是 OpenAI 最先進和最強大的模型之一。
-
"prompt": 是模型要生成的文字提示,即模型需要回答的問題或開始的話題。
-
"temperature": 控制模型生成的多樣性和隨機性。值越高,生成的結果越隨機。這裡的溫度為 0.3,意味著生成的結果相對保守,但仍可能有一些意想不到的答案。
-
"max_tokens": 生成的最大文本數量,即模型生成的回答或文章的最大長度。這裡設置為 1000 個標記(tokens)。
-
"top_p": 用於過濾生成的詞彙,僅保留最有可能出現在答案中的詞。這裡設置為 1,即不進行過濾。
-
"frequency_penalty": 設置生成詞彙頻率的懲罰程度,以避免生成的內容重複或不夠多樣化。這裡設置為 0,即不進行懲罰。
-
"presence_penalty": 設置生成詞彙存在的懲罰程度,以避免生成的內容出現太多相似的詞彙。這裡設置為 0,即不進行懲罰。
3. 寫入權限與載入需要的庫
OK,如果你成功送出並取得回傳的話,恭喜你,你已經完成一半了
接下來就是將這個成果套進去Android中,僅此而已
首先,我們要先在Android權限中寫入網路權限
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:usesCleartextTraffic="true" android:theme="@style/Theme.ChatGPTExample" tools:targetApi="31"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <meta-data android:name="android.app.lib_name" android:value="" /> </activity> </application> </manifest>
再來打開build.gradle(Moudle)
加入
plugins { id 'com.android.application' } android { namespace 'com.noahliu.chatgptexample' compileSdk 33 defaultConfig { applicationId "com.noahliu.chatgptexample" minSdk 28 targetSdk 32 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.8.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' implementation 'com.squareup.okhttp3:okhttp:4.7.2' implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.7.2' implementation 'com.squareup.okhttp3:logging-interceptor:4.7.2' implementation 'com.google.code.gson:gson:2.8.6' }
大致上,以上的函式庫就是okHttp3跟GSON的函式庫而已,我也沒有在加其他的新東西
到這邊就完成基礎設置了,接下來是寫介面跟做實體類
4. 畫介面與撰寫實體類
首先我展示一下我創建好的所有檔案
首先是實體類,由於本人真的很懶,因此我介紹一個線上工具
->https://json2csharp.com/code-converters/json-to-pojo
這個線上工具很方便,他可以自動幫你把Response的json格式自動轉換成Java的程式
在這邊我就不示範操作,我只貼上我轉出來的結果
public class ChatGPTRespond{ public String id; public String object; public int created; public String model; public ArrayList<Choice> choices; public Usage usage; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getObject() { return object; } public void setObject(String object) { this.object = object; } public int getCreated() { return created; } public void setCreated(int created) { this.created = created; } public String getModel() { return model; } public void setModel(String model) { this.model = model; } public ArrayList<Choice> getChoices() { return choices; } public void setChoices(ArrayList<Choice> choices) { this.choices = choices; } public Usage getUsage() { return usage; } public void setUsage(Usage usage) { this.usage = usage; } public class Choice{ public String text; public int index; public Object logprobs; public String finish_reason; public String getText() { return text; } public void setText(String text) { this.text = text; } public int getIndex() { return index; } public void setIndex(int index) { this.index = index; } public Object getLogprobs() { return logprobs; } public void setLogprobs(Object logprobs) { this.logprobs = logprobs; } public String getFinish_reason() { return finish_reason; } public void setFinish_reason(String finish_reason) { this.finish_reason = finish_reason; } } public class Usage{ public int prompt_tokens; public int completion_tokens; public int total_tokens; public int getPrompt_tokens() { return prompt_tokens; } public void setPrompt_tokens(int prompt_tokens) { this.prompt_tokens = prompt_tokens; } public int getCompletion_tokens() { return completion_tokens; } public void setCompletion_tokens(int completion_tokens) { this.completion_tokens = completion_tokens; } public int getTotal_tokens() { return total_tokens; } public void setTotal_tokens(int total_tokens) { this.total_tokens = total_tokens; } } }
再來是介面
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_percent="0.88" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline2" android:layout_width="0dp" android:layout_height="0dp" android:orientation="vertical" app:layout_constraintGuide_percent="0.75" /> <EditText android:id="@+id/edittext_Input" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="4dp" android:ems="10" android:text="如何安裝Android studio" android:hint="如何交到女朋友" android:inputType="textPersonName" android:minHeight="48dp" app:layout_constraintEnd_toStartOf="@+id/guideline2" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/guideline" /> <Button android:id="@+id/button_Send" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Send" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@+id/guideline2" app:layout_constraintTop_toTopOf="@+id/guideline" /> <TextView android:id="@+id/textView_Title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:text="您的問題:" android:textAppearance="@style/TextAppearance.AppCompat.Large" android:textStyle="bold" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_percent="0.2" /> <ScrollView android:layout_width="0dp" android:layout_height="0dp" android:padding="8dp" app:layout_constraintBottom_toTopOf="@+id/guideline" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/guideline3"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:id="@+id/textView_Title2" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="ChatGPT回答:" android:textAppearance="@style/TextAppearance.AppCompat.Large" android:textStyle="bold" /> <TextView android:id="@+id/textView_Answer" android:layout_width="match_parent" android:layout_height="match_parent" android:text="TextView" /> </LinearLayout> </ScrollView> <TextView android:id="@+id/textView_Question" android:layout_width="0dp" android:layout_height="0dp" android:text="TextView" app:layout_constraintBottom_toTopOf="@+id/guideline3" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@+id/textView_Title" app:layout_constraintTop_toBottomOf="@+id/textView_Title" /> </androidx.constraintlayout.widget.ConstraintLayout>
5. 撰寫功能
最後終於來到重點了(累...
首先我先來架一下Http請求的主幹程式
public class HttpRequest { public void sendPOST(String url, RequestBody requestBody,OnCallback callback){ /**建立連線*/ OkHttpClient client = new OkHttpClient() .newBuilder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(20, TimeUnit.SECONDS) .addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC)) .build(); /**設置傳送需求*/ Request request = new Request.Builder() .url(url) .header("Authorization", "Bearer "+MainActivity.YOUR_KEY) .post(requestBody) .build(); /**設置回傳*/ Call call = client.newCall(request); call.enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { callback.onFailCall(e.getMessage()); } @Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { String res = response.body().string(); callback.onOKCall(res); } }); } interface OnCallback{ void onOKCall(String respond); void onFailCall(String error); } }
這裡就是一個很經典的okHttp3的寫法
而由於ChatGPT他的回傳時間通常比較久,因此兩個timeout的時間我拉得比較長
最後在先前的header的部分,則在粉底白字的部分加入
另外,我的程式並沒有提供key讓你用。因此key的部分請照我上面的步驟自己去申請哦!
最後就是主程式的部分
public class MainActivity extends AppCompatActivity { public static final String YOUR_KEY = ""; public static final String URL = "https://api.openai.com/v1/completions"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView tvAnswer = findViewById(R.id.textView_Answer); ((Button)findViewById(R.id.button_Send)).setOnClickListener(view -> { String question = ((EditText)findViewById(R.id.edittext_Input)).getText().toString(); if (question.isEmpty()) return; ((TextView)findViewById(R.id.textView_Question)).setText(question); tvAnswer.setText("請稍候.."); //設置Header中的Content-Type MediaType mediaType = MediaType.parse("application/json"); //寫入body String content = new Gson().toJson(makeRequest(question)); RequestBody requestBody = RequestBody.create(mediaType, content); //發送請求 new HttpRequest().sendPOST(URL, requestBody, new HttpRequest.OnCallback() { @Override public void onOKCall(String respond) { Log.d("TAG", "onOKCall: "+respond); ChatGPTRespond chatGPTRespond = new Gson().fromJson(respond,ChatGPTRespond.class); runOnUiThread(()->{ tvAnswer.setText(chatGPTRespond.getChoices().get(0).getText()); }); } @Override public void onFailCall(String error) { Log.e("TAG", "onFailCall: "+error); tvAnswer.setText(error); } }); }); } //寫入body private WeakHashMap<String,Object> makeRequest(String input){ WeakHashMap<String,Object> weakHashMap = new WeakHashMap<>(); weakHashMap.put("model","text-davinci-003"); weakHashMap.put("prompt",input); weakHashMap.put("temperature",0.5); weakHashMap.put("max_tokens",1000); weakHashMap.put("top_p",1); weakHashMap.put("frequency_penalty",0); weakHashMap.put("presence_penalty",0); return weakHashMap; } }
比較需要注意的點只有這個MediaType的部分。這個MediaType呢他相當於就是取代掉原本必須在header裡面加的content-Type
再來就是由於直接用字串寫body太不直觀,因此我在紫底的部分寫Json body,讓request更易於辨識
做到這邊按下執行應該可以順利跑出來囉!希望大家跟著我做都有成功!
最後..