🧠 上下文管理怎么设计?多轮对话 & Agent
模型没有记忆,所谓多轮就是每次都得把历史重新打包。算预算 → 挑内容 → 排顺序,把打包做得聪明。
一句话定义 + 上下文由什么组成
🎯 一句话定义
上下文管理 = 在有限的 context window 里,每轮调用前对可用信息进行筛选、压缩、排序,组装出最终送给模型的 messages。
模型本身无状态,所谓「多轮」就是你每次都得把历史重新打包。
上下文管理的全部工作,就是把这个打包过程做得聪明。
🧩 一次调用含哪几块
① System Prompt:角色设定、行为约束、工具定义
② 历史对话轮次:用户说了什么、模型回了什么
③ 工具调用 & 结果:Agent 场景的函数记录和返回
④ 检索/外部注入:RAG 召回片段
⑤ 当前用户输入:这一轮最新消息
上下文管理的核心操作:对 ②③④ 做裁剪和压缩,确保总量不超窗口,且最重要的信息排在最好的位置。
裁剪策略:留什么、丢什么
💰 Token 预算制
每次调用前先算账:
可用预算 = 窗口上限 − System Prompt − 当前输入 − 预留输出
剩下的才是历史 + 工具结果 + 检索内容的共享额度。超了就必须砍。
🪟 滑动窗口(最基础)
保留最近 N 轮原文,更早的丢弃或降级为摘要。
N 取决于场景:客服对话 5-10 轮,代码 Agent 3-5 轮(因为代码片段长)。
🏆 重要性评分
给每条历史打分,综合:
• 新鲜度:越近权重越高
• 类型:工具结果 > 用户关键指令 > 闲聊
• 是否被引用:后续提到的权重更高
• 用户标注:说「记住这个」的优先保留
按分数从高到低填进预算,填满为止。
| 策略 | 核心思想 | 适用 |
|---|---|---|
| Token 预算制 | 先算账再裁剪 | 所有场景的基础 |
| 滑动窗口 | 留最近 N 轮 | 客服、通用对话 |
| 重要性评分 | 按分填预算 | 信息价值差异大时 |
压缩策略:怎么缩小体积
📝 摘要压缩
早期对话即将被挤出窗口时,用 LLM 压成紧凑摘要。
• 阶段性摘要:每 N 轮或任务节点生成一次
• 递归摘要:摘要太长再对摘要做摘要
摘要建议结构化:[用户目标] → [已尝试方案] → [当前卡点] → [待确认事项],比自由文本省 token 且信息密度高。
🧰 工具结果精简
Agent 场景最易爆窗口的环节。一个 API 返回几百行 JSON,不能原样塞进历史。
• 只提取关键字段(状态码、核心数据、错误信息)
• 超长结果截断 + 提示「完整结果已存储,需要时可再调」
• 或让模型生成一个「工具结果摘要」替换原始返回
🗂️ 结构化状态对象
任务型 Agent 维护一份独立的 JSON 状态(current_step, slots, decisions),每轮更新。
放在 System Prompt 之后作为固定区块,让模型始终知道「现在在哪、还差什么」。
比从对话历史里推断进度更可靠、更省 token。
排序策略:放在哪
模型对上下文不同位置的注意力不均匀——开头和结尾利用率最高,中间容易被忽略(lost-in-the-middle 现象)。
| 位置 | 放什么 | 原因 |
|---|---|---|
| 最开头(System Prompt 后) | 结构化状态、核心约束 | 模型一定会仔细看 |
| 靠前 | 高度相关的检索结果、关键背景 | primacy 效应 |
| 中间 | 远期摘要、次要历史 | 接受低利用率 |
| 靠后 | 近期 2-3 轮原文、当前输入 | recency 效应,对末尾最敏感 |
Agent 场景的额外考量
🔌 工具调用的上下文隔离
工具调用和结果不该跟对话历史混在同一个 messages 里平铺。
• 工具调用请求作为独立的 tool_calls 结构传递
• 工具结果走单独通道,由上下文管理器决定是否压缩后再展示给模型
• 避免一个长工具返回把后面的对话挤出去
🌿 分支 / 回溯支持
用户可能说「回到刚才那个方案」。
若只维护一条线性历史就没法回溯。
方案:
• 维护一棵对话树,每个节点是一步
• 用户回溯时从那个节点 fork 出新分支
• 上下文编译器按当前分支路径组装 messages
🔁 失败重试的上下文差异
重试同一轮时,不要完全复用上一次的上下文。
至少把上次模型输出的错误部分去掉或标记为「此路不通」,
避免模型重复犯同样的错。
工程落地的关键点
上下文可观测
每一轮到底塞了什么、占多少 token,必须有日志。出问题才能定位是「塞少了」还是「塞多了」。
预算告警
接近窗口上限时主动通知,而不是等到 API 报错。
降级策略
极端情况(工具返回异常大),宁可丢信息也要保证调用成功——返回降级提示而非直接崩。
测试用长对话
用自动化脚本跑 50+ 轮对话,验证压缩和裁剪在极限情况下的表现。
记忆系统是另一个话题,它的产出会作为输入之一进入这个流程;但上下文管理本身的职责就是管好「这一轮模型能看到什么」。
上下文管理 = 在有限 context window 里,每轮调用前对可用信息做筛选、压缩、排序,组装出最终 messages。三件事:① 算预算(窗口−System−当前输入−预留输出)② 挑内容(裁剪:滑动窗口/重要性评分;压缩:摘要/工具结果精简/结构化状态)③ 排顺序(重要放头尾,利用 primacy/recency)。Agent 场景要隔离工具上下文、支持分支回溯、失败重试别复用错误上下文。
核心思想是每轮调用前先算预算,再筛选、压缩、排序。模型本身无状态,所谓多轮就是每次都要把历史重新打包。预算上:可用额度等于窗口上限减去 System Prompt、当前输入、预留输出空间,剩下的才是历史加工具结果加检索内容的共享额度。然后对历史做裁剪和压缩,最后按重要程度排序,最重要的放开头和结尾,因为模型对这两端利用率最高,中间容易被忽略。
这是 Agent 最易爆窗口的环节。我的做法是工具结果不能原样塞进历史,要做精简:只提取关键字段,比如状态码、核心数据、错误信息;超长结果截断并提示完整结果已存储需要时可再调;或者让模型生成一个工具结果摘要替换原始返回。还要注意工具调用和结果别跟对话历史混在同一个 messages 里平铺,走单独的通道,避免一个长返回把后面的对话挤出去。
如果只维护一条线性历史,这种回溯是做不到的。我一般维护一棵对话树,每个节点是一步;用户回溯时,从那个节点 fork 出新的分支;上下文编译器根据当前分支路径来组装 messages。这样既保留了历史路径,又能支持分支回溯。另外重试同一轮时我不会完全复用上次的上下文,会把上次模型输出的错误部分去掉或标记为此路不通,避免模型重复犯同样的错。