把 OpenAI 的 ChatGPT 搬到 iOS 上

目錄

⦿ ChatGPT
⦿ OpenAI API key
⦿ Curl
⦿ Playground
⦿ Parameter
⦿ Xcode
⦿ 解析json
⦿ UI設定

ChatGPT

搭上 ML 模型的熱潮,前陣子研究了一下 Glicko,這幾天玩了一下大家都在討論的 ChatGPT,也從 彼得潘的 iOS App Neverland 那裡看到測試文章驚奇不斷,最後決定搬到手機上來體驗。

ChatGPT 是 OpenAI 開發的人工智慧聊天機器人的程式,使用 GPT-3 這個神經網路模型,GPT 是 Generative Pre-trained Transformer;OpenAI 創始人之一是 Elon Musk,那天,朋友 Peggy Tsai  看到馬斯克的新聞,問我是不是臉書的創始人,我想了想,他一定是在跟我開玩笑吧。

當你連上網站並註冊後,就可以開始提問了,把它想像成是雲端情人的 Samantha 的聲音(Scarlett Johansson),人生頓時光明了起來,事實上,在 iOS 中,我們也可用 AVFoundation、AVSpeechSynthesizer 來朗讀結果,但這篇文章沒有要做這樣的事。先看到下方網頁:

許多人測試時會希望 AI 做到現階段它還不能做到的事,而不是關注在它能做到的事,但模型是有條理的,瞭解它,會比有一天來不及瞭解它還來得好,其實在人與人之間也是這樣。

若希望把 ChatGPT 搬到 iOS 上,剛好 OpenAI 也開了 API 給我們,於是就可用 Restful API 的方式去呈現,先看結果吧:

看起來很醜,但很可愛吧?

繼續閱讀|回目錄

OpenAI API key

當你申請完帳號,就必須到這個網頁去 Create new secret key,如下:

Curl

接著,我們可以從這個網頁中,瞭解 curl 指令,如下:

curl https://api.openai.com/v1/completions \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-d '{
"model": "text-davinci-003",
"prompt": "Say this is a test",
"max_tokens": 7,
"temperature": 0
}'

可以看到它的 HTTP header 中,Content-Type 是 json,Authorization 的 Bearer 後面則要放入你剛才產生的 API key;而 HTTP body 中 data 帶了 model、prompt、max_tokens、temperature 等訊息,是 json 格式。

在你放入 API key 後,將這段 curl 指令複製起來,貼到 CLI 中,就會得到下面的回傳結果:

{
"id":"cmpl-6Khe2owgvY4i8TIaJZPK1tPHK9zAA",
"object":"text_completion",
"created":1670392350,
"model":"text-davinci-003",
"choices":[{"text":"\n\nThis is indeed a test",
"index":0,
"logprobs":null,
"finish_reason":"length"}],
"usage":{"prompt_tokens":5,
"completion_tokens":7,
"total_tokens":12}
}

我們看到 choices 這個 key 的值中的 text 的值為,兩個空行,This is indeed a test,這個就是我們要的回傳結果

等等要在 Xcode 中,用 JSONDecoder 去轉成我們要的資料。

Playground

我們也可到這個網頁使用它的 Playground,點擊 View code 後,同樣選擇 curl,結果如下:

我們看到對 https://api.openai.com/v1/completions request,有同樣的請求頭,不過夾帶的 data 跟前段比,多了一些參數。

Parameter

model

從 model 中,我們知道使用了 text-davinci-003,其實在這個網頁中,可以看到還有其他的模型,有興趣再自己更改參數。

max_tokens

max_tokens 關乎最後回應的字數,它的計算方式不是規律地依照字元,有興趣的可以到這個網頁去測試看看,如下:

不過不同的 model,有不同的 max_tokens,我們也可以連到這個網頁去看看,如下:

若用 token 來簡單判斷,text-davinci-003 算是裡面最厲害的 model 吧。

temperature、top_p

這兩個參數,官方建議不要都改變,只改變其中一個,他們關乎最後得到的答案是制式的答案,還是具創造性的,就像設定 AI 的性格一樣,在統計學上,像是樣本的信賴區間,改變值會改變答案的精確度。

frequency_penalty、presence_penalty

這兩個數值範圍在 -2.0 ~ 2.0 像是對模型的調教,影響 token 出現的頻率,這個 token 就是上面說的 token。

繼續閱讀|回目錄

Xcode

回到正題,我們在 Xcode 創建一個新的專案,建立一個新的 swift 檔,在裡面加入下面的程式碼:

struct ChatGPTAPIKey {
static let key = "你的 API key"
}

struct AIModel {
static let model = "text-davinci-003"
}

struct OpenAIBody: Encodable {
let model: String
let prompt: String
let temperature = 0.0
let max_tokens = 256
let top_p = 1.0
let frequency_penalty = 0.0
let presence_penalty = 0.0
}

