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 / 1M64K(输入),8K(输出)
deepseek-reasoner(R1)$0.14 / 1M$0.55 / 1M$2.19 / 1M64K(输入),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 tokens8K tokens
deepseek-reasoner(R1)64K tokens32K tokens(含思维链)

几个重要的工程细节:

截断方向:从头截,不从尾截。 当输入超出上限时,模型从序列开头开始丢弃内容,保留最近的部分。多轮对话里,最先被"遗忘"的是最早的对话历史——这和人类记忆的遗忘曲线正好相反,是开发者最容易忽略的行为。

超出上限直接报错。 输入 + 输出之和超过总窗口,API 返回 400 错误,messagecontext 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[累计成本监控]

五条实操铁律:

  1. 计数用 tiktoken-go,不用正则估算——正则误差可以达到 20%+,会让你的成本预算失真
  2. 固定 prompt 放前面,变量内容放后面——最大化缓存命中率,输入成本降 75%
  3. max_tokens 动态计算,不写死——根据实际输入 token 数动态设置,防止超出输出窗口
  4. 多轮对话维护 token 计数器——不要等到 API 报错才知道超限,要在发请求前主动检查
  5. 成本监控从第一天就加——哪怕是练手项目,养成记录 usage.total_tokens 的习惯,上线时不会被账单吓到

完整示例代码见 Github


文章有帮助?转发给同样在踩坑的朋友。有不同意见?评论区见。


关注公众号:极客老墨

更多 AI 应用开发、工程实践和效率工具分享,欢迎扫码关注。

极客老墨微信公众号二维码

相关阅读