上下文管理——Agent 的「工作记忆」

📅 2026/8/3 6:02:51 👁️ 阅读次数 📝 编程学习
上下文管理——Agent 的「工作记忆」

上下文管理——Agent 的"工作记忆"

这是「Agent 工程化」系列的第四篇。前三篇我们让 Agent 有了"手"(工具调用),但你可能已经注意到一个隐患:每次工具结果都要放回对话,对话会越来越长,模型早晚"失忆"。这篇讲 Agent 落地绕不开的第一天敌——上下文管理


失忆现场:聊着聊着,Agent 忘了你说过的话

先看一段真实会发生的对话。用户请 AI 旅行助手规划三亚 5 日游:

用户:帮我规划下周五的三亚 5 日游 助手:好的,我先查下航班和酒店(调用工具……) 用户:对了,不要红眼航班,我怕熬夜 助手:好的,记下了(其实它什么都没"记") ……中间又聊了 20 轮:酒店、景点、预算、美食…… 用户:那机票就帮我订了吧 助手:好的,为您预订红眼航班 MU5757,00:30 起飞 ✓ 用户:???我说了不要红眼航班!

助手不是故意装傻,它真的忘了。20 轮之后,"不要红眼航班"这条信息已经不在它的视野里了。

这不是模型笨,而是所有 Agent 都躲不过的物理限制——上下文窗口(Context Window)有限。这篇就把这件事讲透:为什么失忆、怎么防失忆、防失忆的坑在哪。


为什么 Agent 会失忆:上下文窗口是有限的

先搞清一个概念:上下文 = 模型每次思考时能"看到"的全部内容。包括:用户说的、它自己说的、工具返回的……全都要塞进一次请求里发给模型。

上下文不是无限的。每个模型都有一个窗口上限(比如 8K、32K、128K token),像一块固定大小的小黑板:

第1轮: 用户需求 + 工具结果

第2轮

第3轮

每轮都追加进小黑板

小黑板写满了 8 万 token

最早写的内容被挤掉 = 失忆

关键是:每次对话,历史是全部重发的。模型没有"记忆",它只在每次请求时把完整历史再读一遍。历史越长:

后果原因
失忆超过窗口上限的内容直接被丢弃,模型看不到
变贵按 token 计费,历史全是钱
变慢处理长输入更耗时
犯错信息堆太多,模型"注意不到"关键点

为什么说它是"第一天敌"?因为它是物理上限,再聪明的模型、再好的 prompt 都绕不开。你能做的只有一件事:让有限的黑板,装下最重要的信息。

围绕这个目标,业界有三招:滑动窗口、摘要压缩、截断。我们一个个看。


对策一:滑动窗口——只留最近 N 轮

最朴素的做法:只保留最近 K 轮对话,更早的一律丢掉。

KEEP=10# 只留最近 10 轮defslide(messages:list)->list:returnmessages[-KEEP*2:]# 用户和助手各算一条

K 轮之内

K 轮之外

完整历史 30 轮

只保留最近 K 轮

保留原文

直接丢弃

风险: 20轮前的用户偏好丢了

优点:实现一行代码、token 消耗可控。代价:窗口之外的信息全没了。回到开头的例子——"不要红眼航班"是第 3 轮说的,如果 K=10,第 13 轮起它就消失了,第 20 轮订票时助手自然不记得。

所以滑动窗口只适合"历史不重要"的场景。现实里的 Agent 显然不行——用户偏好往往是早期说的,恰恰最不能丢。


对策二:摘要压缩——把旧历史"浓缩"起来

升级版思路:别丢,压缩。用一次 LLM 调用,把窗口外的旧对话浓缩成几句话要点,放回上下文。

defcompress(old_messages:list)->str:prompt="用3句话概括这段对话的关键信息(用户偏好、已完成动作、结论)"resp=client.chat.completions.create(model="gpt-4o-mini",messages=[{"role":"system","content":prompt}]+old_messages,)returnresp.choices[0].message.content

窗口外的旧轮次

LLM 压缩成 3 句话要点

以 system 身份放回上下文

模型仍然知道大概历史

比如"不要红眼航班"就会被浓缩进摘要:

历史摘要:用户要下周五去三亚,5日游。明确偏好:不要红眼航班。 预算经济舱。已确认酒店:三亚湾某酒店。

优点:能装下"整个会话"的信息量。代价有二:

  1. 细节会丢——"已订 MU5101"这种精确事实,如果摘要时没提炼进去,就永远没了
  2. 摘要本身要花钱——每次压缩都是一次 LLM 调用

但总体上是笔划算的买卖:用几十 token 的摘要,换回几万 token 的历史。


对策三:截断——工具结果太大时按 token 砍

前两招对付"对话历史",还有一类更隐蔽的膨胀源:工具返回的结果。第三篇的坑三就是它——一个接口返回几百个字段的 JSON,一次调用就烧掉几千 token。

deftruncate(text:str,max_chars:int=2000)->str:returntext[:max_chars]+"…"

工具返回超大 JSON

超过上限吗

