为什么聊了几轮之后,AI 助手开始"变笨"了
💡 上一篇讲了嵌入向量,解决的是"AI 怎么理解意思相近的两句话"。这一篇讲上下文工程,解决的是另一个问题:给 AI 的资料越喂越多,它反而答得越来越糊涂,这是为什么?零基础也能看懂:文中的 Python 代码不需要会写,看懂代码上面的中文说明就够了,重点都在文字里。
Claude Opus 4.7 的窗口有 20 万 token,GPT-5 有 40 万,Gemini 3 Pro 有 200 万——这些数字听起来大得吓人,直到你真去把它填满。
来看一个真实的编程助手场景:系统提示词占 500 token,50 个工具的定义占 8000 token,检索到的文档占 4000 token,10 轮对话历史占 6000 token,当前提问占 200 token,再留 4000 token 给模型回答——加起来 2.27 万 token,也才占一个 12.8 万 token 窗口的 18%。数字看起来还很宽裕,对吧?
但模型的"注意力"并不会随窗口变大而线性变好。研究发现,放在开头或结尾的信息,模型能接近完美地找到;放在中间的信息,准确率反而会下降 10%-20%。这就是中间遗忘(lost-in-the-middle)现象——下一节马上展开讲。
一个挺实用的结论是:20 万 token 的窗口,不代表用满 20 万 token 就是高效的。一份精心筛选的 1 万 token 上下文,效果往往优于塞满 10 万 token 的杂乱上下文。上下文工程(Context Engineering)要做的事,就是在窗口里最大化"信号"、最小化"噪声"——你放进窗口的每一个 token,都在挤占另一个本可以承载更相关信息的位置。这不怪你以前没听过这个词,很多教程只顾着夸模型窗口大,压根没讲窗口大了之后该怎么管。我把这块经验整理给你。
本篇内容:上下文窗口为什么是稀缺资源 → 中间遗忘现象 → 窗口里都装了什么 → 四种压缩策略 → 三层记忆系统 → 动态组装上下文 → 动手实现一个上下文引擎 → 真实产品都在怎么做,一共 8 个模块。每个模块先讲人话,再看一小段可以直接跑的 Python 代码加深理解。
⸻⸻⸻
🎒 01 | 上下文窗口是稀缺资源:像内存,不像硬盘
先记住这个类比:上下文窗口更像内存,不是硬盘。硬盘(比如你的云盘)可以无限堆东西,但内存(电脑运行时用来"随手取用"的空间)是有限的,跑得快、但装不下太多——你不能什么都往里塞,必须做选择。
对 AI 来说,上下文窗口就是这块"内存":系统提示词、工具说明、检索到的资料、聊天记录、示例、留给它写答案的空间,全都要挤在这一块地方里。每多塞一个不相关的工具说明、每多留一轮过时的对话、每多来一段答非所问的检索内容,都会占掉一个本可以放更有用信息的位置——这就是"信号"被"噪声"稀释了。
上下文工程要做的,就是在这块有限的“内存”里,把信噪比拉到最高——说白了就是精打细算,你说是不是这个理儿?
⸻⸻⸻
🥪 02 | 中间遗忘:AI 记不住"中间说的话"
2023 年 Liu 等人做过一个实验:把一份真正相关的文档,混在 20 份不相关的文档里,分别放在不同位置,看 AI 回答的准确率。结果是:放在最前或最后,准确率有 85%-90%;放在正中间(20 份里的第 10 份),准确率掉到 60%-70%。这就是中间遗忘(lost-in-the-middle)——几乎所有模型都有这个毛病,只是程度不同。
这很像开会:主持人开场说的重点、散会前的总结,大家都记得住;中间念的一长串琐碎事项,大部分人早走神了。你自己开会是不是也这样,散会前才突然认真听一下?AI 也是这样,注意力不是平均分配的,天然更“上心”开头和结尾。
这个发现能直接指导怎么组织提示词,听着挺朴素,但真管用:
① 最重要的信息(系统提示词、关键指令)放最前面
② 当前问题和最相关的内容放最后面(AI 对"最近说的话"记得更牢)
③ 把中间这一段,当成"优先级最低"的区域来用
④ 万一有些内容必须放中间,就在结尾再重复一次关键结论
⸻⸻⸻
🧳 03 | 窗口里到底装了什么?六大组件在抢地盘
要管好这块"内存",得先搞清楚里面都塞了什么。拆开看,一共六项,可以想象成收拾一个行李箱去旅行:
① 系统提示词:定人设、定规则,每轮都不变。Claude Code 的系统提示词(含工具定义)大约 6000 token,写得越长,每次调用都要重复付费,所以要写得精简
② 工具定义:每个工具约占 50-200 token,50 个工具按均价 150 token 算,光工具说明就先烧掉 7500 token;只挑当前用得上的工具,能省下 60%-80%
③ 检索到的资料:翻得准不准,直接决定回答好不好——翻得差比不翻还糟,等于用噪声塞满窗口,还会主动带偏模型
④ 对话历史:聊得越久占得越多,50 轮对话、每轮 200 token,历史就有 1 万 token,但大部分内容和当前问题其实没关系
⑤ 示例:2-3 个精选的"照着做"范例,往往比堆一大段说明文字更管用,但也占地方
⑥ 回答预留空间:留给 AI 写答案的额度,至少留 2000-4000 token,窗口塞太满,AI 就没地方说话了
这六项一直在互相抢地盘:工具说明占得多,历史空间就少;检索内容塞得多,示例空间就少。上下文工程说到底,就是研究怎么分配这份预算,让每一块都刚好够用、又不浪费——跟出门收拾行李箱一码事,对吧?
⸻⸻⸻
🗜️ 04 | 装不下怎么办?四种压缩策略
窗口是稀缺资源,装不下的时候,业界常用四招(说实话,我自己项目里最容易翻车的就是第一招,摘要压得太狠,关键信息也跟着丢了,这个分寸得多试几次才能拿捏准):
① 历史摘要:不逐字保留旧对话,超过阈值(比如 5000 token)就定期压成一句话概括——"讨论了 X,定了 Y,用户还想要 Z",100 token 就能替代原本 2000 token、10 轮的对话
② 相关性过滤:给每份检索到的资料和当前问题打分,低于阈值的直接丢掉。翻出 10 份、只有 3 份真正相关,那就只留这 3 份——宁可要 3 份高度相关的,也不要 10 份不咸不淡的
③ 工具裁剪:先判断这句话属于哪类意图,只带上匹配意图的工具——问代码问题不需要带日历工具,能把工具开销从 8000 token 砍到 1000 token
④ 递归摘要:超长文档分阶段摘要,先摘每个小节,再把小节摘要合并起来再摘一次,50 页的文档最终能压成一份 500 token 的精华摘要,还保留关键信息
⸻⸻⸻
🗃️ 05 | 记忆分三层:便签、笔记本、相册
上下文工程要管的不只是"当下这一轮",还包括时间跨度更长的记忆,一般分三层:
① 短期记忆——像贴在桌上的便签:就是当前这段对话,直接存在窗口里,随对话增长,靠摘要和截断管理
② 长期记忆——像随身携带的笔记本:跨会话保留的事实和偏好,比如"用户偏好 TypeScript",存数据库,开新会话时读取(Claude Code 存在 CLAUDE.md 里,ChatGPT 用"记忆"功能)
③ 情景记忆——像家里的相册:具体的历史交互,比如"上周二我们调过一个类似的认证模块 bug",以嵌入向量存储,等到当前对话和某段历史相似时才被翻出来
三层各管一段时间:便签管眼前,笔记本管长期不变的偏好,相册管“特定场景才会被唤醒”的细节——是不是有点像你自己脑子记事的方式?
⸻⸻⸻
🎯 06 | 动态组装:不同问题,配不同的行李
最关键的一个认识是:不同的问题,需要不同的上下文。系统提示词、工具、历史都写死不变,是一种浪费。真正好的系统,会针对每个问题动态组装上下文,一共六步:
① 判断这句话的意图是什么
② 只挑和意图相关的工具,而不是把所有工具都带上
③ 只检索和当前问题相关的资料,而不是固定一套资料
④ 只带上相关的历史片段,而不是全部聊天记录
⑤ 加几个匹配当前任务类型的示例
⑥ 按重要性排序:最关键的放最前,重要的放最后,可有可无的塞中间
模型都是同一个模型,真正拉开差距的是“喂给它的上下文”——这个道理想通了,很多产品之间的差距一下就说得通了,对不对?这也是接下来这个模块要动手实现的东西。
⸻⸻⸻
🛠️ 07 | 动手实现:从计数器到一个完整的上下文引擎
先说实话:接下来这段代码我也是一边写一边调出来的,不敢说是最优实现,但足够把思路讲透。
def count_tokens(text): if not text: return 0 # 按空格分词,词数乘 1.3,粗略估算 token 数 return int(len(text.split()) * 1.3)
第二步:做一个"预算管家"。给每个组件(系统提示词、工具、资料、历史、当前问题)分配 token 上限,超了就按词数自动截断:
class ContextBudget: def __init__(self, max_tokens=128000, generation_reserve=4000): self.max_tokens = max_tokens # 留给模型回答的空间,谁都不能占用 self.available = max_tokens - generation_reserve self.allocations = {} def allocate(self, component, content, max_tokens=None): tokens = count_tokens(content) # 超过这个组件自己的上限,就按词数截断 if max_tokens and tokens > max_tokens: words = content.split()[:int(max_tokens / 1.3)] content = " ".join(words) tokens = count_tokens(content) self.allocations[component] = tokens return content, tokens def remaining(self): # 总预算减去已经分配出去的,就是还剩多少 return self.available - sum(self.allocations.values())
这样一来,系统提示词、工具、资料、历史、当前问题各自能占多少 token,都提前定死了,谁也不会没边没沿地挤占别人的空间。
第三步:实现"中间遗忘"的应对策略。前面讲过,最重要的信息要放最前和最后。落到代码里,就是先按相关性打分排序,再把结果拆成两半、一头一个地交替摆:
def reorder_lost_in_middle(items, scores): # 先按相关性从高到低排序 paired = sorted(zip(scores, items), reverse=True) sorted_items = [item for _, item in paired] if len(sorted_items) <= 2: return sorted_items # 奇数位放最前,偶数位倒过来放最后,最不相关的自然留在中间 first_half = sorted_items[::2] second_half = sorted_items[1::2] second_half.reverse() return first_half + second_half
效果是:最相关的两份资料,一份排最前、一份排最后,最不相关的自然沉到正中间——模型注意力最弱的地方,刚好放最不重要的东西。
第四步:压缩对话历史。一旦历史总量超过阈值,就把最旧的几轮浓缩成一句话摘要,再从原始历史里删掉,反复执行直到降回阈值以内:
class ConversationManager: def __init__(self, max_history_tokens=5000): self.turns = [] # 近期原始对话 self.summaries = [] # 压缩后的旧对话摘要 self.max_history_tokens = max_history_tokens def add_turn(self, role, content): self.turns.append({"role": role, "content": content}) self._compress_if_needed() def _compress_if_needed(self): total = sum(count_tokens(t["content"]) for t in self.turns) # 超过阈值就把最旧的两轮摘要掉,直到降回阈值以内 while total > self.max_history_tokens and len(self.turns) > 4: old_turns = self.turns[:2] self.summaries.append(f"Previous: {old_turns}") self.turns = self.turns[2:] total = sum(count_tokens(t["content"]) for t in self.turns)
第五步:按意图动态挑工具。先判断这句话属于哪类意图,再只挑对应类别、且不超预算的工具带上:
def classify_intent(query): query_lower = query.lower() # 每类意图对应一批关键词 intent_keywords = { "code": ["code", "bug", "error", "file", "fix"], "calendar": ["meeting", "schedule", "event"], "email": ["email", "send", "inbox"], } # 命中关键词最多的那一类(或几类并列),就是这句话的意图 scores = {k: sum(1 for w in v if w in query_lower) for k, v in intent_keywords.items()} scores = {k: v for k, v in scores.items() if v > 0} return list(scores.keys()) or ["code"]def select_tools(query, token_budget=2000): # 只挑意图匹配、且不超预算的工具,而不是把全部工具都塞进去 intents = classify_intent(query) relevant, total = {}, 0 for name, tool in TOOL_REGISTRY.items(): if any(c in intents for c in tool["categories"]) and total + tool["tokens"] <= token_budget: relevant[name] = tool total += tool["tokens"] return relevant, total
效果很直接:"修复 auth.py 里的 bug",只会带上代码相关的工具;"安排下周二的会议",只会带上日历工具——同一份工具注册表,每次组装出来的内容都不一样。
第六步:把前面五步拼成一个完整的引擎。每次提问,都重新分配预算、挑工具、查资料、拼历史,组装出这次专属的上下文:
class ContextEngine: def __init__(self, max_tokens=128000, generation_reserve=4000): self.budget = ContextBudget(max_tokens, generation_reserve) self.conversation = ConversationManager(max_history_tokens=5000) self.system_prompt = "You are a helpful AI assistant..." self.knowledge_base = ["...公司内部资料的分块列表..."] def assemble(self, query): self.budget = ContextBudget(self.budget.max_tokens, self.budget.generation_reserve) # 1. 系统提示词最先分配,上限 1000 token self.budget.allocate("system_prompt", self.system_prompt, max_tokens=1000) # 2. 按意图动态挑工具,上限 2000 token tools, _ = select_tools(query, token_budget=2000) self.budget.allocate("tools", str(list(tools.keys())), max_tokens=2000) # 3. 检索相关资料,按分数过滤后重排,上限 3000 token relevant_docs = [d for d in self.knowledge_base if is_relevant(query, d)] reordered = reorder_lost_in_middle(relevant_docs, score_relevance(query, relevant_docs)) self.budget.allocate("retrieved_context", "\n".join(reordered), max_tokens=3000) # 4. 对话历史,上限 5000 token self.budget.allocate("conversation_history", self.conversation.get_context(), max_tokens=5000) # 5. 当前问题,上限 500 token self.budget.allocate("user_query", query, max_tokens=500) return self.budget
同一句"修复 auth.py 里 JWT 过期的 bug",引擎会自动只带代码类工具、只翻代码相关的资料;换成"安排下周二的会议",带的工具和资料完全不同——这就是动态组装的意义:不是每次都把所有东西打包带走,而是现场配一份刚刚好的行李。
⸻⸻⸻
🧭 08 | 真实产品都在怎么用
讲了这么多方法论,我们来看看它们在真实产品里是怎么落地的:
• Claude Code:系统提示词加工具定义约 6000 token;打开一个文件,内容才会被注入上下文;旧对话轮次会被自动摘要;CLAUDE.md 提供跨会话的长期记忆。关键决策是:它不会把整个代码库塞进上下文,而是按需检索相关文件
• Cursor:把整个代码库嵌入索引,输入问题时按向量相似度检索最相关的文件和代码块,50 万行代码最终压缩成 5-10 个真正相关的片段——嵌入一切,按需检索,只带真正有用的
• ChatGPT 的记忆功能:把"用户偏好 Python"这类事实存成长期记忆,每次对话开始时取出来放进系统提示词,只占 5 个 token,却能省下后续无数轮对话里重复说明的成本
• RAG(检索增强生成):本质就是上下文工程的具体形式——不训练模型、不写死提示词,而是查询时动态检索相关文档塞进窗口,切块、嵌入、检索、重排,整条链路都是为了把"正确的信息"放进上下文,下一篇我们专门展开讲这个
⚠️ 一个常见误区:
不是"上下文窗口越大,AI 表现就越好"。检索差、工具堆太多、历史不压缩,反而会用无关信息稀释掉真正有用的部分,让模型在同样的问题上表现更差。
⸻⸻⸻
✅ 收个尾:这篇你能带走什么
1. 上下文窗口更像内存不像硬盘,再大也是稀缺资源,关键是信噪比,不是容量
2. AI 对开头结尾的信息记得最牢,中间容易被忽略,重要的事要放在两头说
3. 窗口里的六大组件互相抢地盘,装不下时可以靠摘要、过滤、裁剪来腾空间
4. 记忆分短期、长期、情景三层,各自负责不同时间尺度的"记得住"
5. 好的 AI 应用不是"塞满窗口",而是动态组装——每个问题现场配一份刚刚好的上下文
这一篇看着抽象,但你现在打开的任何一个 AI 编程工具、任何一个智能客服,背后都在做这些事——是不是有种“原来如此”的感觉?希望这篇能帮你把“黑箱”拆开一点点,下次再遇到 AI“聊着聊着变笨”,你大概就能猜到是哪个环节出了问题。
下一篇我们讲 RAG(检索增强生成),欢迎留言告诉我你最想先搞懂哪一块,我也是现学现卖,咱们一起琢磨。
欢迎关注橙子AI小馆,学习更多 AI 编程干货。
我是橙子🍊,用最通俗的语言讲最硬核的技术
夜雨聆风