上下文工程:Skills、压缩、记忆、Prompt 与错误恢复

Coding Agent 最大的限制之一不是工具数量,而是上下文窗口。管理好的上下文窗口不仅仅可以提高缓存命中率节省缓存还可以一定程度上减少Agent的幻觉

Agent 工作时间越长,读取的文件、命令输出、错误日志和中间推理越多。如果没有上下文管理,再大的窗口最终也会被填满。

我现在对上下文工程的理解是:

上下文管理不是保存全部信息,而是让当前最有价值的信息出现在最合适的位置。

一、上下文里有多种不同性质的信息

至少可以分成五类:

静态规则      身份、安全原则、工具使用约束
能力目录      当前有哪些 Skills、工具和外部服务
当前工作      最近几轮对话、正在修改的文件、测试结果
压缩摘要      被清理历史中的目标、约束和进度
长期记忆      跨会话保留的偏好、项目事实和反馈

这些信息不应该采用同一种加载策略。

静态规则适合稳定地放在 System Prompt 前部,以提高缓存命中;能力目录适合常驻但保持简短;详细技能文档和长期记忆则应该按需加载。

二、Skill:Catalog 常驻,正文按需加载

最直接的知识注入方式是把所有文档塞进 System Prompt,但这会造成三个问题:

  • 每轮都重复计费;
  • 大部分内容与当前任务无关;
  • 无关规则会分散模型注意力。

更合理的是两级加载。

第一级:Catalog

Agent 启动时只看到简短目录:

code-review:代码审查流程
sql-style:SQL 风格规范
release:发布检查清单

目录告诉模型“有哪些能力可用”,但不包含完整正文。

第二级:Load

当模型判断当前任务需要某项能力时,调用:

load_skill("code-review")

Harness 再把对应 SKILL.md 正文作为工具结果放进当前上下文。

这种方式的优势是:

小目录每轮可见
+ 大正文按需付费

Skill 文件还可以继续引用脚本、模板、参考资料和资源文件,不必把所有内容直接写进提示词。

三、上下文压缩应当分层进行

压缩不应该一上来就让另一个模型总结全部历史。摘要成本高,而且会丢细节。

合理策略是“便宜的先做,昂贵的后做”。

1. 大工具结果落盘

读取大文件或执行长命令时,将完整内容保存到文件:

完整结果 → .agent/results/xxx.log
上下文   → 结果预览 + 文件路径

模型需要时仍可重新读取,但不必在每一轮携带完整结果。

2. 裁剪中间历史

保留:

  • 最前面的任务和关键约束;
  • 最近的工作状态;
  • 必要的系统消息。

删除已经失去价值的中间过程。

不能只简单保留最后 N 条,因为最初目标可能在最前面。常见策略是“保留头部少量 + 尾部多数”。

3. 旧 Tool Result 占位

上一阶段读取过的完整文件内容,可以替换成:

[旧工具结果已持久化:.agent/results/read_013.txt]

这样保留了可追溯性,又显著减少上下文。

4. LLM 摘要

前三层仍不足时,再让模型生成结构化摘要,至少包含:

当前目标
用户约束
已经完成的工作
当前代码状态
失败过的方案
剩余任务
关键文件路径

摘要是一种有损压缩,所以必须把长期重要的信息提前转移到记忆或任务系统,而不是指望摘要永久保真。

四、Context Management 不只是本地删除消息

本地 Harness 可以删除普通历史和 Tool Result,但部分服务端管理的信息可能无法在客户端完整控制,例如流式思考块或服务端维护的上下文编辑状态。

因此,成熟系统可能同时存在:

客户端压缩
+ 服务端 Context Management
+ LLM 摘要

这三者解决的问题不同:

  • 客户端压缩:管理自己能够看到和持久化的消息;
  • 服务端管理:处理客户端无法直接编辑的服务端上下文;
  • LLM 摘要:提取语义上的目标和进度。

五、Memory:跨压缩、跨会话保留重要事实

压缩解决“当前会话太长”,但无法保证细节永远不丢。新会话启动后,摘要也可能不存在。

因此需要独立记忆层。

一个简单实用的结构是:

.memory/
├── MEMORY.md
├── user-preference.md
├── project-auth.md
└── feedback-testing.md

其中 MEMORY.md 是简短索引:

- user-preference:用户希望代码注释简洁
- project-auth:认证模块正在重构
- feedback-testing:不要用 Mock 替代真实数据库测试

详细正文按需读取。

为什么不一定要用 RAG

在 Coding Agent 中,很多记忆数量有限、结构明确,而且文件路径和描述本身就能形成良好索引。

此时:

Markdown 文件 + 索引 + 模型选择

可能比完整向量数据库更简单、更透明。

RAG 更适合:

  • 文档量非常大;
  • 查询表达和原文差异明显;
  • 需要语义召回和排序;
  • 仅靠目录难以定位内容。

所以不是“Memory 必须使用 RAG”,而是根据规模和检索难度选择最简单可靠的方案。

六、记忆写入比记忆读取更难

记忆系统不能把每句话都永久保存,否则会迅速堆积冲突和噪声。

比较适合保存的内容包括:

  • 用户明确要求记住的信息;
  • 长期稳定的工作偏好;
  • 反复出现的项目事实;
  • 用户对 Agent 行为的明确反馈;
  • 未来会再次使用的位置索引。

记忆写入还需要考虑:

  • 文件锁,避免并发修改;
  • 去重;
  • 合并相近条目;
  • 处理矛盾;
  • 定期清理过时信息。

长期记忆本质上是一个小型知识维护系统,而不是简单追加日志。

七、System Prompt 应当运行时组装

随着功能增加,System Prompt 不应继续写成一个巨大字符串,而应该拆成 Section:

identity
tools
workspace
permissions
skills_catalog
memory_index
connected_mcp_servers

然后根据真实运行状态拼接:

sections = [identity, tools, workspace]

if skills:
    sections.append(skills_catalog)

if memory_index:
    sections.append(memory_index)

if mcp_servers:
    sections.append(mcp_state)

关键点是根据真实状态判断,而不是在用户消息里搜索关键词。

同时,稳定内容应尽量放在前面,动态内容放在后面,以提高 Prompt Cache 的复用率。

八、错误恢复也是上下文工程的一部分

Agent 运行时常见三类错误。

输出达到上限

先提高输出预算;仍被截断时,保存已生成内容并追加续写提示:

直接从中断处继续,不要重复总结。

输入上下文过长

触发更激进的 Reactive Compact,压缩后重试一次。若仍然超限,应明确退出,而不是无限压缩。

限流和服务过载

对 429、529 等临时错误采用:

指数退避 + 随机抖动 + 最大重试次数

持续过载时可以切换备用模型。

错误恢复必须分类处理。所有错误都立即重试,会制造请求风暴;所有错误都直接退出,又无法满足长期运行要求。

九、总结

我现在会把 Coding Agent 的上下文系统分成下面几层:

System Prompt Sections     稳定规则和运行状态
Skill Catalog              能力目录
On-demand Skill Content    按需知识
Recent Messages            当前工作集
Compaction Summary         被压缩历史
Memory Files               跨会话长期事实
Task Files                 可恢复的目标和进度
External Result Files      大工具结果

一个好的上下文系统不是单纯追求“塞得更多”,而是让:

  • 规则稳定;
  • 当前任务清晰;
  • 历史可压缩;
  • 细节可回查;
  • 重要信息可长期保留;
  • 错误后可以恢复。

这也是 Coding Agent 能否从 Demo 走向长期工作的关键分界线。

更多推荐