本文主要介绍上下文管理,问题知识点比较多,将分为两篇文档介绍。
1、上下文窗口(context window)、上下文(context)、记忆(memory)三者是什么关系?
一句话概括:上下文窗口是容器,上下文是容器里当前装的东西,记忆是容器外面那个仓库。记忆必须经过"召回 + 筛选 + 注入"才能变成上下文的一部分;没被召回的记忆,对本次推理等于不存在。
| 概念 | 本质 | 边界 | 谁决定 |
|---|---|---|---|
| 上下文窗口(context window) | 模型单次前向推理能接收的 token 上限 | 硬性物理约束,如 128k / 200k | 模型架构 + 位置编码 |
| 上下文(context) | 本次推理实际拼进去的全部内容 | 必须 ≤ 窗口上限 | 你的 prompt 组装逻辑 |
| 记忆(memory) | 落在外部存储、跨会话持久化的信息 | 不受窗口限制,可无限增长 | 你的存储与检索设计 |
2、上下文压缩和记忆压缩,区别在哪?
- 上下文压缩:把当前上下文中"占地方但当前用不上"的内容换成更紧凑的表示。
- 记忆压缩:对已写入外部存储的记忆做合并、去重、抽象、淘汰,让他在规模增长后仍然可检索。
上下文压缩发生在本次推理的输入侧,目标是让当前上下文窗口能放得下;记忆压缩发生在外部存储,目标是防止记忆膨胀,让长期积累的信息可检索等。
2.1、上下文压缩和记忆压缩本质对比
| 维度 | 上下文压缩 | 记忆压缩 |
|---|---|---|
| 作用对象 | 本次推理的输入 token | 外部存储中的记忆条目 |
| 触发时机 | 运行中,token 水位超阈值 | 写入时 / 定期离线任务 |
| 约束 | 硬约束:必须 ≤ 窗口 | 软约束:容量、检索精度、成本 |
| 目标 | 塞得下 + 保住当前任务关键信息 | 去重、合并、抽象、防噪声淹没 |
| 丢失后果 | 本轮任务失败 / 质量下降 | 未来某次召回失败(延迟暴露) |
| 可逆性 | 通常可逆(原文落盘可回溯) | 往往不可逆(合并后原始细节消失) |
| 评价指标 | 压缩后任务成功率保持率 | 召回准确率 / 召回覆盖率 |
2.2、上下文压缩
-
常见手段
- 丢弃冗余工具返回原文:保留摘要 + 引用id
- 合并同类消息:连续多轮工具调用合并为一条
- 早期对话滚动摘要
- 结构化裁剪:大json只留关键字段
- 外置+句柄:内容落盘,上下文中只放"内容摘要+文件路径+查询接口"
-
红线
永不压缩任务目标、用户显示约束、已确认的关键事实、当前正在处理的任务等
-
关键特性
- 有明确的"当前任务"作为判断基准------什么该留、什么该丢,取决于这轮要干什么
- 可回溯:压缩 ≠ 删除,原文要能按 ID 取回
- 高频发生:长会话里可能每几轮就触发一次
2.3、记忆压缩
- 常见手段
- 去重:同一事实多次写入 → 覆盖而非追加(保留最新 + 时间戳)
- 合并:多条碎片化记忆 → 合成一条完整事实
- 抽象/升华:多次具体事件 → 提炼为一条规律("用户三次要求用中文回答" → "用户偏好中文")
- 淘汰:过期、低价值、长期未被召回的条目降权或删除
- 分层:原始日志 / 摘要 / 抽象结论 分层存储,检索时按需下钻
- 关键特性
- 没有"当前任务"作为基准------判断标准是"未来是否可能有用",这是预测性问题,比上下文压缩难得多
- 往往不可逆:合并后原始细节就没了
- 低频但重:通常是离线批处理或写入时触发
3、 为什么说"上下文管理"是 Agent 工程里最容易出事故的地方?
因为它的失败是"静默降质"------不报错、不崩溃、不告警,只是慢慢变笨。
这是它区别于其他工程问题的根本特征。
3.1、原因如下
- 失败模式是"静默的"(最根本原因):模型不会说"我缺信息",它会用残缺信息自信地编一个答案。格式完整、语气笃定、逻辑通顺------只有对照事实才能发现它错了
- 错误会连锁放大(Agent 特有):Agent 的每一步输出都是下一步的输入, 一次污染不是"错一次",而是被反复消费、逐层放大
- 污染源极多且不可控:上下文是多方拼装的产物(比如工具返回、检索结果、历史对话、压缩摘要),每个来源都可能是污染源,且大部分无法控制
- 约束是硬性的,但需求是软的:上下文窗口有限,必须对进入上下文的信息进行选择
3.2、解决方案
| 层面 | 措施 |
|---|---|
| 可观测 | 每轮记录上下文快照(各分区 token 占比、压缩记录、召回列表、工具返回摘要) |
| 可回溯 | 压缩不删除,原文落盘 + ID 引用,支持按需取回 |
| 防御性 | 工具返回设上限、格式校验、异常隔离;召回结果做相关性阈值过滤 |
| 状态外置 | 任务目标、关键事实用结构化字段维护,不依赖模型从历史里自己找 |
| 分级告警 | token 水位、压缩次数、截断事件、召回命中率 全部埋点 |
| 回归测试 | 建立"上下文污染"场景的测试集,验证压缩后任务成功率保持率 |
3.3、具体原因分析如下
-
原因1:失败模式是"静默的"(最根本原因)
对比一下其他类型的故障:
故障类型 表现 可发现性 代码 bug 抛异常、崩溃 立刻,有堆栈 接口超时 报错、重试 立刻,有日志 模型能力不足 答错、答不出 较快,可感知 上下文管理出错 答得"像模像样"但不对 极慢,且常被误判 上下文出问题时,模型不会说"我缺信息",它会用残缺的信息自信地编一个答案。你看到的是一个格式完整、语气笃定、逻辑通顺的回复------只有对照事实才能发现它错了。
更糟的是:这类问题常被归因为"模型不行""幻觉",于是团队去换模型、调 prompt,方向完全错了。
-
原因2:错误会连锁放大(Agent 特有)
单轮对话里,上下文错了就是这一轮错。但 Agent 是多步循环:
txt上下文污染 → 模型基于错误信息决策 → 调用错误的工具 → 工具返回错误结果 → 写回上下文 → 下一轮基于更错的信息决策 → ...(雪崩)关键点:Agent 的每一步输出都会成为下一步的输入。一次污染不是"错一次",而是被反复消费、逐层放大。而且越到后面,错误越难追溯------你看到的是第 15 步的荒谬结果,根因在第 3 步的一条脏数据。
-
原因3:污染源极多且不可控
上下文是"多方拼装"的产物,每个来源都可能是污染源:
来源 典型污染 工具返回 格式错乱、超大JSON、报错信息混入、过期缓存 检索/召回 召回了不相关或过时的记忆、相似但错误的条目 历史对话 用户前后矛盾、模型自己之前的错误结论被当事实 压缩过程 摘要丢关键细节、摘要引入幻觉 多 Agent 协作 子 Agent 的错误结论被主 Agent 采信 并发/重试 重复注入、顺序错乱、旧版本覆盖新版本 你无法控制外部工具返回什么,也无法保证检索永远精准。这意味着上下文管理本质上是在不可信输入上做防御性工程。
-
原因4:约束是硬性的,但需求是软的
- 硬约束:token 上限是物理的,超了就是跑不了
- 软需求:"这轮到底该保留哪些信息"没有标准答案
于是必须做取舍,而取舍没有 ground truth:
- 压太狠 → 关键信息丢失,任务失败
- 压太松 → 塞不下,或注意力被稀释
- 丢错了 → 当时看不出,几轮后才暴露
这是一个持续在做的、无法验证正确性的决策。而且同一个策略,在不同任务上最优解完全不同。
4、一个 Agent 的上下文里通常有哪些组成部分?请按"稳定性"排序并说明理由。
Agent 的上下文的组成部分如下:
| 序 | 组成部分 | 稳定性 | 变化频率 | 典型占比 |
|---|---|---|---|---|
| 1 | System prompt(角色、规则、ReAct 格式约束) | 极高 | 几乎不变 | 2%~5% |
| 2 | 工具定义(含检索工具、业务工具) | 极高 | 版本级 | 5%~15% |
| 3 | 长期记忆注入(用户偏好、项目事实) | 高 | 天/周级 | 2%~8% |
| 4 | 会话历史 | 中→低 | --- | 25%~55% |
| 4a | ├ 会话摘要(压缩点之前,有损索引) | 中 | 压缩时重写 | 5%~15% |
| 4b | └ 增量原文(压缩点之后,保真记录) | 低 | 每轮追加 | 20%~40% |
| 5 | 本轮新增工具返回(含检索结果) | 极低 | 每次调用变 | 10%~40% |
| 6 | 本轮新增用户输入 | 极低 | 每轮变(循环中常为空) | <1% |
排序理由:
- KV Cache 只能前缀命中------易变内容放前面,后面全部失效
- 成本差一个数量级------缓存命中价约为未命中的 1/10
- 稳定部分可离线优化,易变部分只能在线控制
5、 上下文快满了,你的压缩策略是什么?请给出具体触发条件和执行顺序。
核心原则:先切输出、再留工具、剩下才是输入;分级触发、按价值密度从低到高依次丢弃;压缩不等于删除。
5.1、触发条件
上下文窗口使用率,以"可用预算 = 窗口 − 安全垫(15%)"为分母
5.2、执行顺序
txt
检测水位
↓
< 60% → 监控,结束
↓
60%~70%(预警) → ① 清理工具返回原文 → ② 合并同类消息 → 结束
↓
70%~85%(压缩) → ③ 滚动摘要(增量区 → 摘要,原文落盘)
↓
85%~95%(强制) → ④ 结构化裁剪 → ⑤ 外置+句柄 → 只留目标+事实+近3轮
↓
> 95% → 熔断:拆分子任务 / 提示用户
问题:为什么 70% 就开始压?
压缩本身要消耗 token(调 LLM 做摘要),且压缩后还要留空间给后续轮次。等到 90% 才动手,已经没有空间做压缩这件事了。
5.3、永不压缩的红线
| 内容 | 理由 |
|---|---|
| 任务目标 | 丢了就不知道在干什么 |
| 用户显式约束 | 丢了会违反要求 |
| 已确认的关键事实 | 丢了会重复询问或基于错误信息决策 |
| 当前正在处理的实体(ID、文件名、错误码) | 丢了会答非所问 |
| 安全边界 | 丢了可能产生违规输出 |
5.4、执行操作说明
-
外置工具返回(收益最大、损失最小)
txt旧工具返回原文 → 摘要 + 引用id + 取回方法(原文落盘) -
合并同类消息
txt连续多轮工具调用 → 合成一条("调用了 A/B/C,结果摘要如下") 重复的 Thought → 只保留结论 -
滚动摘要(增量原文 → 摘要)
txt[增量原文区] → 压入摘要 → 摘要重写 → 增量区清空 边界:以"上次压缩点"为界,摘要与增量原文严格互斥 摘要要求:结构化字段(任务目标、已确认事实、待办、关键结论),不用自由文本 保留锚点:摘要里标注"第 3 轮用户提到接口超时 3s",需要细节可回查原文 -
结构化裁剪
txt大 JSON → 只留当前任务需要的字段 长列表 → 只留前 N 条 + 总数 -
system prompt 和任务目标
这两项是红线,原则上不压缩,只在 85% 以上极端情况做"精简措辞",绝不删除约束。
5.4、问题
-
1、为什么先动工具返回,而不是先摘要对话?
工具返回价值密度最低、体积最大(常占 30%~60%),清理它收益最高、损失最小。摘要对话会丢语义细节,代价更高。按"价值密度从低到高"排序,不是按"体积从大到小"。
-
2、压缩用 LLM 做摘要,有什么风险?
① 丢细节且模型自己不知道丢了什么;② 多轮累积衰减;③ 可能引入幻觉。
对策:结构化摘要 + 事实校验 + 原文可回溯。
-
3、怎么衡量这套方案有效?
衡量压缩效果不能看单点结果指标(无基线、无法归因),要做对照实验(离线 A/B 或灰度)+ 探针;日常看可归因信号:探针保真率、约束违反率、重复询问率、回查有效率。
| 指标 | 衡量什么 | 怎么算 |
|---|---|---|
| 探针事实保真率 | 压缩后关键事实是否还在 | 埋探针 → 压缩后提问 → 答对率 |
| 答案一致性 | 压缩前后同一问题答案是否一致 | 离线 A/B,同问题对比 |
| 约束违反率 | 结构化状态是否生效 | 规则校验输出是否违反已知约束 |
| 重复询问率 | 已知信息是否丢失 | 检测模型是否重问 state 里已有的信息 |
| 压缩比 vs 保真率曲线 | 压得越狠,保真掉多少 | 不同压缩比下测保真率,找拐点 |
| 回查有效率(不是次数) | 回查是否解决了问题 | 回查后任务是否推进 / 是否还需再查 |
6、工具返回结果特别大(比如一个 200KB 的 JSON),你怎么处理?
核心思想:把"塞进上下文"换成"让模型能按需取"。
核心思路:入口先设限,源头能裁就裁,列表能分就分,剩下的一律外化------但外化必须配"目录式摘要 + 能读回来的工具"。
6.1、解决方案
| 层 | 手段 | 适用 | 优先级 |
|---|---|---|---|
| 0. 入口防护 | 设响应大小上限,超限直接走外化 | 所有工具调用 | 必做 |
| 1. 源头裁剪 | 服务方改 / 适配层裁剪字段 | 能改或能包一层 | 最优 |
| 2. 分页 | 按 token 分页 + 告知总数 | 列表型数据 | 优先 |
| 3. 结构化裁剪 | 只取需要的字段/区块 | 已知结构 | 优先 |
| 4. 外化 | 落盘 + 目录式摘要 + 查询接口 | 兜底,通用 | 必做 |
执行顺序:0 永远先执行 → 然后按 1→2→3→4 依次尝试,前一层能解决就不进下一层。
外化说明:"落盘 + 摘要 + 查询接口"三件套
| 要素 | 作用 | 常见错误 |
|---|---|---|
| 落盘 | 原文完整保留,可回溯 | 只存摘要,原文丢了 |
| 摘要 | 让模型知道"里面有什么" | 摘要太笼统,模型无法判断相关性 |
| 查询接口 | 让模型能按需深挖 | 只给路径,模型没有读取手段 |
摘要要写成"目录"而不是"概述":
txt
❌ 差:这是一个包含用户信息的 JSON 文件(200KB)
✅ 好:用户数据,含 3 个区块:
- profile(基本信息,12 字段)
- orders(订单列表,340 条,字段:id/amount/status/date)
- logs(操作日志,1200 条,按时间倒序)
可用 read_file 或 query_json 按区块读取
外化之后,模型必须有能力把需要的信息取回来,否则外化等于丢弃。
查询接口的三种实现:
- 给工具:read_file(path, offset, limit) 或 query_json(path, jq_expr)
- 给路径 + 说明:告诉模型文件在哪、什么格式、怎么读
- 预置常用查询:把高频查询封装成工具(如 get_user_orders(user_id, status))
6.2、问题
-
1、为什么不直接截断?
- 可能截掉关键字段,且截断后 JSON 不合法;
- 模型不知道数据残缺,会基于不完整信息下结论;
- 无法回溯。截断只适合"确定后面无关"的场景。
-
2、外化后模型不去读怎么办?
原因:
- 摘要没写清"里面有什么",模型不知道值得读;
- 没有读取工具。
对策:摘要写成目录式(列区块和字段),确保有 read_file/query_json 可用,必要时在摘要里给行动建议("如需订单明细,请读取 orders 区块")。
-
3、落盘文件什么时候清理?
按会话生命周期清理,或设 TTL(如 24h)。清理前确认任务已结束,否则模型后续想回溯就找不到了。
-
4、如果模型需要的数据分散在多个大文件里?
先给所有文件的目录式摘要(每个约 100 token),让模型规划"读哪几个、读哪部分",再逐个按需读取。不要一次性全读进来。
7、 多轮对话里,用户前面说的约束被后面淹没了,模型开始违反,怎么解?
核心解法:约束不能靠"写在历史里等模型自己找",要提成结构化状态、放稳定前缀区每轮固定注入,再用程序化校验兜底------能代码拦住的,绝不靠模型自觉。
关键认知:约束"在上下文里"≠"被模型用上"。所以解法不是"把约束写得更醒目",而是改变它的存在形式。
7.1、问题本质
约束被淹没,是三个机制叠加的结果:
| 机制 | 表现 |
|---|---|
| 位置衰减(lost in the middle) | 早期内容在长上下文中注意力权重低 |
| 稀释效应 | 大量无关内容淹没关键信号 |
| 覆盖效应 | 后出现的弱指令覆盖前面的强约束 |
7.2、解决方案
完整方案流程如下:
txt
用户消息到达
↓
① 约束提取(规则/小模型)→ 更新约束状态(覆盖而非追加)
↓
② 组装上下文:[system][约束清单][tools][memory][历史][工具返回][用户输入]
↓
③ 模型生成(system 里含"输出前逐条自检"指令)
↓
④ 程序化校验(格式/单位/禁用词)
↓ 失败
⑤ 带错误信息重试(最多 2 次)
↓ 成功
输出
下面具体进行说明:
1、约束外置为结构化状态
核心做法
txt
对话历史(自然语言,会衰减)
↓ 提取
约束清单(结构化,每轮固定注入)
约束清单示例:
JSON
{
"hard_constraints": [
{"id": "C1", "rule": "输出必须是 JSON,不含 markdown 代码块", "source": "第1轮用户", "priority": "P0"},
{"id": "C2", "rule": "金额单位统一用人民币元", "source": "第3轮用户", "priority": "P0"},
{"id": "C3", "rule": "不要调用外部网络工具", "source": "第2轮用户", "priority": "P0"}
],
"soft_preferences": [
{"id": "P1", "rule": "回答尽量简洁", "source": "第5轮用户", "priority": "P2"}
]
}
2、注入位置
放在稳定前缀区(system prompt 之后、会话历史之前),每轮原样注入:
txt
[system][约束清单][tools][memory][摘要][增量原文][工具返回][用户输入]
↑ 每轮固定,位置靠前,不参与压缩
为什么放这里:
- 位置靠前 → 不受中间衰减影响
- 每轮固定 → 不依赖模型从历史里检索
- 不参与压缩 → 不会被摘要丢掉
- 内容稳定 → 缓存友好(只有约束变化时才失效)
3、约束的提取与维护
-
提取时机
时机 做法 用户消息到达时 轻量规则/小模型判断"这句话里有没有新约束" 压缩前 强制检查:增量原文里有没有未提取的约束 定期 每 N 轮复核一次,防止遗漏 -
提取规则
- 什么算约束:
- 显式否定:"不要..."、"禁止..."、"别..."
- 显式要求:"必须..."、"一定要..."、"只能..."
- 格式要求:"用 JSON"、"不超过 100 字"、"分三点"
- 范围限制:"只考虑..."、"排除..."
- 什么不算:
- 一次性指令("这次帮我查下...")
- 可协商的偏好("如果能...更好")→ 归入 soft_preferences
- 什么算约束:
-
冲突处理
因为约束清单是"当前有效状态",对话历史是"时间序列"。两者天然存在"旧约束 vs 新约束"的冲突。关键不是避免冲突,而是明确"谁赢"和"怎么处理旧约束"。
冲突的四种来源:
冲突类型 例子 本质 约束被更新 第 3 轮说"输出 JSON",第 8 轮说"改成 Markdown" 同一约束的新旧版本并存 约束被撤销 第 3 轮说"超时 5s",第 10 轮说"超时不用管了" 旧约束已失效但仍在历史里 约束被细化/例外 "输出 JSON" → "输出 JSON,但错误信息用纯文本" 一般规则与例外并存 约束本身矛盾 用户前后说法不一致(可能忘了自己说过) 用户侧不一致 这四种在对话历史里都会留下痕迹,而约束清单只保留"当前有效"的那一版------冲突就发生在这里。
解决原则:约束清单是唯一真相源,历史必须标注失效
-
原则 1:约束清单优先级最高,且必须带版本
PYTHONconstraints = { "output_format": { "value": "Markdown", "since_turn": 8, "supersedes": ["JSON (turn 3)"], # 显式记录被替代的旧值 "status": "active" } }关键:supersedes 字段让"旧约束已失效"这件事本身成为结构化信息。
-
原则 2:历史中的旧约束必须被标记,不能裸奔
做法 说明 摘要生成时标注 "用户曾要求 JSON(turn 3),后于 turn 8 改为 Markdown" 或直接不写入摘要 已被替代的约束,摘要里只留最新版 + 一句"曾变更" 禁止 摘要里只写旧约束、不写变更 → 模型必然选错 -
原则 3:注入时显式声明优先级
txt【当前有效约束】(以此为准) - 输出格式:Markdown(turn 8 起) 【历史变更记录】(仅供参考,勿执行) - turn 3 曾要求 JSON,已于 turn 8 废止用格式把"谁赢"写死,不靠模型自己推断。
-
原则 4:矛盾无法消解时,问用户
用户前后说法不一致(类型④),系统无法判断哪个是真实意图 → 必须澄清,不能猜。
注意事项:约束的作用域
不是所有约束都全局有效:
作用域 例子 处理 全局 "输出用中文" 一直有效,直到被更新 阶段 "先只做分析,不要改代码" 阶段结束即失效 单次 "这次回答简短点" 只对下一轮有效 约束清单必须带作用域,否则"单次约束"会被误当成"全局约束"长期生效------这也是一种冲突。
4、三层防护(不只靠注入)
| 层 | 手段 | 作用 |
|---|---|---|
| 1. 注入层 | 约束清单每轮固定注入 | 保证模型"看得到" |
| 2. 生成层 | 输出前自检:逐条核对约束 | 保证模型"用得上" |
| 3. 校验层 | 程序化校验(格式、单位、禁用词) | 保证"违反能被拦住" |
-
输出前自检
在 system prompt 里加一段固定指令:
txt输出前,逐条核对以下约束,任一不满足则修正后重新输出: - C1: 输出必须是 JSON,不含 markdown 代码块 - C2: 金额单位统一用美元 - C3: 不要调用外部网络工具注意:自检清单要动态生成(从约束状态渲染),不能写死在 prompt 里。
-
程序化校验(最可靠)
能程序化校验的,绝不靠模型自觉
PYTHONdef validate(output, constraints): errors = [] if "C1" in constraints and not is_valid_json(output): errors.append("C1 违反:输出不是合法 JSON") if "C2" in constraints and "¥" in output: errors.append("C2 违反:出现人民币符号") return errors校验失败 → 带着错误信息重试,比让模型自己发现有效得多。
分两类:
- 可程序化的(格式、单位、禁用词、长度)走代码校验;
- 不可程序化的(语气、逻辑一致性)走模型自检 + 人工抽检。
优先把约束设计成可程序化校验的形式------这是 prompt 工程的一部分。
7.3、问题
1、为什么不能只靠"把约束写得更醒目"
常见但无效的做法:
| 做法 | 为什么无效 |
|---|---|
| 在 system prompt 里加粗、加"非常重要" | 位置仍在前面,长上下文下照样衰减 |
| 把约束重复写三遍 | 占用 token,且重复内容本身会被稀释 |
| 每轮在用户消息里重述约束 | 污染对话历史,且用户不会配合 |
| 让模型"记住之前的约束" | 依赖模型自觉,不可靠 |
根本问题:这些做法都在"自然语言层面"加强,而衰减是注意力机制层面的问题。必须改变存在形式(结构化 + 固定位置 + 程序校验)。
2、为什么不把约束放在用户消息里每轮重述?
- 污染对话历史,让历史变长且重复;
- 用户不会配合;
- 位置仍在"每轮变"的区域,缓存不友好。
放稳定前缀区,每轮固定注入,才是正解。
3、约束太多(比如 20 条)怎么办?
- 分级:P0 硬约束每轮注入,P2 软偏好按需注入;
- 合并同类;
- 把可程序化的约束从 prompt 里移出,只留校验代码(模型不需要"看到",只需要"被拦住")。
4、为什么不能"只留约束清单、删掉历史里的约束"
看似干净,实则有害:
| 问题 | 说明 |
|---|---|
| 丢失"为什么改" | 用户改约束往往有原因(如发现 JSON 不好读),这个原因影响后续决策 |
| 模型可能"重新发现"旧约束 | 如果历史里完全没痕迹,模型可能从更早的对话里推断出旧约束 |
| 审计需要 | 合规场景要能还原"约束变更史" |
正确做法:清单存"当前值 + 变更链",历史存"变更记录",两者一致。
后续内容敬请期待下一篇文章。