技术创想99 | 浅析chat completion中token以及计费规则

文摘   科技   2024-01-11 17:25   北京  

什么是token?

Text generation and embeddings models process text in chunks called tokens. Tokens represent commonly occurring sequences of characters.

简单来说,文本生成和嵌入的模型,是以一种“chunk(块)” 的形式来被处理的,这种chunk就被称之为token。当句子很短时,例如"the",那它就只占用了一个token;如果一行句子足够长时,文本就会被打散成多个token,一般来说,一个token约等于4个字符或0.75个单词。

这里的关键点在于,token只是针对文本生成(text generation)嵌入(embeddings)模型 的概念,言外之意对于像Image、Audio等模型此概念并不适用。

本文讨论的chat completion可以被视作text generation 的一种特殊形式,它模拟了一套对话场景,目标是为对话提供补充内容,生成连贯的响应,以模拟真实对话中的连续性和交互性。

token的限制

GPT对话的机制是可以携带上下文的,上下文 = 当前的输入消息 + 历史输入和响应的消息,然而这个上下文的尺寸针对不同模型是有着严格限定的,具体参考:

GPT4: https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo

GPT3.5: https://platform.openai.com/docs/models/gpt-3-5

为什么要关注token

对于文本生成和嵌入模型来说,计费规则就是围绕token展开的,为了了解一次chat completion API请求中究竟产生了多少费用,对于token的计算是极其必要的。

token的计费规则

如前所述,在text generation任务中计费规则是围绕token进行的,这里的token并非只是用户输入时产生的token,同时在的到openai响应的时候,输出文本的token,同样需纳入费用计算。而且在不同模型下,输入和输出所包含的token收费各不相同,以GPT3.5和4.0为例:

GPT-3.5 Turbo:

Model

Input

Output

gpt-3.5-turbo-1106

$0.0010 / 1K tokens

$0.0020 / 1K tokens

gpt-3.5-turbo-instruct

$0.0015 / 1K tokens

$0.0020 / 1K tokens

GPT-4:

Model

Input

Output

gpt-4

$0.03 / 1K tokens

$0.06 / 1K tokens

gpt-4-32k

$0.06 / 1K tokens

$0.12 / 1K tokens

如何计算通过API中消耗的token

在chat completion的API中,一次会话消耗的总token数 = 输入文本包含token + 输出文本包含token,所以在一次会话中,如何计算给定一段文本能够转化为多少token,是计算token消耗的重点。
将自然语言转化为token要依赖分词器,目前比较主流的开源分词器工具为tiktoken,也是openai官方推荐的一款,针对不同的开发语言,tiktoken也有各自的开源组件,这里以go的开源库tiktoken-go为例分析该流程:
  1. 根据model类型生成分词器
 tkm, err := tiktoken.EncodingForModel(model)


这里其实是包含了两个步骤:

  • 通过指定的model获取到对应的Encoding

  • 通过Encoding生成对应的分词器实例。

Encoding是指导文本转化为token的方式,它与模型是有着关联关系的,具体关系映射如下:


Encoding nameOpenAI models
cl100k_basegpt-4, gpt-3.5-turbo, text-embedding-ada-002
p50k_baseCodex models, text-davinci-002, text-davinci-003
r50k_base (or gpt2)GPT-3 models like davinci
所以如果可以确定encoding的情况下,可直接调用以下方法直接获取分词器:
GetEncoding(encodingName)
  1. 加上每条消息的固定token开销。