完整保留

只留关键字段 + 前 N token

保留结论字段 丢弃日志数组

注意截断不是"从前往后砍"那么简单——砍尾巴可能砍掉结论。正确姿势是按"字段重要度"砍:

做法说明
result[:2000]从头砍,关键字段可能在最后
只保留结论字段,丢弃明细数组({status, error, message}
结构化解构:结论字段全保留,明细字段只取前 N 条 + “共 X 条已省略”

这一步在工具设计时就要考虑:工具返回的 JSON 结构,应该天生"结论在前、明细在后"。从源头设计好,截断就简单。


三招怎么组合:生产级的分层保留

单用哪一招都有缺陷,生产环境是三招组合的分层结构——把上下文分成三层:

滑动区 只留最近K轮

第21到30轮原文细节

摘要区 定期压缩

第1到20轮对话要点

锁住区 永不删除

用户偏好 不要红眼航班

已完成操作 已订MU5101

任务目标 三亚5日游

  • 锁住区:任务目标、用户明确偏好、已完成的写操作——任何时候都不能丢
  • 摘要区:更早的历史定期浓缩成要点
  • 滑动区:最近 K 轮原文,保持最新细节

核心代码长这样(分层 + 摘要 + 滑动组合):

LOCKED="用户偏好:不要红眼航班;已订:MU5101;目标:三亚5日游"defbuild_context(messages:list)->list:history=messages[:-KEEP*2]# 窗口外的旧历史recent=messages[-KEEP*2:]# 最近 K 轮原文summary=compress(history)ifhistoryelse""return[{"role":"system","content":f"不可遗忘的信息:{LOCKED}"},{"role":"system","content":f"历史摘要:{summary}"},]+recent

回到开头的例子:因为"不要红眼航班"被提升进了锁住区,20 轮后订票时,助手依然能看到它,就不会再订红眼航班了。


生产级考虑:token 记账——知道钱花哪了

上下文管理的本质是"用有限的 token 装重要信息",所以你得先知道每次会话花了多少 token。按 token 计费是公开的,记账公式很简单:

单次请求费用 = 输入 token × 输入单价 + 输出 token × 输出单价

估算一个典型会话(以某个常见模型为例,单价约 输入 $0.15/M、输出 $0.6/M):

会话阶段累计上下文单次请求费用(约)累计费用(约)
第 1 轮1K token$0.0008$0.0008
第 10 轮6K token$0.003$0.02
第 30 轮20K token$0.008$0.15
第 50 轮(未管理)40K token$0.015$0.6+

单次看都不贵,但线上流量一大,这就是纯成本。所以生产环境至少做两件事:

  1. 每轮记账:记录输入/输出 token,会话结束汇总
  2. 预算护栏:单次会话设置 token 上限(如 30K),超了就强制压缩/截断

(完整的预算护栏体系在第 9 篇讲,这里先知道"要记账"就够了。)


踩坑:按新旧砍,还是按重要度砍?

这是上下文管理最容易犯的错。很多人一上来就用滑动窗口,“旧的丢掉”——结果把最重要的信息砍了。我们开头那个例子就是活教材:"不要红眼航班"按新旧排在第 3 轮,早该被砍;按重要度排,它是最该留的。

正确的判断标准不是"新旧",而是"重要度":

信息类型重要度处置
用户明确偏好(不要红眼航班)极高锁住区,永不删
任务目标(三亚 5 日游)极高锁住区
已完成的关键操作(已订 MU5101)锁住区或摘要
中途的推理过程滑动窗口,可丢
工具返回的大段明细截断
闲聊极低直接丢

每产生一条新信息,都要问一句:它值得进锁住区吗?值得就提取进去。这个"提取"动作可以交给 LLM 自动做——每轮结束后让模型总结一句"本轮有没有值得记住的偏好/事实",有就更新锁住区。这样锁住区会越用越准。


小结

  • 失忆的根因:上下文窗口有限,历史是每次全量重发的,装不下就被挤掉
  • 三招组合:滑动窗口(留新鲜)+ 摘要压缩(留大意)+ 截断(管工具结果)
  • 生产分层:锁住区(永不删)+ 摘要区(定期压缩)+ 滑动区(最近原文)
  • 判断标准:按重要度砍,不按新旧砍
  • 别忘了记账:token 是钱,记账是上下文管理的第一步

下篇预告

这篇讲的都是"一次会话内"的事:怎么让 Agent 在 50 轮长对话里不忘事。

但还有一个更大的问题没解决:昨天你说"去北京要坐靠窗",今天新开一个会话,Agent 还记得吗?会话一关,上下文清空,什么都留不下。下一篇讲Memory 记忆系统——让 Agent 把记忆存下来,跨会话记住用户。

本系列路线(从 0 到 1):Agent 是什么 → 手写最小 ReAct → Function Calling 与工具设计 → 上下文管理 → Memory 记忆系统 → RAG 知识库 → Skill 自学习 → 编排模式与多 Agent → 给 Agent 装护栏 → 生产部署与可观测 → 评测与回归