Context 管理:六种上下文工程手段,各自解决哪一层问题

Context 管理不是把 prompt 写短,而是在一个固定大小的窗口里做预算分配。窗口里同时住着系统提示、工具定义、对话历史、工具返回值和当前任务,每一类都在涨,而模型的注意力不会因为窗口变大而变好。

过去一年多,做 agent 的人基本收敛到六种手段:长对话摘要、工具结果卸载到文件、用 Skills 做渐进披露、用工具检索应对工具爆炸、用 subagent 做上下文隔离、把 Bash 当主要工具。它们不是六个可以随便挑的选项,而是作用在三个不同时间尺度上的机制——进入窗口前、窗口内、窗口快满时。单独用任何一个都会碰到天花板,组合起来才是今天主流 agent 的默认形态。

下面逐条拆机制、代价和失效边界,最后讲它们怎么拼在一起。

先算清账:一个 agent 的窗口里装了什么

以一个典型的编码 agent 为例,200k 的窗口大致被五样东西瓜分:

  • 系统提示:身份、行为规则、输出格式,通常几千 token,稳定不变
  • 工具定义:每个工具的 JSON Schema 加描述,单个 200–800 token
  • 对话历史:每一轮用户消息 + 模型回复,只增不减
  • 工具返回值:一次 grep 可能几千行,一次网页抓取可能几万 token
  • 当前任务:正在处理的那一段代码或那个文件

问题在于后三项的增长速度和前两项完全不是一个量级。系统提示是常数,工具定义是常数,而工具返回值是脉冲式的——一次读大文件就能吃掉半个窗口。Claude Code 早期最典型的翻车场景就是这个:agent 信心满满地 cat 了一个 3000 行的日志,然后整轮对话开始胡言乱语。

MERMAID_BLOCK_0

这里有个容易被忽略的事实:窗口没满,性能也会下降。Chroma 在 2025 年做的 Context Rot 研究测了 18 个主流模型,在 needle-in-a-haystack、长文档问答、重复计数这类任务上,输入从几千 token 涨到几十万 token 的过程中,准确率普遍下滑,而且不是阶梯式下滑,是连续衰减。也就是说「还没到上限,所以没问题」是个错误假设。上下文管理的目标不是"别撑爆",而是"别让无关内容稀释注意力"。

于是成本有三个维度,不只是钱:

  1. 钱:每个 token 都进 prompt,按输入价计费;缓存命中能打下来一大截,但缓存的是前缀,任何前缀变化都会失效
  2. 注意力:窗口里 40% 是过期的工具输出,模型就得在这些噪声里找信号
  3. 延迟:首 token 时间随输入长度线性上涨,长上下文还会拖慢生成

记住这三个维度,后面每一种手段都是在拿其中一项换另外两项。

手段一:摘要,把历史换成一个状态包

最古老也最通用的做法:对话太长时,让模型把到目前为止的对话压缩成一段结构化摘要,然后丢掉原始消息,用「摘要 + 最近几轮」重建一个新窗口。Claude Code 里叫 compact,API 层叫 context editing,社区里有人叫 rolling summary。

机制上它有一个关键设计点:摘要不要写成叙事,要写成状态。叙事摘要("用户先让我改 A,然后我发现 B 有问题……")几乎没用,因为它保留的是过程。有用的是状态包:

Goal: 把 auth 模块从 session 迁移到 JWT
Constraints: 不能改 public API;必须保持 refresh token 语义
Files touched:
  - src/auth/session.ts (deleted)
  - src/auth/jwt.ts (new, 210 lines)
  - src/middleware/auth.ts (modified, line 44-88)
Decisions: 用 jose 而不是 jsonwebtoken(因为 edge runtime)
Failed attempts: 直接替换 express-session 导致 3 个测试超时,已回滚
Next: 修 test/auth.refresh.spec.ts 的过期断言

这七行能替代两万 token 的对话,而且信息密度更高。Anthropic 在早期的研究型 agent 上验证过这一点:他们跑过一个需要约 300 万 token 的检索任务,靠的就是每轮结束后把结果摘要进下一轮,否则单窗口根本装不下。

在 API 层,这件事已经有原生支持。Anthropic 的 context editing 可以按 token 阈值自动清理旧的工具结果:

