このページではOpenAIのChat Completions APIについて解説します。OpenAIは新しいResponses APIに移行するつもりのようです(OpenAIのresponses APIを使う 参照)。しかし、Chat Completionsは他社のAPIもお手本にする基本的な方法なので、使ってみて損はしません。
まずこちらで登録してAPIキーを発行してもらいます。サブスクのChatGPTと異なり、料金は従量制で、百万トークンあたり何ドルという具合に課金されます。値段の比較はLLM API比較がわかりやすいと思います。
APIの概要はOpenAIの OpenAI developer platform からドキュメンテーション、APIレファレンスなどをご覧ください。APIで送られたデータは学習用に使われることはありません。不正使用の監視のために30日間保持され、特に問題なければ消去されるようです。
Pythonのパッケージは pip install openai
でインストールできます。
APIキーは、プログラムに直接書き込まず、環境変数に設定しておくのが安全・便利です。MacやLinuxでは、ターミナルに
export OPENAI_API_KEY="sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
と打ち込めば環境変数が設定されます。.bashrc
か .zshenv
等に書き込んでおけばシェル起動時に設定されます。APIキーを書き込んだファイルは他人に見られないようにパーミッションを正しく設定しておきましょう。環境変数が使えないときは python-dotenv が便利そうです。
使い方の基本は次の通りです(2023-11-07に大きく変わりました):
from openai import OpenAI client = OpenAI() # 環境変数からAPIキーを取得する場合 # もし環境変数が使えないなら次のようにする: # client = OpenAI(api_key="sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX") res = client.chat.completions.create( model="gpt-4.1", messages=[ {"role": "system", "content": "あなたは賢いAIです。"}, # 役割設定(省略可) {"role": "user", "content": "1たす1は?"} # 最初の質問 ], temperature=0 # 温度(0-2, デフォルト1) ) print(res.choices[0].message.content) # 答えが返る
Chat Completions APIは、質問・応答の履歴を記憶しません。以前の質問・応答を前提としたい場合は、次のように履歴を与えた上で質問をします:
res = client.chat.completions.create( model="gpt-4.1", messages=[ {"role": "system", "content": "あなたは賢いAIです。"}, # 役割設定(省略可) {"role": "user", "content": "1たす1は?"}, # 最初の質問 {"role": "assistant", "content": "2です。"}, # 最初の答え {"role": "user", "content": "それを3倍して。"} # 次の質問 ], temperature=0 ) print(res.choices[0].message.content) # 答えが返る
履歴と質問、答えを合わせたトークン数には上限(モデルによって違いますが、gpt-4.1
では100万トークン)があります。トークンはほぼ単語に相当するものです(後述)。トークン数を表示するには、次のようにします:
print(res.usage)
CompletionUsage(completion_tokens=8, prompt_tokens=27, total_tokens=35)
トークン上限を超えて会話を続けるには、不要な履歴を削除する必要があります。削除も含めて、簡単な会話を続けるには、例えば次のようにすればいいでしょう(通常はそんなに長い会話をしないでしょうけれど):
from openai import OpenAI client = OpenAI() msg = [{"role": "system", "content": "あなたは賢いAIです。"}] while True: prompt = input("> ").strip() if prompt in ["quit", "exit"]: break msg.append({"role": "user", "content": prompt}) res = client.chat.completions.create(model="gpt-4.1", messages=msg) ans = res.choices[0].message.content.strip() print(ans) msg.append({"role": "assistant", "content": ans}) if res.usage.total_tokens > 900000: msg.pop(1) msg.pop(1)
LLMは次のトークンの「確率分布」(確信度)を計算し、それに従って実際の次のトークンをランダムに選びます。ただしそれは temperature=1
あたりに設定したときで、temperature=0
にすればつねに最大確率のトークンが選ばれます。「確率分布」(実際には確率の対数logprobs)は次のようにして調べることができます。
from openai import OpenAI client = OpenAI() res = client.chat.completions.create( model="gpt-4.1", messages=[ { "role": "user", "content": "Write a one-sentence bedtime story about a unicorn." } ], logprobs=True, top_logprobs=3, temperature=0 ) print(res.choices[0].message.content)
Under a blanket of twinkling stars, a gentle unicorn tiptoed through a moonlit meadow, leaving trails of sparkling dreams for all the sleeping children.
for x in res.choices[0].logprobs.content[:5]: print(repr(x.token)) for t in x.top_logprobs: print(" ", repr(t.token), t.logprob)
'Under' 'Under' -0.18204644322395325 'As' -1.9320464134216309 'B' -4.682046413421631 ' a' ' a' -0.29004523158073425 ' the' -1.4150452613830566 'neath' -4.790045261383057 ' blanket' ' blanket' -1.3965630531311035 ' sky' -1.7715630531311035 ' silver' -1.7715630531311035 ' of' ' of' -6.704273118884885e-07 ' woven' -15.250000953674316 ' stitched' -15.375000953674316 ' tw' ' tw' -0.0457843616604805 ' stars' -4.2957844734191895 ' shimmering' -4.2957844734191895
Web 版 ChatGPT のように文字単位(単語単位)で出力するようにしてみましょう。モデル名として Gemini のモデル("gemini-1.5-pro"
など)や xAI のモデル("grok-2-latest"
など)や PLaMo のモデルを与えてもいいようにしました(それぞれ環境変数 GOOGLE_API_KEY
、XAI_API_KEY
、PLAMO_API_KEY
にAPIキーを入れておきます)。
import os from openai import OpenAI class Chatbot: def __init__(self, model="chatgpt-4o-latest", messages=None, temperature=0, stream=True): if "gemini" in model: self.client = OpenAI( api_key=os.getenv("GOOGLE_API_KEY"), base_url="https://generativelanguage.googleapis.com/v1beta/openai/" ) elif "grok" in model: self.client = OpenAI( api_key=os.getenv("XAI_API_KEY"), base_url="https://api.x.ai/v1" ) elif "plamo" in model: self.client = OpenAI( api_key=os.getenv("PLAMO_API_KEY"), base_url="https://platform.preferredai.jp/api/completion/v1", ) else: self.client = OpenAI() self.model = model self.messages = messages or [] self.temperature = temperature self.stream = stream def chat(self, prompt, temperature=None, stream=None, verbose=0, **kwargs): prompt = prompt.strip() if len(self.messages) > 0 and self.messages[-1]["role"] == "user": self.messages.pop() self.messages.append({"role": "user", "content": prompt}) stream = stream if stream is not None else self.stream temperature = temperature if temperature is not None else self.temperature try: res = self.client.chat.completions.create(model=self.model, messages=self.messages, temperature=temperature, stream=stream, **kwargs) except Exception as e: print("Error", e) return if stream: ans = "" for chunk in res: if chunk.choices and hasattr(chunk.choices[0].delta, "content"): content = chunk.choices[0].delta.content or "" else: content = "" print(content, end="") ans += content else: ans = res.choices[0].message.content.strip() print(ans) if verbose: u = res.usage print(u.prompt_tokens, u.completion_tokens, u.total_tokens) if verbose > 1: print(res) self.messages.append({"role": "assistant", "content": ans}) def get_messages(self): return self.messages
これを使うには例えば
chatbot = Chatbot() chatbot.chat("こんにちは!")
のようにします。会話を続けるには
chatbot.chat(""" ○○について説明してください。 """)
のようにします。コンテクストが溢れた場合は、とりあえず
chatbot = Chatbot(messages=chatbot.get_messages()[2:])
のようにして新しいチャットのインスタンスを作ってください。
上で作った Chatbot()
クラスの応用として、URLを与えてページを要約するアプリを作ってみましょう。ここでは Trafilatura というライブラリを使ってWebページのテキストを取り出しています。
import trafilatura URL = "https://....." text = trafilatura.extract(trafilatura.fetch_url(URL)) chatbot = Chatbot(model="gpt-3.5-turbo-16k") chatbot.chat(prompt="Provide a long and detailed summary of what follows:\n\n" + text)
トークンはほぼ単語に相当する概念で、gpt-3.5-turbo や gpt-4 では tiktoken の cl100k_base というエンコーディングが使われています(→ OpenAI 言語モデルごとのエンコーディング一覧)。頻出単語は1トークン、そうでない単語は2トークン以上に分割されます。日本語の場合は、2〜3文字が1トークンになることも、逆に1文字が2〜3トークンに分割されることもあります。平均してトークンあたり日本語0.9170文字程度です(OpenAI 言語モデルで日本語を扱う際のトークン数推定指標)。
pip install tiktoken
して試してみましょう:
import tiktoken enc = tiktoken.get_encoding("cl100k_base")
次の例で試してみましょう(山本義隆『熱学思想の史的展開』(現代数学社,1987年)より):
s = "「何人ものニュートンがいた(There were several Newtons)」と言ったのは,科学史家ハイルブロンである.同様にコーヘンは「ニュートンはつねに二つの貌を持っていた(Newton was always ambivalent)」と語っている."
e = enc.encode(s) for i in e: c = enc.decode([i]) if len(c) == 1 and ord(c) == 65533: # 65533は「�」 print(i, end="|") else: print(c, end="|") print()
次のように88トークンに分割されていることがわかります:
「|何|人|も|の|ニ|ュ|ート|ン|が|い|た|(|There| were| several| Newton|s|)|」|と|言|っ|た|の|は|,|科|学|5877|110|家|2845|237|イ|ル|ブ|ロ|ン|で|あ|る|.|同|162|100|246|に|コ|ー�|246|ン|は|「|ニ|ュ|ート|ン|は|つ|2243|255|に|二|つ|の|80631|234|を|持|って|い|た|(|Newton| was| always| amb|ivalent|)|」|と|45918|252|って|い|る|.|
英語はだいたい1語1トークンですが、直前のスペースも含めてトークンになっていることがけっこうあります。日本語はだいたい1文字1トークンですが、「様」のようにUTF-8の3バイトがそれぞれトークンになっている場合や、「ーヘ」の「ヘ」の最初の2バイトが「ー」とくっついて1トークンになっているような場合もあります。平均して10トークンが日本語9文字少々という感じです。
ちなみに、同じ文字列をOpenAIの Tokenizer に入れると、GPT-3では120トークンになります。
[2024-05-14追記] GPT-4o では新しいトークナイザで日本語のトークン数も減りました。tiktoken 0.7.0 以降で試せます:
enc = tiktoken.get_encoding("o200k_base") # または enc = tiktoken.encoding_for_model("gpt-4o")
上の例でトークン数は67です:
「|何|人|もの|ニュ|ート|ン|が|いた|(|There| were| several| Newton|s|)」|と言|った|の|は|,|科学|史|家|ハ|イル|ブ|ロン|で|ある|.|同|様|に|コ|ー�|246|ン|は|「|ニュ|ート|ン|は|つ|ね|に|二|つ|の|貌|を|持|って|いた|(|Newton| was| always| amb|ivalent|)」|と|語|って|いる|.|
「名無しさん」「転載は禁止」「VIPがお送りします」みたいなのがそれぞれ1トークンになるので、何を学習させたかの想像がついてしまいます。