05 Context

Context

Agent 要解决的绝大多数问题,本质上都是上下文问题。

本章聚焦  →  上下文是绝大多数 Agent 技术的核心落脚点

一个 Agent好不好用,常常不取决于它「会不会」,而取决于它此刻脑子里「装着什么」——也就是上下文(Context)

上下文是 Agent 唯一的工作记忆:每一次决策,都建立在当下这一整段上下文之上——系统指令、工具定义、工具返回、检索到的资料,以及一轮轮累积的对话与轨迹。而这段记忆既有限花钱,把它管好本身就是一门工程,业界称之为上下文工程(Context Engineering)。下面几个日常场景,都是它要面对的问题:

这三个场景面对的问题、用的手段各不相同,但共同的本质,都是在管理Agent输入给 LLM 的上下文

5.1 上下文窗口的物理性质

我们首先要清楚上下文窗口本身受哪些物理约束所影响:

长度上限:每个模型支持的 token 输入输出总和都有硬上限,从最早的 1024\8196 到现在的 256K、1M,虽然上下文窗口已经增长到了一个客观的量级,但任务的复杂程度增长当然超过了上下文窗口的增幅。一旦输入过长,轻则输出被截断导致输出无效,重则报错导致流程崩溃。

计算复杂度:由于在 Transfomer 中 self-attention 机制让每个 token 都与其它 token 做注意力计算,因此 Attention 是 O(n2) 的计算复杂度。这意味着:

图 · 为什么是 O(n²):注意力分数来自 Q × Kᵀ 矩阵相乘
Q [n × d] × Kᵀ [d × n] = 注意力分数 [n × n] 一行 · 一列 → 一个格子(点积 Qᵢ·Kⱼ) 输出是 n×n 方阵 → 共 n² 个元素 n 翻倍 → 元素数 ×4,即 O(n²) (d 是注意力头维度,固定不变;只有序列长度 n 会随上下文增长)
原理:注意力分数是 Q(n×d)与 Kᵀ(d×n)做矩阵相乘得到的。矩阵里每个格子 = 一行 Q 与一列 K 的点积(一个 token 对另一个 token 的相似度),而输出矩阵有 n×n 个格子。d 固定、只有 n 增长——所以序列长度翻倍,矩阵元素数就 ×4——这就是"翻一倍、贵四倍",上下文越长推理越慢越贵的根源。

上下文衰减(Context Rot): 一篇来自 Anthropic 的 context engineering 文章中阐述了这个现象:

as the number of tokens in the context window increases, the model's ability to accurately recall information from that context decreases. from Anthropic · context engineering

通俗来讲,就是塞得下不等于看得清。即使没超过上下文窗口硬上限,大模型也会变笨。通常表现为:

图 · Context Rot:塞得下 ≠ 看得清
窗口硬上限 召回准确率 % 上下文 token 数 → 塞得下 ≠ 看得清 即使没超上限,也已经变笨
还没撞到窗口硬上限,召回准确率就已经一路下滑:能塞进去,不代表模型看得清。表现出来就是中间信息被忽略、后半段幻觉、工具名叫错漏参。

Attention Sink:Agent 训练数据中的 prompt 开头通常带 system prompt,模型学到把开头的 token 注意力锁定。这意味着 prompt 开头位置是黄金注意力区。而实际上,一段较长的上下文中,开头和结尾往往对输出的约束力更强,中间的部分则较弱。这个问题在长上下文场景下尤为突出。

图 · Attention Sink:两头高、中间塌陷
注意力权重 → 开头 · 黄金区(system) 结尾 中间被忽略 位置:开头 → 结尾
开头(system,黄金注意力区)与结尾权重最高,中间那一段最容易被忽略。所以真正重要的指令与信息,别埋在长上下文的正中间。
类比 · 办公桌面

把上下文窗口想成一张办公桌面,也就是 Agent 的“工作记忆”:

上下文窗口桌面的面积,就那么大
往里塞的资料摊在桌上的文件、便签、翻开的书
Context Rot桌面越堆越乱,找一张纸越来越慢,还容易看漏
Attention Sink手边(桌角)的东西随手就够到,压在最底下的早忘了

所以“塞得下”从来不是目标——桌面再大,堆满了照样找不到东西。Harness 要做的,就是帮 Agent 随手把桌面收拾干净。

5.2 记忆与压缩

上下文工程是管理Agent送给LLM的的上下文内容的专业范畴,凡事涉及到上下文的组织、编排、CRUD等等的工程都是上下文工程要面对的问题。我们以当前一个非常热门的Agent框架 hermes-agent,看看它内部是如何实现上下文工程中最常见的两个技术环节:记忆(memory)与上下文压缩(compression)。它最值得学的一点是:记忆和压缩不是两件事,而是同一条流水线的两端。

先看它怎么组织上下文

在讲记忆和压缩之前,先看 hermes 每次调用时把上下文排成什么样。它的载荷分两块:一个 messages 数组 + 一个顶层 tools 参数。

