乐于分享
好东西不私藏

为什么聊了几轮之后,AI 助手开始"变笨"了-《0基础到AI工程师》系列 · 第4篇

为什么聊了几轮之后,AI 助手开始"变笨"了-《0基础到AI工程师》系列 · 第4篇

为什么聊了几轮之后,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 | 动手实现:从计数器到一个完整的上下文引擎

先说实话:接下来这段代码我也是一边写一边调出来的,不敢说是最优实现,但足够把思路讲透。

第一步:数一数有多少 token。管不了预算,就先量一下大小。这里用一个简化算法估算 token 数:按空格分词,词数乘 1.3(近似模拟真实分词器的效果,不用管细节):
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 编程干货。

我是橙子🍊,用最通俗的语言讲最硬核的技术