上下文管理——Agent 的"工作记忆"
这是「Agent 工程化」系列的第四篇。前三篇我们让 Agent 有了"手"(工具调用),但你可能已经注意到一个隐患:每次工具结果都要放回对话,对话会越来越长,模型早晚"失忆"。这篇讲 Agent 落地绕不开的第一天敌——上下文管理。
失忆现场:聊着聊着,Agent 忘了你说过的话
先看一段真实会发生的对话。用户请 AI 旅行助手规划三亚 5 日游:
用户:帮我规划下周五的三亚 5 日游 助手:好的,我先查下航班和酒店(调用工具……) 用户:对了,不要红眼航班,我怕熬夜 助手:好的,记下了(其实它什么都没"记") ……中间又聊了 20 轮:酒店、景点、预算、美食…… 用户:那机票就帮我订了吧 助手:好的,为您预订红眼航班 MU5757,00:30 起飞 ✓ 用户:???我说了不要红眼航班!助手不是故意装傻,它真的忘了。20 轮之后,"不要红眼航班"这条信息已经不在它的视野里了。
这不是模型笨,而是所有 Agent 都躲不过的物理限制——上下文窗口(Context Window)有限。这篇就把这件事讲透:为什么失忆、怎么防失忆、防失忆的坑在哪。
为什么 Agent 会失忆:上下文窗口是有限的
先搞清一个概念:上下文 = 模型每次思考时能"看到"的全部内容。包括:用户说的、它自己说的、工具返回的……全都要塞进一次请求里发给模型。
上下文不是无限的。每个模型都有一个窗口上限(比如 8K、32K、128K token),像一块固定大小的小黑板:
关键是:每次对话,历史是全部重发的。模型没有"记忆",它只在每次请求时把完整历史再读一遍。历史越长:
| 后果 | 原因 |
|---|---|
| 失忆 | 超过窗口上限的内容直接被丢弃,模型看不到 |
| 变贵 | 按 token 计费,历史全是钱 |
| 变慢 | 处理长输入更耗时 |
| 犯错 | 信息堆太多,模型"注意不到"关键点 |
为什么说它是"第一天敌"?因为它是物理上限,再聪明的模型、再好的 prompt 都绕不开。你能做的只有一件事:让有限的黑板,装下最重要的信息。
围绕这个目标,业界有三招:滑动窗口、摘要压缩、截断。我们一个个看。
对策一:滑动窗口——只留最近 N 轮
最朴素的做法:只保留最近 K 轮对话,更早的一律丢掉。
KEEP=10# 只留最近 10 轮defslide(messages:list)->list:returnmessages[-KEEP*2:]# 用户和助手各算一条优点:实现一行代码、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比如"不要红眼航班"就会被浓缩进摘要:
历史摘要:用户要下周五去三亚,5日游。明确偏好:不要红眼航班。 预算经济舱。已确认酒店:三亚湾某酒店。优点:能装下"整个会话"的信息量。代价有二:
- 细节会丢——"已订 MU5101"这种精确事实,如果摘要时没提炼进去,就永远没了
- 摘要本身要花钱——每次压缩都是一次 LLM 调用
但总体上是笔划算的买卖:用几十 token 的摘要,换回几万 token 的历史。
对策三:截断——工具结果太大时按 token 砍
前两招对付"对话历史",还有一类更隐蔽的膨胀源:工具返回的结果。第三篇的坑三就是它——一个接口返回几百个字段的 JSON,一次调用就烧掉几千 token。
deftruncate(text:str,max_chars:int=2000)->str:returntext[:max_chars]+"…"注意截断不是"从前往后砍"那么简单——砍尾巴可能砍掉结论。正确姿势是按"字段重要度"砍:
| 做法 | 说明 |
|---|---|
| 差 | result[:2000]从头砍,关键字段可能在最后 |
| 中 | 只保留结论字段,丢弃明细数组({status, error, message}) |
| 好 | 结构化解构:结论字段全保留,明细字段只取前 N 条 + “共 X 条已省略” |
这一步在工具设计时就要考虑:工具返回的 JSON 结构,应该天生"结论在前、明细在后"。从源头设计好,截断就简单。
三招怎么组合:生产级的分层保留
单用哪一招都有缺陷,生产环境是三招组合的分层结构——把上下文分成三层:
- 锁住区:任务目标、用户明确偏好、已完成的写操作——任何时候都不能丢
- 摘要区:更早的历史定期浓缩成要点
- 滑动区:最近 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+ |
单次看都不贵,但线上流量一大,这就是纯成本。所以生产环境至少做两件事:
- 每轮记账:记录输入/输出 token,会话结束汇总
- 预算护栏:单次会话设置 token 上限(如 30K),超了就强制压缩/截断
(完整的预算护栏体系在第 9 篇讲,这里先知道"要记账"就够了。)
踩坑:按新旧砍,还是按重要度砍?
这是上下文管理最容易犯的错。很多人一上来就用滑动窗口,“旧的丢掉”——结果把最重要的信息砍了。我们开头那个例子就是活教材:"不要红眼航班"按新旧排在第 3 轮,早该被砍;按重要度排,它是最该留的。
正确的判断标准不是"新旧",而是"重要度":
| 信息类型 | 重要度 | 处置 |
|---|---|---|
| 用户明确偏好(不要红眼航班) | 极高 | 锁住区,永不删 |
| 任务目标(三亚 5 日游) | 极高 | 锁住区 |
| 已完成的关键操作(已订 MU5101) | 高 | 锁住区或摘要 |
| 中途的推理过程 | 低 | 滑动窗口,可丢 |
| 工具返回的大段明细 | 低 | 截断 |
| 闲聊 | 极低 | 直接丢 |
每产生一条新信息,都要问一句:它值得进锁住区吗?值得就提取进去。这个"提取"动作可以交给 LLM 自动做——每轮结束后让模型总结一句"本轮有没有值得记住的偏好/事实",有就更新锁住区。这样锁住区会越用越准。
小结
- 失忆的根因:上下文窗口有限,历史是每次全量重发的,装不下就被挤掉
- 三招组合:滑动窗口(留新鲜)+ 摘要压缩(留大意)+ 截断(管工具结果)
- 生产分层:锁住区(永不删)+ 摘要区(定期压缩)+ 滑动区(最近原文)
- 判断标准:按重要度砍,不按新旧砍
- 别忘了记账:token 是钱,记账是上下文管理的第一步
下篇预告
这篇讲的都是"一次会话内"的事:怎么让 Agent 在 50 轮长对话里不忘事。
但还有一个更大的问题没解决:昨天你说"去北京要坐靠窗",今天新开一个会话,Agent 还记得吗?会话一关,上下文清空,什么都留不下。下一篇讲Memory 记忆系统——让 Agent 把记忆存下来,跨会话记住用户。
本系列路线(从 0 到 1):Agent 是什么 → 手写最小 ReAct → Function Calling 与工具设计 → 上下文管理 → Memory 记忆系统 → RAG 知识库 → Skill 自学习 → 编排模式与多 Agent → 给 Agent 装护栏 → 生产部署与可观测 → 评测与回归