Agent 聊着聊着就"失忆"?拆解 DeepAgents 的上下文压缩黑科技
来源:互联网
时间:2026-06-06 07:28:34
开场:为什么“上下文”会变成负担?
大模型有个硬性约束,叫作“上下文窗口”——它一次能“看”的 token 数,是有限的。 短对话当然无所谓。但当你让 Agent 干点长活的时候,每一轮“调用工具 → 拿到结果 → 再思考”都会往历史里追加一大坨内容:读了哪些文件、跑了哪些命令、命令吐了多长的日志……结果呢?几十轮下来,历史列表就轻松膨胀到几十万 token。
一旦撞上窗口上限,会遇到三件糟心事:
- “请求被拒”:provider 直接报个 context overflow 的错误;
- “费用飙升”:每轮对话都得把全部历史重发一遍,token 越多越贵;
- “早期信息丢失”:粗暴截断会把最初的任务目标也一起咔嚓掉,Agent 当场“失忆”。
所以,长任务型 Agent 必须有一套**上下文压缩(Summarization / Compaction)**机制:把那些“旧的、暂时用不上的”折叠成摘要,腾出空间继续干活。
这事听起来简单——“把旧的对话历史总结一下嘛”——但真要做得对、做得稳、做得不丢信息,里头的讲究比你想的要多得多。我们就以 DeepAgents 的实现为样本,一层层剥开来看。
---
一、一句话看懂 DeepAgents 的设计哲学
DeepAgents 的压缩有个很“工程师”的特点:**它没有重造轮子。** 它直接复用了 LangChain 自带的压缩内核——那套“判阈值 → 选切点 → 切分 → 生成摘要”的核心逻辑,原封不动地委托给上游。DeepAgents 自己只在外层加了「四件长任务 Agent 真正需要的增强」: - “旧历史落盘,可回溯”——压缩掉的消息不是删了,是归档了; - “压缩前先截大参数”——一层更便宜的优化,常常截一截就够,省掉一次摘要; - “撞墙了能兜底”——真超窗报错时,自动转去压缩重试; - “非破坏式压缩”——原始对话一个字不删,只记下“怎么压的”。 再加一个「手动压缩工具」,让模型能自己喊“我要清场了”。 记住这个骨架,下面逐条展开。 ---二、主流程:进模型前的三步流水线
DeepAgents 把压缩逻辑挂在“每次调用大模型之前”这个时机上。每次要调模型,它都会先跑一条三步流水线:
**第 0 步|重建有效消息**:先看看上一次有没有压缩过,如果有,就把“摘要 + 后面的新消息”拼成这次实际要用的视图。原稿不动,这点很关键,第四节会细说。
**第 1 步|截断大工具参数**:这是最便宜的一招。把历史里那些超长的 `write_file` / `edit_file` 参数(比如一次写了几千行的文件内容)裁短成“前 20 个字符 + (已截断)”。很多时候,光这一步省下的 token 就够了,根本不用惊动大模型。
**第 2 步|判阈值**:算一下当前 token,看有没有超过触发线。没超,就直接发给模型。但这里藏了个巧思:万一模型还是返回了“上下文溢出”错误,它不会把错误抛给你,而是“就地转入压缩路径重试”。相当于给阈值估算留了个安全气囊。
**第 3 步|真正压缩**:超阈值了,才走完整压缩。顺序很讲究——“先选安全切点 → 再把旧历史落盘 → 最后才让 LLM 生成摘要”,然后用“摘要 + 近期消息”替代原来那一长串,发给模型。
---
三、魔鬼在细节①:切点不能乱切
“把旧消息总结掉”听着简单,但**“从哪儿下刀”**是个技术活。 Agent 的历史里有大量的“工具调用对”:AI 说“我要调用 bash 跑个命令”(一条 AI 消息),紧跟着是“命令的返回结果”(一条 Tool 消息)。这俩是一对,谁也离不开谁。 如果切点不小心落在它们中间,就会留下一条“没有提问的答案”——一个孤儿 Tool 消息。绝大多数模型看到这种残缺结构,会直接报错。
DeepAgents(借助 LangChain 内核)的处理很优雅:如果发现切点正好压在某一条 Tool 消息上,就“向前回溯”,找到发起这次调用的那条 AI 消息,把切点提前,让整对要么一起被摘要、要么一起被保留。token 预算用二分查找精确逼近,但**绝不以切断工具对为代价。**
---
四、魔鬼在细节②:压缩了,但原稿一个字不删
这是 DeepAgents 相比 LangChain 原生实现“最本质的一处分歧”,也是最值得记一笔的设计。 LangChain 原生怎么做?简单粗暴:在压缩节点里“直接重写对话状态”——把原始消息全删掉(`RemoveMessage(REMOVE_ALL_MESSAGES)`),换成“摘要 + 近期消息”。干净,但原稿没了。 DeepAgents 偏不。它**一个字都不删**原始消息,只在一个私有字段 `_summarization_event` 里记三样东西:“在哪儿切的、摘要内容是什么、旧历史归档到哪个文件了”。每次要调模型时,再根据这个“事件”临时算出精简后的视图喂给模型。
为什么要这么“轴”?因为保留原稿能换来一堆好处:
- “可回放(replay)”:原始对话还在,随时能重跑、复盘;
- “可做评测(evals)”:拿真实完整轨迹去跑评估,不失真;
- “能和手动压缩工具共享状态”:两条压缩路径读写同一份原稿,不会互相打架(见第六节)。
代价当然有:每次调用都要“临时重建视图”,还要做一点切点坐标的换算。但对一个要长期、可观测、可调试运行的 Agent 平台来说,**“留住原稿的价值远大于这点开销”。**
---
五、魔鬼在细节③:压缩 = 折叠 + 归档,不是删除
很多人以为“压缩”就是“把旧的扔了,换一个摘要”。DeepAgents 的理解更高级一层:**“压缩是折叠,旧内容要归档,还要让 Agent 知道去哪儿找。”** 正式压缩前,它会先把要丢弃的那批消息“追加写入一个 Markdown 存档文件”(按线程一个文件,每次压缩追加一节,带时间戳)。然后——这步最妙——**“让摘要消息里带上这个存档文件的路径”。**
于是模型收到的摘要长这样(大意):
> “在文件 `thread_xxx_archive.md` 中,记录了第 1 到 20 轮对话的完整历史。本次会话的目标是……已执行的操作有……接下来的计划是……”
这意味着上下文瘦下来了,但**任何被折叠掉的细节,Agent 都能用 `read_file` 翻回来。** 摘要漏掉了某个关键命令的输出?没关系,去存档里查。这一手把“有损压缩”变成了“无损归档 + 有损视图”,是它最实用的增强。
顺带一提,摘要本身也不是随便写的。提示词强制 LLM 按四段 checklist 输出:「SESSION INTENT(这次到底要干嘛) / SUMMARY(关键决策与结论) / ARTIFACTS(动了哪些文件) / NEXT STEPS(接下来做什么)」——专门防止压缩后 Agent 重复劳动,或忘了产出物。
---
六、自动 + 手动:两层触发,一份状态
DeepAgents 给压缩配了“两套触发方式”,覆盖不同场景:
- **自动层**(`SummarizationMiddleware`):后台兜底。token 一旦超过触发线(模型 profile 可用时,默认是窗口的 **85%**)就自动压,全程不用模型或用户操心。
- **手动层**(`SummarizationToolMiddleware`):给 Agent 装一个 `compact_conversation` 工具,并在系统提示里悄悄告诉它——“当你切换到全新任务、或已经拿到结果、旧上下文用不上时,可以主动调用它清场”。
手动层有个防呆设计:**“50% 资格闸门”。** 如果上下文还没到自动触发线的一半,模型就算手贱调了 `compact_conversation`,也会被礼貌拒绝(“还没到该压的时候”),避免过早压缩白白浪费一次 LLM 调用。
两层最聪明的地方在于:**它们共享同一个 `_summarization_event` 状态字段。** 自动压的和手动压的写同一份账,互相可见、协同工作,不会出现“压了两遍”“坐标对不上”的混乱。
---