Azure OpenAIのgpt-35-turboモデルで過去履歴を含めたトークン数を数える方法を調べてみた

Azure OpenAIのモデルにおいてトークン数は料金やリクエストの制限に関わることなので重要な要素です。

APIからの回答の中に使用したトークン数が記載されていますが、それ以外の方法でトークン数を数えたい場合もあります。

今回は私がよく利用しているgpt-35-turboモデルのトークン数の数え方について調査したのでこの記事で共有したいと思います。

この記事の対象者

この記事は、「基本的なトークンの数え方はわかったが、gpt-35-turboモデルでどのように扱えばいいか分からない」という方向けに書きました。

基本的な方法は以下の記事を参考にしてください。

blog.jbs.co.jp

環境

  • python 3.10.9
  • tiktoken 0.4.0
  • Microsoft.DeepDev.TokenizerLib 1.3.0

なぜトークン数を数える必要があるのか?

私の考えでは以下の2通りの場合が考えられます。

  • 送信する前にpromptトークン数を知りたい場合
    Azure OpenAIでどのモデルを使うにしても、リクエストを送る際にトークン数の制限があります。この制限を超えてリクエストを送るとエラーになってしまうため、事前にトークン数を計算しておく必要がある場合は多いと思います。

  • streamをtrueにする場合
    以前ブログにも書きましたが、streamをtrueにするとリクエストの回答からトークン数を取得することができません。トークン数を取得するには自前で計算する実装が必要があります。

参考記事:

blog.jbs.co.jp

自分がつまずいた点:Promptはどのように数える?

gpt-35-turboモデルの場合、Promptには以下のようにシステムメッセージや過去の履歴を含めることが多いと思います。

{
    "messages":
        [
            {"role": "system", "content": "あなたは優秀なアシスタントです。"},
            {"role": "user", "content": "こんにちは!"},
            {"role": "assistant", "content": "こんにちは。何か質問はありますか?"},
            {"role": "user", "content": "Azureについて教えてください。"}
        ]
}

この場合にプロンプトのトークン数を数えるためにはどこを対象にすればいいのでしょうか?

  • contentの中身
  • roleとcontentの中身
  • 全て

色々試しましたが、どれもAzrure OpenAIから返ってくる回答の消費トークン数とは一致しませんでした。

OpenAIのドキュメントを確認したところ、以下の場所に数え方の記載がありました。

platform.openai.com

ドキュメントによると、各contentの中身に加えて固定でトークン数を足す処理が必要みたいですね。

Completionは?

こちらは返ってきた回答のcontentの中身を利用すれば大丈夫でした。

実際に試してみた

上記のドキュメントを参考にトークン数を数えてみましょう。また、APIからの回答と比べて正確性も確認します。

Pythonの場合

トークン数を数えるためのライブラリは、ドキュメント通りにtiktokenを使います。
以下のコードによってtiktokenを使って計算したトークン数と、Azure OpenAIのモデルが実際に消費したトークン数を比べることができます。
(Promptはドキュメント通り、Completionは返ってきた回答のcontentの中身を計算しています)

import openai
import tiktoken

openai.api_type = "azure"
openai.api_base = "[Azure OpenAIのエンドポイント]" 
openai.api_version = "2023-05-15"
openai.api_key = "[Azure OpenAIのキー]"
deployment = "[デプロイ名]"

messages = [
    {"role": "system", "content": "あなたは優秀なアシスタントです。"},
    {"role": "user", "content": "こんにちは!"},
    {"role": "assistant", "content": "こんにちは。何か質問はありますか?"},
    {"role": "user", "content": "Azureについて教えてください。"}
]


response = openai.ChatCompletion.create(
    deployment_id=deployment,
    messages=messages,
)

print(f'実際に利用したPromptのトークン数:{response["usage"]["prompt_tokens"]}')
print(f'実際に利用したCompletionのトークン数:{response["usage"]["completion_tokens"]}')

def num_tokens_from_messages(messages, model):
  try:
      encoding = tiktoken.encoding_for_model(model)
  except KeyError:
      encoding = tiktoken.get_encoding("cl100k_base")
  if model == "gpt-3.5-turbo":
      num_tokens = 0
      for message in messages:
          num_tokens += 4
          for key, value in message.items():
              num_tokens += len(encoding.encode(value))
              if key == "name":
                  num_tokens += -1
      num_tokens += 2
      return num_tokens
  else:
      raise NotImplementedError(f"""num_tokens_from_messages() is not presently implemented for model {model}.
  See https://github.com/openai/openai-python/blob/main/chatml.md for information on how messages are converted to tokens.""")

model = "gpt-3.5-turbo"

print(f"計算したPromptのトークン数:{num_tokens_from_messages(messages, model)}")