图 · hermes 的上下文分层
messages 数组(发给模型的对话载荷) ① system prompt · messages[0] stable 身份 · 准则 · 工具指引 · 技能索引 · 环境 context system_message · AGENTS.md volatile MEMORY.md · USER.md · 日期戳(只到「日」) ③ 历史消息(user / assistant / tool 交替) user:上一轮提问 assistant:思考 + tool_call tool:执行结果 ⑤ 当前 user 消息(messages 末尾) 最新用户输入 + <memory-context> 围栏块 召回的记忆 · 调用时挂上 · 不落库 · 不进 system 静态 会话内只建一次 = 可缓存前缀 动态 每轮增长 / 变化 tools = 顶层参数 不在 messages 文本里, 走原生 tools 字段(静态); 文本化 <tools> 只用于轨迹 cache_control 断点 共 4 个 = system + 末尾 3 末尾断点每轮滑动到最新 静态前缀反复命中, 新增尾部才走全价 ⑥ 触发压缩时:历史中段被「摘要消息」替换(见下方流水线图)
三条编排原则:静态在前、动态在后(system 内部也按 stable → context → volatile 排);动态内容一律挂到当前 user 消息尾巴<memory-context> 就挂这里),绝不改 system 以保前缀缓存;工具走原生 tools 字段不塞文本。配合 system_and_3 的 4 个缓存断点,静态前缀反复命中、只有新增尾部走全价。

记忆 Memory

  1. 文件优先,外部可插拔。内置记忆就是两个 Markdown 文件——MEMORY.md(agent 自己的经验)与 USER.md(用户画像),没有数据库、没有向量检索。需要语义检索时,才挂一个外部 provider(Honcho / Mem0 / Hindsight…),且同一时刻只允许挂一个,以免工具定义把上下文撑大。
  2. 写,靠模型自主。模型通过一个 memory 工具自己决定「记什么」(add / replace / remove),而非规则硬塞;每轮结束还会在后台线程异步把这一轮同步给外部 provider,不阻塞主流程。
  3. 读,靠「围栏注入」而非检索拼接。召回的记忆文本被包进一个 <memory-context> 权威块,只在发起 API 调用的那一刻追加到当前用户消息上——不改写原始对话、不做持久化,还附一句「这是召回的记忆、不是新的用户输入」,专门防提示注入。

压缩 Compression

  1. 客户端自建的「保头 + 保尾 + 中段摘要」。不调用厂商的服务端压缩接口,全在本地做:上下文超过阈值(约「有效窗口」的 50%,已扣掉输出预留)就触发——先无损预清理(旧工具返回换成一行摘要、同一文件的重复读取只留最新),再只把中间段交给一个(可以更小的)摘要模型压成结构化快照(目标 / 已完成动作 / 活动状态 / 关键决策 / 相关文件…),而开头(system)与最近约 20% 窗口逐字保留,且绝不切断成对的 tool_call / tool_result。
  2. 压缩不是「丢弃」,是「软归档」。被压掉的旧对话不会真删:在 SQLite 里标记为 active=0,仍可全文检索(FTS)、仍可恢复;摘要作为新的 active=1 消息插入,会话 id 原地不变。
  3. 压缩与记忆共用一条流水线。压缩真正落地前,会先触发一个 on_pre_compress 钩子把要点抽进记忆,然后才归档旧对话、写入摘要。所以一次压缩 = 抽记忆 + 归档 + 摘要 三合一——这正是「上下文工程」最精炼的一幕。
图 · hermes-agent 的记忆–压缩流水线
每一轮对话(API 调用时) 用户消息 注入围栏块 <memory-context> LLM 推理 + 工具调用 记忆 MEMORY.md · USER.md + 可选外部 provider 后台同步 prefetch → 注入 超过阈值? 约 50% 否 · 继续 ① 无损预清理 旧 tool result → 一行摘要 · 去重 ② 抽记忆 on_pre_compress 钩子 ③ 中段结构化摘要 保头 + 保尾 ~20% · 不切断 tool 对 ④ 软归档 archive_and_compact 摘要 active=1 · session_id 不变 抽进记忆 SessionDB(SQLite + FTS) 旧 turn active=0 · 可搜 / 可恢复 软归档 压缩后继续
记忆在每轮以 <memory-context> 围栏块注入、绝不改写原始消息;一旦上下文触阈值,压缩沿「预清理 → 抽记忆 → 中段摘要 → 软归档」四步走完,旧对话软归档进 SQLite(可搜可恢复)而非丢弃——记忆与压缩共用这同一条流水线。
延伸 · 第四种记忆

技能(skills)是可以自策展的记忆。

/learn 让 agent 自己写 SKILL.md 沉淀方法论;一个 Curator 每隔约 7 天用辅助模型 fork 出一个 agent 在后台整理技能(合并 / 归档 / 置顶,永不删除、只归档),全程不碰主会话的 prompt cache。

5.3 小结与思考

本章小结
  • 上下文是 Agent 的工作记忆——每一次决策都建立在当下这段上下文之上;它有限又花钱,把它管好本身就是上下文工程
  • 注意力是 O(n²)——序列长度翻倍、计算量翻四倍,窗口越长推理越慢越贵。
  • Context Rot——还没撞到窗口硬上限,召回准确率就已一路下滑:塞得下 ≠ 看得清
  • Attention Sink——开头(system,黄金区)与结尾权重最高,重要的东西别埋在中间。
  • 记忆与压缩是同一条流水线——hermes-agent 的一次压缩 = 抽记忆 + 软归档 + 中段摘要三合一,共用一套 SessionDB。
  • 记忆:文件优先、围栏注入——内置就是 MEMORY.md / USER.md,召回内容以 <memory-context> 只在调用时注入、绝不改写原始消息。
  • 压缩:保头 + 保尾 + 中段摘要,且软归档不丢——本地约 50% 阈值触发;旧对话 active=0 仍可检索、可恢复。
思考题
  • 有哪些方式可以实现 Agent 记忆(至少设计 6 种以上的机制)
  • 有哪些方式可以实现 Agent 上下文压缩
  • 理解 SummarizationDrift,有哪些方法可以缓解该现象