为什么管不好上下文窗口,再聪明的 Agent 也会变蠢

上下文是 Agent 最稀缺的资源,用七维透镜和构成审计把它当系统来工程化。

Module B · 第 1 讲

立靶:上下文管不好,Agent 会怎么死

先说一个反直觉的事实:决定一个 Agent 好不好用的,往往不是模型多强、prompt 写得多漂亮,而是它每一回合的上下文窗口里到底装了什么

上下文窗口是 Agent 最稀缺的资源。它有硬上限,每多塞一个 token 都在挤占别的 token 的位置,而且每塞一次都要付钱。把它管坏,会以三种典型方式出事:

第一种,窗口爆。 多轮对话、多工具调用、长文档检索注入,token 只增不减。一旦逼近上限,要么直接报错截断,要么框架悄悄丢掉最早的几轮——而被丢掉的,常常正是最初那条「用户到底想要什么」的关键约束。Agent 于是开始答非所问,你还以为是模型笨。

第二种,lost in the middle。 这是被反复验证的现象:当关键信息被埋在一段很长的上下文中间,模型对它的利用率显著低于放在开头或结尾。也就是说,不是「塞进去了」就等于「用上了」。你检索了十段资料一股脑灌进去,真正有用的那段被夹在第六段,模型大概率视而不见。塞进去 ≠ 被读到。

第三种,也是最隐蔽的——暗默认污染。 你用的 Agent SDK、框架、插件,会在你不知情的情况下往上下文里注入东西:默认的 system preamble、自动拼接的工具描述、隐式的历史摘要、某个 hook 偷偷加进来的 skill。这些「暗默认」不写在你的代码里,却实实在在占着窗口、影响着输出。你调了半天 prompt 没效果,真正的污染源可能根本不在你视野里。

这三种病有一个共同的病根:大多数人把上下文当「文案」在优化,而它其实是一个需要工程化的「系统」。 优化一段话怎么写得好,是 Prompt Engineering;装配 Agent 每一轮看到的完整上下文,是 Context Engineering。后者才是 Agent 工程的核心战场。

框架:七维拆解透镜

任何「给 Agent 按需注入知识」的扩展系统——插件、skill 路由、rules、RAG 注入——本质都在回答同一道题:在对的时刻、用对的判断、在预算内、把对的知识塞进 Agent 的脑子,还不能塞错。

把这道题拆开,就是七个维度。它们不是随意罗列的检查项,而是一条决策链:先决定何时介入,再决定靠什么判断,然后排序、卡预算、设防错,最后解决组件协作和可观测。

#维度(要回答的问题)关注点 / 拆解时该追问
1When · 何时介入在 Agent loop 的哪个节点插手?早了浪费预算,晚了来不及影响决策
2Signal · 靠什么判断用什么信号决定要不要注入?精确率与召回率怎么权衡
3Rank · 怎么排序多个候选怎么排优先级?打分可解释还是凭玄学
4Budget · 资源约束上下文预算怎么管?有没有把「稀缺」当成硬约束写进代码
5Guardrail · 怎么防错怎么防止注入错的、注入过多?认不认「注错比不注更糟」
6State · 组件间协作无状态的组件之间,怎么共享已经算出来的判断结果
7Observe · 可观测这个黑盒能不能被调试、被验证、被回归

这七维之所以有用,是因为它把一个看不见摸不着的「注意力调度」问题,拆成了七个可以逐格回答的具体工程问题。它既是拆解别人系统的透镜,也是设计自己系统的清单。

为什么重点在第 4 维和第 5 维?因为成熟 Agent 工具的克制全在这两维:宁可漏注入,也不污染上下文。 一个分不清预算、对错误注入毫无防御的系统,无论前面三维做得多花哨,都会在真实负载下退化成第一节里那三种病。

数据 / case:一个生产级注入引擎长什么样

下面用一个真实的扩展系统(脱敏为「某 Agent 插件」)把七维填满,看一个被认真工程化的注入引擎是什么形状。这个插件管理着二十多个 skill 和若干命令,它真正的产品命题不是「功能多」,而是:在每一回合,决定哪几页知识值得占用宝贵的窗口。

