π Pi 轻松学
首页 / 第 5 篇

时间旅行 —— 会话、树,和后悔药

30 秒版本: 每段 Pi 对话都自动保存成纯 JSONL 文件里的一棵。回退从不删东西——只是移动一个指针。所以你可以先试方案 A、倒回去再试方案 B,两段历史都永远留着。


对话不该只有一条直线

大多数聊天工具把历史当列表:最新的在底下,删除就是撤销。闲聊没问题,但工程工作不行,因为工程里全是这种事:

  • "让我重来一次"——Agent 答偏了,你想倒回一轮重新问
  • "如果当初走左边呢?"——方案 A 做到一半,方案 B 一直在心里挠你
  • "把那天那个找回来"——三天后你需要周二那次会话的上下文

线性历史只能靠销毁数据来回答第一个问题。Pi 的答案:别把对话存成列表,存成

会话树是怎么长出来的

每个会话都住在 ~/.pi/agent/sessions/ 下的一个 JSONL 文件里(按项目目录组织)。一行一个条目(entry),每个条目对整棵树只了解一件事:它的父节点是谁

e1  模型切换(换到了 Claude)
 └─ e2  你:"auth.ts 里 salt 验证为什么失败?"
     ├─ e3  助手:读 auth.ts
     │   └─ e4  工具结果:文件内容
     │       └─ e5  助手:"第 23 行——salt 没编码"
     │
     └─ e6  你:"先给我看 hash 函数"      ← 第二个分支!
         └─ e7  助手:grep hash……

注意 e6:它的父节点是 e2——和 e3 同一个爹。分支就这么简单:两个条目认领同一个父节点。没有别的。没有特殊的分支对象,没有额外的记账结构。

而让整个设计成立的关键决策是:条目只往后指。节点知道自己的父节点,但父节点不维护孩子列表。为什么?因为这样新增节点就永远不需要修改旧节点。文件只会生长——append-only,直到永远。(如果父节点要记录孩子,每长一个分支都得重写某行旧数据,整个保证就塌了。)

回退,就是挪一下指针

在 Pi 里回退——比如你对"第 23 行"的分析不满意,想从 e2 重新问——什么都不会被删。内部操作本质上就一行:

当前位置 ← e2

旧分支(e3 → e4 → e5)原封不动留在文件里,永远。你明天还能回去。这就是让 git 强大的同款戏法:历史不可变,"撤销"只是选择站到另一个位置。

日常命令:

你想……
续上最近一次会话 pi -c
浏览历史会话 pi -r(会话内 /resume
看当前会话的树 /tree
从某条早期消息开叉新会话 /fork(或 pi --fork
把当前分支复制成新文件 /clone
导出会话为 HTML /export

最好玩的是 /tree:当前会话完整分支结构的交互式视图,你可以跳到任意节点——然后从那里继续走下去。

等等——模型的记忆怎么办?

问得好。文件留着所有分支,但模型永远只看到一条路径:从当前位置一路回溯到根。跳到另一个分支,从模型视角看,旧时间线就蒸发了。

Pi 用分支摘要(branch summarization)来缓和这件事:当你在树上跳转时,它可以把你正要离开的时间线总结一份,注入新分支。于是模型知道"之前试过 PostgreSQL 触发器,因为性能问题放弃了",而不用背着那个死胡同的完整记录。

用的是上下文压缩同一套结构化摘要机器——同模板、同文件跟踪——只是对准了不同的问题。压缩对抗太长的历史,分支摘要对抗被抛弃的历史。

纯文本是特性,不是妥协

这套设计还有个安静的美德:会话就是 JSONL 文件。你可以 grep 它、diff 它、用任何工具备份它,或者凌晨两点觉得哪里不对时用编辑器直接读。没有数据库要维护,没有导出仪式——档案本身就是数据。

(删会话时,如果你装了 trash CLI,Pi 还会把它扔进系统回收站而不是永久删除。小小的善意,很 Pi。)

带走的话

Pi 的会话模型就是一个思想的彻底贯彻:历史只增不改,导航只是指针,每一次后悔都留档。 在一个"回退"免费且永久的 Agent 里工作过之后,再回到线性聊天历史,就像开一辆没有倒挡的车。


资料来源: Sessions · Session format