上下文管理——Agent 的「工作记忆」
上下文管理——Agent 的工作记忆这是「Agent 工程化」系列的第四篇。前三篇我们让 Agent 有了手工具调用但你可能已经注意到一个隐患每次工具结果都要放回对话对话会越来越长模型早晚失忆。这篇讲 Agent 落地绕不开的第一天敌——上下文管理。失忆现场聊着聊着Agent 忘了你说过的话先看一段真实会发生的对话。用户请 AI 旅行助手规划三亚 5 日游用户帮我规划下周五的三亚 5 日游 助手好的我先查下航班和酒店调用工具…… 用户对了不要红眼航班我怕熬夜 助手好的记下了其实它什么都没记 ……中间又聊了 20 轮酒店、景点、预算、美食…… 用户那机票就帮我订了吧 助手好的为您预订红眼航班 MU575700:30 起飞 ✓ 用户我说了不要红眼航班助手不是故意装傻它真的忘了。20 轮之后不要红眼航班这条信息已经不在它的视野里了。这不是模型笨而是所有 Agent 都躲不过的物理限制——上下文窗口Context Window有限。这篇就把这件事讲透为什么失忆、怎么防失忆、防失忆的坑在哪。为什么 Agent 会失忆上下文窗口是有限的先搞清一个概念上下文 模型每次思考时能看到的全部内容。包括用户说的、它自己说的、工具返回的……全都要塞进一次请求里发给模型。上下文不是无限的。每个模型都有一个窗口上限比如 8K、32K、128K token像一块固定大小的小黑板第1轮: 用户需求 工具结果第2轮第3轮每轮都追加进小黑板小黑板写满了 8 万 token最早写的内容被挤掉 失忆关键是每次对话历史是全部重发的。模型没有记忆它只在每次请求时把完整历史再读一遍。历史越长后果原因失忆超过窗口上限的内容直接被丢弃模型看不到变贵按 token 计费历史全是钱变慢处理长输入更耗时犯错信息堆太多模型注意不到关键点为什么说它是第一天敌因为它是物理上限再聪明的模型、再好的 prompt 都绕不开。你能做的只有一件事让有限的黑板装下最重要的信息。围绕这个目标业界有三招滑动窗口、摘要压缩、截断。我们一个个看。对策一滑动窗口——只留最近 N 轮最朴素的做法只保留最近 K 轮对话更早的一律丢掉。KEEP10# 只留最近 10 轮defslide(messages:list)-list:returnmessages[-KEEP*2:]# 用户和助手各算一条K 轮之内K 轮之外完整历史 30 轮只保留最近 K 轮保留原文直接丢弃风险: 20轮前的用户偏好丢了优点实现一行代码、token 消耗可控。代价窗口之外的信息全没了。回到开头的例子——不要红眼航班是第 3 轮说的如果 K10第 13 轮起它就消失了第 20 轮订票时助手自然不记得。所以滑动窗口只适合历史不重要的场景。现实里的 Agent 显然不行——用户偏好往往是早期说的恰恰最不能丢。对策二摘要压缩——把旧历史浓缩起来升级版思路别丢压缩。用一次 LLM 调用把窗口外的旧对话浓缩成几句话要点放回上下文。defcompress(old_messages:list)-str:prompt用3句话概括这段对话的关键信息用户偏好、已完成动作、结论respclient.chat.completions.create(modelgpt-4o-mini,messages[{role:system,content:prompt}]old_messages,)returnresp.choices[0].message.content窗口外的旧轮次LLM 压缩成 3 句话要点以 system 身份放回上下文模型仍然知道大概历史比如不要红眼航班就会被浓缩进摘要历史摘要用户要下周五去三亚5日游。明确偏好不要红眼航班。 预算经济舱。已确认酒店三亚湾某酒店。优点能装下整个会话的信息量。代价有二细节会丢——已订 MU5101这种精确事实如果摘要时没提炼进去就永远没了摘要本身要花钱——每次压缩都是一次 LLM 调用但总体上是笔划算的买卖用几十 token 的摘要换回几万 token 的历史。对策三截断——工具结果太大时按 token 砍前两招对付对话历史还有一类更隐蔽的膨胀源工具返回的结果。第三篇的坑三就是它——一个接口返回几百个字段的 JSON一次调用就烧掉几千 token。deftruncate(text:str,max_chars:int2000)-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:historymessages[:-KEEP*2]# 窗口外的旧历史recentmessages[-KEEP*2:]# 最近 K 轮原文summarycompress(history)ifhistoryelsereturn[{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 到 1Agent 是什么 → 手写最小 ReAct → Function Calling 与工具设计 → 上下文管理 → Memory 记忆系统 → RAG 知识库 → Skill 自学习 → 编排模式与多 Agent → 给 Agent 装护栏 → 生产部署与可观测 → 评测与回归