即使每次读取的内容都是相关的，长时间的编码代理运行仍然会出现性能退化。解决之道不是更聪明的上下文压缩，而是每个新鲜的上下文窗口只执行一个有边界的任务，并在迭代之间将运行轨迹持久化到磁盘。

即使每次读取都切中要害，长时间运行的代理依然会退化。因为窗口会被代理自身过时的思考所填满：它早已放弃的旧方案、已经跳过的失败尝试。模型必须将这些内容与代码的当前状态进行协调，运行时间越长，协调就越困难。SWE-Pruner 等工具可以显著减少无用的读取（日记 #4 详细讨论了这一点），但还有一个剪枝无法解决的第二重上下文管理问题：累积状态。

## 上下文腐化是可以衡量的

我在 Arianna 的长时间运行中亲眼目睹过这种现象：代理重新审视了30分钟前做出的决策，提出了一个它已经尝试过并回滚的更改。这不是感觉，而是事实。Chroma Research 测试了18种不同的 LLM，发现每一个都会随着上下文被填满和干扰信息增加而退化。退化开始得很早，并非在某个安全阈值之后才出现。在有语义干扰的任务中，简单检索准确率95%以上的模型会降到60-70%。

"迷失在中间"（Lost in the Middle）论文发现了更具体的规律：模型对长输入开头和结尾的信息的检索和利用能力，远高于中间的信息。在长时间的编码运行中，这意味着代理的初始指令和最新操作保持清晰，但中间的一切（即大部分工作内容）变得越来越难以推理。

| 阶段 | 状态 |
|------|------|
| 新鲜上下文 | 100% 召回率 |
| 填充50% | 中间开始模糊 |
| 填充70%+ | 可衡量的退化 |
| 长时间运行 | 矛盾状态累积 |

直觉上的解决方案是总结之前的工作并压缩上下文。JetBrains 进行了测试，发现 LLM 生成的摘要和另一种方法——从上下文中完全丢弃已完成步骤（称为"观察掩码"）——相比未管理的上下文，都降低了超过50%的成本。但摘要是有代价的：代理需要额外13-15%的步骤来完成相同的任务。摘要会压缩失败信号。如果代理尝试了某种方法但失败了，摘要可能只记录"尝试了X"，而没有保留那个应该阻止代理再次尝试的具体错误。观察掩码通过移除不相关的上下文而非压缩它，避免了这一惩罚。

## 不要长时间运行

我最终采用的解决方案很简单：不要长时间运行。每个新鲜的上下文窗口只处理一个有边界的任务。加载规格说明，加载计划，选择一个任务，执行，验证，提交，清空窗口，从下一个任务重新开始。

**迭代之间重置的是：**整个模型上下文。每个任务都从一个干净的窗口开始。代理从磁盘加载其指令和当前计划，而不是从记忆中。它不会携带之前任务的残留。

**迭代之间持久化的是：**一切重要的内容，但存储在磁盘上，而非窗口中。

- **计划。** 一个以文件形式存在的结构化任务列表。每次迭代读取它，选择下一个任务，标记已完成的任务。计划就是迭代之间的连续性保障。
- **Git 提交。** 每完成一个任务就提交一次。代码库本身是最可靠的发生记录。
- **结构化轨迹。** JSONL 日志记录每次迭代做了什么：哪个任务、改变了什么、验证了什么、什么失败了。这些轨迹是机器可读的，因此未来的迭代在需要了解前一步的上下文时，可以加载特定条目。

上下文窗口用于当前推理。文件系统用于连续性。把这两种功能混在一起，就是长时间运行崩溃的原因。

## 两层隔离

我在 Arianna 中在两个层面实现了这一点。

**在迭代层面**，每个编码任务获得一个新鲜的上下文窗口。代理从磁盘加载其指令和当前计划，选择一个有边界的任务，执行，验证通过，提交，然后退出。下一次迭代从零开始。如果任务验证失败，轨迹会捕获原因，下一次迭代可以在重试前读取该轨迹。不会因为失败的尝试残留在窗口中而产生累积的混乱。

**在流水线层面**，每次运行都从代码库的一个隔离副本开始。流水线是一系列节点，其中每个节点都是任务在流水线层面的等价物：一个有边界的、拥有自己新鲜上下文窗口的工作单元。每个节点完成后，它提交并将状态检查点到结构化数据库中，该数据库记录运行事件和产物。如果流水线在节点C失败，它可以从节点B的检查点恢复，而无需重新运行之前的节点。节点共享代码库，但每个节点都获得一个新鲜的上下文窗口，因此代理的推理会重置，尽管代码会继续推进。

| 节点 | 状态 |
|------|------|
| 节点A：新鲜上下文 | 提交 + 检查点 |
| 节点B：新鲜上下文 | 提交 + 检查点 |
| 节点C：新鲜上下文 | 失败 |

> 从检查点B恢复

## 复合工程让重置变得无代价

显而易见的反驳是：新鲜的上下文不会丢失前一次迭代学到的所有东西吗？不会。这正是复合工程的用途。每次迭代将其学到的知识写入磁盘上的共享文件：AGENTS.md 用架构决策更新，计划文件标注什么有效、什么无效，轨迹捕获具体的错误及其导致的方法。下一次迭代在加载其新鲜上下文时读取这些文件。没有任何东西丢失。知识存在于代码库中，而非模型的记忆中。

当早期版本的轨迹格式只记录"完成任务X"而没有记录导致第一次尝试失败的具体错误时，我深刻体会到了这一点的重要性。下一次迭代尝试了相同的方法，遇到了相同的错误，浪费了整个周期。解决方案不是更好的上下文管理，而是更好的轨迹：结构化的、具体的、包含失败信息的。一旦持久化层运作正常，新鲜上下文模式就严格优于旧模式。你获得了干净的推理，没有累积的混乱，同时拥有复合知识，没有过时的状态。

这也是模型分层自然契合的地方。像 Opus 这样的昂贵推理模型可以在单个新鲜窗口内处理困难任务。像 Sonnet 这样的便宜模型处理大部分有边界的任务，在这些任务中上下文保持小而专注。每个都从零开始，读取共享产物，做一件事，然后退出。

## 同一个问题的两个半面

编码代理中的上下文管理分为两个问题：

- **单次运行内：**窗口的大部分被代码读取填满。剪枝它们。（日记 #4。）
- **跨运行：**累积状态腐蚀工作集。不要让它累积。每个新鲜窗口一个任务，轨迹存储在磁盘上。（本文。）

两者都基于同一原则：上下文窗口是工作记忆，不是存储。

---

*获取新文章，请通过邮件订阅*
