返回

OpenAI Assistant流式传输Token计数详解与代码示例

Ai

OpenAI Assistant 流式传输消息时的 Token 计数方法

使用 OpenAI Assistant API 构建应用时,我们经常会用到流式传输(streaming)模式来接收消息更新。 这样做的好处是可以实现类似“打字机”的效果,提升用户体验。但一个问题来了,如何在流式传输的过程中计算已消耗的 Token 数量呢?这篇文章就来详细说说这个问题,分析产生原因,并给出解决方案。

一、为什么需要计算 Token?

OpenAI API 的计费是基于 Token 数量的。了解 Token 消耗情况,能帮我们:

  1. 控制成本: 准确掌握 API 使用费用,避免超支。
  2. 优化性能: 分析不同请求的 Token 消耗,找出潜在的优化点,减少不必要的 Token 使用。
  3. 资源限制: OpenAI API 有速率限制和 Token 限制,知道 Token 消耗,就能更好地管理这些限制。

二、流式传输时 Token 计数难点

与普通的一次性请求不同,流式传输是分段接收数据的。streamingUpdate 对象包含一系列增量更新。直接在每一次的增量中试图统计 token 消耗,并不可靠。主要原因:

  1. 增量更新的粒度: OpenAI 返回的增量更新可能只包含几个字符、一个单词,甚至只是标点符号,或者没有任何更新。每一次统计都将造成麻烦。
  2. Token 化过程的复杂性: OpenAI 使用特定的 Tokenizer 将文本转换为 Token。Token 并不是简单地对应于单词或字符,它还受到上下文的影响。我们很难在客户端准确地模拟这个 Tokenizer。
  3. 输出没有直接可用的token 信息。 CreateRunStreaming 方式获取的信息流里并不直接包括每一次的token信息。

三、解决方案

由于上述难点,直接对 streamingUpdate 做实时的 token 统计是不推荐的。更可行的办法是采用其他思路:

1. 利用 Run 对象获取最终 Token 计数 (推荐)

最简单可靠的方法是在 Run 完成后,通过 Run 对象获取总的 Token 使用量。OpenAI 会在 Run 结束后提供 usage 字段,其中包含了 prompt tokens 和 completion tokens 的计数。

原理:

OpenAI 服务端会在 Run 结束后,统计整个过程的 Token 消耗。 我们只需要在确认Run 结束后,读取 usage 字段就行了。