openai为了识别每条消息的起止点,都设置了内部可识别的分隔符,这将带来每句话中额外的开销,而对于不同的模型,openai内部的分隔符的使用情况也不尽相同,具体详见下方代码:
 var tokensPerMessage, tokensPerName int        switch model {        case "gpt-3.5-turbo-0613",                "gpt-3.5-turbo-16k-0613",                "gpt-4-0314",                "gpt-4-32k-0314",                "gpt-4-0613",                "gpt-4-32k-0613":                tokensPerMessage = 3                tokensPerName = 1        case "gpt-3.5-turbo-0301":                tokensPerMessage = 4 // every message follows <|start|>{role/name}\n{content}<|end|>\n                tokensPerName = -1   // if there's a name, the role is omitted        default:                if strings.Contains(model, "gpt-3.5-turbo") {                        log.Println("warning: gpt-3.5-turbo may update over time. Returning num tokens assuming gpt-3.5-turbo-0613.")                        return NumTokensFromMessages(messages, "gpt-3.5-turbo-0613")                } else if strings.Contains(model, "gpt-4") {                        log.Println("warning: gpt-4 may update over time. Returning num tokens assuming gpt-4-0613.")                        return NumTokensFromMessages(messages, "gpt-4-0613")                } else {                        err = fmt.Errorf("num_tokens_from_messages() is not implemented for model %s. See https://github.com/openai/openai-python/blob/main/chatml.md for information on how messages are converted to tokens.", model)                        log.Println(err)                        return                }        }
  1. 遍历全部消息,计算每条消息中的Content/Role/Name字段专程的token数之和,再将此和进行累加。
前面提到过,chat completion既是模拟对话场景,是可以携带上下文对话的,因此一次对话中所包含的消息可能不只一条,提现在代码中,一次完整的上下文大致会是如下形式:
[    {"role": "system", "content": "You are a helpful, pattern-following assistant."},    {"role": "user", "content": "Help me translate the following corporate jargon into plain English."},    {"role": "assistant", "content": "Sure, I'd be happy to!"},    {"role": "user", "content": "New synergies will help drive top-line growth."},    {"role": "assistant", "content": "Things working well together will increase revenue."},    {"role": "user", "content": "Let's circle back when we have more bandwidth to touch base on opportunities for increased leverage."},    {"role": "assistant", "content": "Let's talk later when we're less busy about how to do better."},    {"role": "user", "content": "This late pivot means we don't have time to boil the ocean for the client deliverable."},]

这其中:

content字段就是每次对话的消息正文,无需赘述;

role则是对应产生当前消息的角色,分为以下几种:

  • system:非必须字段,用于定义一个明确的上下文,指明chatgpt接下来的响应应该以一个什么样的身份来回答,对接下来的响应结果的准确度有一定提升。

  • user:代表当前发送消息的用户。

  • assistant:代表响应、回答消息的一方。

name是当此轮对话需要进行function calling时才会携带的字段,关于function calling本文不作过多讨论,详见:https://platform.openai.com/docs/guides/function-calling

基于上述考虑,此处代码如下:

for _, message := range messages {    numTokens += tokensPerMessage    numTokens += len(tkm.Encode(message.Content, nil, nil))    numTokens += len(tkm.Encode(message.Role, nil, nil))    numTokens += len(tkm.Encode(message.Name, nil, nil))    if message.Name != "" {            numTokens += tokensPerName    }}numTokens += 3 
最后的numTokens += 3是因为每次响应都要以<|start|>assistant<|message|>开头,这里提前要将其算入。

以下为全部代码:

package main
import ( "fmt"
"github.com/pkoukk/tiktoken-go" "github.com/sashabaranov/go-openai")
func NumTokensFromMessages(messages []openai.ChatCompletionMessage, model string) (numTokens int) { tkm, err := tiktoken.EncodingForModel(model) if err != nil { err = fmt.Errorf("encoding for model: %v", err) log.Println(err) return }
var tokensPerMessage, tokensPerName int switch model { case "gpt-3.5-turbo-0613", "gpt-3.5-turbo-16k-0613", "gpt-4-0314", "gpt-4-32k-0314", "gpt-4-0613", "gpt-4-32k-0613": tokensPerMessage = 3 tokensPerName = 1 case "gpt-3.5-turbo-0301": tokensPerMessage = 4 // every message follows <|start|>{role/name}\n{content}<|end|>\n tokensPerName = -1 // if there's a name, the role is omitted default: if strings.Contains(model, "gpt-3.5-turbo") { log.Println("warning: gpt-3.5-turbo may update over time. Returning num tokens assuming gpt-3.5-turbo-0613.") return NumTokensFromMessages(messages, "gpt-3.5-turbo-0613") } else if strings.Contains(model, "gpt-4") { log.Println("warning: gpt-4 may update over time. Returning num tokens assuming gpt-4-0613.") return NumTokensFromMessages(messages, "gpt-4-0613") } else { err = fmt.Errorf("num_tokens_from_messages() is not implemented for model %s. See https://github.com/openai/openai-python/blob/main/chatml.md for information on how messages are converted to tokens.", model) log.Println(err) return } }
for _, message := range messages { numTokens += tokensPerMessage numTokens += len(tkm.Encode(message.Content, nil, nil)) numTokens += len(tkm.Encode(message.Role, nil, nil)) numTokens += len(tkm.Encode(message.Name, nil, nil)) if message.Name != "" { numTokens += tokensPerName } } numTokens += 3 // every reply is primed with <|start|>assistant<|message|> return numTokens}


总结和说明

  • 本文对token的定义、以及chat completion下API的token计费规则做了一些浅析,值得一提的是,文中给出的算法只适用于截止本文发布之日,后期openai可能会有所调整,最终算法要依照官方文档。

  • 上文中的算法,一般用作计算输入文本包含的token ,对于输出文本包含的token的计算分以下两种形式:

    • streaming = false,即调用API时采取非流式访问,那么此时响应信息中会提供一个competion_tokens 字段,不用计算,此字段即代表了此时输出文本的token个数。

    • streaming = true 即流式访问,此时若想计算输出文本包含的token的话,需要合并完整所有的响应片段,再调用上方所述的算法进行计算。



关于领创集团

(Advance Intelligence Group)
领创集团成立于 2016年,致力于通过科技创新的本地化应用,改造和重塑金融和零售行业,以多元化的业务布局打造一个服务于消费者、企业和商户的生态圈。集团旗下包含企业业务和消费者业务两大板块,企业业务包含 ADVANCE.AI 和 Ginee,分别为银行、金融、金融科技、零售和电商行业客户提供基于 AI 技术的数字身份验证、风险管理产品和全渠道电商服务解决方案;消费者业务 Atome Financial 包括亚洲领先的先享后付平台 Atome 和数字金融服务。2021年 9月,领创集团宣布完成超4亿美元 D 轮融资,融资完成后领创集团估值已超 20亿美元,成为新加坡最大的独立科技创业公司之一。




领创集团Advance Group
领创集团是亚太地区AI技术驱动的科技集团。
 最新文章