想象你要出一次远门,面前摆着一只容量固定的行李箱。你想带上的每件衣服、每瓶洗漱用品,都像大模型眼中的 Token。箱子装不下时,要么顶破拉链,要么只能忍痛放弃一部分物品。大模型的“上下文窗口”就是这只行李箱,Token 就是行李,而开发者就是那位既要装得多、又怕超重的旅行整理师。本文会带你搞懂三件事:如何数清每一件行李(Token 计数)、如何安排空间避免爆箱(上下文限制管理)、以及超载时如何取舍与压缩(截断、压缩、滑动窗口)。学会了这些,你的聊天机器人、文档问答、代码助手就能在有限窗口内稳定运行,既不报错,也不丢关键信息。

岗位关联

对 AI 从业者来说,Token 计数与上下文管理是应用工程的“精算基本功”。掌握它,你开发的 LLM 应用才能低成本、低故障地服务真实用户,而不是在长对话和长文档面前频繁翻车。

目录
  1. 可视化演示
  2. Token 基础认知
  3. 上下文窗口机制
  4. Token 计数方法
  5. 超长截断策略
  6. 对话历史压缩
  7. 滑动窗口管理
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示LLM上下文窗口管理核心策略:Token预算分配(input+output)、截断与滑动窗口(保留最新N轮)、对话历史压缩(摘要替代原始文本)和tiktoken预防检测机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

'+captions.window+'

Token 基础认知

出发前,你并不会把整个行李箱当作一件“行李”来看待,而是先把衣物拆成一件件小物件:上衣、裤子、袜子、洗漱包。在大模型的世界里,分词器就是拆行李的师傅,它把一段文本拆成一个个 Token。一个 Token 可能是一个英文单词的一部分,也可能是一个完整的汉字。你问模型“你好”,模型看到的不是两个字符,而是两个 Token。

Token 是大模型处理文本的最小单位,也是计费的基本单位。你发送提示词和接收回复,都要按 Token 数付费。中英文的“体积”差别很大:一句 100 个汉字的文本大约消耗 100 到 200 个 Token,而 100 个英文单词大约消耗 130 个左右的 Token。不同模型有各自的分词表,就像不同航空公司的行李尺寸标准并不完全相同。

估算 Token 数时,虽然不同模型差异不小,但社区里常用一个粗略经验值来表示不同语言与 Token 的折算关系。这个公式并不精确,却能帮你在开发早期快速判断一段文本的重量,避免把窗口撑爆,公式如下:

这个公式只是估算,真正确认行李重量,要用模型自带的 tokenizer。就像行李箱上虽然有“大约装 20 kg”的标签,但上了称才知道真实重量。理解 Token 的基础差异后,你才能开始规划上下文空间。

Test=⌈CASCII4⌉+⌈C中文1.5⌉\displaystyle T_{\text{est}} = \left\lceil \frac{C_{\text{ASCII}}}{4} \right\rceil + \left\lceil \frac{C_{\text{中文}}}{1.5} \right\rceil
  1. 统计 ASCII 字符数把英文、数字、半角符号等字符单独计数,平均每 4 个约折合 1 个 Token。
    CASCII4\frac{C_{\text{ASCII}}}{4}
  2. 统计中文字符数大多数词表里 1 个汉字约需 1 到 2 个 Token,经验取 1.5 做估算。
    C中文1.5\frac{C_{\text{中文}}}{1.5}
  3. 向上取整零头不足一个 Token 也要占一个位置,所以对每一部分用天花板函数。
    ⌈⋅⌉\left\lceil \cdot \right\rceil
  4. 相加两部分合计就是整段文本的 Token 估算值。
    Test=⌈CASCII4⌉+⌈C中文1.5⌉T_{\text{est}} = \left\lceil \frac{C_{\text{ASCII}}}{4} \right\rceil + \left\lceil \frac{C_{\text{中文}}}{1.5} \right\rceil

上下文窗口机制