代码示例 (C#):

using Azure.AI.OpenAI.Assistants;

// 假设你已经有了 assistantClient、threadId 和 runId
// 等待 Run 完成
while (true)
{
	Run run = await assistantClient.GetRunAsync(threadId, runId);
    if (run.Status == RunStatus.Completed)
    {
	   // 获取 token 使用量统计结果
        Console.WriteLine($"Prompt tokens: {run.Usage.PromptTokens}");
        Console.WriteLine($"Completion tokens: {run.Usage.CompletionTokens}");
        Console.WriteLine($"Total tokens: {run.Usage.TotalTokens}");
        break;
    }
    else if (run.Status == RunStatus.Failed || run.Status == RunStatus.Cancelled || run.Status == RunStatus.Expired)
    {
       Console.WriteLine($"运行状态:{run.Status}");
        Console.WriteLine($"错误信息:{run.LastError}");

	   //处理运行异常情况
        break;
    }
    // 稍作等待,避免过于频繁的轮询
    await Task.Delay(TimeSpan.FromSeconds(1));
}

说明:

  • run.Usage.PromptTokens:表示提示(输入)部分消耗的 Token 数。
  • run.Usage.CompletionTokens:表示补全(输出)部分消耗的 Token 数。
  • run.Usage.TotalTokens:总的 Token 消耗。

安全建议:

  • run.Status 进行判断,处理 FailedCancelledExpired 等异常情况。
  • 设置合适的等待间隔 (Task.Delay),避免轮询过于频繁,消耗过多请求次数。
  • 检查错误信息:在生产环境,要认真处理所有可能的异常。

2. 结合非流式方法预估 (适用于预估消耗)

如果需要在流式传输开始 之前 就预估 Token 消耗(例如,用于限制输入长度),可以使用非流式方法获取现有内容的token,再加上新内容的Token统计,作为整体的 token 计数,并判断是否满足最大允许值。

原理:

虽然流式传输过程中很难精确计算,但我们可以“曲线救国”,先用非流式方法算出已有会话的 Token。 增加内容的时候, 也用同样方法预估增量内容的token 数。

步骤:

  1. 获取历史消息的 Token 计数: 将整个对话历史组织成非流式的输入。 调用如: tiktoken-go 的方法获取当前会话内容的token 计数。
  2. 计算新消息的Token: 使用 tiktoken-go 一样的工具。获取新增输入内容的Token数量。
  3. 预估流的总体token 数。

代码示例 (结合 C# 和 go 服务, tiktoken-go示例):

假设你有一个 go 服务, 可以根据字符串来计算 token数. 可以构建以下HTTP 调用。 (你需要有可用的Go环境来构建和提供服务,这里仅简略示意,具体go 实现请搜索tiktoken-go)

//go
func CountToken(w http.ResponseWriter, r *http.Request){

   reqBody,err := io.ReadAll(r.Body)
	//处理错误
   //使用tiktoken 来获得编码器
   enc, err := tiktoken.GetEncoding("cl100k_base") // 或者 p50k_base 等

   //处理错误
    //将文本转换为token
	token := enc.Encode(string(reqBody), nil, nil)

	strResult := strconv.Itoa(len(token))

   io.WriteString(w,strResult )
}
//C#

// 假定你的Go 服务运行在 localhost:8081
using System.Net.Http;

public static  async Task<int> EstimateMessageTokenByGoServer(string msg)
{
  HttpClient client = new HttpClient();
  HttpRequestMessage reqMsg = new HttpRequestMessage(HttpMethod.Post, "http://localhost:8081/api/count_token");
    reqMsg.Content = new StringContent(msg, Encoding.UTF8);
    HttpResponseMessage response = await client.SendAsync(reqMsg);
   response.EnsureSuccessStatusCode();
   var rspStr  =  await response.Content.ReadAsStringAsync();
  if(int.TryParse(rspStr, out int n)) {

     return n;

  }else
    {
   //log the error.
     return 0;
    }

}

进阶技巧 (更精确地估算):

为了提高估算的精度,可以使用针对 Assistant API 优化的 Tokenizer。 不同模型使用不同的 Tokenizer. 必须匹配一致。 比如 "cl100k_base" 用于 gpt-4gpt-3.5-turbotext-embedding-ada-002 等. "p50k_base" 用于 text-davinci-002text-davinci-003 等。
注意这只是一个估算。 特别注意系统消息,Assistant 可能对最终用户的输出的消息会调整系统提示,并多次执行流传输, 所以会有额外的计算成本。 尤其是在流式处理中可能会出现估算偏差。 最终统计还是建议通过第一种方式实现。

注意事项:

  • 请先安装 tiktoken-go,并搭建对应 go 服务, 以暴露给 C# 服务调用.
  • 务必正确匹配所选模型对应的 encoding.

3.本地模拟估算(适用少量文本)

如果消息内容较为简短(小于几百字),而且希望在不联网的情况下实现粗略估算,可以使用类似 SharpToken 这类 C# 的开源库,但这种方式不能实现很精确的估算. 误差会比第2种方法更大一些。

原理类似方案2, 区别在于可以在 C# 本地简单完成token统计工作。 并不需要远程调用.

安装:

Install-Package SharpToken -Version 1.0.35

代码示例:

using SharpToken;

var encoding = GptEncoding.GetEncodingForModel("gpt-3.5-turbo"); // 假设使用 gpt-3.5-turbo 模型
var prompt = "你好,世界!";
var tokenCount = encoding.Encode(prompt).Count;
Console.WriteLine(tokenCount);

进阶技巧:
和方案二的进阶相同。 需要注意 Encoding 必须选择与你的 Assistant 模型对应。
重要说明:
无论是使用何种客户端 token 计数库。由于可能存在版本不同步等问题, 请务必不要在生产环境依赖这样的方式进行任何形式的token 消耗审计和成本结算。 这会导致明显的误差.
这种方式适合非实时的粗略token数量统计, 尤其在测试的时候适用。

四、 总结

给使用 OpenAI Assistant API 流式传输的朋友们提个醒,记得一定要在Run 完成后,检查一下 Run 对象的 Usage 信息,拿到实际消耗的Token数量。要是想提前预估Token数量,试试第二和第三种方法。但这些方法只能估个大概,最后还得看 Run.Usage 的结果。 再次强调, 使用任何第三方的 token 计数工具都有偏差. 不要做为成本依据。