encoding = tiktoken.encoding_for_model(model)

print(f'計算したCompletionのトークン数:{len(encoding.encode(response["choices"][0]["message"]["content"]))}')

実行した結果は以下のようになりました。

実際に利用したPromptのトークン数:65
実際に利用したCompletionのトークン数:398
計算したPromptのトークン数:64
計算したCompletionのトークン数:398

Completionは正確に計算できおり、Promptに関しては1トークン少ないという結果になりました。何回かPromptを変更して試しましたが、どの場合も実際に利用したトークン数よりも1少ないという結果になりました。

C#の場合

自分がよく使うC#でも試してみました。トークン数をカウントするライブラリは以下のようなものがあります。

github.com

github.com

github.com

今回はTokenizerを使って試してみます。
ソースコードは以下のようにしてみました。(.NET 6 のコンソールアプリを使用)

using Microsoft.DeepDev;
using System.Net.Http.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;


string _azureOpenAiUri = "[Azure OpenAIのURI]";
string _azureOpenAiKey= "[Azure OpenAIのキー]";
string deployment = "[デプロイ名]";
string azureOpenAiChatCompletionApiVersion = "2023-05-15";


HttpClient _httpClient = new HttpClient();
string _chatCompletionEndpoint = $"{_azureOpenAiUri}openai/deployments/{deployment}/chat/completions?api-version={azureOpenAiChatCompletionApiVersion}";


var list = new List<OpenAIMessage>
{
    new OpenAIMessage("user","こんにちは。"),
    new OpenAIMessage("assistant","こんにちは!私はAIアシスタントです。何かお手伝いできることがありますか?"),
    new OpenAIMessage("user","お元気ですか?"),
};

var request = new HttpRequestMessage(HttpMethod.Post, _chatCompletionEndpoint);
request.Headers.Add("api-key", _azureOpenAiKey);

request.Content = JsonContent.Create(new
{
    messages = list,
});


var response = await _httpClient.SendAsync(request);
if (response.IsSuccessStatusCode)
{
    var resultString = await response.Content.ReadAsStringAsync();
    var completionTokens = (int?)JsonNode.Parse(resultString)?["usage"]?["completion_tokens"] ?? 0;
    var promptTokens = (int?)JsonNode.Parse(resultString)?["usage"]?["prompt_tokens"] ?? 0;
    var answerContent = JsonNode.Parse(resultString)?["choices"]?[0]?["message"]?["content"]?.ToString() ?? "";
    
    Console.WriteLine("実際に利用したPromptのトークン数:" + promptTokens);
    Console.WriteLine("実際に利用したCompletionのトークン数:" + completionTokens);

    //Tokenizer
    var tokenizer = TokenizerBuilder.CreateByModelName("gpt-3.5-turbo");
    int num_prompt_tokens = 0;
    int num_completion_tokens = 0;

    //Promptのトークン数をカウント
    foreach (var item in list)
    {
        num_prompt_tokens += 4;
        num_prompt_tokens += tokenizer.Encode(item.Role.ToString(), Array.Empty<string>()).Count;
        num_prompt_tokens += tokenizer.Encode(item.Content, Array.Empty<string>()).Count;
        if (item.Role == "name")
            num_prompt_tokens += -1;
    }
    num_prompt_tokens += 2;
    Console.WriteLine($"計算したPromptのトークン数:{num_prompt_tokens}");

    //Completionのトークン数をカウント
    num_completion_tokens += tokenizer.Encode(answerContent, Array.Empty<string>()).Count;
    Console.WriteLine($"計算したCompletionのトークン数:{num_completion_tokens}");
}

//JSONを作成するためのクラス
public class OpenAIMessage
{
    public OpenAIMessage(string role, string content)
    {
        Role = role;
        Content = content;
    }

    [JsonPropertyName("role")]
    public string Role { get; set; }

    [JsonPropertyName("content")]
    public string Content { get; set; }
}

実行すると以下のような結果が得られました。

実際に利用したPromptのトークン数:55
実際に利用したCompletionのトークン数:47
計算したPromptのトークン数:54
計算したCompletionのトークン数:47

Pythonの時と同様にCompletionは正確に計算できおり、Promptに関しては1トークン少ないという結果になりました。

まとめ

以上の結果から、ライブラリを使用することによってほぼ同じくらいのトークン数を計算することができました。Promptに関しては1トークン少ない結果となりましたが、ここはある程度妥協する必要があると思います。

以上、トークン数を計算する方法について書きました。是非参考にしてみてください。

執筆担当者プロフィール
古川 貴浩

古川 貴浩(日本ビジネスシステムズ株式会社)

HoloLens 2アプリケーションの開発をしています。

担当記事一覧