想象你要出一次远门,面前摆着一只容量固定的行李箱。你想带上的每件衣服、每瓶洗漱用品,都像大模型眼中的 Token。箱子装不下时,要么顶破拉链,要么只能忍痛放弃一部分物品。大模型的“上下文窗口”就是这只行李箱,Token 就是行李,而开发者就是那位既要装得多、又怕超重的旅行整理师。本文会带你搞懂三件事:如何数清每一件行李(Token 计数)、如何安排空间避免爆箱(上下文限制管理)、以及超载时如何取舍与压缩(截断、压缩、滑动窗口)。学会了这些,你的聊天机器人、文档问答、代码助手就能在有限窗口内稳定运行,既不报错,也不丢关键信息。
对 AI 从业者来说,Token 计数与上下文管理是应用工程的“精算基本功”。掌握它,你开发的 LLM 应用才能低成本、低故障地服务真实用户,而不是在长对话和长文档面前频繁翻车。
可视化演示
展示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 的基础差异后,你才能开始规划上下文空间。
- 统计 ASCII 字符数把英文、数字、半角符号等字符单独计数,平均每 4 个约折合 1 个 Token。
- 统计中文字符数大多数词表里 1 个汉字约需 1 到 2 个 Token,经验取 1.5 做估算。
- 向上取整零头不足一个 Token 也要占一个位置,所以对每一部分用天花板函数。
- 相加两部分合计就是整段文本的 Token 估算值。
上下文窗口机制
行李箱的容量是固定的,大模型的“上下文窗口”就是那只行李箱。它规定了模型在一次请求中,最多能同时“看到”多少 Token。这个数字通常写在模型规格里,比如 4K、8K、32K、128K。窗口不是输入专用的,而是输入和输出共享的:你塞进箱子里的提示词、历史对话、系统指令,以及模型将要生成的回复,全都占同一个空间。
很多新手以为窗口限制只针对输入,却忽略了输出也占地方。假设窗口 8K,你输入 7K,模型最多只能生成 1K,再想多写就会报错或截断。更严谨地说,窗口约束可以写成:
理解了这个不等式,你自然会想到:要把行李箱装满而不爆,就得提前留出放“回复”的位置。这也是为什么好的应用不会把窗口塞到 100%,而是留出安全边际。超限的报错就像是行李箱拉链崩开的警告,正确的做法是在拉链崩开前主动调整。
- 输入 Token 数系统提示、历史对话、用户问题、工具返回等内容全部累积成输入。
- 输出 Token 数模型回复的每一字都由输出 Token 构成,同样占据窗口。
- 窗口上限模型一次请求的上下文总容量,超过这个值就无法正常处理。
- 不等式约束输入与输出之和必须小于等于窗口上限,这是所有上下文管理的底层约束。
Token 计数方法
旅行装箱前要逐个称重,Token 计数就是 LLM 应用开发者的“行李秤”。精确的 Token 数必须用模型配套的分词器计算。OpenAI 系的模型常用 tiktoken,开源模型多用 tokenizers 库。调用 API 后,返回结果里通常带有 usage 字段,明确告诉你这一请求用了多少输入 Token、多少输出 Token。比如 usage.prompt_tokens 和 usage.completion_tokens。
在业务代码里,我们应该在发送请求前先估算,拿到响应后再核对。输入和输出要分开统计,因为它们不仅占用同一窗口,计费单价也不同。假设输入 Token 为 T_prompt,输出 Token 为 T_completion,那么本次请求总共消耗:
有了精确的行李秤,你才知道每次请求花了多少钱、占了多少空间。统计输入输出还可以帮你发现异常:如果某次请求输入突然翻倍,多半是历史消息没有清理,或者有人往系统提示词里堆了大段文案——就像箱子里混进了不该带的铁块。
- 输入 Token发送给模型的完整 prompt,包括系统指令、历史消息和当前问题。
- 输出 Token模型生成的 completion 内容,逐段返回并最终计入账单。
- 总消耗二者相加得到本次请求的总 Token 数,也是窗口占用和费用结算的依据。
超长截断策略
箱子空间不够,最直接的办法是扔掉一些物品,但要有选择地扔。超长截断并非简单地砍掉头部,而是优先保住“系统的核心指令”和“用户最近的需求”。系统提示词像是护照和身份证,丢了就寸步难行;最近的问题像是地图,丢了你就不知道去哪儿。中间的旧闲聊、重复信息,才是优先放弃的部分。
在实现中,你需要先算出一段“可保留历史”的预算。假设窗口上限为 L_window,为输出预留 T_out,再留一块安全边际 T_safe,剩下才是能装历史的额度:
实际截断时,可以采用“保头保尾、压缩中间”的经典结构:系统提示放最前,最近问题放最后,中间放摘要或少量关键消息。这就像整理行李箱时,把最重要的证件放在最上层、目的地衣物放在易取处,最下面才塞不常用的纪念品。
- 窗口上限当前模型上下文窗口的总长度,所有内容的预算天花板。
- 输出预留必须为模型回复保留的空间,否则输出会被截断成半句话。
- 安全边际为意外内容留出的余量,防止刚好卡在临界点触发随机失败。
- 可保留历史用窗口上限减掉输出预留和安全边际,得到历史消息可占用的最大 Token 数。
对话历史压缩
扔东西太心疼,那就试试真空压缩袋。对话历史压缩的目标,是把早期多轮消息用几句话概括,只保留关键事实,再和最近的完整消息拼在一起。比如客服机器人与用户聊了 10 轮,前 8 轮可以压成一句“用户反馈商品发货慢,要求尽快补发”,后 2 轮保持原样。这样一来,窗口占用大幅下降,而核心诉求没有丢失。
压缩后的新历史可以写作:
压缩不是无脑丢历史,还要做好“重要性筛选”:用户的名字、订单号、当前诉求、明确偏好必须保留,寒暄和重复表达可以删。就像真空压缩虽然让衣服变小,但你把身份证也压进去,到了机场就傻眼。所以摘要逻辑里要设置“关键信息清单”。
- 摘要 Token早期消息经过大模型或规则压缩后的一句话概要。
- 最近消息区间从第 k 条到当前第 n 条,表示只保留最近的关键对话。
- 消息 Token 求和把最近每一条消息的 Token 数相加,得到占用的空间。
- 组合新历史摘要与最近消息相加,构成新的对话历史,供模型读取。
滑动窗口管理
旅行途中你不断买新的纪念品,行李箱装不下,那就把最早买的纪念品先寄回家。滑动窗口管理做的就是这件事:在对话系统里维护一个固定长度的消息队列,每来一条新消息就塞进去,如果超限,就从队头弹出最旧的、非关键的旧消息。窗口始终装着“最新 + 最重要”的内容。
完整的窗口占用包括系统提示词、压缩摘要、最近消息和输出预算。约束关系可以表示为:
实际工程里还要配合“动态控制”:实时监控当前占用率,超过阈值就触发压缩或截断,空闲时则完整保留历史。这就像开车时盯着油表,油量不足就提前找加油站,而不是等停在高速上才后悔。滑动窗口看似简单,却能让长会话应用保持低延迟、低成本。
- 系统提示词始终保留的模型指令,是上下文的“地基”。
- 最近消息范围只保留第 k 条到第 n 条消息,更早的记录在滑动中移出窗口。
- 输出预算为模型回复预留的空间。
- 窗口约束所有占用相加不超过窗口上限,否则继续淘汰旧消息或触发压缩。
AI 应用场景
智能客服机器人的历史压缩
阶段:上下文管理
解决的问题:传统客服系统每轮对话都携带全部历史,用户多聊几轮就超出窗口,直接报错或丢失早期诉求。AI 方案在对话过程中持续维护“用户诉求摘要”,用摘要加最近消息拼成 prompt,既保留关键信息,又控制 token 占用。
具体案例:某电商客服机器人会在上下文占用超过 70% 时,调用大模型把前几轮消息压缩成“用户反馈收货慢,要求补发赠品”这类摘要,再与最新两轮消息一起发给模型,使单次会话支持 50 轮以上流畅对话。
长文档问答的分块截断
阶段:数据处理与检索
解决的问题:一份合同可能几万字,直接塞进窗口必然超限。AI 方案先把文档按章节切块,再利用向量检索找出与问题最相关的几个块,拼进 prompt,避免截断丢失关键条款。
具体案例:法律合同问答系统把每份合同切成 500 Token 的块并建立索引,用户提问后只检索 top-3 相关块,加上问题本身,总 token 控制在 2K 以内,既准确又省钱。
代码助手的滑动窗口
阶段:会话管理
解决的问题:IDE 内与代码助手对话时,早期讨论的文件内容越来越多,占用窗口影响最新代码生成。AI 方案维护一个滑动窗口,只保留最近打开的代码片段、报错信息和用户当前问题,旧讨论自动淘汰。
具体案例:某代码助手在连续对话时记录最近 6 条消息,并在窗口接近上限时自动移除最早的消息;若用户提到“刚才那个 Bug”,系统再从本地缓存找回相关片段补入上下文。开发者无需手动清理历史,也能保持稳定输出。
Python 演示
# -*- 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()
self.window_size = window_size设置上下文窗口总上限,相当于行李箱容量。return int(ascii_chars / 4 + other_chars * 1.5)按字符数粗略估算 Token,英文约 4 字符 1 Token,中文约 1.5 字符 1 Token。def effective_limit(self): return self.window_size - self.reserve_output输出预留之外的空间才是历史消息的可用预算。self.history.append({"role": role, "content": content})每条消息进入队列,模拟多轮对话持续累加。while self._used_tokens() > self.effective_limit():每次新增消息后循环检查是否超限,超限就触发处理。_compress_history把最早一半消息合并成一句摘要,相当于用真空压缩袋节省空间。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 最准确的说法是?
- A.模型处理文本的最小基本单位← 正确答案
- B.输入文字的单个字符
- C.一条完整的用户消息
- D.上下文窗口的长度数字
答案:A。Token 是分词后的最小单位,既不是单个字符,也不是完整消息或窗口长度。它更像是把行李拆成的一件件小物品,模型以 Token 为单位理解文本。
Q2. 下面关于上下文窗口的理解,正确的是?
- A.指模型训练数据的数量大小
- B.指模型一次能容纳的输入输出总长度← 正确答案
- C.指服务器允许的请求并发上限
- D.指模型支持的语言种类数量
答案:B。上下文窗口是单次请求可处理的总 Token 数,输入和输出共享该空间。它和训练数据量、并发上限、语言数量都没有关系。
Q3. 当 prompt 与输出总 token 数超过窗口限制时,常见结果是?
- A.请求自动排队等待更长窗口
- B.模型自动扩展窗口大小
- C.报错或输出被截断← 正确答案
- D.费用自动变为免费
答案:C。超过窗口上限时,API 通常会直接报错,或者截断模型输出。模型不会自动扩窗,更不会免费。这就像行李箱拉链崩开,东西只能掉出来而不是变出更大空间。
Q4. 粗略估算英文文本 token 数,以下哪个经验规则最常用?
- 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,最合适的做法是?
- A.直接发送请求,等待模型自动处理
- B.压缩或截断 prompt,使总占用低于 3072← 正确答案
- C.提高 max_tokens 到 4096
- D.降低上下文窗口长度到 2048
答案:B。输入 3300 加输出 1024 已经超过 4096,所以需要把 prompt 压到 3072 以下。提高 max_tokens 只会让超限更严重,降低窗口长度不解决根本问题。
Q6. 关于对话历史摘要压缩的说法,正确的有?
- A.压缩后历史占用 token 更少← 正确答案
- B.摘要可能丢失部分细节← 正确答案
- C.摘要必须逐字保留原始消息
- D.摘要可以与最近消息拼成新历史← 正确答案
答案:ABD。摘要压缩用少量 token 概括早期消息,会牺牲部分细节,所以不能逐字保留。压缩后需要和最近消息拼接在一起作为新历史。这就像真空压缩袋减小体积,但压得太狠也会让衣服起皱。
Q7. 滑动窗口策略中,窗口移动时最先淘汰的是?
- A.最新的系统提示词
- B.最近一条用户消息
- C.最旧的非关键历史消息← 正确答案
- D.模型刚生成的回答
答案:C。滑动窗口遵循“新进旧出”原则,最先淘汰最旧的非关键消息。系统提示词通常固定保留,最新消息和刚生成的回答都要保留给模型使用。
Q8. 下列哪些方法可以降低请求中的 token 占用?
- A.对历史对话做摘要压缩← 正确答案
- B.只保留最近几条消息← 正确答案
- C.长文档分块后只发送相关块← 正确答案
- D.把输入文字全部改成大写
答案:ABC。摘要压缩、保留最近消息、分块检索都能有效减少 token。全部改成大写不会减少 token,反而可能改变分词结果甚至增加长度。
Q9. 关于输入输出 token 核算,正确的说法有?
- A.输入输出共享同一个上下文窗口← 正确答案
- B.prompt 和 completion 分别计数← 正确答案
- C.输出 token 通常单价高于输入 token← 正确答案
- D.输出 token 不会计入账单
答案:ABC。输入和输出共享窗口,API 通常会分别返回 prompt_tokens 和 completion_tokens,且输出单价往往更高。只要生成了输出,就一定会计入账单。
Q10. 某 8K 窗口模型,预留 1K 输出,系统提示词固定占 0.5K,当前摘要占 0.3K,若要加入最近对话,最近对话 token 数最多约为?
- 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。不能漏掉摘要或系统提示占用的空间。