在真实的 AI 编码会话里,上下文很快会变得庞大:用户指令、模型回复、工具调用结果、bash 输出、文件内容都会不断累积。直接把全部历史塞进模型窗口,不仅会逼近上下文限制,也可能触发 API 错误。开源编程 Agent 项目 pi 的源码给出了另一种思路:对话历史作为 append-only 树永久保存,模型最终看到的只是一份在读取时生成的投影视图。
从源码看,pi 将“存储”和“读取”明确拆开。存储层保证所有信息可追溯、可回退、可分支;读取层则负责决定哪些内容进入模型上下文。这种设计让上下文管理不再是简单截断,而是变成一套可组合的投影规则。
会话不是消息列表,而是一棵只追加的树
pi 的会话被组织为树状结构,而不是一条可以被覆盖的消息线。源码中的 SessionManager 将会话保存为 JSONL 文件,每一行是一个 SessionEntry。每个条目都有 id 和 parentId,通过 parentId 指向父节点,从而构成一棵树。
这棵树的关键约束是 append-only:新内容只追加,不重写,也不删除已有记录。源码中的 _appendEntry 会把新条目加入内存结构并更新当前叶子节点,随后通过 appendFileSync 写入文件。首次创建文件时使用 openSync 的 “wx” 模式,后续所有写入都是向文件末尾追加一行 JSON。
这种结构直接服务于会话分支。用户可以从某个历史节点继续提问,而无需破坏原有对话路径。新消息会生成新的 id,并把 parentId 指向当前叶子节点,于是树上新增一条分支,旧分支仍保持完整。
- 会话文件是 JSONL 格式,一行一个条目。
- 每个条目都有 id、parentId 和 timestamp。
- 写入只追加,不修改旧数据。
- 树结构支持分支、回退和导出。
在这个设计里,压缩和改写也不会删除原始数据。即使后续模型不再看到某些早期消息,这些消息仍然保存在文件中,随时可以被重新读取。这为 fork、回退和导出 JSONL 等功能提供了基础。
模型看到的,是读取时生成的投影
永久保留解决了“存”,但真正影响模型效果的是“取”。pi 在读取阶段通过 buildSessionContext 构建上下文,其内部会调用 buildSessionProjection,把整棵树转换成一条适合模型消费的消息列表。
整个过程分为几个阶段:先从 entries 构建路径,再筛选进入上下文的条目,然后套用运行时设置和 context_edit,最后把每个条目转换成 AgentMessage。换句话说,模型看到的不是完整历史,而是当前分支上经过投影后的视图。
第一步是 buildSessionPath。由于模型上下文必须是线性序列,而会话本身是树,系统需要先确定当前分支。该方法从当前叶子节点沿 parentId 一路回溯到根节点,再反转成正序路径。后续所有投影都发生在这条路径上,而不是整棵树的全部节点。
这一步本身就是第一层裁剪:只保留当前分支。其他分支仍然保存在文件里,但不会进入当前模型上下文。
压缩、系统提示与 context_edit:不删数据的裁剪方式
buildContextEntries 是裁剪逻辑的核心。它不会删除任何底层条目,而是决定当前路径上的哪些条目进入 contextEntries。
如果路径上没有 compaction 压缩条目,整条路径都会进入上下文。如果存在压缩条目,系统会取路径上最新的一条作为检查点。随后,压缩点之前的内容只保留从 firstKeptEntryId 到压缩点之间的部分,压缩点之后的新消息则全部保留。
这里有一个细节:旧的 system 消息会被跳过。源码中的条件判断会排除压缩点之前保留范围内的 system role 消息。原因是压缩时,当时的 system prompt 状态已经被完整快照进 compaction entry 的 systemMessage 字段。后续重放时由这个快照负责,不需要重复加载旧 system 消息。
压缩条目本身的投影方式也能说明这一点。当 sessionEntryToContextMessages 处理 compaction 类型时,会根据 entry 中的 summary、tokensBefore 和 timestamp 生成压缩摘要消息;如果 entry 中带有 systemMessage,则会先返回系统消息,再返回摘要。
另一类值得注意的条目是 context_edit。它不是对原始数据的原地修改,而是一条追加进来的改写指令。投影时,系统会收集每个 targetId 对应的最新编辑,再逐条套用。如果 replacement 为 null,该条目会从模型视角中移除,但原始 entry 仍然保存在文件里;如果 replacement 有值,则只替换消息内容,同时保留原 entry 的 role 和元数据。
- compaction:把较早历史折叠成摘要,但不删除原始条目。
- context_edit:以追加方式记录改写,投影时才生效。
- system prompt:压缩时快照,避免重复重放。
- 分支路径:旧分支保留,但默认不进入当前模型视图。
这种“修改也是追加”的语义,使所有历史状态都可以被审计和恢复。上下文编辑不再是对数据的破坏性操作,而是新增一条影响后续投影的规则。
对编程 Agent 的意义:上下文工程开始走向基础设施
pi 的源码展示了一种更成熟的上下文管理方式。它没有把上下文窗口当作一个需要不断塞满的容器,而是把它视为从持久化状态中动态生成的视图。存储层负责完整保留事件,投影层负责根据当前任务决定模型能看到什么。
这对编程 Agent 尤其重要。编码会话中的信息密度高,类型复杂:工具调用、命令执行输出、文件内容、错误栈、用户反馈都会持续产生。如果只做最近 N 条截断,早期关键信息可能丢失;如果全部保留,又会迅速逼近上下文上限。pi 的做法是在二者之间加入一个可解释的中间层:历史永远存在,但模型只接收当前路径上经过筛选、压缩和改写的结果。
从工程角度看,这种设计也降低了后续扩展成本。分支、回退、导出、压缩、上下文编辑等功能,都可以在同一套 append-only 数据上实现,而不需要反复修改原始消息记录。上下文管理因此不再是临时的 prompt 拼接技巧,而逐渐变成 Agent 系统的一项基础能力。
原创文章,作者:点点,如若转载,请注明出处:https://www.dian8dian.com/bian-cheng-agent-de-shang-xia-wen-guan-li-bu-shi-ba-chuang