Token是怎么来的:大模型计费和上下文管理的底层逻辑

大家好,我是极客老墨。
上一篇讲大模型工作原理时,Token 出现了很多次。这一篇专门把它讲透——不是因为概念难,而是因为它的每一个细节都直接影响你的 API 账单和代码行为。
成本超支、上下文莫名截断、多轮对话"失忆"——这些新手最常踩的坑,根子都在 Token 上。
一、Token 是什么:BPE 分词不是按字切
Token(中文翻译定义为“词元”) 是大模型处理文本的最小单位。但很多人对它有一个根本性的误解:Token 不等于字,不等于词,更不等于汉字。
主流大模型(包括 DeepSeek)使用的分词算法叫 BPE(Byte-Pair Encoding,字节对编码)。它的逻辑是:从字节出发,统计语料中最高频的字节对,反复合并,最终形成一个包含几万到十几万个"子词单元"的词表。
结果就是:Token 的边界是由训练语料的统计规律决定的,不是人为规定的。
# 英文示例(DeepSeek tokenizer)
"developer" → ["developer"] # 1 token(高频词,整词入表)
"tokenization" → ["token", "ization"] # 2 tokens(低频长词,拆分)
"DeepSeek" → ["Deep", "Seek"] # 2 tokens(专有名词)
# 中文示例
"大模型" → ["大", "模型"] # 2 tokens("模型"是高频词组)
"量子纠缠" → ["量", "子", "纠", "缠"] # 4 tokens(低频,逐字)
"API" → ["API"] # 1 token
中英文的核心差异:
同样的语义内容,中文消耗的 token 数通常比英文多 30%–50%。原因是英文词汇复用率高,大量常用词整词入表;中文虽然每个汉字本身是高频单元,但组合出的词组不一定在词表里,往往逐字切割。
粗略估算(用于成本预算,不用于精确计算):
| 文本类型 | Token 估算 |
|---|---|
| 英文 | 约 1 token / 4 个字符,或 1 token / 0.75 个单词 |
| 中文 | 约 1 token / 1.5–2 个汉字 |
| 代码 | 约 1 token / 3–4 个字符(关键字效率高,变量名低) |
老墨说: 你没法用正则表达式或者"按汉字数"来精准计算 Token,因为 BPE 的切割结果取决于训练词表,同一段文字,不同模型的 tokenizer 切出来的结果可能不同。要精准计数,必须用对应模型的 tokenizer 工具——后面的代码实战会讲。
二、DeepSeek 计费逻辑:看清账单再写代码
DeepSeek API 按 百万 tokens(1M tokens) 为单位计费,分输入和输出两个维度,且输入有缓存命中价和未命中价之分。
以下是截至 2026 年 4 月的官方定价(来源:DeepSeek API Pricing):
| 模型 | 输入(缓存命中) | 输入(未命中) | 输出 | Context Window |
|---|---|---|---|---|
deepseek-chat(V3.2) | $0.07 / 1M | $0.27 / 1M | $1.10 / 1M | 64K(输入),8K(输出) |
deepseek-reasoner(R1) | $0.14 / 1M | $0.55 / 1M | $2.19 / 1M | 64K(输入),32K(输出) |
三个关键点:
1. 缓存命中(Cache Hit)大幅省钱。 当你的 prompt 前缀和上一次请求相同时,服务器复用了 KV Cache,输入价格降至 1/4 左右。系统 prompt 通常是固定的,反复请求时天然命中缓存。设计 prompt 时把固定部分放前面,把变化部分放后面,是最直接的省钱手段。
2. 输出 token 比输入贵 4 倍。 控制输出长度比控制输入更重要。能用 max_tokens 卡住的地方一定要卡,不要让模型漫无边际地生成。
3. deepseek-reasoner 有隐藏的思维链 token 消耗。 R1 在生成最终答案前会产生大量内部推理 token(reasoning_content 字段),这部分也计入输出费用。简单任务别用 R1,成本会大幅超预期。
成本估算示例(deepseek-chat,缓存未命中):
输入 prompt:500 tokens
输出 result:800 tokens
输入成本:500 / 1,000,000 × $0.27 = $0.000135
输出成本:800 / 1,000,000 × $1.10 = $0.00088
单次总成本:$0.001015 ≈ 约 0.007 元
日调用 10,000 次:约 70 元
单次看起来极便宜,但高频 batch 任务下成本会快速累积。上线前一定要压测估算。
老墨说: 很多人省成本的方式是"精简 prompt",但效果其实有限——因为输入便宜,真正的大头是输出。如果你的应用场景允许,让模型输出结构化 JSON 而不是自然语言长文,是控制输出 token 最有效的手段之一。
三、Context Window:大模型的"工作记忆"上限
Context Window 是单次 API 调用中,模型能处理的输入 + 输出 token 总量上限。超出这个上限,要么调用报错,要么早期内容被截断——两种结果对应用来说都是灾难。
DeepSeek 主流模型的窗口规格(来源:DeepSeek API 文档):
| 模型 | 最大输入 | 最大输出 |
|---|---|---|
deepseek-chat(V3.2) | 64K tokens | 8K tokens |
deepseek-reasoner(R1) | 64K tokens | 32K tokens(含思维链) |
几个重要的工程细节:
截断方向:从头截,不从尾截。 当输入超出上限时,模型从序列开头开始丢弃内容,保留最近的部分。多轮对话里,最先被"遗忘"的是最早的对话历史——这和人类记忆的遗忘曲线正好相反,是开发者最容易忽略的行为。
超出上限直接报错。 输入 + 输出之和超过总窗口,API 返回 400 错误,message 为 context length exceeds the max limit。不是优雅降级,是直接失败。
输出窗口是独立的硬约束。 deepseek-chat 最大输出 8K,哪怕你把 max_tokens 设成 10000,实际也只会输出 8K。
flowchart TD
A["用户输入(Prompt)"] --> C{输入 Token 数 ≤ 64K?}
C -- 否 --> D["截断开头内容,保留最近部分"]
C -- 是 --> E["正常进入模型"]
D --> E
E --> F{输入+输出总量≤ Context Window?}
F -- 否 --> G["API 返回 400 错误 context length exceeds"]
F -- 是 --> H["正常生成输出"]
老墨说: 实际工程里,我建议把输入 token 控制在模型最大输入的 70% 以内,给输出留足空间。
deepseek-chat就是 64K × 70% ≈ 45K。超过这条线,要么做文本截断,要么切换更大窗口的模型。
四、Context Window 对开发的三个实际影响
影响一:多轮对话"失忆"
多轮对话的实现方式是把历史消息全部拼进 messages 数组。随着对话轮数增加,历史消息的 token 累积超出窗口,最早的对话被截断——模型真的"忘了"你们之前说了什么。
工程解法:滑动窗口 + 摘要压缩
当 历史消息 token 总量 > 窗口上限 × 70% 时:
→ 方案 A(简单):丢弃最早的若干条消息,保留最近 N 轮
→ 方案 B(质量更好):用一次 LLM 调用把早期对话压缩成摘要,摘要 + 最近若干轮 + 当前问题 拼成新的 messages
方案 B 多一次 API 调用,但上下文质量远高于方案 A。长对话应用(客服、AI 助手)几乎都用方案 B。
影响二:长文档处理
几万字的 PDF、几千行的代码库,token 数轻松超过 64K。解决方案有两条路:
切片 + 逐段处理:把文档切成每段 ≤ 45K token 的片段,逐段调用,最后汇总结果。简单,但片段间的上下文会丢失。
RAG(检索增强生成):把文档向量化存入向量数据库,用户提问时只检索最相关的片段塞进 prompt,不是把整篇文档都扔进去。这是生产环境的标准方案——后续的RAG实战会完整编码实现。
影响三:动态计算可用输出空间
在代码里,max_tokens 不应该是硬编码的固定值,而应该根据实际输入动态计算:
1const (
2 modelMaxInput = 64000 // deepseek-chat 最大输入
3 modelMaxOutput = 8000 // deepseek-chat 最大输出
4 safetyRatio = 0.7 // 保留 30% 余量
5)
6
7func calcMaxTokens(inputTokens int) int {
8 remaining := int(float64(modelMaxInput) * safetyRatio) - inputTokens
9 if remaining <= 0 {
10 return 0 // 输入已超限,需要截断
11 }
12 if remaining > modelMaxOutput {
13 return modelMaxOutput
14 }
15 return remaining
16}
五、Go 实战:精准 Token 计数器
前面说了,正则模拟 BPE 是不准的。精准计数必须用对应模型的 tokenizer。
DeepSeek 使用的是和 OpenAI cl100k_base 词表接近的 BPE 编码。Go 生态里有一个对应的库:github.com/pkoukk/tiktoken-go,由社区维护,支持 cl100k_base 词表,对 DeepSeek 的估算误差通常在 1–3% 以内,完全满足成本预算需求。
1go get github.com/pkoukk/tiktoken-go
1package main
2
3import (
4 "fmt"
5 "log"
6
7 tiktoken "github.com/pkoukk/tiktoken-go"
8)
9
10// DeepSeek 使用与 cl100k_base 接近的词表
11const encodingName = "cl100k_base"
12
13// DeepSeek API 定价(美元 / 1M tokens,缓存未命中,2026-04)
14type ModelPricing struct {
15 InputPerM float64
16 OutputPerM float64
17}
18
19var pricing = map[string]ModelPricing{
20 "deepseek-chat": {InputPerM: 0.27, OutputPerM: 1.10},
21 "deepseek-reasoner": {InputPerM: 0.55, OutputPerM: 2.19},
22}
23
24// CountTokens 返回文本的精准 token 数
25func CountTokens(text string) (int, error) {
26 enc, err := tiktoken.GetEncoding(encodingName)
27 if err != nil {
28 return 0, fmt.Errorf("加载 tokenizer 失败: %w", err)
29 }
30 return len(enc.Encode(text, nil, nil)), nil
31}
32
33// EstimateCost 估算 API 调用成本(美元)
34func EstimateCost(model string, inputTokens, outputTokens int) (float64, error) {
35 p, ok := pricing[model]
36 if !ok {
37 return 0, fmt.Errorf("未知模型: %s", model)
38 }
39 cost := float64(inputTokens)/1_000_000*p.InputPerM +
40 float64(outputTokens)/1_000_000*p.OutputPerM
41 return cost, nil
42}
43
44// CheckContextLimit 检查是否超出窗口限制
45func CheckContextLimit(inputTokens int, maxInput int) (ok bool, overBy int) {
46 if inputTokens > maxInput {
47 return false, inputTokens - maxInput
48 }
49 return true, 0
50}
51
52func main() {
53 texts := []string{
54 "Developer uses DeepSeek API to generate code.",
55 "开发者使用 DeepSeek API 生成代码。",
56 "func main() {\n\tfmt.Println(\"Hello, World!\")\n}",
57 }
58
59 enc, err := tiktoken.GetEncoding(encodingName)
60 if err != nil {
61 log.Fatal(err)
62 }
63
64 fmt.Println("=== Token 计数 ===")
65 for _, text := range texts {
66 tokens := enc.Encode(text, nil, nil)
67 fmt.Printf("文本: %q\n", text)
68 fmt.Printf("Token 数: %d | Token 列表: %v\n\n", len(tokens), tokens)
69 }
70
71 // 成本估算示例
72 fmt.Println("=== 成本估算(deepseek-chat,缓存未命中)===")
73 inputTokens := 500
74 outputTokens := 800
75 cost, _ := EstimateCost("deepseek-chat", inputTokens, outputTokens)
76 fmt.Printf("输入 %d tokens + 输出 %d tokens\n", inputTokens, outputTokens)
77 fmt.Printf("单次成本:$%.6f(约 %.4f 元)\n", cost, cost*7.2)
78 fmt.Printf("日调用 10,000 次:约 %.1f 元\n", cost*7.2*10000)
79
80 // 窗口检查
81 fmt.Println("\n=== 上下文窗口检查 ===")
82 const deepseekChatMaxInput = 64000
83 testInputs := []int{30000, 64001, 50000}
84 for _, n := range testInputs {
85 ok, over := CheckContextLimit(n, deepseekChatMaxInput)
86 if ok {
87 fmt.Printf("输入 %d tokens:✅ 在窗口内,可用输出空间约 %d tokens\n",
88 n, calcMaxTokens(n))
89 } else {
90 fmt.Printf("输入 %d tokens:❌ 超出限制 %d tokens,需要截断\n", n, over)
91 }
92 }
93}
94
95func calcMaxTokens(inputTokens int) int {
96 const (
97 maxInput = 64000
98 maxOutput = 8000
99 )
100 remaining := int(float64(maxInput)*0.7) - inputTokens
101 if remaining <= 0 {
102 return 0
103 }
104 if remaining > maxOutput {
105 return maxOutput
106 }
107 return remaining
运行输出:
=== Token 计数 ===
文本: "Developer uses DeepSeek API to generate code."
Token 数: 9 | Token 列表: [...省略...]
文本: "开发者使用 DeepSeek API 生成代码。"
Token 数: 14 | Token 列表: [...]
文本: "func main() {\n\tfmt.Println(\"Hello, World!\")\n}"
Token 数: 18 | Token 列表: [...]
=== 成本估算(deepseek-chat,缓存未命中)===
输入 500 tokens + 输出 800 tokens
单次成本:$0.001015(约 0.0073 元)
日调用 10,000 次:约 73.1 元
=== 上下文窗口检查 ===
输入 30000 tokens:✅ 在窗口内,可用输出空间约 8000 tokens
输入 64001 tokens:❌ 超出限制 1 tokens,需要截断
输入 50000 tokens:✅ 在窗口内,可用输出空间约 800 tokens
老墨说:
tiktoken-go第一次运行会从网络下载词表文件(约几 MB),之后缓存到本地。如果你的部署环境没有外网,可以提前下载词表文件打进镜像,设置TIKTOKEN_CACHE_DIR环境变量指向本地路径。
六、工程实践总结
把这几条记住,Token 相关的坑基本踩不到:
flowchart TD
A[开始一次 API 调用] --> B[用 tiktoken 计算输入 token 数]
B --> C{超出 maxInput × 70%?}
C -- 是 --> D[截断 / 压缩 / 分片]
D --> B
C -- 否 --> E[动态计算 max_tokens]
E --> F[发起 API 请求]
F --> G{响应正常?}
G -- 400 context exceeded --> D
G -- 正常 --> H[统计实际 token 消耗]
H --> I[累计成本监控]
五条实操铁律:
- 计数用
tiktoken-go,不用正则估算——正则误差可以达到 20%+,会让你的成本预算失真 - 固定 prompt 放前面,变量内容放后面——最大化缓存命中率,输入成本降 75%
max_tokens动态计算,不写死——根据实际输入 token 数动态设置,防止超出输出窗口- 多轮对话维护 token 计数器——不要等到 API 报错才知道超限,要在发请求前主动检查
- 成本监控从第一天就加——哪怕是练手项目,养成记录
usage.total_tokens的习惯,上线时不会被账单吓到
完整示例代码见 Github
文章有帮助?转发给同样在踩坑的朋友。有不同意见?评论区见。
关注公众号:极客老墨
更多 AI 应用开发、工程实践和效率工具分享,欢迎扫码关注。