行李箱的容量是固定的,大模型的“上下文窗口”就是那只行李箱。它规定了模型在一次请求中,最多能同时“看到”多少 Token。这个数字通常写在模型规格里,比如 4K、8K、32K、128K。窗口不是输入专用的,而是输入和输出共享的:你塞进箱子里的提示词、历史对话、系统指令,以及模型将要生成的回复,全都占同一个空间。

很多新手以为窗口限制只针对输入,却忽略了输出也占地方。假设窗口 8K,你输入 7K,模型最多只能生成 1K,再想多写就会报错或截断。更严谨地说,窗口约束可以写成:

理解了这个不等式,你自然会想到:要把行李箱装满而不爆,就得提前留出放“回复”的位置。这也是为什么好的应用不会把窗口塞到 100%,而是留出安全边际。超限的报错就像是行李箱拉链崩开的警告,正确的做法是在拉链崩开前主动调整。

Lin+Lout≤Lwindow\displaystyle L_{\text{in}} + L_{\text{out}} \leq L_{\text{window}}
  1. 输入 Token 数系统提示、历史对话、用户问题、工具返回等内容全部累积成输入。
    LinL_{\text{in}}
  2. 输出 Token 数模型回复的每一字都由输出 Token 构成,同样占据窗口。
    LoutL_{\text{out}}
  3. 窗口上限模型一次请求的上下文总容量,超过这个值就无法正常处理。
    LwindowL_{\text{window}}
  4. 不等式约束输入与输出之和必须小于等于窗口上限,这是所有上下文管理的底层约束。
    Lin+Lout≤LwindowL_{\text{in}} + L_{\text{out}} \leq L_{\text{window}}

Token 计数方法

旅行装箱前要逐个称重,Token 计数就是 LLM 应用开发者的“行李秤”。精确的 Token 数必须用模型配套的分词器计算。OpenAI 系的模型常用 tiktoken,开源模型多用 tokenizers 库。调用 API 后,返回结果里通常带有 usage 字段,明确告诉你这一请求用了多少输入 Token、多少输出 Token。比如 usage.prompt_tokens 和 usage.completion_tokens。

在业务代码里,我们应该在发送请求前先估算,拿到响应后再核对。输入和输出要分开统计,因为它们不仅占用同一窗口,计费单价也不同。假设输入 Token 为 T_prompt,输出 Token 为 T_completion,那么本次请求总共消耗:

有了精确的行李秤,你才知道每次请求花了多少钱、占了多少空间。统计输入输出还可以帮你发现异常:如果某次请求输入突然翻倍,多半是历史消息没有清理,或者有人往系统提示词里堆了大段文案——就像箱子里混进了不该带的铁块。

Ttotal=Tprompt+Tcompletion\displaystyle T_{\text{total}} = T_{\text{prompt}} + T_{\text{completion}}
  1. 输入 Token发送给模型的完整 prompt,包括系统指令、历史消息和当前问题。
    TpromptT_{\text{prompt}}
  2. 输出 Token模型生成的 completion 内容,逐段返回并最终计入账单。
    TcompletionT_{\text{completion}}
  3. 总消耗二者相加得到本次请求的总 Token 数,也是窗口占用和费用结算的依据。
    Ttotal=Tprompt+TcompletionT_{\text{total}} = T_{\text{prompt}} + T_{\text{completion}}

超长截断策略

箱子空间不够,最直接的办法是扔掉一些物品,但要有选择地扔。超长截断并非简单地砍掉头部,而是优先保住“系统的核心指令”和“用户最近的需求”。系统提示词像是护照和身份证,丢了就寸步难行;最近的问题像是地图,丢了你就不知道去哪儿。中间的旧闲聊、重复信息,才是优先放弃的部分。

在实现中,你需要先算出一段“可保留历史”的预算。假设窗口上限为 L_window,为输出预留 T_out,再留一块安全边际 T_safe,剩下才是能装历史的额度:

实际截断时,可以采用“保头保尾、压缩中间”的经典结构:系统提示放最前,最近问题放最后,中间放摘要或少量关键消息。这就像整理行李箱时,把最重要的证件放在最上层、目的地衣物放在易取处,最下面才塞不常用的纪念品。

