聊得越久,我的客服 Agent 越"健忘":用户开头说的过敏信息,聊到后面被它忘得一干二净,我才搞懂上下文窗口的残酷
我做了一个智能客服 Agent,接在一个卖食品的电商上。短对话里,它表现得很惊艳:用户问什么,它答得又准又贴心,我一度觉得这玩意儿快赶上真人客服了。可上线后,陆陆续续有用户反馈一个诡异的问题:聊得久一点,这个 Agent 就开始"犯糊涂"。有个用户一上来就明确说了"我对花生过敏,帮我推荐零食",前面几轮它推荐得好好的、都避开了花生;可聊了二十多轮、东拉西扯地问了一堆别的之后,用户再让它推荐,它居然热情地推荐了一款花生酥——把用户开头千叮咛万嘱咐的过敏信息,忘得一干二净。
这种"前面记得、聊久了就忘"的健忘症,让我百思不得其解。代码逻辑明明没问题,怎么会"选择性失忆"?直到我深入研究了大语言模型的工作机制,才终于明白,我撞上的,是所有基于大模型的 Agent 都绕不开的一道根本性约束——上下文窗口(Context Window)。我那个 Agent 之所以"健忘",不是因为它"记性差",而是因为它能"看到"的对话历史,是有长度上限的;聊得太久,早期的对话(包括那条关键的过敏信息),要么被挤出了这个窗口、它根本看不到了,要么虽然还在窗口里、却淹没在了一大堆无关信息中间,被它"忽略"了。这,就是上下文窗口的残酷。
故障现场:被"挤出"记忆的过敏信息
我把我那个 Agent 处理对话的逻辑,简化一下。问题就藏在"如何把历史对话喂给模型"这件事上:
# 我的客服 Agent(有缺陷的版本): 朴素地把所有历史都堆进去
conversation_history = [] # 累积所有对话
def chat(user_message: str) -> str:
conversation_history.append({"role": "user", "content": user_message})
# 把【全部】历史, 一股脑塞给模型
response = llm.chat(messages=[
{"role": "system", "content": "你是一个食品店客服。"},
*conversation_history, # ← 所有历史! 但模型有"上下文窗口"上限!
])
reply = response.content
conversation_history.append({"role": "assistant", "content": reply})
return reply
# 问题: 大模型能接收的 token 是有上限的(上下文窗口, 比如 8k/32k tokens)。
# 当 conversation_history 越来越长, 超过这个上限时, 会发生什么?
# - 要么: API 直接报错(超出最大 token)
# - 要么(更隐蔽): 框架/我自己"截断"历史, 砍掉最早的几条 ——
# 而最早的那几条里, 正好有"我对花生过敏"这句话!
# → 过敏信息被砍掉, 模型再也看不到它, 自然就推荐了花生酥!
问题的根源,清晰地浮现了出来。大语言模型,一次能"看到"并处理的文本量,是有一个硬性上限的,这个上限就是"上下文窗口"(以 token 计,比如 8k、32k 等)。而我那个 Agent,朴素地把所有历史对话,一股脑地塞给模型——这在对话短的时候没问题,可一旦对话变长、累积的历史超过了上下文窗口的上限,问题就来了。要么 API 直接因为"超出最大 token"而报错;要么(这更常见、也更隐蔽)——为了不报错,系统会把历史"截断",砍掉最早的那几条对话,只保留最近的。而我那个用户的悲剧在于:他那句至关重要的"我对花生过敏",正是在对话的最开头说的;当对话变长、历史被从头截断时,这句话,第一个被砍掉了。于是模型在后面的对话里,根本就"看不到"这条过敏信息了——它不是"忘了",而是这条信息,压根就没出现在它能看到的视野里。它推荐花生酥,是因为在它眼中,这个用户从没说过自己过敏。
更让我警醒的是,我还发现了一个即便"没被截断"也会出问题的、更隐蔽的现象:
即便过敏信息"还在"上下文窗口里, 也可能被模型"忽略"!
这叫 "Lost in the Middle"(迷失在中间)现象:
研究发现, 大模型对一段长上下文里【不同位置】信息的"关注度", 是不均匀的:
- 对【开头】的信息, 关注度高 (记得清)
- 对【结尾】的信息, 关注度高 (记得清)
- 对【中间】的信息, 关注度明显下降 (容易忽略)!
所以, 当对话很长时, 那条"我对花生过敏"的信息, 哪怕没被截断,
也可能因为它处在长长的对话历史的"中间位置", 而被模型"轻视"、"忽略"。
→ 上下文窗口的问题, 不只是"装不装得下"(截断),
还有"装下了, 但模型有没有真正注意到"(lost in the middle)。
第一件事:搞懂上下文窗口——大模型的"工作记忆"是有限的
定位到问题,我把"上下文窗口"这个概念,以及它背后的本质,彻底搞懂了:大语言模型,本身是"无状态"的——它并不会真正地"记住"你和它之前聊过的任何东西。它之所以"显得"记得,是因为我们每次请求时,都把之前的对话历史,一起作为"上下文"发给了它。而这个能一起发送的上下文,有一个长度上限,这就是"上下文窗口"——它就像是大模型的"工作记忆",容量有限,且每次对话都是"重新读一遍"全部上下文。
关键认知: 大模型是"无状态"的, 它不会"记忆", 全靠每次把上下文重新发给它。
你以为的"对话":
你: 我对花生过敏 ← 模型"记住"了?
你: 推荐个零食 ← 模型"想起"了过敏, 避开花生?
实际发生的:
第1次请求 = 发送["我对花生过敏"]
第2次请求 = 发送["我对花生过敏", "推荐个零食"] ← 把历史重新发一遍!
...
第N次请求 = 发送[...前面所有历史...] ← 越来越长, 直到超过上下文窗口!
所以:
- 模型的"记忆", 完全等于"你这次发给它的上下文"。
- 上下文窗口, 就是它的"工作记忆"容量上限。
- 超出上限, 早期信息就得被"丢弃"(截断), 模型就"失忆"了。
- 这不是模型"笨", 而是它"看不到"被丢弃的那部分 —— 巧妇难为无米之炊。
上下文窗口, 是大模型一个【根本性的、物理性的】约束,
做 Agent, 绕不开它, 必须正面应对它。
原理终于清晰了。大语言模型,在本质上是"无状态"的——它没有我们想象中那种"持续的记忆",每一次对话,对它来说都是"全新的一次":它读入你这次发给它的全部上下文,生成一个回复,然后就"忘了"——下一次,全靠你再把历史发给它,它才能"接上"。所以,模型的"记忆",严格地等于"你这一次请求里发给它的上下文";而这个上下文的长度,被"上下文窗口"死死地限制着。这个窗口,就是大模型的"工作记忆"容量——它是有限的、物理性的约束。当对话累积的历史超过这个容量,早期的信息就必然要被丢弃(截断),模型也就随之"失忆"了。我那个 Agent 的健忘,根源正在于此:它不是"记性差"、不是"笨",而是那条过敏信息,被挤出了它有限的工作记忆,它根本就"看不到"了——巧妇难为无米之炊。这是一个做任何基于大模型的 Agent,都绕不开、都必须正面应对的根本约束。
第二件事:正解——主动地"管理"上下文,而非朴素地"堆砌"
搞懂了根因——"上下文窗口有限,朴素堆砌历史会导致关键信息被挤掉或忽略"——正解的方向就清晰了:我不能再朴素地把所有历史一股脑堆给模型,而要主动地"管理"上下文——智能地决定"哪些信息必须保留、放在哪、怎么放",确保那些关键信息(比如过敏)永远在模型的视野里、且处在它关注度高的位置。我落地了几种关键的上下文管理策略:
# 正解1: 提取并"固定"关键信息, 让它永不被截断
# 把对话里的关键事实(过敏、订单号、偏好), 单独提取出来, 放进 system prompt,
# 这部分永远不参与"截断", 每次都喂给模型。
key_facts = {"过敏": "花生", "订单号": "12345"} # 从对话中提取的关键事实
system_prompt = f"你是食品店客服。【重要】用户的关键信息: {key_facts}, 务必牢记并遵守!"
# 正解2: 滑动窗口 + 摘要 —— 历史太长时, 把早期对话"压缩"成摘要
def build_context(history):
if total_tokens(history) < LIMIT:
return history # 没超限, 直接用
# 超限了: 把"较早的对话"压缩成一段摘要, 保留"最近的对话"原文
old, recent = history[:-6], history[-6:] # 最近6条保留原文
summary = llm.summarize(old) # 把更早的压缩成摘要
return [{"role": "system", "content": f"早前对话摘要: {summary}"}, *recent]
# 正解3: 把关键信息放在"开头"或"结尾"(对抗 lost in the middle)
# 既然模型对中间关注度低, 就把最重要的信息, 放在上下文的开头和结尾, 别埋在中间。
messages = [
{"role": "system", "content": f"【关键约束】{key_facts}"}, # 开头: 关键信息
*middle_history, # 中间: 普通历史
{"role": "system", "content": f"【再次提醒】别忘了: {key_facts}"}, # 结尾: 再强调
]
# 正解4: 用外部"记忆库"(向量数据库 RAG), 突破窗口限制
# 把所有历史/知识存进向量库, 每次只检索"和当前问题最相关"的几条, 喂给模型。
# 这样, 模型的"记忆"就不再受上下文窗口的硬限制了。
这几种策略,层层递进地解决了上下文管理的问题。正解1(提取并固定关键信息)是最直接、最有效的:把对话里那些"绝不能忘"的关键事实(过敏、订单号、核心诉求),主动地提取出来,放进每次都会发送、且永不被截断的 system prompt 里——这样,无论对话多长,这条过敏信息都稳稳地待在模型眼前。正解2(滑动窗口 + 摘要)解决"历史太长装不下"的问题:不再保留全部原文,而是把较早的对话压缩成摘要,只保留最近几轮的原文——用一段精炼的摘要,替代冗长的早期历史,既省 token 又不丢关键信息。正解3(关键信息放开头结尾)专门对抗 "lost in the middle":既然模型对中间关注度低,就把最重要的信息,刻意地放在上下文的开头和结尾,甚至在结尾"再提醒一遍"。正解4(外部记忆库 RAG)则是最根本的突破:把海量历史和知识存进向量数据库,每次只检索出和当前问题最相关的几条喂给模型——这样,模型的"记忆"就不再受限于那个小小的上下文窗口了。
下面这张图,对比了"朴素堆砌"和"主动管理"两种上下文处理方式:
这张图的对比一目了然:左边红色那条,"朴素堆砌"要么因超限而截断丢失早期信息,要么因信息埋在中间而被忽略,最终导致 Agent 健忘;右边绿色那条,"主动管理"通过提取固定关键事实、压缩摘要、关键信息放开头结尾、检索相关内容,确保了那条过敏信息永远在模型的视野里、且处在它会认真关注的位置。两条路的根本区别,在于你是把上下文当成一个"无脑往里塞的垃圾桶",还是一个"需要精心编排的、宝贵的有限空间"。
第三件事:上下文工程——做好 Agent 的"必修课"
这次踩坑,让我接触到了一个做 Agent 绕不开的核心命题——"上下文工程(Context Engineering)"。我才意识到,如何精心地"组织、管理、优化"喂给模型的那有限的上下文,是做出一个可靠 Agent 的关键功夫,其重要性,丝毫不亚于写提示词:
上下文工程(Context Engineering): 精心管理"喂给模型的有限上下文"的艺术。
它要回答的核心问题是:
"在有限的上下文窗口里, 我到底该放进去什么, 才能让模型表现最好?"
几个关键的考量维度:
1. 该放什么? —— 关键事实、相关历史、必要知识 (而非所有历史)
2. 放多少? —— 在"信息充分"和"不超窗口/不稀释重点"之间权衡
3. 怎么排? —— 重要的放开头结尾, 对抗 lost in the middle
4. 怎么压缩? —— 摘要、提取、结构化, 用更少 token 表达同样信息
5. 怎么动态取? —— RAG: 按当前问题, 从外部记忆检索最相关的
为什么重要?
- 上下文窗口是有限且昂贵的(token 要花钱、长上下文还更慢、更易出错)
- 模型的表现, 极度依赖你给它的上下文质量 ——
"垃圾进, 垃圾出"在 Agent 上体现得淋漓尽致。
- 同样的模型, 上下文组织得好不好, 效果天差地别。
→ 做 Agent, 一半的功夫在"提示词", 另一半, 就在"上下文工程"。
这个认知,让我对做 Agent 这件事,有了一个更全面的理解。"上下文工程",指的是精心地组织、管理、优化"喂给大模型的那段有限上下文"的一整套方法与艺术。它要回答的核心问题是:在上下文窗口这块寸土寸金的有限空间里,我到底应该放进去什么、放多少、怎么排列、怎么压缩、怎么动态获取,才能让模型发挥出最好的表现?我这次的健忘 bug,本质上就是一次"上下文工程"的失败——我没有去思考"该放什么、关键信息放哪",而是无脑地把所有历史一股脑堆了进去,结果让关键的过敏信息,要么被挤掉、要么被淹没。而我从这次踩坑里学到的最重要的一课是:做一个可靠的 Agent,功夫不只在"写好提示词"上,还有同样重要的一半,在"做好上下文工程"上——因为模型的表现,极度依赖于你喂给它的上下文的质量;同样一个模型,你把上下文组织得清晰、精炼、重点突出,它就聪明伶俐;你把上下文堆得杂乱、冗长、重点淹没,它就糊里糊涂。'垃圾进,垃圾出'这句老话,在 Agent 的上下文上,体现得淋漓尽致。
第四件事:别迷信"长上下文"——窗口大了,新问题也来了
有人可能会说:现在不是有些模型支持几十万、上百万 token 的"超长上下文"了吗?直接全塞进去不就行了?我研究后发现,事情没那么简单——窗口变大,缓解了"装不下"的问题,但绝不意味着"上下文工程"就不重要了;恰恰相反,长上下文带来了它自己的一系列新问题:
"上下文窗口变大"≠"可以无脑全塞进去", 长上下文有它自己的代价和坑:
代价1: 贵! token 是要花钱的, 上下文越长, 每次请求越贵。
把10万token的历史每轮都发一遍, 成本会高得吓人。
代价2: 慢! 上下文越长, 模型处理越慢, 响应延迟越高。
代价3: lost in the middle 在长上下文里更严重!
上下文越长, 中间被忽略的信息就越多 ——
塞了10万token, 但关键信息淹没在中间, 模型照样"看不见"。
代价4: 注意力被"稀释"。
无关信息越多, 模型的注意力越分散, 越容易被干扰、被带偏。
给它10条相关 + 990条无关, 远不如只给它那10条相关。
代价5: 更易出错/幻觉。
超长、嘈杂的上下文里, 模型更容易混淆信息、产生幻觉。
→ 所以: "精简、相关、重点突出"的短上下文,
往往比"大而全、嘈杂"的长上下文, 效果更好、更省、更快!
上下文工程的目标, 不是"塞得多", 而是"塞得准"。
这个认识,纠正了我一个潜在的误区——"窗口够大就万事大吉"。上下文窗口变大,确实缓解了"硬性装不下"的燃眉之急,但它远不是上下文问题的"终极解药";相反,无脑地往一个大窗口里猛塞,会带来一系列新的代价:成本飙升(token 要花钱)、响应变慢(长上下文处理慢)、"lost in the middle"更严重(中间被忽略的更多)、注意力被无关信息稀释、以及更容易产生幻觉。这让我领悟到上下文工程一个反直觉、却极其重要的原则:上下文的目标,从来不是"塞得多",而是"塞得准"——给模型 10 条精准相关的信息,效果往往远胜于给它 10 条相关 + 990 条无关的信息。无关信息不仅浪费 token、拖慢速度,还会像噪音一样,稀释模型的注意力、干扰它的判断。所以,即便在长上下文时代,"精简、相关、重点突出"依然是上下文工程的金科玉律。把"短而精的上下文"和"长而杂的上下文"对比成一张表:
| 维度 | 短而精上下文 | 长而杂上下文 |
|---|---|---|
| 成本 | 低(token 少) | 高(token 多) |
| 速度 | 快 | 慢 |
| 关键信息被关注 | 高(重点突出) | 低(淹没在噪音里) |
| 幻觉风险 | 低 | 高(信息混淆) |
| 效果 | 往往更好 | 往往更差 |
第五件事:把"Agent 记忆管理"沉淀成一套分层策略
这次踩坑,让我把"Agent 该如何管理它的记忆/上下文"系统地想了一遍,沉淀成了一套"分层记忆"的策略。它借鉴了人类记忆的分层思想,让 Agent 在有限的"工作记忆"之外,拥有更持久、更广阔的记忆能力:
Agent 的分层记忆策略(类比人类记忆):
1. 工作记忆 (Working Memory) = 当前上下文窗口
- 放: 最近几轮对话 + 当前任务最相关的信息
- 特点: 容量小、但模型直接"看得见"、用得上
- 管理: 滑动窗口, 满了就把旧的"沉淀"到下层
2. 短期记忆 (Short-term) = 本次会话的关键事实
- 放: 从对话中提取的关键信息(过敏、订单号、用户偏好)
- 形式: 结构化存储, 每轮都注入工作记忆的固定区(如 system)
- 作用: 保证本次会话的关键约束"永不遗忘"
3. 长期记忆 (Long-term) = 跨会话的持久记忆
- 放: 用户的长期画像、历史交互、知识库
- 形式: 向量数据库 / 数据库, 按需 RAG 检索
- 作用: 让 Agent "认识"老用户, 突破单次会话的限制
核心思想:
- 不是所有信息都该在"工作记忆"里 —— 那太挤、太贵。
- 而是分层存储, 按需调取: 关键的常驻, 相关的检索, 其余的沉淀。
- 就像人: 不会把所有记忆都"想"在脑子里, 而是需要时再"回忆"。
这套"分层记忆"策略,是这次踩坑给我最体系化的收获。它的核心思想,是借鉴人类的记忆机制——我们人类,也不会把所有记忆,都时时刻刻地"想"在脑子里(那样大脑会崩溃),而是分了层:当前正在思考的,是"工作记忆"(容量很小);最近、重要的事,是"短期记忆";而海量的、长期的知识与经历,则存在"长期记忆"里,需要时再"回忆"(检索)出来。一个健壮的 Agent,也该如此分层:工作记忆(当前上下文窗口)只放最近对话和最相关信息;短期记忆把本次会话的关键事实(过敏、订单号)结构化地存起来,每轮固定注入,确保"永不遗忘";长期记忆则用向量库存储用户画像、历史交互,按需 RAG 检索,让 Agent 能"认识"老用户、突破单次会话的局限。这套策略的精髓在于:不是把所有信息都硬塞进那个又小又贵的"工作记忆",而是分层存储、按需调取——关键的常驻、相关的检索、其余的沉淀。我那个 Agent 的健忘,根源就是它只有"工作记忆"这一层,且管理粗暴;而有了这套分层记忆,那条过敏信息,就会被妥善地放进"短期记忆"、每轮固定注入,再也不会被遗忘。把这三层记忆的分工整理成一张表:
| 记忆层 | 存什么 | 形式 | 作用 |
|---|---|---|---|
| 工作记忆 | 最近对话+最相关信息 | 上下文窗口 | 模型直接可见可用 |
| 短期记忆 | 本次会话关键事实 | 结构化, 固定注入 | 关键约束永不遗忘 |
| 长期记忆 | 用户画像/历史/知识库 | 向量库, RAG 检索 | 跨会话, 认识老用户 |
一张"对话历史该怎么处理"的决策图
把这次踩坑沉淀成一张图。每当你的 Agent 要构建给模型的上下文时,照着它走:
这张图把上下文管理串成了一条流水线:先提取关键事实并固定注入(确保永不遗忘)→ 历史超长就摘要压缩 → 需要更早信息就 RAG 检索 → 最后把关键信息放在开头结尾。把这条流水线变成你 Agent 处理每一轮对话的标准流程,那个"聊久了就健忘"的坑,就被你从根上堵死了。
我立下的几条 Agent 上下文管理规矩
这次"Agent 忘了用户过敏信息"的事故后,我给自己立了几条规矩:
- 别朴素堆砌历史:绝不无脑把所有对话历史塞给模型,而要主动管理上下文,这是做 Agent 的基本功。
- 关键事实提取固定:把过敏、订单号、核心约束等关键信息提取出来,固定注入每轮的 system,永不被截断。
- 超长就摘要压缩:历史超过窗口安全线时,把早期对话压缩成摘要,保留最近几轮原文。
- 关键信息放头尾:把最重要的信息放在上下文的开头和结尾,对抗 lost in the middle,别埋在中间。
- 分层管理记忆:工作记忆放当前、短期记忆存会话关键事实、长期记忆用向量库,按需调取。
- 追求"准"而非"多":上下文目标是精简相关、重点突出,而非塞得越多越好,无关信息会稀释注意力。
- 长对话专门测:测试 Agent 时,专门测"很长的、绕了很多弯的"对话,验证它会不会遗忘早期的关键约束。
这几条里,第二条"关键事实提取固定"是直接根治这次 bug 的核心。而贯穿所有规矩的那条主线,是对"大模型工作机制"的尊重与顺应。我这次栽跟头,根子上是我把大模型当成了一个"有无限记忆、过目不忘"的智能体,却没意识到它其实是个"无状态、工作记忆有限、还会迷失在中间"的、有着明确能力约束的工具。我用一种'它应该什么都记得'的理想化想象去使用它,而没有去顺应它'记忆有限、需要精心喂养上下文'的真实特性——于是,在它能力的约束处,我栽了跟头。做好 Agent 的关键,恰恰是要深刻地理解并尊重大模型这些真实的、底层的工作机制(无状态、上下文窗口、注意力分布……),然后顺着它的特性,去设计你的系统,而非违背它、对抗它。
写在最后:与其对抗约束,不如顺应约束去设计
这次被上下文窗口教育的经历,给我一个超越 Agent 开发本身的、深刻的启示:任何强大的工具或技术,都有它内在的、根本性的约束;而用好它的关键,不在于'假装这些约束不存在'、或'徒劳地对抗它们',而在于'深刻地理解这些约束,然后顺应着它们,去做巧妙的设计'。上下文窗口的有限性,是大模型一个根本性的、短期内无法消除的约束。我最初的错误,是无视它——我假装模型有无限记忆,把所有历史一股脑地堆给它,结果撞了南墙。而正确的做法,是承认并顺应这个约束:既然工作记忆有限,我就精心地管理它、给它喂最关键的信息、把它沉淀分层——在"约束"的框架之内,设计出最优的方案。这种'顺应约束去设计'的智慧,和'无视约束去蛮干'的天真,是工程能力成熟与否的一道分水岭。
想通这一点,我对"约束"这个词,有了一种全新的、近乎欣赏的态度。我们常常把"约束"看成是讨厌的、阻碍我们的东西,巴不得它消失;可这次让我体会到,约束,其实是一切优秀设计的'母亲'——正是因为有了上下文窗口的约束,才催生了'上下文工程''分层记忆''RAG'这些精巧的设计;正是因为有了约束,我们才被逼着去思考'什么才是真正重要的、该被保留的',从而做出更精炼、更智能的方案。一个没有任何约束的世界,反而催生不出精巧的设计——因为无需取舍、无需权衡。真正的工程之美,恰恰绽放在"约束"之中:在有限的资源、有限的窗口、有限的条件下,通过巧妙的设计,把效果做到极致。理解约束、尊重约束、并在约束中跳出最优雅的舞步,是一个工程师真正的功力所在。
所以,如果你也在使用各种有着内在约束的强大工具(而几乎所有工具都有约束),我想把这次踩坑最想说的话送给你:别去无视、或徒劳地对抗一个工具的根本约束,而要去深刻地理解它,并顺应着它,做出巧妙的设计。大模型有上下文窗口的约束,数据库有性能的约束,网络有延迟的约束,系统有资源的约束……面对每一个约束,别天真地假装它不存在,也别愤怒地与它死磕,而要冷静地理解它的边界,然后问自己:在这个约束之内,我能设计出怎样的、最优的方案?因为真正的工程智慧,从来不是拥有'无限的资源'、'没有约束'的理想环境,而是在种种现实的约束之中,依然能通过精巧的设计,把事情做好、做漂亮;约束不是创造力的敌人,恰恰是它最好的磨刀石。那个忘了用户过敏的健忘 Agent,最终教给我的,正是这份对"约束"的理解与顺应——它让我懂得,面对大模型有限的上下文窗口,真正的高手,不是抱怨它太小、或假装它无限大,而是顺着这份'有限',去设计出一套让有限的记忆,发挥出无限价值的、精巧的上下文工程。
—— 别看了 · 2026