{
  "context_management": {
    "edits": [
      {
        "type": "clear_tool_uses_20250919",
        "trigger": { "type": "input_tokens", "value": 100000 },
        "keep": { "type": "tool_uses", "value": 3 },
        "clear_at_least": { "type": "input_tokens", "value": 20000 }
      }
    ]
  }
}

这段配置的语义是:当输入超过 10 万 token,只保留最近 3 次工具调用,至少清掉 2 万 token。注意 clear_at_least 这个参数——它防的是「清了一次没清动,每轮都触发一次清理」的抖动。

为什么有效:它丢掉的是"怎么走到这里"的路径,保留的是"现在在哪、下一步去哪"的状态。对绝大多数 agent 任务来说,前者只在调试时有用。

代价和失效边界:

  • 摘要本身是一次模型调用,长对话摘要要几秒到十几秒,还可能要几千 token 的输出
  • 摘要会丢细节,而且是不可逆的。模型之后说"我记得之前试过 X 不行",但说不出为什么不行的具体报错,就容易再试一次
  • 递归摘要会漂移。摘要的摘要的摘要,几轮之后约束条件会消失,这是长任务最常见的失控原因
  • 任务本身依赖精确细节时不能用:逐行 diff、法律条款比对、需要引用原文的写作,摘要一次就废

所以摘要的定位应该是兜底机制,不是第一道防线。真要靠它跑长任务,就得把摘要 prompt 当核心代码来维护,明确列出"必须保留"的字段清单。

手段二:工具结果落盘,窗口里只留指针

比摘要更细粒度的一招:单个工具结果太大时,不要让它整块进上下文,而是写到文件里,窗口里只放「路径 + 行数 + 前 N 行 + 一句说明」。

MAX_INLINE = 2000          # tokens, roughly
PREVIEW_LINES = 20

def offload(result: str, name: str) -> str:
    if count_tokens(result) <= MAX_INLINE:
        return result
    path = f"/tmp/tool-results/{name}-{uuid4().hex[:8]}.log"
    write_file(path, result)
    lines = result.count("\n")
    preview = "\n".join(result.splitlines()[:PREVIEW_LINES])
    return (
        f"[offloaded] {lines} lines, {len(result)} bytes -> {path}\n"
        f"--- first {PREVIEW_LINES} lines ---\n{preview}\n"
        f"--- end preview ---\n"
        f"Use grep/head on this file if you need more."
    )

关键在于返回值的形状。很多人只返回一句 "output saved to /tmp/x.log",然后模型就忘了这个文件存在。带上行数、字节数、前 20 行预览,模型才有足够信息判断"要不要去读"。

为什么有效:工具返回值的角色是"中间产物",模型对它做的操作无非是三种——判断有没有目标字符串、提取几个字段、看看有没有报错。这三种操作都不需要全文进上下文。落盘之后,agent 可以用一次 grep 拿到精确答案,付出的成本是几十 token 而不是几万。

代价:

  • 多一次往返。模型看到 preview 决定去读文件,这是额外的 turn,延迟增加
  • 文件需要生命周期管理。长会话会攒几百个临时文件,得有清理策略,否则磁盘和检索都会变脏
  • 模型可能"忘记"去看。这个失败模式很常见,解法是在系统提示里明确写"遇到 [offloaded] 标记时必须用 grep 确认内容,不要凭预览下结论"

有个反直觉的细节:返回结构化预览比返回自然语言摘要更可靠。摘要由模型生成,会引入二次幻觉;而"前 20 行原文 + 总行数"是确定性的,模型自己能从原文里提取信息。所以落盘策略的核心是"给指针和样本",不是"给总结"。

手段三:Skills 渐进披露,把知识做成按需加载的模块

Skills 解决的是另一个方向的膨胀:不是对话太长,而是领域知识太多。一个通用 agent 可能要知道怎么写前端组件、怎么配 K8s、怎么用公司内部的发布系统,全塞进系统提示就是几万 token 的常数开销,而且大部分时候用不上。

渐进披露分成三层:

第一层,常驻的元数据。只有 name 和 description,加起来一百来个 token,永远在系统提示里:

---
name: cnblogs-publisher
description: Publish Markdown articles to CNBlogs using the MetaWeblog API.
  Use when the user asks to publish, post, or push articles to CNBlogs.
---

