一个把每一步的工具结果都原样堆进上下文的 AI Agent,跑到几十步后要么报 token 超限、要么"忘"了最初的任务:一次 Agent 上下文管理的深度复盘
那个问题是 Agent 处理复杂任务时暴露的:我们的 Agent 能调用一堆工具(查数据库、读文档、调接口)来一步步完成任务,简单任务三五步就搞定、表现很好。可一旦遇到需要几十步的复杂任务,就开始出两种诡异故障:要么跑到一半直接报错——context length exceeded(超出最大上下文长度);要么更邪门,它跑着跑着就"跑偏"了——明明任务是"统计上个月华东区的销售额",它跑到第三十几步时,却开始查起华南区、甚至忘了要"统计销售额"这回事,答非所问。我排查了大半天,把每一步喂给模型的完整上下文打出来一看,才明白根源,后背发凉:我们的 Agent,每执行一步,都把"这一步调用的工具、以及工具返回的完整结果"原原本本地追加进对话上下文,然后把越来越长的整个历史全部再喂给模型去想下一步。有的工具(比如查数据库、读文档)一次就返回几千上万字;几十步累积下来,上下文飞快地膨胀,很快就撑爆了模型的上下文窗口(于是报 token 超限);而即使没爆,过长的上下文里,最初的任务目标和关键约束,被淹没在了海量的、大部分已经无关的工具结果里——模型的注意力被中间那些冗长的细节带跑了,于是"忘"了它最初到底要干什么(这也是所谓"大海捞针"问题,长上下文里的关键信息容易被忽略)。这篇就把这次"Agent 上下文无限膨胀、失控又失忆"的坑,从头到尾复盘一遍。
故障现场:每步都把完整工具结果堆进上下文
问题代码,是一个对上下文"只增不管"的 Agent 循环:
# ✗ 出问题的 Agent: 每步都把完整工具结果追加进上下文, 上下文只增不裁
def run_agent(task: str):
messages = [
{"role": "system", "content": SYSTEM_PROMPT}, # 系统指令(含任务约束)
{"role": "user", "content": task}, # 最初的任务
]
for step in range(MAX_STEPS):
resp = llm.chat(messages, tools=TOOLS) # ✗ 每次都喂【全部】历史
if not resp.tool_calls:
return resp.content
for call in resp.tool_calls:
result = execute_tool(call) # 工具返回, 可能几千上万字!
messages.append({"role": "assistant", "tool_calls": [call]})
messages.append({"role": "tool", "content": result}) # ✗ 原样追加完整结果
# → messages 越来越长, 每一步都把这越来越长的东西全部喂给模型
# 两种故障:
# 故障A(token超限): 工具结果大(查库/读文档返回上万字)+ 步数多(几十步)→
# messages的总token飞速增长 → 超过模型上下文窗口(如128k)→ 报错 context length exceeded。
# 故障B(失忆/跑偏): 即使没超限, 上下文长到几万token时:
# - 最初的"任务目标"(在最前面)被埋在海量工具结果中间;
# - 模型对"长上下文中间部分"的注意力较弱(lost in the middle / 大海捞针);
# - → 模型被近期那些冗长的工具结果带偏, "忘"了最初的目标 → 跑偏、答非所问。
# 缺失: 没有任何"上下文管理"——不裁剪、不摘要、不把大结果外置, 任由它无限膨胀。
# 关键: 上下文窗口是有限的、宝贵的资源; Agent每步堆完整工具结果会让它快速耗尽,
# 既可能token超限崩溃, 又会因关键信息被淹没而"失忆跑偏"。
第一次理清这两种故障时,我恍然又无奈:"我光想着把所有信息都给模型、生怕它信息不够,却没想到'信息太多、太杂'反而会害了它。"这个坑最容易被忽视的地方在于:它在短任务时完全正常——三五步、上下文没多长,既不会超限、关键信息也还突出;它只在长任务(多步+大工具结果)时才暴露,而且暴露成两副面孔(硬性的 token 超限崩溃、和软性的失忆跑偏),后者尤其隐蔽——它不报错,只是默默地变笨、跑偏。下面就来拆解,上下文为什么是有限资源、该怎么管理。
第一件事:搞懂上下文窗口是有限资源,以及它怎么影响 Agent
我顺着这次事故,把"上下文窗口"这个 Agent 的核心约束彻底理清了。
上下文窗口(context window): Agent 必须精打细算的有限资源
【核心: 上下文窗口大小有限; Agent每步都带着全部历史, 它会膨胀; 既有"硬上限(超限崩)", 也有"软衰减(太长变笨)"】
1. 什么是上下文窗口:
- 大模型一次能"看到"的输入+输出的最大token数(如8k/32k/128k...);
- Agent每一步, 都要把【到目前为止的全部对话历史 + 工具结果】作为输入喂给模型;
- → 这个输入不能超过上下文窗口的大小, 否则报错(硬上限)。
2. Agent为什么会快速撑爆它:
- Agent是"多步循环", 每步都会往历史里追加内容(模型的思考、工具调用、工具结果);
- 尤其工具结果常常很大(查库返回上万行、读文档返回全文);
- → 历史"只增不减", 几十步后轻松达到几万、十几万token, 撑爆窗口。
3. 两种危害(一硬一软):
- 硬: token超过窗口上限 → 直接报错, Agent崩溃, 任务失败。
- 软: 即使没超限, 上下文过长时模型表现会下降——
"lost in the middle": 模型对长上下文【中间部分】的信息利用较弱;
→ 最初的任务目标/约束(在开头)、和关键信息, 容易被淹没在海量细节里 → 失忆、跑偏。
4. 所以: 上下文是"有限且宝贵"的, 要【精打细算地管理】, 而不是无脑全塞:
- 不是"给模型的信息越多越好", 而是"给它【恰好够用、且突出重点】的信息最好";
- 塞进太多无关/冗长的内容, 既浪费窗口、又稀释了关键信息、还增加成本和延迟。
类比: 上下文窗口像一个人的"工作记忆(短期记忆)", 容量有限;
你不停地往他脑子里塞各种细节, 他要么"记不下了"(超限), 要么"被细节淹没、忘了正事"(跑偏);
高效的做法是: 只让他记住"当前最相关的关键信息", 其余的放到"外部笔记"里需要时再查。
一句话: 上下文窗口有限且宝贵; Agent每步带全部历史会让它膨胀, 既可能token超限崩溃、
又会因过长而"失忆跑偏"(lost in the middle); 必须精打细算地管理上下文, 而非无脑全塞。
这套认知,是整个坑的根。上下文窗口是大模型一次能"看到"的最大 token 数(8k/32k/128k…),Agent 每步都要把到目前为止的全部历史+工具结果喂给模型,这个输入不能超过窗口(否则报错)。Agent 为什么快速撑爆?它是多步循环、每步往历史追加内容(思考、工具调用、工具结果),尤其工具结果常常很大,历史只增不减、几十步后轻松几万十几万 token。两种危害:硬——token 超窗口上限直接报错崩溃;软——即使没超限,上下文过长时模型表现下降("lost in the middle":对长上下文中间部分利用弱),最初的任务目标和关键信息被淹没→失忆跑偏。所以上下文是有限且宝贵的,要精打细算地管理而非无脑全塞:不是给的信息越多越好,而是给它"恰好够用、突出重点"的信息最好。就像上下文像人的工作记忆、容量有限,不停塞细节他要么记不下、要么被淹没忘了正事;高效做法是只让他记住当前最相关的关键信息、其余放外部笔记需要时再查。一句话:上下文窗口有限且宝贵;Agent 每步带全部历史会膨胀,既可能 token 超限崩溃、又会因过长失忆跑偏;必须精打细算管理上下文而非无脑全塞。
第二件事:正解——裁剪/摘要历史、外置大结果、固定保留关键信息
搞懂了原理,正解就清晰了:主动管理上下文——把大工具结果外置(只在上下文放摘要/引用)、对久远的历史做摘要压缩、始终把任务目标等关键信息固定保留在显眼处。
# ✓ 正解: 给 Agent 加上下文管理
def run_agent(task: str):
system = {"role": "system", "content": SYSTEM_PROMPT}
goal = {"role": "user", "content": f"【核心任务, 始终牢记】: {task}"} # ★ 关键目标
history = [] # 中间过程的历史(会被管理)
for step in range(MAX_STEPS):
# ★ 每步构造上下文: 系统 + 固定的核心任务 + (被管理过的)历史
messages = [system, goal] + manage_context(history)
resp = llm.chat(messages, tools=TOOLS)
if not resp.tool_calls:
return resp.content
for call in resp.tool_calls:
result = execute_tool(call)
# ★ 大结果外置: 太大就存外部, 上下文里只放摘要+引用
stored = store_if_large(result) # 存到外部(文件/KV), 返回引用或摘要
history.append({"role": "assistant", "tool_calls": [call]})
history.append({"role": "tool", "content": stored})
def manage_context(history):
# 策略: 保留最近K步的完整内容; 更早的历史摘要压缩成几句话
if total_tokens(history) < THRESHOLD:
return history
recent = history[-RECENT_K:] # 最近K步保留细节
old_summary = summarize(history[:-RECENT_K]) # 早期历史用模型摘要成简短要点
return [{"role": "user", "content": f"【早期步骤摘要】: {old_summary}"}] + recent
def store_if_large(result, max_inline=1000):
if len(result) <= max_inline:
return result # 小结果直接放
ref = save_to_store(result) # 大结果存外部
return f"[结果较大已存储, 摘要: {summarize(result)[:500]} | 需完整内容可用引用 {ref} 再查]"
# 上下文管理的几个核心策略
# 1. 大工具结果外置(offloading):
# 工具返回的大数据(查库结果/文档全文)别全塞上下文; 存到外部(文件/向量库/KV),
# 上下文里只放"摘要 + 引用ID", 模型需要细节时再用工具按引用去取。
# 2. 历史摘要/压缩(summarization):
# 保留最近K步的完整细节(近期最相关); 更早的历史用模型摘要成简短要点, 节省token又保留脉络。
# 3. 关键信息固定保留(pinning):
# 把"核心任务目标、关键约束、已确定的中间结论"固定放在上下文显眼位置(如紧跟system),
# 不让它被裁剪掉、不被淹没 → 防止"失忆跑偏"。
# 4. 滑动窗口 + 监控token:
# 实时统计上下文token, 接近阈值就触发裁剪/摘要; 给上下文用量留足余量。
# 5. 结构化记忆(进阶):
# 把Agent的"记忆"分为短期(当前上下文)和长期(外部存储/向量库); 按需检索相关记忆进上下文。
# 核心: 主动管理上下文——大结果外置(只放摘要引用)、早期历史摘要压缩、关键目标固定保留、
# 监控token及时裁剪; 给模型"恰好够用、重点突出"的上下文, 而非"全部、冗长"的上下文。
修复的核心,是"主动管理上下文,给模型恰好够用、重点突出的信息"。策略一:大工具结果外置——大数据存外部(文件/向量库),上下文只放"摘要+引用 ID",需要细节时再按引用取。策略二:历史摘要压缩——保留最近 K 步完整细节,更早的历史摘要成简短要点。策略三:关键信息固定保留(pinning)——把核心任务目标/约束固定放在显眼处(紧跟 system),不被裁剪、不被淹没,防失忆跑偏。策略四:滑动窗口+监控 token(接近阈值就裁剪);策略五:结构化记忆(短期上下文+长期外部存储按需检索)。归根结底:主动管理上下文——大结果外置(只放摘要引用)、早期历史摘要压缩、关键目标固定保留、监控 token 及时裁剪;给模型"恰好够用、重点突出"的上下文,而非"全部、冗长"的上下文。
第三件事:Agent 上下文与记忆相关的其他常见坑
排查后我把 Agent 上下文/记忆相关的其他常见坑也系统梳理了一遍。
Agent 上下文 / 记忆的其他常见坑
# 1. 上下文无限膨胀(本文): 每步堆完整结果致超限/失忆。→ 外置+摘要+pinning+监控。
# 2. 大工具结果直接入上下文: 一个工具吐回上万字塞爆窗口。→ 工具结果截断/摘要/外置。
# 3. 关键约束被淹没: 任务目标在开头被海量历史挤到"中间"被忽略。→ 关键信息固定/重申。
# 4. 没有长期记忆: Agent跨会话/跨任务记不住已知信息。→ 外部记忆库+检索。
# 5. 摘要丢了关键信息: 压缩历史时把重要的中间结论也摘没了。→ 摘要要保留关键事实/决策。
# 6. 上下文里全是噪声: 塞了大量无关工具结果, 稀释了有用信息、还增成本。→ 只放相关的。
# 7. 成本/延迟随上下文暴涨: 上下文越长, 每步调用越贵越慢。→ 控制上下文大小=控制成本延迟。
# 8. 多Agent间传递整个上下文: 协作时把巨大上下文整个传来传去。→ 传摘要/结构化结果。
# 共同根源: 把"上下文窗口"当成"无限的、免费的、信息越多越好的"空间, 无脑往里堆;
# 而它是"有限、昂贵、且过载会降低模型表现"的稀缺资源, 必须当成核心约束来精心管理。
# 核心: 把上下文窗口当稀缺资源精心管理: 大结果外置、历史摘要、关键信息pinning、监控token、
# 分短期/长期记忆; 追求"信息相关且精炼", 而非"信息全且冗长"——这是Agent工程的核心能力。
排查让我把 Agent 上下文的其他坑也梳理清了。一、上下文无限膨胀(本文)。二、大工具结果直接入上下文。三、关键约束被淹没(固定/重申)。四、没有长期记忆(外部记忆库+检索)。五、摘要丢了关键信息。六、上下文全是噪声。七、成本/延迟随上下文暴涨。八、多 Agent 间传递整个上下文。它们的共同根源是:把"上下文窗口"当成"无限的、免费的、信息越多越好的"空间无脑往里堆;而它是"有限、昂贵、且过载会降低模型表现"的稀缺资源,必须当核心约束来精心管理。核心是:把上下文窗口当稀缺资源精心管理:大结果外置、历史摘要、关键信息 pinning、监控 token、分短期/长期记忆;追求"信息相关且精炼"而非"信息全且冗长"——这是 Agent 工程的核心能力。下面这张图,是这次上下文膨胀坑的成因与解法:
第四件事:上下文管理策略速查表
这次踩坑后,我把 Agent 上下文管理的几种策略整理成一张表,按需组合使用。
| 策略 | 解决什么 | 怎么做 |
|---|---|---|
| 大结果外置 | 大工具结果撑爆窗口 | 存外部, 上下文放摘要+引用 |
| 历史摘要压缩 | 历史无限增长 | 早期摘要, 近期保留细节 |
| 关键信息pinning | 目标被淹没/失忆 | 核心任务固定在显眼处 |
| 滑动窗口 | token超限 | 只保留最近N步 |
| 结构化记忆 | 跨步/跨会话记忆 | 短期上下文+长期外部库检索 |
| 监控token | 及时干预 | 接近阈值触发裁剪 |
这张表把上下文管理策略钉清了。核心是:管理上下文不是单一手段,而是一套组合——大结果外置(减体积)、历史摘要(压时间维度)、关键信息 pinning(保重点)、滑动窗口(控长度)、结构化记忆(扩容到外部)、监控 token(及时干预),按任务复杂度组合使用。它给我的最大启发是:这套上下文管理,本质是在解决一个经典问题——"有限的快速存储"和"无限的数据"之间的矛盾——这和操作系统的内存管理(有限内存 vs 大量数据:换页、缓存、LRU淘汰)、CPU 的缓存层级(小而快的缓存 vs 大而慢的内存)惊人地相似;"上下文窗口"就是 Agent 的"快速但有限的工作内存",而外部存储就是它的"慢但海量的硬盘",上下文管理就是在做"把最相关的放进有限的快速区、其余放慢速区按需调入"的调度。这让我看到了知识的迁移:计算机科学里"分层存储 + 把热数据放快速层 + 冷数据放慢速层 + 按需调度"这套经典思想,在 Agent 上下文管理上完美复现——理解了内存管理/缓存的人,理解 Agent 上下文管理几乎是降维的;"有限快速资源 + 无限慢速资源 + 智能调度"是一个跨越无数领域的通用模式。把上下文管理看成经典的分层存储/缓存调度问题、迁移已有知识——是这个坑带给我的认知。
第五件事:Agent 工程里"上下文就是一切"
这次让我深刻体会到,Agent 的能力很大程度上取决于"你给它什么样的上下文"。我整理成表。
| 上下文的状态 | 对Agent的影响 |
|---|---|
| 关键信息突出 | Agent抓得住重点, 不跑偏 |
| 信息相关精炼 | 推理准、速度快、成本低 |
| 信息冗长噪杂 | 被带偏、变慢、变贵 |
| 关键信息缺失 | "巧妇难为无米之炊", 答不对 |
| 超出窗口 | 直接崩溃 |
这张表道出了 Agent 工程的一个核心真相。核心是:Agent 的表现,极大程度上取决于"喂给它的上下文质量"——关键信息突出、相关且精炼的上下文,让它推理准、不跑偏、又快又省;冗长噪杂或缺失关键的上下文,让它被带偏、变慢变贵、甚至答不对或崩溃;"给模型什么上下文",几乎决定了它能产出什么。它给我的深刻启发是:构建 Agent/LLM 应用,有一项核心工作就是"上下文工程(context engineering)"——不只是写好 prompt,而是系统地设计"在每一步,该把哪些信息、以什么形式、放在上下文的什么位置";从 RAG 检索相关知识、到工具结果的取舍、到历史的摘要、到关键信息的强调——本质都是在"为模型精心组织它这一步推理所需的、恰到好处的信息";"模型能力是给定的, 你能优化的是喂给它的上下文"——上下文工程, 是 AI 应用工程师最核心的技能之一。这给了我一个清晰的努力方向:做 AI 应用,要把大量心思花在"上下文的构建与管理"上——确保每次调用模型时,它拿到的是"解决当前问题所需的、最相关的、组织良好的、不超量的"信息;"把对的信息,在对的时候,以对的方式,放进有限的上下文",是驾驭大模型、做出好用 Agent 的关键功夫。认清上下文质量决定 Agent 表现、把上下文工程当核心技能——是这个坑带给我的、关于 AI 工程的方向性认知。
第六件事:设计 Agent 上下文时,我现在的思考习惯
现在每当我设计一个 Agent 的上下文,我都会按这张图先想清楚:
这张图的精髓,是"长任务必须管理上下文:外置大结果、摘要历史、固定目标、监控 token"。短任务简单堆历史即可(注意工具结果别太大);长多步任务必须管理:大结果外置、早期历史摘要、核心目标 pinning、监控 token、需要时加外部记忆。这套习惯,让我从"把所有信息都堆给模型"变成了"精心组织恰好够用的上下文"——核心始终是:上下文窗口是稀缺资源,长任务必须主动管理,给模型相关精炼、重点突出的上下文。
我立下的几条规矩
这场"上下文无限膨胀、Agent 失控失忆"的事故,换来了我做 Agent 时,刻进骨子里的几条铁律:
- 上下文窗口是有限且宝贵的资源。不是信息越多越好,要精打细算。
- Agent 每步带全部历史会快速膨胀。工具结果大+步数多,轻松撑爆窗口。
- 过长上下文既会 token 超限,也会失忆跑偏。lost in the middle。
- 大工具结果外置。上下文只放摘要+引用,需要细节再按引用取。
- 早期历史摘要压缩,核心目标固定保留。压时间维度、保重点不被淹没。
- 实时监控 token,接近阈值就裁剪。控上下文大小=控成本和延迟。
- 上下文工程是 Agent 工程的核心技能。给对的信息,以对的方式,在对的位置。
写在最后
回头看,这场由"上下文无限堆积"引发的、Agent 又崩又"失忆"的事故,真正教给我的,远不止"Agent 要做上下文管理"这一个技巧。它让我对"'给得越多越好'是一种危险的直觉,真正有价值的,往往是'恰到好处的、突出重点的少'",有了一次刻骨的体会。我栽跟头,源于一个根深蒂固的、却是错误的直觉:"信息越多越好"——我生怕模型"缺信息"而做不好决策,于是把每一步的所有细节、所有工具结果,都毫无保留地塞给它,以为"我把知道的全告诉它,它就能做得最好"。可现实恰恰相反:过量的、未经组织的信息,不仅没帮上忙,反而害了它——要么撑爆了它的容量(它根本"装不下"),要么淹没了真正重要的信息(它在海量细节里"找不到重点"、被带偏);我以为的"慷慨地给足信息",实际是"用噪声稀释了信号、用冗余挤掉了关键"。这让我领悟到一个超越 Agent、关于"信息与决策"的深刻认知:"更多的信息"不等于"更好的决策"——决策质量取决于"相关信息的信噪比",而非"信息的总量";过量的信息会带来"信息过载":淹没关键、增加噪声、消耗有限的处理能力(无论是模型的上下文窗口,还是人的注意力);真正的高手, 不是"掌握信息最多"的, 而是"能从海量信息里精准地筛出、组织出当下最相关的那一点点"的。这给了我一种"少即是多"的设计智慧:无论是给模型构造上下文、还是给人做汇报、还是设计接口——都要追求"精准、相关、突出重点",而非"详尽、全面、事无巨细"——主动地筛选、提炼、组织、强调,把"最相关的关键信息"清晰地呈现出来,把无关的噪声滤掉;"恰到好处的少",远胜于"不分主次的多"——这是处理一切"有限的处理能力 + 过量的信息"问题的核心智慧。认清更多信息不等于更好决策、追求信噪比而非信息量、奉行少即是多——这,是我用一次 Agent 上下文失控的事故,换来的、关于 AI Agent、也关于如何处理信息与决策的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次给 Agent(或给任何需要决策的对象)准备信息时,先想一句"哪些是真正关键的、能不能更精炼",而不是一股脑全塞进去,那我对着那个又崩又跑偏的 Agent 排查的这大半天,就值了。
—— 别看了 · 2026