iOSアプリにサクッとOpenAIのチャット機能を埋め込む方法

2023.02.28

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

ChatGPTでお馴染みOpenAIのAIチャットの機能をiOSアプリに埋め込みする方法を調べました。

OpenAI導入を助けてくれるライブラリを出している方がいたおかげで、とても簡単にサクッと埋め込むことが出来ました。

環境

  • Xcode 14.2
  • iOS 16.2
  • OpenAISwift 1.1.1

OpenAISwiftを追加

今回はOpenAIの導入を助けてくれるライブラリOpenAISwiftを使用します。

File > Add Packagesで検索窓に下記を入力します。

https://github.com/adamrushy/OpenAISwift

表示された OpenAISwiftを追加します。

APIキーを取得

OpenAIのAPIを使用するにはAPIキーが必要です。

APIキーを取得する為には、OpenAIのサイトに登録する必要があります。登録後、ユーザーアカウントメニューのView API Keysで表示されるページでAPIキーの発行を行えます。

AIの会話の回答を取得

あとは、OpenAISwiftを呼び出して、質問したい文字列を渡すだけです。

import OpenAISwift

// ~中略

func generatedAnswer(from prompt: String) async throws -> String {
    let openAI = OpenAISwift(authToken: "authtoken0123456789")
    let result = try await openAI.sendCompletion(with: prompt)

    return result.choices.first?.text ?? ""
}

上記のコードだけでAIチャットの回答を取得出来ます。

openAI.sendCompletion

sendCompletionメソッドは、async awaitにも対応しており、今回はそちらを使用してみました。

使用モデルはデフォルトで.gpt3(.davinci)になっており、トークン限度は16トークンになっています。

public func sendCompletion(with prompt: String, model: OpenAIModelType = .gpt3(.davinci), maxTokens: Int = 16) async throws -> OpenAI {
    return try await withCheckedThrowingContinuation { continuation in
        sendCompletion(with: prompt, model: model, maxTokens: maxTokens) { result in
            continuation.resume(with: result)
        }
    }
}

使用モデル

現在使用できるGPT-3のモデルは下記の4点になります。

最新モデル 説明 最大リクエスト 訓練データ
text-davinci-003 最も有能な GPT-3 モデル。多くの場合、より高い品質、より長い出力、およびより優れた指示に従うことで、他のモデルが実行できるタスクを実行できます。テキスト内への補完の挿入もサポートしています。 4,000トークン 2021年6月まで
text-curie-001 非常に有能ですが、Davinci よりも高速で低コストです。 2,048トークン 2019年10月まで
text-babbage-001 簡単なタスク、非常に高速、低コストが可能です。 2,048トークン 2019年10月まで
text-ada-001 非常に単純なタスクが可能で、通常は GPT-3 シリーズで最速のモデルであり、低コストです。 2,048トークン 2019年10月まで

トークンとは

ChatGPTにおけるトークンは、テキストデータを意味的に意味のある単位に分割したものです。例えば、英語の文章をトークン単位に分割する場合、スペース、句読点、単語などがトークンになります。

サンプルアプリ

TextFieldに文字を入力して、送信ボタンを押すとAIの回答が反映されるアプリです。

AIChatDemo

コード

import SwiftUI
import OpenAISwift

struct ContentView: View {

    @State private var inputText = ""
    @State private var generateAnswerState: GenerateAnswerState = .none

    private let openAI = OpenAISwift(authToken: "authtoken0123456789")

    var body: some View {
        VStack {

            Spacer()

            Text(generateAnswerState.resultText)

            Spacer()

            HStack {
                TextField(text: $inputText) {
                    Text("会話したい内容を入力してください")
                }
                .disabled(generateAnswerState.isRequesting)

                // Generate Text Action
                Button {

                    Task {
                        do {
                            generateAnswerState = .requesting

                            let result = try await openAI.sendCompletion(with: inputText,
                                                                         maxTokens: 100)
                            inputText = ""

                            let generatedText = result.choices.first?.text ?? ""
                            generateAnswerState = .success(text: generatedText)
                        } catch {
                            print("Do some error handling")
                            generateAnswerState = .error(message: "Error")
                        }
                    }

                } label: {
                    Image(systemName: "paperplane")
                }
                .disabled(inputText.isEmpty)
                .disabled(generateAnswerState.isRequesting)
            }
        }
        .padding()
        .overlay {
            if case .requesting = generateAnswerState {
                ProgressView()
            }
        }
    }
}

enum GenerateAnswerState {
    case none
    case success(text: String)
    case error(message: String)
    case requesting

    var isRequesting: Bool {
        if case .requesting = self {
            return true
        }
        return false
    }

    var resultText: String {
        switch self {
        case .none, .requesting:
            return ""
        case let .success(text), let .error(text):
            return text
        }
    }
}

不可解な点

今回は、OpenAIの出力されるトークン数を100に設定しています。

let result = try await openAI.sendCompletion(with: inputText,
                                             maxTokens: 100)

しかし、出力された結果を見てみると、明らかにトークンが少ないように感じます。

はじめまして、私は環です。大学在学中で経済学を専攻しており、元々は金融業界での経験があります。その時のノウハウを活かして投資

日本語は英語の時よりもトークン数が多くなる

OpenAIのAPI利用の料金を解説!文章生成AIと画像生成AIの費用を説明の記事の中に日本語のトークン数について記載がありました。

OpenAIによるとトークンとは文字数ではなく、1単語と説明されています。

英語の場合、1単語を1トークンとして、カンマ(,)やピリオド(.)、クエスチョンマーク(?)も1トークンとして扱うため分かりやすいです。

「What is interesting sport?」のトークン数は5になります。

しかし、日本語の場合、トークンの計算が英語よりも複雑になっています。

パソコンの歴史:9トークン

機械学習とは:11トークン

スクレイピングでどんなことができる?:22トークン

十八番とは?:11トークン

おはことは?:9トークン

英語の場合の単語単位というルールに沿っておらず、同じ文字数でもトークン数が異なります。

どうやら漢字が含まれると1文字あたりのトークン数が増えてしまうようです。

入力テキストがどれぐらいのトークンか知りたい場合は、OpenAIのPlaygroundで確認できます。

どうやら漢字が含まれると1文字あたりのトークン数が増えてしまうようです。

実際にPlaygroundでトークン数を確認してみると、100トークンになっていました。

GPT-3のAPIの無料枠は1ヶ月あたり約12万トークンなので、日本語出力だと早く到達しそうですね、、。

まとめ

  • サクッとOpenAIの機能を導入できる
  • 英語と日本語ではトークンの計算方法が変わる
  • 日本語だと文字数に対してトークンが多めになる

おわりに

日本語だと無料枠のみでの運用は難しそうな為、英語で出力させて翻訳する方法で節約するとか、使用量を制限するとか、AIに解答をできるだけシンプルにするように吹き込むとか対策必要そうだなと感じました。

しかし、便利な機能なので適材適所で使っていきたいと思いました。

参考