在 ViewController 中,另外寫一個方法,再放到 viewDidLoad( ) 中呼叫,如下:

    private let urlString = "https://api.openai.com/v1/completions"  


override func viewDidLoad() {
super.viewDidLoad()
callChatGPTAPI()

}

private func callChatGPTAPI() {

let url = URL(string: urlString)!
var request = URLRequest(url: url)
request.setValue("application/json",
forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(ChatGPTAPIKey.key)",
forHTTPHeaderField: "Authorization")
let openAIBody = OpenAIBody(model: AIModel.model,
prompt: "How’s going?")
request.httpBody = try? JSONEncoder().encode(openAIBody)
request.httpMethod = "post"

URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data,
let content = String(data: data, encoding: .utf8) {

print(content)

}
}.resume()

}

在 request 中,把請求頭該有的東西放進去後,我們還要在 httpBody 裡放入 openAIBody,在前段中,我們知道要用 JSONEncoder(),httpMethod 是 “post”

接著將 request 放入 URLSession 中,記得 .resume(),印出轉成 string 的 data(即 content),如果你只印 data,會得到 data 的大小。

最後會得到這樣的結果:

{
"id":"cmpl-6Kj3NlV8nESELoTbWn7iSUWtp9hjO",
"object":"text_completion",
"created":1670397765,
"model":"text-davinci-003",
"choices":[{"text":"\n\nIt's going well, thank you. How about you?",
"index":0,
"logprobs":null,
"finish_reason":"stop"}],
"usage":{"prompt_tokens":6,
"completion_tokens":14,
"total_tokens":20}
}

前段 curl 的結果是不是一樣呢?那麼,剩下的就是我們熟悉的解析 json 了。

繼續閱讀|回目錄

解析json

我們再開一個 swift 檔案,加入下面程式碼:

struct JsonData: Codable {
let id: String
let object: String
let created: Int
let model: String
let choices: [ChoiceSet]
}

struct ChoiceSet: Codable {
let text: String
}

由於需要的只是 choices 裡的 text,所以盡可能簡化程式碼,最後再把下面的程式碼,放到剛剛的 URLSession 中:

do {
let data = try JSONDecoder().decode(JsonData.self, from: data)

DispatchQueue.main.async {
self.answerTextView.text = data.choices[0].text
}

} catch {
print(error.localizedDescription)
}

解析出 data 後,讓 textView 顯示,結果如下:

是不是很棒呢?

繼續閱讀|回目錄

UI設定

如果你的 UI 長這樣:

程式裡可能會有這樣的宣告與設定:

    @IBOutlet weak var questionTextView: UITextView!
@IBOutlet weak var answerTextView: UITextView!

private let urlString = "https://api.openai.com/v1/completions"
private let answerHint = "\n\n 讓我思考一下..."

@IBAction func clearBtnTapped(_ sender: UIButton) {
// 自己寫
}

@IBAction func sendBtnTapped(_ sender: UIButton) {
// 自己寫
}

按下清空按鈕的動作裡,兩個 textView 要清空,接著,通常還希望鍵盤會收回,於是會加上 view.endEditing(true)

而 answerTextView 不希望被編輯,所以在 Attributed Inspector 中取消 editable,如下:

// 點擊旁邊鍵盤收回
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
questionTextView.resignFirstResponder()
}

extension ViewController: UITextViewDelegate {
// 按下 return 後,鍵盤收回
func textView(_ textView: UITextView,
shouldChangeTextIn range: NSRange,
replacementText text: String) -> Bool {
if text == "\n" {
view.endEditing(false)
}
return true
}
}

再來,我們也希望 textView 輸入時,在按下 return 後,鍵盤會收回,就會遵循 UITextViewDelegate,利用 textView 的 function 來做到;如果希望按到旁邊時,鍵盤也會收回,就會覆寫 touchesEnded 這個 function。

回到 callChatGPTAPI(),這時候你已經不希望在 viewDidLoad() 中呼叫它,而是希望在按下送出按鈕時呼叫它。

但我們要改掉 OpenAIBody 的 prompt 為 text,即是你輸入的問題,如下:

// 新問題等待答案時的提示
DispatchQueue.main.async {
self.answerTextView.text = self.answerHint
}

// 把新問題放到 httpBody 裡
let text = questionTextView.text!
let openAIBody = OpenAIBody(model: AIModel.model,
prompt: text)

完成了!這次分享就到這,感謝您的閱讀。

繼續閱讀|回目錄

Reference:

--

--

Chun-Li 春麗
彼得潘的 Swift iOS / Flutter App 開發教室

Do not go gentle into that good night, Old age should burn and rave at close of day; Rage, rage, against the dying of the light.