Tkept=Lwindow−Tout−Tsafe\displaystyle T_{\text{kept}} = L_{\text{window}} - T_{\text{out}} - T_{\text{safe}}
  1. 窗口上限当前模型上下文窗口的总长度,所有内容的预算天花板。
    LwindowL_{\text{window}}
  2. 输出预留必须为模型回复保留的空间,否则输出会被截断成半句话。
    ToutT_{\text{out}}
  3. 安全边际为意外内容留出的余量,防止刚好卡在临界点触发随机失败。
    TsafeT_{\text{safe}}
  4. 可保留历史用窗口上限减掉输出预留和安全边际,得到历史消息可占用的最大 Token 数。
    Tkept=Lwindow−Tout−TsafeT_{\text{kept}} = L_{\text{window}} - T_{\text{out}} - T_{\text{safe}}

对话历史压缩

扔东西太心疼,那就试试真空压缩袋。对话历史压缩的目标,是把早期多轮消息用几句话概括,只保留关键事实,再和最近的完整消息拼在一起。比如客服机器人与用户聊了 10 轮,前 8 轮可以压成一句“用户反馈商品发货慢,要求尽快补发”,后 2 轮保持原样。这样一来,窗口占用大幅下降,而核心诉求没有丢失。

压缩后的新历史可以写作:

压缩不是无脑丢历史,还要做好“重要性筛选”:用户的名字、订单号、当前诉求、明确偏好必须保留,寒暄和重复表达可以删。就像真空压缩虽然让衣服变小,但你把身份证也压进去,到了机场就傻眼。所以摘要逻辑里要设置“关键信息清单”。

Tnew=Tsummary+∑i=knTmsgi\displaystyle T_{\text{new}} = T_{\text{summary}} + \sum_{i=k}^{n} T_{\text{msg}_i}
  1. 摘要 Token早期消息经过大模型或规则压缩后的一句话概要。
    TsummaryT_{\text{summary}}
  2. 最近消息区间从第 k 条到当前第 n 条,表示只保留最近的关键对话。
    ∑i=kn\sum_{i=k}^{n}
  3. 消息 Token 求和把最近每一条消息的 Token 数相加,得到占用的空间。
    ∑i=knTmsgi\sum_{i=k}^{n} T_{\text{msg}_i}
  4. 组合新历史摘要与最近消息相加,构成新的对话历史,供模型读取。
    Tnew=Tsummary+∑i=knTmsgiT_{\text{new}} = T_{\text{summary}} + \sum_{i=k}^{n} T_{\text{msg}_i}

滑动窗口管理

旅行途中你不断买新的纪念品,行李箱装不下,那就把最早买的纪念品先寄回家。滑动窗口管理做的就是这件事:在对话系统里维护一个固定长度的消息队列,每来一条新消息就塞进去,如果超限,就从队头弹出最旧的、非关键的旧消息。窗口始终装着“最新 + 最重要”的内容。

完整的窗口占用包括系统提示词、压缩摘要、最近消息和输出预算。约束关系可以表示为:

实际工程里还要配合“动态控制”:实时监控当前占用率,超过阈值就触发压缩或截断,空闲时则完整保留历史。这就像开车时盯着油表,油量不足就提前找加油站,而不是等停在高速上才后悔。滑动窗口看似简单,却能让长会话应用保持低延迟、低成本。

Tsystem+∑i=knTmsgi+Tout≤Lwindow\displaystyle T_{\text{system}} + \sum_{i=k}^{n} T_{\text{msg}_i} + T_{\text{out}} \leq L_{\text{window}}
  1. 系统提示词始终保留的模型指令,是上下文的“地基”。
    TsystemT_{\text{system}}
  2. 最近消息范围只保留第 k 条到第 n 条消息,更早的记录在滑动中移出窗口。
    ∑i=knTmsgi\sum_{i=k}^{n} T_{\text{msg}_i}
  3. 输出预算为模型回复预留的空间。
    ToutT_{\text{out}}
  4. 窗口约束所有占用相加不超过窗口上限,否则继续淘汰旧消息或触发压缩。
    Tsystem+∑i=knTmsgi+Tout≤LwindowT_{\text{system}} + \sum_{i=k}^{n} T_{\text{msg}_i} + T_{\text{out}} \leq L_{\text{window}}

