做 AI 应用的朋友大概率都遇到过这样的深夜场景用户连续追问了几轮你精心设计的 Prompt 还在工作代码也没问题结果请求一发出去API 直接甩回来一句This models maximum context length is 8192 tokens ... you requested 8500 tokens。那一刻你才意识到真正卡住你的不是模型能力而是上下文context这堵墙。context-mode 这个模式本质上就是专门解决这类问题的在有限的上下文窗口内怎么组织、筛选、压缩、缓存输入给模型的所有内容让它在不爆 token 的前提下尽量记住该记住的、忘掉该忘掉的。这篇文章不是给你讲概念而是从一次真实的超限事故入手把四种主流 context-mode 的取舍、一个可以直接抄走的 Python 实现以及我踩过的几个坑全部摊开讲清楚。适合正在做 LLM 应用开发、Agent 编排或个人知识库问答的开发者参考。1. 理解 context-mode先搞清楚 context 里到底装了什么1.1 五个常见来源少一个你都会踩坑很多人一提 context 就想到聊天记录这是最典型的误解。我在项目里拆过一遍实际发到模型的 payloadcontext 至少包含五个来源系统提示词system prompt、少量示例few-shot、对话历史多轮 user/assistant 消息、外部检索结果RAG 召回的知识片段、以及工具调用返回的结果。这五个来源里系统提示词最容易犯的错是越写越长。团队里经常有人往 system prompt 里堆规则今天加一条请用中文回答明天加一条不要编造事实后天又加一条如果不知道就说不知道结果 system 部分就吃掉一两千 token。而工具调用返回的结果很多人会忽略它其实占的是 assistant 消息里的 function_call 和 tool 消息的 content这些内容往往是一大段 JSON非常占地方。我通常建议在项目初期就把这五类内容分开存储、统一打上元数据来源、时间、优先级而不是混在一个数组里。这样后续做裁剪、压缩、检索才有抓手。否则等上线之后发现 context 爆了你想优化都无从下手只能把窗口调大继续烧钱。1.2 context 不是越大越好窗口、成本、延迟的三角关系有一个很反直觉的现象即使模型支持很大的上下文窗口你也不应该把所有内容都塞进去。这里存在一个三角关系窗口越大token 越多成本线性上涨同时模型处理更长输入需要更多计算延迟明显增加更隐蔽的是相关性稀释——当上下文里塞了几万 token 的无关内容时模型对关键信息的注意力会被分散回答质量反而下降。我之前做过一个对比实验同样一个问题一个请求只带 2000 token 的精准上下文另一个请求把整本产品手册的 30000 token 全塞进去。结果前者的回答准确率高出不少而且响应速度快了将近一倍。这就是为什么 context-mode 的核心不是怎么把窗口用满而是怎么用最少的 token 达到最好的效果。所以在设计阶段我给项目定的目标是正常情况下把单次请求的 token 控制在窗口上限的 80% 以内留出 20% 给模型生成答案。这个预留比例看起来很保守但实际运行中经常会有突发情况比如某次检索结果特别多或者用户突然贴了一大段文本预留的空间能救你一命。1.3 判断你的业务需要哪种 context-modecontext-mode 不是某种固定的技术而是一类策略的总称。你需要根据业务形态来选择甚至可以组合使用。我习惯把需求分成三类如果业务是简单的单轮问答或短对话比如客服机器人处理单一问题那滑动窗口就够了把 system prompt 写好只保留最近两三轮记录即可。如果业务是长对话陪伴或复杂任务编排比如角色扮演、代码生成助手那必须考虑摘要压缩否则聊到第 50 轮时历史记录会吃掉所有预算。如果业务是知识库问答、文档分析那核心是检索召回模式对话历史反而不是重点重点是能不能从知识库里捞到最相关的片段。正经做产品的话几乎都是混合模式知识类问题走检索流程类问题看历史摘要简单寒暄靠滑动窗口。下面我逐个拆解这四种模式的具体实现和取舍。2. 四种主流 context-mode 拆解与选型2.1 滑动窗口模式最简单但也最容易失忆滑动窗口是大家的第一反应只保留最近 N 轮对话更早的直接丢弃。它的实现太简单了一个数组append 新消息然后从头部弹出旧消息保证数组长度不超过阈值。但它的代价很隐蔽用户在第 3 轮抛出的关键约束我只要 Java 版本的代码在第 8 轮可能还留着在第 20 轮时已经被挤出窗口了。然后模型就开始一本正经地给你写 Python 代码你还以为是模型变笨了其实是它压根没看到那条约束。我在实际项目里的做法是滑动窗口只负责最近对话的流畅性同时单独维护一个关键信息锚点池。用户在对话中明确表达的需求、选择、偏好都会被结构化提取出来作为一条高优先级消息常驻在 system 区域。这样既控制了 token 总量又不会丢掉核心意图。这个模式适合的工具和场景也比较明确任何轻量级对话、demo、MVP 阶段、或者作为混合模式的兜底方案。它的优点就是快、便宜、好实现缺点就是没有长期记忆不适合复杂任务。2.2 摘要压缩模式用额外计算换记忆当滑动窗口撑不住时大家会想到摘要。这个模式的思路也很直观把最老的一批对话交给模型总结成一段短文本然后用这段摘要替代原来的完整对话。相当于给对话做笔记。实现的坑在于该在什么时候触发压缩。如果每一轮都压缩成本太高如果压缩得太晚窗口早就爆了。我用的触发策略是每当新消息加入后检查总 token 数如果超过预算的 70%就对最早的对话片段做一次摘要。这样压缩是滞后的但频率足够低平均下来成本可控。还有一个很关键的细节摘要本身也是 token。有些人看到摘要能省 token就不停地嵌套压缩把摘要又摘要结果出现了摘要的摘要。这种多层压缩会导致信息严重失真最后模型只能记住用户问了问题我回答了这种毫无意义的内容。我建议最多做两层而且每层摘要都要标注时间区间一旦发现摘要质量下降就应该停止继续压缩改为裁剪或转储到外部存储。2.3 检索召回模式把知识库搬进上下文知识库问答是 RAG 的天下也是 context-mode 里最有技术含量的一种。它的核心思路是不把全部知识塞进上下文而是根据用户当前问题从知识库中检索最相关的片段只把这几段拼接到上下文里。真正做好检索召回关键其实在召回之后的排序。很多人只做到向量检索这一步把 top-20 结果全部塞进去上下文中大量不相关内容直接稀释了模型注意力。我个人的经验是向量检索取 top-50再用一个轻量级 rerank 模型或者基于关键词重叠度做粗排最终取 top-5 左右拼进上下文。宁可少给不可滥给。另外一个常见误区是检索只做一次。对于复杂问题比如对比 A 方案和 B 方案在成本上的差异一次检索往往只能命中其中一部分。我习惯把问题拆成多个子查询分别检索、合并结果、再去重排序。这个过程看似增加了调用量但每次调用都是小的、精准的整体效果远好于一次大检索。2.4 混合模式面向复杂任务的正解真实生产环境里只靠单一模式的项目少之又少。混合模式不是简单的几种策略叠加而是设计一套路由规则根据用户意图、上下文规模、知识需求动态决定走哪条路径。我搭过一套比较典型的混合链路用户消息进来先做意图分类如果是代码生成类走滑动窗口加关键锚点如果是知识咨询类走检索召回如果对话超过 10 轮启动摘要压缩。判断条件并不复杂就是几个 if-else 加 token 计数器但效果比单一模式好很多。选型上没有绝对标准我给下面这张表是基于我自己项目经验的实践排序可以按这个思路做初步判断模式记忆能力成本延迟实现难度最适合的场景滑动窗口弱低低极低短对话、寒暄、MVP 原型摘要压缩中中中中长对话、角色陪伴、Agent 多步任务检索召回中低-中中高知识库问答、文档分析、客服混合模式强中-高中-高高生产级复杂应用、Copilot 类产品3. 实操实现一个可用的 context-mode 管理器3.1 环境准备与核心数据结构真正写代码的时候我会用一个独立的 ContextManager 类来统一管理上下文避免在业务代码里到处拼接消息数组。环境上用 Python 3.9 以上装一个tiktoken用于 token 计数模型这里我用gpt-4o作为例子你可以换成其他模型。pip install tiktoken openai核心数据结构上我建议用带元数据的消息对象而不是裸的字典。每条消息除了 role 和 content还要记录 token 数、时间戳、优先级、来源类型。这样后面做裁剪时能根据元数据精确判断这条能不能删而不是一股脑从头开始删。from dataclasses import dataclass from datetime import datetime from typing import Optional dataclass class Message: role: str # system / user / assistant / tool content: str token_count: int 0 timestamp: datetime None priority: int 3 # 0最高, 5最低 source: str chat # system / chat / tool / retrieval / anchor is_compressed: bool False优先级这个字段非常重要它解决了我前面说的关键信息被挤出窗口的问题。用户明确表达过的偏好和要求我会把对应的历史消息做一次提取生成一条 source 为 anchor、priority 为 0 的消息它在裁剪时永不被移出除非用户主动变更。3.2 核心代码token 计算、裁剪与组装先写 token 计数函数。tiktoken能按模型精确统计 token 数比用字符数估算靠谱得多尤其在处理中文时字符数和 token 数的比例根本不固定。import tiktoken class ContextManager: def __init__(self, modelgpt-4o, max_tokens8192, reserved_tokens1024): self.enc tiktoken.encoding_for_model(model) self.max_tokens max_tokens self.reserved_tokens reserved_tokens self.system_messages [] self.history_messages [] self.anchor_messages [] self.budget_tokens max_tokens - reserved_tokens def _count(self, text: str) - int: return len(self.enc.encode(text)) def add_message(self, role: str, content: str, sourcechat, priority3): msg Message( rolerole, contentcontent, token_countself._count(content), timestampdatetime.now(), prioritypriority, sourcesource ) if source anchor: self.anchor_messages.append(msg) elif role system: self.system_messages.append(msg) else: self.history_messages.append(msg)然后是最核心的裁剪逻辑。我的做法是先把 system 和 anchor 固定住它们不可裁剪然后在 history 里从最旧的消息开始淘汰优先级高的尽量保留优先级相同则优先淘汰旧的。如果压缩模式下开了摘要压缩在裁剪前会先尝试对最旧的一段历史做摘要替换。def trim(self): total self._total_tokens() if total self.budget_tokens: return # 先尝试把可压缩的旧消息合并成摘要 if len(self.history_messages) 6: summary self._summarize_old_messages() if summary: self.history_messages summary self.history_messages[-4:] total self._total_tokens() # 仍然超限从低优先级最旧的消息开始删 while total self.budget_tokens and self.history_messages: # 按 priority 排序, priority 越高越先被删 candidate min(self.history_messages, keylambda m: (m.priority, m.timestamp)) if candidate.priority 0: break self.history_messages.remove(candidate) total - candidate.token_count def _total_tokens(self): all_msgs self.system_messages self.anchor_messages self.history_messages return sum(m.token_count for m in all_msgs)这里有个细节摘要压缩后保留最近 4 条完整消息是为了保住对话的近期连贯性。这一段我踩过坑最早实现时压缩完保留最近 2 条结果模型经常答非所问因为它看不到上一句在讨论什么。组装上下文的逻辑就简单了按顺序拼接system - anchors - history - 用户当前问题。注意不要把当前问题混入 history 再组装否则你每次构造请求当前问题都会被重复计算 token。def build_context(self, current_user_question: str) - list[dict]: self.add_message(user, current_user_question, sourcechat, priority5) self.trim() messages [] for msg in self.system_messages self.anchor_messages self.history_messages: messages.append({role: msg.role, content: msg.content}) return messages3.3 预算分配一个典型的 8k 窗口怎么分假设我们用的是 8192 token 的窗口为了例子方便真实环境一般用更大的比如 128k但分配逻辑一致预留 1024 token 给模型输出实际可用预算就是 7168 token。我的建议分配比例是这样的system prompt不超过 800 token最好压到 500 以内锚点消息用户的固定偏好、任务核心约束不超过 800 token检索召回的外部知识不超过 2000 token优先取 top-5对话历史2200 token 左右超过就触发裁剪或摘要用户当前问题 临时内容剩余约 1400 token这个分配不是拍脑袋定的。system 和锚点信息价值密度最高但不可膨胀检索知识决定了回答的专业性所以给到中等比例对话历史在长对话中弹性最大是压缩和裁剪的首要目标。在代码里我会把每个分区作为一个独立计数器来跟踪一旦某个分区超预算就转储或压缩。比如检索召回命中太多时我会在 rerank 阶段就提前卡掉而不是等到 build_context 时才发现超限。3.4 接入完整请求流程最后把 ContextManager 接入 OpenAI 调用的流程完成闭环。from openai import OpenAI client OpenAI() cm ContextManager(modelgpt-4o, max_tokens8192) # 初始化 system prompt cm.add_message(system, 你是一个只使用 Java 编写代码的资深工程师助手。, sourcesystem, priority0) # 用户对话过程中随时更新锚点 cm.add_message(user, 我只做 Java 项目不要给我 Python 代码。, sourceanchor, priority0) # 某次请求 question 帮我写一个读取 CSV 文件并统计行数的例子 messages cm.build_context(question) response client.chat.completions.create( modelgpt-4o, messagesmessages, max_tokens1024, )这个流程跑起来后你会在日志里发现当用户连续提问几十轮请求的 token 数始终在 7000 上下波动而不是一路涨到爆。这正是 context-mode 的价值——它让上下文进入一种动态平衡而不是无限堆积。4. 常见问题与排查技巧实录4.1 token 超限报错里的英文说了什么遇到 token 超限大多数人第一反应是把 max_tokens 调大但这明显不对。报错提示里的maximum context length是模型总窗口max_tokens是你允许模型输出的长度。如果你请求占了 7500 token预留的max_tokens是 1024加起来已经超过 8192 的上限API 就会拒绝执行。排查顺序我建议先从打印实际请求的 token 结构开始。把每条消息打印出来看 system 占多少、历史占多少、当前问题占多少。很多时候一打印就发现问题了不是历史太多而是某个单条消息就超了比如用户粘贴了一份 6000 token 的日志文件。这种情况裁剪历史没用需要在入口对单条消息做截断或提示用户分段上传。4.2 模型失忆是窗口太小还是裁剪策略不对很多反映模型聊着聊着就忘了的案例放大窗口后依然出问题这时候就该怀疑是裁剪策略太粗暴了。我之前用简单的前截断始终从最早的记录开始删结果用户在第 2 轮说的使用 PostgreSQL在第 15 轮就被删了模型从第 16 轮开始就反复建议 MySQL气得用户直接弃用。解决这个问题的核心就是前面说的锚点机制。你在每一轮对话后做个轻量级的关键信息提取把用户明确表达的约束、偏好结构化保存为锚点消息Trimming 时永远不删它们。我实测下来这个改动比单纯加大窗口对记忆持久性的提升明显得多。4.3 上下文太杂导致智商下降另一个很常见的坑是什么都想塞进去。尤其在做 RAG 时我见过有人把 top-20 片段全部拼进 context结果模型被大量泛泛而谈的段落带偏连最核心的数据都答错了。这种现象就是前面说的相关性稀释本质上是上下文噪声太多模型的特征提取被干扰了。我的方案是两段式检索先靠向量相似度召回尽量多的候选然后用权重规则做精排比如关键词重叠数、元数据时间接近度、来源可信度加权。精排后只保留 top-5。这一个改动让我的知识库问答准确率从 61% 提到了 83%成本还降了一半。4.4 延迟与成本上下文越多越贵很多人只盯着不爆 token却忽略了上下文膨胀带来的成本问题。你的 context 越长单次请求费用越高延迟越长。外部 API 按 token 计费而 token 就是真金白银。我见过一个团队上线三天被账单吓到的排查发现他们的 system prompt 里挂了一整份 5000 token 的 FAQ每次都原样发送相当于每个请求都在为无关内容付费。优化方向有两个一是把静态内容移到服务端做预处理让 system prompt 保持在最小必要集二是对重复使用的上下文块做缓存或重写比如工具调用返回的 JSON 只保留关键字段裁剪掉空值和嵌套结构。这些优化对回答质量毫无影响但成本能立刻降下来。5. 踩坑后的几条长期建议聊到这儿我再分享一点自己真实操作中的体会。context-mode 不是一个固定的库或者框架而是一套分层思考问题的方式最小化 system、锚定关键信息、压缩长对话、检索限定相关、预留生成空间。你用任何语言、任何模型都可以按这个思路搭建自己的上下文管理模块。同时我建议从简单方案开始迭代。不要一开始就上一套复杂的混合路由先把滑动窗口加上锚点跑通观察真实用户会话中 token 的分布和模型失忆的频率再逐步加入摘要压缩和检索召回。过早优化反而会引入一堆调试成本。这个方向后续还可以扩展的地方不少比如多租户场景下按用户维度持久化记忆、跨会话的长期记忆沉淀、以及根据模型返回的 token 使用量反向调优预算分配。我自己正在尝试把 context-mode 做成一个独立的服务让业务层只关心消息内容不关心窗口策略这可能是下一个版本的思路。