第二层,触发时加载的正文。模型判断这个 skill 相关,才去读 SKILL.md 的正文——通常是几百到几千 token 的操作步骤、注意事项、判断标准。

第三层,执行中按需读取的附件。references/ 下的详细文档、scripts/ 下的可执行脚本、assets/ 下的模板,正文里只写"需要 X 的时候读 references/x.md",模型真的需要时才打开。

为什么有效:它把"知识"变成了一个检索系统,索引常驻、正文懒加载。系统提示的常数开销从几万 token 降到几百,而且窗口里的内容永远是当前任务相关的,注意力不被稀释。

这里面有一个设计上的必然结果:description 就是检索键。它写得好不好,直接决定 skill 会不会被触发。写成"帮助处理博客相关事务"基本等于不存在;写成"Publish Markdown articles to CNBlogs using MetaWeblog API. Use when the user asks to publish to 博客园"才有明确的使用场景和触发词。我见过最多的 skill 失败案例,不是正文写得差,是 description 写得太抽象,模型根本意识不到该用它。

代价和失效边界:

  • 多一层文件读取,一次 skill 加载多几百毫秒
  • 模型可能只读了正文就动手,跳过了 references 里的关键约束
  • 有些知识不能懒加载。安全红线、输出格式、绝对不能做的事,必须放在系统提示里常驻——因为"没被触发"等于"不存在"
  • 层级设计不好会变成俄罗斯套娃:正文说"详见 references",references 说"详见另一个文件",模型读到第三层就放弃了。实践上建议最多三层,且每层都要能独立完成任务

Skills 管知识,工具定义管能力,但两者会撞上同一个问题:数量。一个接了三四个 MCP server 的 agent,工具数轻松过百。按每个工具平均 300 token 算,光工具定义就是三万 token,还没开始干活。

Tool search 的思路和 skills 完全一致:默认只加载工具的名字,需要时再拉取完整定义。

{
  "tools": [
    { "type": "tool_search_tool_regex_20251119", "name": "tool_search" },
    {
      "name": "create_issue",
      "description": "Create a GitHub issue",
      "input_schema": { "...": "..." },
      "defer_loading": true
    },
    {
      "name": "query_metrics",
      "description": "Query production metrics by service",
      "input_schema": { "...": "..." },
      "defer_loading": true
    }
  ]
}

带 defer_loading: true 的工具,完整 schema 不进首轮 prompt,只保留名字和一行描述。模型调用 tool_search,用正则或 BM25 检索工具名与描述,命中的工具才被展开成完整定义。Anthropic 的 API 提供了 regex 和 BM25 两个版本,前者适合命名规范统一的内部工具(jira_create_issue、jira_search_issues),后者适合名字不规律的第三方工具。

为什么有效:找工具本质上是检索问题。模型的推理能力再强,也没法在 100 个 schema 里"想"出哪个工具能查生产指标——但一次关键词匹配可以。

代价:

  • 多一轮往返。先搜、再调,等于每个新任务多一次模型调用
  • 检索失败等于工具不存在。工具命名混乱时 BM25 的命中率会掉得很厉害,比如把"查日志"的工具叫 fetch_events_v2
  • 模型不知道有这个东西可用。得在系统提示里明确告诉它"工具很多,需要时先搜索",否则它可能直接用 bash 硬干

失效边界:工具总数在 20 个以下时,检索的往返成本大于省下的 token,不如全量加载。工具数超过几百个时,一次检索又会返回太多结果,需要分页或二次过滤。甜区大概在 30 到 200 之间。

手段五:subagent,用进程隔离换上下文隔离

前面四种手段都在"窗口内"做文章,subagent 换了个思路:让中间过程根本不进主窗口。

机制很简单:主 agent 派一个子 agent 去干一件封闭的活,子 agent 有自己的上下文窗口、自己的工具集、自己的循环,干完只回传一段结论。

主 agent 的上下文:
  "找一个能处理 10 万 QPS 的 Rust 限流库,比较三个候选的许可证和维护活跃度"

子 agent 的上下文(独立 200k 窗口):
  读了 14 个仓库的 README
  跑了 6 次 GitHub API
  读了 3 个 benchmark 文件
  → 回传 400 字结论

主 agent 实际看到的:
  400 字 + 一句"需要细节我再展开"