AI 应用场景

智能客服机器人的历史压缩

阶段:上下文管理

解决的问题:传统客服系统每轮对话都携带全部历史,用户多聊几轮就超出窗口,直接报错或丢失早期诉求。AI 方案在对话过程中持续维护“用户诉求摘要”,用摘要加最近消息拼成 prompt,既保留关键信息,又控制 token 占用。

具体案例:某电商客服机器人会在上下文占用超过 70% 时,调用大模型把前几轮消息压缩成“用户反馈收货慢,要求补发赠品”这类摘要,再与最新两轮消息一起发给模型,使单次会话支持 50 轮以上流畅对话。

长文档问答的分块截断

阶段:数据处理与检索

解决的问题:一份合同可能几万字,直接塞进窗口必然超限。AI 方案先把文档按章节切块,再利用向量检索找出与问题最相关的几个块,拼进 prompt,避免截断丢失关键条款。

具体案例:法律合同问答系统把每份合同切成 500 Token 的块并建立索引,用户提问后只检索 top-3 相关块,加上问题本身,总 token 控制在 2K 以内,既准确又省钱。

代码助手的滑动窗口

阶段:会话管理

解决的问题:IDE 内与代码助手对话时,早期讨论的文件内容越来越多,占用窗口影响最新代码生成。AI 方案维护一个滑动窗口,只保留最近打开的代码片段、报错信息和用户当前问题,旧讨论自动淘汰。

具体案例:某代码助手在连续对话时记录最近 6 条消息,并在窗口接近上限时自动移除最早的消息;若用户提到“刚才那个 Bug”,系统再从本地缓存找回相关片段补入上下文。开发者无需手动清理历史,也能保持稳定输出。

Python 演示

python61 行
# -*- coding: utf-8 -*-"""模拟 LLM 应用的 Token 预算管理工具。"""import sys  class TokenBudgetManager:    def __init__(self, window_size=4096, reserve_output=1024):        self.window_size = window_size        self.reserve_output = reserve_output        self.history = []        self.compress_count = 0     @staticmethod    def count_tokens(text):        ascii_chars = sum(1 for ch in text if ord(ch) < 128)        other_chars = max(0, len(text) - ascii_chars)        return int(ascii_chars / 4 + other_chars * 1.5)     def effective_limit(self):        return self.window_size - self.reserve_output     def add_message(self, role, content):        self.history.append({"role": role, "content": content})        self._enforce_limit()     def _compress_history(self):        if len(self.history) < 4:            return False        mid = len(self.history) // 2        old = self.history[:mid]        combined = ";".join(item["content"] for item in old)        self.history = [{"role": "system", "content": "前文摘要:" + combined[:60]}] + self.history[mid:]        self.compress_count += 1        return True     def _enforce_limit(self):        while self._used_tokens() > self.effective_limit():            if self._compress_history():                continue            if not self.history:                break            self.history.pop(0)     def _used_tokens(self):        return sum(self.count_tokens(item["content"]) for item in self.history)  def main():    manager = TokenBudgetManager(window_size=80, reserve_output=10)    for i in range(1, 9):        manager.add_message("user", "第%d轮问题" % i + "细节" * 5)        manager.add_message("assistant", "第%d轮回答" % i + "信息" * 4)        print("第%d轮后占用:" % i, manager._used_tokens())    print("压缩次数:", manager.compress_count)    print("最终消息数:", len(manager.history))    for msg in manager.history:        print(msg["role"], msg["content"][:20])  if __name__ == "__main__":    main()
目的:用一个小类演示了 Token 估算、窗口预算、对话压缩和滑动窗口淘汰逻辑,可直接运行观察不同策略如何控制上下文占用。
  1. self.window_size = window_size设置上下文窗口总上限,相当于行李箱容量。
  2. return int(ascii_chars / 4 + other_chars * 1.5)按字符数粗略估算 Token,英文约 4 字符 1 Token,中文约 1.5 字符 1 Token。
  3. def effective_limit(self): return self.window_size - self.reserve_output输出预留之外的空间才是历史消息的可用预算。
  4. self.history.append({"role": role, "content": content})每条消息进入队列,模拟多轮对话持续累加。
  5. while self._used_tokens() > self.effective_limit():每次新增消息后循环检查是否超限,超限就触发处理。
  6. _compress_history把最早一半消息合并成一句摘要,相当于用真空压缩袋节省空间。
  7. self.history.pop(0)从队头移除最旧消息,实现滑动窗口的“旧消息淘汰”。