维度该插件的工程答案
When4 个 hook 点:会话开始 / 工具调用前 / 用户提交 prompt 时 / 会话结束
Signal文件 glob、bash 命令正则、import 语句、prompt 关键词、依赖清单映射,多信号融合
Rank加权打分:精确短语 +6 / 全部命中 +4 / 任一命中 +1(封顶 2),再叠加嗅探器命中 +5
Budget工具调用前最多注入 18KB、用户提交时最多 8KB;分别最多注入 3 个 / 2 个 skill
Guardrail反向信号一票否决(分数打到负无穷);检测到已在用测试框架就抑制相关注入;会话级原子去重
Statehook 之间用环境变量当总线:嗅探器先写一个「可能相关 skill」列表,后续 hook 读它来加分
Observe提供 explain 与 doctor 命令、分级日志、golden 快照测试

这里有三个设计值得单独拎出来,它们正好对应第一节的三种病:

  • 反向信号 = 负无穷。 一个反向信号能瞬间毙掉整个 skill 的注入。这是「宁可不加载也不加载错」的保守取向——直接对治「暗默认污染」。
  • 自适应词法兜底。 精确分数越低,越敢用模糊匹配放大;分数快到阈值时就只轻推一下。这是一套信心校准,本质是在精确率与召回率之间动态调档。
  • 把抱怨翻译成 skill。 正则识别「本地能跑线上不行」「卡住 / 超时」「白屏」这类自然语言抱怨,路由到对应的排障 skill;但一旦用户提到自己在用某测试框架,就抑制注入。
上下文拆成稳定块(人设+工具+指令,整通不变,命中 cache)与易变块(runtime 状态,每轮重写)
上下文拆成稳定块(人设+工具+指令,整通不变,命中 cache)与易变块(runtime 状态,每轮重写)

再看反面教训。另一个语音 Agent 项目早期踩过一个典型的 Budget / 可观测坑:把每一轮都在变的 runtime 上下文(当前状态、已采集字段、下一通提示)混进了稳定的 system 块。结果是 prompt 缓存全数失效,一通三十轮的电话把二十多万 token 全价重写了一遍。修法是把上下文拆成两块——稳定块(人设 + 工具 + 阶段指令,整通不变)和易变块(runtime 上下文,不缓存)。拆完缓存命中率回到约 97%,单通成本直降八成多。

这个教训的普适意义是:上下文不只有「塞什么」,还有「以什么结构塞」。 稳定与易变混在一起,既烧钱(缓存失效)又难调试(不知道哪一块在变)。

可操作做法:两份清单

清单一 · 怎么画上下文构成表(构建前做):在写第一行 Agent 代码之前,把「这一回合模型会看到的每一段内容」列成一张表,每一段都回答清楚:

  • 这一段是什么(system 人设 / 工具描述 / 历史 / 检索注入 / runtime 状态 / 框架默认)
  • 它从哪来(我写的 / SDK 加的 / hook 注入的 / 上一轮残留的)
  • 它每轮变不变(稳定 → 可进缓存块;易变 → 单独成块,不缓存)
  • 它占多少预算(给出 token 量级,标出谁是大头)
  • 它放在窗口的什么位置(关键约束别埋进中间,对治 lost in the middle)
  • 它的注入条件是什么(无条件常驻 / 满足某信号才进 / 有反向信号就毙)
  • 出错了怎么知道(这一段有没有日志、能不能被单独验证)

清单二 · 怎么审计 SDK 的暗默认(接手现成框架时做):你用的每个 Agent SDK / 框架 / 插件,都在背着你往上下文里塞东西。审计步骤:

  • 把一次真实请求的完整最终 prompt dump 出来,逐段读
  • 逐段标注来源:哪些是我显式写的,哪些是框架默认拼的(system preamble、工具 schema、自动摘要、隐式 few-shot)
  • 数 token:默认配置 vs 关掉所有可关项,对比窗口占用差多少,差出来的就是「暗默认」的体量
  • 查 hook / 中间件:有没有第三方组件在某个生命周期节点偷偷注入
  • 把审计结论落成一份 CONTEXT.md,写清楚「这个 Agent 每轮上下文由哪几块构成、各自归谁管」

两份清单合起来,就是把「上下文」从一团你看不清的黑盒,变成一张你能逐格审查、逐项防御的工程图纸。

收口

上下文是 Agent 最稀缺的资源——在对的时刻、用对的判断、在预算内,把对的知识塞进去,还不能塞错。别再把它当「一段要写好的话」,把它当成一个有预算、有结构、有故障模式、需要被观测的系统来工程化。