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
根据model类型生成分词器
tkm, err := tiktoken.EncodingForModel(model)
这里其实是包含了两个步骤:
通过指定的model获取到对应的Encoding
通过Encoding生成对应的分词器实例。
Encoding是指导文本转化为token的方式,它与模型是有着关联关系的,具体关系映射如下:
Encoding name | OpenAI models |
cl100k_base | gpt-4, gpt-3.5-turbo, text-embedding-ada-002 |
p50k_base | Codex models, text-davinci-002, text-davinci-003 |
r50k_base (or gpt2) | GPT-3 models like davinci |
GetEncoding(encodingName)
加上每条消息的固定token开销。
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
}
}
遍历全部消息,计算每条消息中的Content/Role/Name字段专程的token数之和,再将此和进行累加。
[
{"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
以下为全部代码:
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的话,需要合并完整所有的响应片段,再调用上方所述的算法进行计算。
关于领创集团