常见误区

误区我以为 token 数量就是字数,写多少字就消耗多少 token,统计字数就能估算成本。

正解Token 与字数不是 1:1。英文常 4 个字符约等于 1 个 Token,中文 1 个字约需要 1 到 2 个 Token,不同分词器结果也不同。如果按字数估算成本,误差会很大。这就像行李箱按件数算,但羽绒服和袜子占的空间完全不同。

为何常见:因为人在阅读时以字或词为单位,而模型内部使用子词 token,两者的统计口径天然不同。

误区我一直以为上下文窗口只限制输入长度,输出是另外算的,不会占窗口空间。

正解输入和输出共享同一个窗口。比如 8K 窗口,你塞了 6K 输入,输出最多只剩 2K,超出就会报错或截断。这像行李箱的总容量是固定的,不会因为装的是礼物还是自用衣物就额外扩容。

为何常见:很多 API 文档把输入输出分开计费,容易让人误以为窗口空间也分开。

误区既然窗口放不下,把前面的文字直接删掉就好,模型看后面也能懂。

正解直接砍头往往丢掉系统提示和早期重要指令,导致模型行为异常。正确做法是保留开头结尾、压缩中间旧内容;长文档则应该分块检索后再拼接。这就像旅行时把护照扔了,虽然箱子轻了,但寸步难行。

为何常见:因为人的记忆有“近因效应”,让人觉得结尾比开头更重要,但模型任务往往依赖开头定义的角色和规则。

误区压缩历史无非就是把旧消息删掉,删得越多越省 token,反正用户不会回头翻。

正解无脑删历史会丢失用户需求、偏好等关键信息。压缩应结合摘要、意图抽取和重要性筛选,保留关键事实。就像把衣服真空压缩而不是扔掉,重要物品依然要留下。

为何常见:简单删除实现成本低,所以容易被采用,但后续模型总是忘记用户说过的话,体验反而更差。

误区模型都支持 128K 窗口了,随便塞,不需要做任何上下文管理。

正解窗口越大,成本越高、延迟越长,而且模型对长上下文中部内容的关注度会下降。即使有大箱子,也不该把所有东西都塞进去。预算管理、压缩和滑动窗口依然是必要的。

为何常见:厂商宣传大窗口制造了“无限容量”的错觉,但实际推理开销和质量限制依然存在。

面试题

Q1. 在 LLM 中,Token 最准确的说法是?

单选题 · easy

  • A.模型处理文本的最小基本单位← 正确答案
  • B.输入文字的单个字符
  • C.一条完整的用户消息
  • D.上下文窗口的长度数字

答案:A。Token 是分词后的最小单位,既不是单个字符,也不是完整消息或窗口长度。它更像是把行李拆成的一件件小物品,模型以 Token 为单位理解文本。

Q2. 下面关于上下文窗口的理解,正确的是?

单选题 · easy

  • A.指模型训练数据的数量大小
  • B.指模型一次能容纳的输入输出总长度← 正确答案
  • C.指服务器允许的请求并发上限
  • D.指模型支持的语言种类数量

答案:B。上下文窗口是单次请求可处理的总 Token 数,输入和输出共享该空间。它和训练数据量、并发上限、语言数量都没有关系。

Q3. 当 prompt 与输出总 token 数超过窗口限制时,常见结果是?