为什么有效:探索型任务的输入输出比极度不对称。读 14 个仓库可能消耗 8 万 token,但结论只有 400 字。如果这些过程都进主窗口,主 agent 在第 15 个仓库时就已经被噪声淹没了。子 agent 相当于一个有损压缩器,而且压缩损失由子 agent 自己承担,主 agent 拿到的是已经提炼过的信号。

代价,而且不小:

  • 子 agent 看不到主对话。你派它去"修一下刚才那个失败的测试",它不知道"刚才那个"是哪个。briefing 的质量直接决定成败,这是 subagent 最常见的失败点
  • token 总量上升。并行 5 个子 agent,每个都要带一份系统提示和工具定义,总消耗可能是单线程的 3–5 倍。省的是主窗口的注意力,不是钱
  • 不能中途纠偏。子 agent 跑偏了你只能等它跑完,或者杀掉重来
  • 汇总会丢信息。子 agent 觉得不重要的细节,可能恰恰是主 agent 需要的

失效边界:任务之间需要强共享状态时别用(比如连续改同一个文件的不同部分);需要人工实时审批的操作也别用,因为子 agent 弹权限请求时你未必在看。它最适合的场景是只读的、可并行的、结论形态明确的调研。

手段六:Bash 作为主工具,让过滤发生在进入窗口之前

前五种手段都是在结果产生之后做处理,bash 的思路更彻底:在结果进入上下文之前就把它变小。

对比一下两种做法。给 agent 一个 read_log_file 工具,它返回 8000 行,全进窗口;给 agent 一个 shell,它写:

