AI 专题 FEB 10, 2026

Token 计算与上下文窗口管理实战

#Token#上下文管理#成本优化#tiktoken

Token 计算与上下文窗口管理实战

本文是【AI 专题精讲】系列第 10 篇。 上一篇:AI 缓存策略:精确缓存 + 语义缓存,省钱又提速 | 下一篇:大模型微调实战:从 LoRA 到部署上线的完整方案


这篇文章你会得到什么

你一定遇到过这些情况:

  • API 返回 context_length_exceeded,对话直接断了
  • 月底一看账单,Token 用量比预期多了 3 倍
  • 用户聊了 20 轮,模型开始”忘记”之前说的话
  • 不同模型的上下文窗口不一样,切换模型就爆了

Token 是 LLM 的”计量单位”——你花的每一分钱、模型能记住的每一句话,都以 Token 为单位计算

今天给你:Token 精确计算方法、各模型窗口对比、动态上下文管理策略,以及一个即插即用的聊天历史自动裁剪中间件。


Token 到底是什么

BPE 编码

Token 不是字、不是词、不是字符。它是 BPE(Byte Pair Encoding) 算法切出来的”片段”:

英文:
"Hello world" → ["Hello", " world"]  → 2 tokens

中文:
"你好世界" → ["你好", "世界"]  → 2 tokens(GPT-4o)
"你好世界" → ["你", "好", "世", "界"]  → 4 tokens(GPT-3.5)

代码:
"console.log('hello')" → ["console", ".", "log", "('", "hello", "')"]  → 6 tokens

关键认知:中文每个字大约 0.51.5 个 Token(取决于模型),英文每个单词约 11.5 个 Token。

为什么 Token 数不好估

同一句话,不同模型的 Token 数可能不同——因为每个模型的 tokenizer(分词器)不同:

模型”如何申请退货退款” 的 Token 数
GPT-4o / GPT-4o-mini4
GPT-3.5-turbo7
Claude 35

所以不能用粗略估算,必须用对应模型的 tokenizer 精确计算


精确计算:tiktoken

Python

pip install tiktoken
import tiktoken

def count_tokens(text: str, model: str = "gpt-4o") -> int:
    encoding = tiktoken.encoding_for_model(model)
    return len(encoding.encode(text))

# 单条文本
print(count_tokens("如何申请退货退款"))  # 4

# 查看 Token 细节
encoding = tiktoken.encoding_for_model("gpt-4o")
tokens = encoding.encode("如何申请退货退款")
print(tokens)  # [12345, 23456, ...]
print([encoding.decode([t]) for t in tokens])  # ['如何', '申请', '退货', '退款']

计算完整 messages 的 Token 数

ChatGPT API 的 messages 格式有额外的结构开销(role 标签、分隔符等),不能只算 content:

def count_messages_tokens(messages: list[dict], model: str = "gpt-4o") -> int:
    encoding = tiktoken.encoding_for_model(model)

    # 不同模型的结构开销不同
    if model.startswith("gpt-4o") or model.startswith("gpt-4"):
        tokens_per_message = 3  # <|im_start|>role\ncontent<|im_end|>
        tokens_per_name = 1
    else:
        tokens_per_message = 4
        tokens_per_name = -1

    num_tokens = 0
    for message in messages:
        num_tokens += tokens_per_message
        for key, value in message.items():
            num_tokens += len(encoding.encode(str(value)))
            if key == "name":
                num_tokens += tokens_per_name

    num_tokens += 3  # 每个回复的 priming
    return num_tokens

JavaScript / TypeScript

npm install tiktoken
import { encoding_for_model } from 'tiktoken';

function countTokens(text: string, model: string = 'gpt-4o'): number {
  const enc = encoding_for_model(model as any);
  const tokens = enc.encode(text);
  enc.free();
  return tokens.length;
}

console.log(countTokens('如何申请退货退款')); // 4

快速估算(不用 tiktoken)

有些场景不需要精确计算(比如粗略估计成本),可以用经验公式:

def estimate_tokens(text: str) -> int:
    """
    粗略估算,误差 ±20%
    中文约 0.6 token/字,英文约 0.25 token/词
    """
    chinese_chars = sum(1 for c in text if '\u4e00' <= c <= '\u9fff')
    other_chars = len(text) - chinese_chars
    return int(chinese_chars * 0.6 + other_chars * 0.25)

各模型上下文窗口对比

模型上下文窗口输入价格输出价格
GPT-4o-mini128K$0.15/1M$0.60/1M
GPT-4o128K$2.50/1M$10.00/1M
Claude 3.5 Sonnet200K$3.00/1M$15.00/1M
Claude 3 Haiku200K$0.25/1M$1.25/1M
DeepSeek V364K¥1.00/1M¥2.00/1M
Gemini 1.5 Pro1M$1.25/1M$5.00/1M
Qwen-Max32K¥20/1M¥60/1M

窗口大不等于随便用

128K 窗口能装大约 10 万字中文——但这不代表你该把 10 万字都塞进去

  1. 成本线性增长:塞 10 万 Token 比 1000 Token 贵 100 倍
  2. 注意力稀释:上下文太长,模型对中间内容的关注度下降(“Lost in the Middle” 问题)
  3. 延迟增加:Token 越多,首字响应时间越长

经验法则:实际使用的上下文控制在窗口的 50%~70%,留余量给输出。


成本计算

Input / Output 分别计价

大部分 API 的输入和输出 Token 分别计价,且输出通常更贵:

def estimate_cost(
    input_tokens: int,
    output_tokens: int,
    model: str = "gpt-4o-mini",
) -> float:
    pricing = {
        "gpt-4o-mini": {"input": 0.15, "output": 0.60},
        "gpt-4o": {"input": 2.50, "output": 10.00},
        "claude-3-5-sonnet": {"input": 3.00, "output": 15.00},
        "deepseek-v3": {"input": 0.14, "output": 0.28},
    }

    prices = pricing.get(model, {"input": 1.0, "output": 2.0})
    cost = (input_tokens * prices["input"] + output_tokens * prices["output"]) / 1_000_000
    return round(cost, 6)


# 一次 RAG 问答的典型成本
input_tokens = 2000   # system prompt + 检索结果 + 用户问题
output_tokens = 500   # AI 回答

print(f"GPT-4o-mini: ${estimate_cost(input_tokens, output_tokens, 'gpt-4o-mini')}")
# $0.0006
print(f"GPT-4o: ${estimate_cost(input_tokens, output_tokens, 'gpt-4o')}")
# $0.01

月成本预估

def monthly_cost(
    daily_requests: int,
    avg_input_tokens: int,
    avg_output_tokens: int,
    model: str,
) -> float:
    per_request = estimate_cost(avg_input_tokens, avg_output_tokens, model)
    return per_request * daily_requests * 30


# 日均 1000 次请求
print(f"GPT-4o-mini: ${monthly_cost(1000, 2000, 500, 'gpt-4o-mini'):.2f}/月")
# $18.00/月
print(f"GPT-4o: ${monthly_cost(1000, 2000, 500, 'gpt-4o'):.2f}/月")
# $300.00/月

动态上下文管理

问题:聊天历史无限增长

用户聊了 50 轮,对话历史可能有几万 Token,远超窗口限制。必须有策略裁剪。

策略 1:滑动窗口

最简单——只保留最近 N 轮对话:

def sliding_window(messages: list[dict], max_turns: int = 10) -> list[dict]:
    system = [m for m in messages if m["role"] == "system"]
    non_system = [m for m in messages if m["role"] != "system"]
    recent = non_system[-(max_turns * 2):]  # 每轮 = user + assistant
    return system + recent

缺点:早期重要信息直接丢了。用户第 1 轮说的需求,到第 15 轮就没了。

策略 2:Token 预算裁剪

不按轮数,按 Token 总量裁剪:

def trim_by_token_budget(
    messages: list[dict],
    max_tokens: int,
    model: str = "gpt-4o",
    reserve_for_output: int = 1000,
) -> list[dict]:
    budget = max_tokens - reserve_for_output

    system = [m for m in messages if m["role"] == "system"]
    non_system = [m for m in messages if m["role"] != "system"]

    system_tokens = count_messages_tokens(system, model)
    remaining = budget - system_tokens

    kept = []
    for msg in reversed(non_system):
        msg_tokens = count_messages_tokens([msg], model)
        if remaining >= msg_tokens:
            kept.insert(0, msg)
            remaining -= msg_tokens
        else:
            break

    return system + kept

策略 3:摘要压缩

把旧的对话历史用 LLM 压缩成摘要,然后只保留摘要 + 最近几轮:

class ConversationSummarizer:
    def __init__(self, client, model: str = "gpt-4o-mini"):
        self.client = client
        self.model = model

    def summarize(self, messages: list[dict]) -> str:
        conversation = "\n".join(
            f"{'用户' if m['role'] == 'user' else '助手'}: {m['content']}"
            for m in messages
            if m["role"] != "system"
        )

        response = self.client.chat.completions.create(
            model=self.model,
            messages=[
                {
                    "role": "system",
                    "content": "请将以下对话历史压缩为简洁的摘要,保留关键信息和决策。200字以内。",
                },
                {"role": "user", "content": conversation},
            ],
            temperature=0,
            max_tokens=300,
        )
        return response.choices[0].message.content


class SmartContextManager:
    def __init__(
        self,
        client,
        model: str = "gpt-4o",
        max_context_tokens: int = 100000,
        summary_threshold: int = 20,
        keep_recent: int = 6,
    ):
        self.client = client
        self.model = model
        self.max_tokens = max_context_tokens
        self.summary_threshold = summary_threshold
        self.keep_recent = keep_recent
        self.summarizer = ConversationSummarizer(client)
        self.summary = ""

    def build_messages(
        self,
        system_prompt: str,
        history: list[dict],
        user_message: str,
    ) -> list[dict]:
        messages = [{"role": "system", "content": system_prompt}]

        # 如果历史超过阈值,压缩旧的部分
        if len(history) > self.summary_threshold:
            old = history[:-self.keep_recent]
            recent = history[-self.keep_recent:]

            self.summary = self.summarizer.summarize(old)
            messages.append({
                "role": "system",
                "content": f"之前的对话摘要:{self.summary}",
            })
            messages.extend(recent)
        else:
            messages.extend(history)

        messages.append({"role": "user", "content": user_message})

        # 最终 Token 预算检查
        total = count_messages_tokens(messages, self.model)
        if total > self.max_tokens * 0.7:
            messages = trim_by_token_budget(messages, self.max_tokens, self.model)

        return messages

策略 4:重要性排序

不是所有消息都同等重要。给消息打分,优先保留重要的:

def importance_score(message: dict) -> float:
    content = message.get("content", "")
    score = 0.5

    # 包含用户明确需求的更重要
    if any(kw in content for kw in ["需求", "要求", "目标", "请", "帮我"]):
        score += 0.3

    # 包含决策结论的更重要
    if any(kw in content for kw in ["决定", "方案", "总结", "结论"]):
        score += 0.2

    # 短消息(确认、闲聊)不太重要
    if len(content) < 10:
        score -= 0.3

    return min(max(score, 0), 1)


def trim_by_importance(
    messages: list[dict],
    max_tokens: int,
    model: str = "gpt-4o",
) -> list[dict]:
    system = [m for m in messages if m["role"] == "system"]
    non_system = [m for m in messages if m["role"] != "system"]

    scored = [(m, importance_score(m)) for m in non_system]
    scored.sort(key=lambda x: x[1], reverse=True)

    budget = max_tokens - count_messages_tokens(system, model) - 1000
    kept = []
    used = 0

    for msg, score in scored:
        msg_tokens = count_messages_tokens([msg], model)
        if used + msg_tokens <= budget:
            kept.append(msg)
            used += msg_tokens

    # 恢复原始顺序
    order = {id(m): i for i, m in enumerate(non_system)}
    kept.sort(key=lambda m: order.get(id(m), 0))

    return system + kept

即插即用中间件

把上面的逻辑封装成中间件,透明地在 AI 调用前自动处理上下文:

class TokenMiddleware:
    def __init__(
        self,
        model: str = "gpt-4o",
        max_context_ratio: float = 0.7,
        strategy: str = "trim",
    ):
        self.model = model
        model_limits = {
            "gpt-4o-mini": 128000,
            "gpt-4o": 128000,
            "claude-3-5-sonnet": 200000,
            "deepseek-v3": 64000,
        }
        self.max_tokens = int(model_limits.get(model, 128000) * max_context_ratio)
        self.strategy = strategy

    def process(self, messages: list[dict]) -> dict:
        input_tokens = count_messages_tokens(messages, self.model)

        if input_tokens <= self.max_tokens:
            return {
                "messages": messages,
                "input_tokens": input_tokens,
                "trimmed": False,
            }

        if self.strategy == "trim":
            trimmed = trim_by_token_budget(messages, self.max_tokens, self.model)
        elif self.strategy == "importance":
            trimmed = trim_by_importance(messages, self.max_tokens, self.model)
        else:
            trimmed = sliding_window(messages, max_turns=10)

        return {
            "messages": trimmed,
            "input_tokens": count_messages_tokens(trimmed, self.model),
            "trimmed": True,
            "original_tokens": input_tokens,
        }


# 使用
middleware = TokenMiddleware(model="gpt-4o-mini", strategy="trim")

result = middleware.process(messages)
if result["trimmed"]:
    print(f"上下文已裁剪: {result['original_tokens']}{result['input_tokens']} tokens")

response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=result["messages"],
)

实用工具函数

Token 用量追踪器

class TokenTracker:
    def __init__(self):
        self.total_input = 0
        self.total_output = 0
        self.request_count = 0
        self.history = []

    def record(self, model: str, input_tokens: int, output_tokens: int):
        cost = estimate_cost(input_tokens, output_tokens, model)
        self.total_input += input_tokens
        self.total_output += output_tokens
        self.request_count += 1
        self.history.append({
            "model": model,
            "input": input_tokens,
            "output": output_tokens,
            "cost": cost,
            "timestamp": time.time(),
        })

    def report(self) -> dict:
        total_cost = sum(h["cost"] for h in self.history)
        return {
            "requests": self.request_count,
            "total_input_tokens": self.total_input,
            "total_output_tokens": self.total_output,
            "total_cost": f"${total_cost:.4f}",
            "avg_input_per_request": self.total_input // max(self.request_count, 1),
            "avg_output_per_request": self.total_output // max(self.request_count, 1),
        }

max_tokens 该设多少

max_tokens 限制的是输出 Token 数量。设太小回答被截断,设太大浪费预算:

场景推荐 max_tokens
简短回答(是/否、分类)50~100
知识库问答500~1000
长文生成2000~4000
代码生成1000~3000
JSON 提取200~500
def smart_max_tokens(intent: str) -> int:
    defaults = {
        "chitchat": 100,
        "knowledge_query": 800,
        "data_query": 300,
        "code_generation": 2000,
        "structured_output": 500,
    }
    return defaults.get(intent, 1000)

总结

  1. Token 是 LLM 的计量单位——中文约 0.6 token/字,必须用 tiktoken 精确计算,不能估算。
  2. 上下文窗口大不等于随便用——成本线性增长,注意力稀释,控制在窗口的 50~70%。
  3. Input/Output 分别计价——输出通常更贵,用 max_tokens 控制输出长度。
  4. 四种上下文管理策略:滑动窗口(最简单)→ Token 预算裁剪(推荐)→ 摘要压缩(最智能)→ 重要性排序(最精细)。
  5. 中间件模式——TokenMiddleware 透明处理,业务代码无需关心 Token 限制。
  6. 追踪成本——TokenTracker 记录每次调用的用量和花费,月底不再惊讶。

下一篇是本系列最后一篇:大模型微调实战。Prompt 调不动了、RAG 也不够时,怎么用 LoRA 低成本微调一个垂直领域模型?


下一篇预告11 | 大模型微调实战:从 LoRA 到部署上线的完整方案


讨论话题:你的 AI 应用一个月花多少 Token 钱?有做过成本优化吗?评论区聊聊。

评论