单选题 · easy

  • A.请求自动排队等待更长窗口
  • B.模型自动扩展窗口大小
  • C.报错或输出被截断← 正确答案
  • D.费用自动变为免费

答案:C。超过窗口上限时,API 通常会直接报错,或者截断模型输出。模型不会自动扩窗,更不会免费。这就像行李箱拉链崩开,东西只能掉出来而不是变出更大空间。

Q4. 粗略估算英文文本 token 数,以下哪个经验规则最常用?

单选题 · mid

  • A.每 1 个字符约等于 1 token
  • B.每 4 个字符约等于 1 token← 正确答案
  • C.每 100 个字符约等于 1 token
  • D.每 10 个字符约等于 1 token

答案:B。英文大约每 4 个字符折合 1 个 Token,这是社区常用的经验值。如果按 1 字符 1 Token 估算,会严重高估成本。

Q5. 某模型上下文窗口为 4096,设置 max_tokens=1024,当前 prompt 已用 3300 token,最合适的做法是?

单选题 · mid

  • A.直接发送请求,等待模型自动处理
  • B.压缩或截断 prompt,使总占用低于 3072← 正确答案
  • C.提高 max_tokens 到 4096
  • D.降低上下文窗口长度到 2048

答案:B。输入 3300 加输出 1024 已经超过 4096,所以需要把 prompt 压到 3072 以下。提高 max_tokens 只会让超限更严重,降低窗口长度不解决根本问题。

Q6. 关于对话历史摘要压缩的说法,正确的有?

多选题 · mid

  • A.压缩后历史占用 token 更少← 正确答案
  • B.摘要可能丢失部分细节← 正确答案
  • C.摘要必须逐字保留原始消息
  • D.摘要可以与最近消息拼成新历史← 正确答案

答案:ABD。摘要压缩用少量 token 概括早期消息,会牺牲部分细节,所以不能逐字保留。压缩后需要和最近消息拼接在一起作为新历史。这就像真空压缩袋减小体积,但压得太狠也会让衣服起皱。

Q7. 滑动窗口策略中,窗口移动时最先淘汰的是?

单选题 · hard

  • A.最新的系统提示词
  • B.最近一条用户消息
  • C.最旧的非关键历史消息← 正确答案
  • D.模型刚生成的回答

答案:C。滑动窗口遵循“新进旧出”原则,最先淘汰最旧的非关键消息。系统提示词通常固定保留,最新消息和刚生成的回答都要保留给模型使用。

Q8. 下列哪些方法可以降低请求中的 token 占用?

多选题 · mid

  • A.对历史对话做摘要压缩← 正确答案
  • B.只保留最近几条消息← 正确答案
  • C.长文档分块后只发送相关块← 正确答案
  • D.把输入文字全部改成大写

答案:ABC。摘要压缩、保留最近消息、分块检索都能有效减少 token。全部改成大写不会减少 token,反而可能改变分词结果甚至增加长度。

Q9. 关于输入输出 token 核算,正确的说法有?

多选题 · hard

  • A.输入输出共享同一个上下文窗口← 正确答案
  • B.prompt 和 completion 分别计数← 正确答案
  • C.输出 token 通常单价高于输入 token← 正确答案
  • D.输出 token 不会计入账单

答案:ABC。输入和输出共享窗口,API 通常会分别返回 prompt_tokens 和 completion_tokens,且输出单价往往更高。只要生成了输出,就一定会计入账单。

Q10. 某 8K 窗口模型,预留 1K 输出,系统提示词固定占 0.5K,当前摘要占 0.3K,若要加入最近对话,最近对话 token 数最多约为?

单选题 · extreme

  • A.8K 减去 1K 和 0.5K,等于 6.5K
  • B.8K 减去 1K、0.5K 和 0.3K,等于 6.2K← 正确答案
  • C.8K 减去 1K,等于 7K
  • D.8K 减去 0.5K,等于 7.5K

答案:B。窗口总容量等于系统提示、摘要、最近对话和输出预留之和,因此最近对话最多为 8K-1K-0.5K-0.3K=6.2K。不能漏掉摘要或系统提示占用的空间。