rg -c "ERROR|FATAL" /var/log/app/*.log | sort -t: -k2 -nr | head -5
# app-2026-11-03.log: 412
# app-2026-11-04.log: 87
# app-2026-11-02.log: 31

三行输出,信息量比 8000 行高。差别不在工具能力,在于过滤发生在哪一侧。专用工具的返回值是"全量",shell 的返回值是"模型自己定义的投影"。

这也是 Anthropic 在 MCP 的 code execution 方案里强调的点:与其把 MCP 工具一个个暴露成 tool,不如把它们暴露成代码里的函数,让 agent 写代码调用,中间结果留在执行环境里,只有 console.log 的内容进上下文。一个典型例子是"把会议转录稿加到日历"——传统做法要先把整份转录稿拉进上下文,再用模型逐条提取;代码执行方案里,转录稿在执行环境里被脚本处理,窗口里只有最终确认的几行。

为什么有效:上下文成本由"进入窗口的字节数"决定。shell 的重定向、管道、head、jq、awk 都是免费的过滤器,它们不消耗 token,只消耗 CPU。

代价:

  • 安全边界变模糊。一个自由 shell 等于给了 agent 任意执行能力,必须配沙箱、白名单、路径限制
  • 可观测性变差。结构化工具调用容易审计(谁在什么时候调了什么参数),自由文本命令难做策略
  • 对模型能力有要求。弱模型写 shell 的错误率明显更高,一个引号错误就是一次失败的重试
  • 跨平台问题。Windows 上没有 rg、jq 是常态,得准备降级路径

失效边界:需要细粒度权限控制的场景不适合纯 bash——每个工具单独授权、单独计费、单独审计,这是结构化工具的优势,shell 给不了。

这套东西是怎么收敛到六条的

2023 年那批 agent 的上下文策略和现在完全不同,值得看一眼,因为淘汰过程解释了为什么最后剩下的是这六种。

最早的做法是全量塞:把整个代码库、整个文档、整段历史都拼进 prompt,赌窗口够大。窗口从 4k 涨到 200k 的过程中,这个策略看起来是对的——直到发现成本和延迟都线性上涨,而且模型在长输入上的准确率反而下降(也就是前面说的 context rot)。全量塞的隐含假设是"模型会自己忽略无关内容",这个假设被实验反复否掉。

第二代是检索式记忆:把历史对话向量化存进数据库,每轮检索 top-k 塞回去。听起来优雅,实际用起来问题很多——向量检索按语义相似度召回,但 agent 需要的往往是"三分钟前刚确认的那个约束",语义上和当前问题毫无相似性,检索不回来。而且检索进来的片段是碎片化的,模型拿不到完整因果链。这套方案今天的定位已经退到"跨会话的长期记忆",不再承担单次任务内的上下文管理。

第三代是滑动窗口:只保留最近 N 轮。简单,但会硬生生切掉任务开头定下的约束,长任务做到一半突然忘了最初的目标。

现在的六种手段本质上是同一个转向的结果:把记忆从"模型内部"外化成"外部环境"。文件系统负责存,shell 负责查,索引负责找,子进程负责隔离,摘要只负责保留不可重建的那部分状态。模型不再需要"记住"所有东西,只需要知道去哪里找。这个转向之所以成立,是因为编码 agent 本来就工作在一个有文件系统的环境里——外部记忆的存储和检索成本近乎为零,而窗口里的 token 是稀缺的。

组合:六种手段分在三个时间尺度上

单独看每一条都有明显短板,但它们的时间尺度不同,所以能叠起来用:

MERMAID_BLOCK_1

按一个请求的生命周期排,决策顺序大概是:

  1. 进窗口前:先在工具索引里搜一下有没有现成能力(tool search),再看有没有相关 skill 的正文需要读(渐进披露)
  2. 执行时:优先用 shell 管道把输出投影成小结果(bash),探索型子任务丢给 subagent
  3. 结果回来时:超过阈值的落盘,只留指针和预览(offloading)
  4. 窗口涨到 60–70%:清掉旧工具结果,保留最近几次(clear tool uses)
  5. 窗口快满:结构化摘要,重建窗口(compaction)

这五步构成一个闭环。少了任何一环,下一个环节的压力就会陡增——比如没有 bash 过滤,落盘的文件会多到没人管;没有落盘,清理就只能清掉整段有用信息;没有清理,摘要会来得越来越频繁,每次摘要都在丢细节。

失效边界:什么时候这套东西会帮倒忙

上面六种手段都有共同的假设:上下文里的大部分内容是噪声。这个假设在多数 agent 任务里成立,但有几个明确的反例。

任务本身就是长上下文。代码审查一个 5000 行 diff、逐条比对两份合同、按原文引用写报道——这些任务里每一行都可能是信号,任何压缩都是信息损失。这时候该做的是换更大的窗口或者拆任务,不是压缩。

压缩破坏了因果链。模型需要知道"为什么排除了方案 A"才能不重复尝试。摘要只保留"已排除 A",不保留理由,模型就会在下一轮重新提 A。这个失败模式在长任务里非常顽固,解法是在摘要模板里强制要求"每条决策保留一句理由"。

手段之间互相打架。subagent 隔离了上下文,但它的结论没法被主 agent 验证;工具结果落盘省了 token,但 agent 每次都要多一次读取;bash 过滤掉了噪声,也过滤掉了"看起来无关但其实相关"的线索。压缩比越高,agent 越像一个"只记得结论的实习生",执行快但容易在细节上翻车。

成本会转移,不会消失。subagent 省主窗口,但总 token 翻倍;tool search 省工具定义,但每次多一轮调用;落盘省上下文,但多一次读写和一套文件管理。真正省钱的做法是减少无效的探索——这靠的是更好的任务描述和更准的工具,不是更狠的压缩。

观测指标

调这套东西的时候,别只看"有没有报 context length exceeded"。有四个指标能提前暴露问题:

  • 每轮输入 token 的斜率:正常任务应该是缓慢上升,如果出现垂直跳变,说明有工具在返回大块内容
  • 缓存命中率:缓存的是前缀,任何系统提示或工具集的改动都会让命中率归零。命中率突然掉,通常是动态拼接了时间戳或随机 ID
  • 摘要触发频率:一小时触发三次以上,说明前面的过滤环节失效了,该去看是哪个工具在灌水
  • subagent token 占比:超过总量 60% 说明主 agent 已经退化成调度器,这时候要考虑是不是任务拆分本身有问题

这些数字比"感觉变快了"可靠得多,也更容易定位到具体是哪个环节漏了。

参考文档

你的 agent 现在卡在哪一环?是工具结果灌爆窗口,还是摘要之后模型开始"失忆"?评论区聊聊你踩过的坑。


作者: itech001
来源: 公众号:AI人工智能时代(the-ai-era)
网站: https://www.theaiera.top/
关注每日最新AI新闻和技术博客,主页有更多的文章的AI 技术参考:https://www.theaiera.top

本文首发于 AI 人工智能时代,转载请注明出处。

posted @ 2026-11-04 21:16  iTech  阅读(2)  评论(0)    收藏  举报