返回博客

Pi 为什么把聊天记录存成一棵树?

Pi 为什么把聊天记录存成一棵树? /tree ​、 /fork ​、 /clone 与编程试错的真正关系 让 AI 修改一段认证逻辑。 它先给出方案 A:把校验统一放进中间件。代码改完,测试也跑了,但旧接口开始报错。 这时你发现,真正的问题不是“AI 答错了一次”。 麻烦在于,方案 A 已经走了很远:它读过相关文件,分析过调用关系,修改过代码,还留下了一组

本文目录
  1. 对话真正丢失的,往往不是答案
  2. Pi 的 Session 本来就是一棵树
  3. 真正的入口不是 /undo​,而是 /tree
  4. 同一个问题,怎么并行尝试方案 A 和 B
  5. /tree​、/fork​、/clone 不是一回事
  6. /tree:仍然在同一棵树里
  7. /fork:从旧问题长出一个新 Session
  8. /clone:复制当前完整进度
  9. 树状 Session 不等于 Git 分支
  10. 分支之间真的完全隔离吗?
  11. 分支多了,会不会浪费很多 Token
  12. 三个最适合树状 Session 的编码场景
  13. 1. 修 Bug:同时调查多个假设
  14. 2. 架构选择:让方案真正可比较
  15. 3. 高风险重构:先保存现场,再大胆实验
  16. Pi 真正改变的是“走错以后怎么办”
  17. Sources

/tree​、/fork​、/clone 与编程试错的真正关系

Pi 为什么把聊天记录存成一棵树?

让 AI 修改一段认证逻辑。

它先给出方案 A:把校验统一放进中间件。代码改完,测试也跑了,但旧接口开始报错。

这时你发现,真正的问题不是“AI 答错了一次”。

麻烦在于,方案 A 已经走了很远:它读过相关文件,分析过调用关系,修改过代码,还留下了一组失败测试。你想换方案 B,又不想丢掉刚才得到的判断。

在线性聊天里,常见做法只有几个:继续在错误方案上打补丁,编辑之前的问题重新发送,或者干脆新开一个会话,把项目背景再讲一遍。

Pi 给了另一种处理方式:

Text
走错了
→ 回到真正的分叉点
→ 保留旧路线
→ 从那里尝试新方向

这不是普通的“撤销一条消息”。

Pi 的 Session 从底层就是一棵树。


#对话真正丢失的,往往不是答案

写代码的过程很少是一条直线。

我们经常同时面对几个假设:

Text
登录失败
├── 可能是缓存没有失效
├── 可能是 Token 刷新有竞态
└── 可能是事务提交顺序不对

这些假设不会按顺序自动排除。你可能先查缓存,改了两轮后确认方向不对;再查并发,得到新的日志;最后才发现问题藏在事务边界里。

如果聊天记录只有一条时间线,前面的失败路径很容易变成噪声:

  • 模型继续带着错误假设回答;
  • 你需要反复解释“忽略刚才那套方案”;
  • 有价值的失败证据被后续消息淹没;
  • 想比较两种方案时,只能人工翻聊天记录。

但错误路径不一定是垃圾。

它至少记录了三件事:

  1. 哪个假设已经验证过;
  2. 哪种改法产生了什么副作用;
  3. 我们是从哪一个判断开始走偏的。

Pi 把这些东西保留下来,不要求你用一条新消息把旧路线强行覆盖掉。


#Pi 的 Session 本来就是一棵树

Pi 把 Session 保存为 JSONL 文件。除了文件头,每条 Session 记录都有自己的 id​ 和 parentId。前者标识当前节点,后者指向它的父节点。[1][2]

简化后,可以理解成这样:

JSON
{"id":"a1","parentId":null,"message":"重构认证模块"}
{"id":"b1","parentId":"a1","message":"方案 A:统一中间件"}
{"id":"c1","parentId":"b1","message":"测试失败"}
{"id":"b2","parentId":"a1","message":"方案 B:保留路由依赖"}
{"id":"c2","parentId":"b2","message":"测试通过"}

画成树就是:

Text
需求:重构认证模块
├── 方案 A:统一中间件
│   ├── 修改实现
│   └── 测试失败
└── 方案 B:保留路由依赖
    ├── 修改实现
    └── 测试通过

两个方案共享分叉前的需求和项目背景,但分叉后的消息、工具调用和结果分别属于自己的路径。

Pi 在构建模型上下文时,不是把整棵树全部塞进去,而是从当前叶子节点一路向上走到根节点,组装出当前活动路径。官方 Session Format 把这一步称为 buildContextEntries()​,之后再由 buildSessionContext() 转成发送给模型的消息。[2]

所以,Session 文件里可以保留很多旧分支,模型当前看到的仍然主要是你正在走的这一条路。

这也是树状 Session 最重要的价值:

完整历史可以保留,当前上下文仍然可以保持聚焦。


#真正的入口不是 /undo​,而是 /tree

不少介绍会把 Pi 的分支理解成“输入 /undo 后自动产生”。这个说法不准确。

Pi 官方命令列表没有把 /undo 列为会话分支命令。编辑器里的文字撤销,和回到某个历史消息继续对话,也不是同一层操作。

Pi 管理 Session 分支的核心入口是:

Text
/tree

也可以连续按两次 Escape 打开 Tree View。[1]

一次常见操作是:

Text
当前方案走不通
→ 输入 /tree
→ 选择此前某条消息
→ 回到该节点
→ 输入新的要求
→ 新路径从这里继续生长

旧路径不会被删除。

新消息只是成为这个历史节点的另一个子节点。两条路线仍然保存在同一个 Session 文件里,之后可以继续切换。[1][2]

/tree 也不只是一个聊天记录浏览器。官方界面支持搜索、折叠和展开分支、在兄弟分支间跳转、过滤工具消息、复制选中消息,还能给重要节点添加标签。[1]

因此,一条长 Session 可以逐渐变成可检索的技术决策树,而不只是按时间堆起来的聊天流水账。

Pi 官方 Tree View:同一个 Session 中保留并切换多条路径

图:Pi 官方仓库中的 Tree View 截图。它直接展示树状层级、消息角色、工具调用与当前节点选择;截图中的具体开发对话只用于界面示例。


#同一个问题,怎么并行尝试方案 A 和 B

假设你问 Pi:

Text
这条 SQL 在数据量上来后变慢了,帮我优化。

Pi 首先选择方案 A:增加复合索引。

Text
优化 SQL
└── 方案 A:增加复合索引
    ├── 分析执行计划
    └── 写出迁移脚本

后来你发现,这张表的写入频率很高,增加大索引会显著放大写入成本。

这时不需要在方案 A 下面继续解释“请忘掉刚才的索引方案”。打开 /tree,回到最初提出 SQL 问题的节点,再输入:

Text
先不增加索引,换成改写查询和子查询的思路。

Session 会变成:

Text
优化 SQL
├── 方案 A:增加复合索引
│   ├── 分析执行计划
│   └── 写出迁移脚本
└── 方案 B:改写查询
    ├── 消除相关子查询
    └── 对比执行计划

这种方式有两个直接好处。

第一,两条方案共享同一段公共背景。你不用重新粘贴表结构、数据量和原始 SQL。

第二,方案 A 的失败原因仍然保留。如果几天后发现写入压力没有预想中那么高,你还能切回去继续验证索引路线。

这比“删除重发”更接近真实的工程决策过程。


#/tree​、/fork​、/clone 不是一回事

Pi 同时提供 /tree​、/fork​ 和 /clone。它们都和历史路径有关,但解决的问题不同。[1]

什么时候使用 tree、fork 和 clone

*图:*​ /tree​ *留在同一个 Session 内切换路线;*​ /fork /clone会创建新的 Session 文件。

操作 发生位置 是否创建新 Session 文件 适合场景
/tree 当前 Session 内 快速试错、切换路线、比较方案
/fork 从历史用户消息开始 把某条旧路线独立成新任务
/clone 复制当前活动分支 保留当前完整现场,再做高风险实验

#/tree:仍然在同一棵树里

/tree 适合短周期探索。

你想回到 20 分钟前,换一种实现方式;或者想查看刚才被放弃的方案,都不需要创建新文件。

历史仍保存在当前 Session 的 JSONL 中。

#/fork:从旧问题长出一个新 Session

/fork 会让你选择活动路径上的一条历史用户消息,把走到那里为止的路径复制到新的 Session 文件,并把选中的 Prompt 放回编辑器,方便修改后重新发送。[1]

它适合这样的场景:

Text
同一个需求
├── 新 Session A:交给模型 A,强调最小改动
└── 新 Session B:交给模型 B,允许重构

两边需要长期发展时,独立 Session 会比在一棵巨大树里反复切换更好管理。

#/clone:复制当前完整进度

/clone 复制的是当前活动分支,并在新 Session 中打开一个空编辑器。[1]

它更像“保留当前现场,再继续实验”。

比如当前功能已经做到 80%,测试也基本通过,但你想尝试一次大范围结构调整。此时可以先 /clone

Text
当前稳定路线
└── clone 后的新 Session:尝试大规模重构

重构失败,原 Session 的技术讨论和活动路径仍然完整。


#树状 Session 不等于 Git 分支

这是 Pi 分支机制最容易产生误解的地方。

/tree​ 管理的是对话上下文

它不会自动回滚你的工作目录。

假设方案 A 已经让 Pi 修改了这些文件:

Text
src/auth.ts
src/middleware.ts
tests/auth.test.ts

随后你通过 /tree 回到方案 A 之前,开始方案 B。模型的对话上下文确实回到了旧节点,但磁盘上的三个文件不会因此恢复原状。

也就是说:

Text
Session 路径切换 ≠ 文件系统快照恢复

如果不注意这一点,方案 B 可能继续建立在方案 A 留下的代码上。你以为自己换了一条干净路线,实际工作区里已经混入了旧方案。

因此,高风险试错最好配合版本控制:

  • 切换路线前先提交一个 Git commit;
  • 给不同方案使用独立 worktree;
  • 至少在分叉前检查 git diff
  • 长期实验使用 /fork​ 或 /clone 分开会话,再配合不同工作目录。

Pi 的 Session 树保存“为什么这么做”,Git 保存“文件变成了什么样”。

两者结合,才是真正可恢复的试错环境。

Session 分支不会自动恢复代码文件

图:切换 Session 路径只改变模型上下文。工作目录中的 modified文件不会因此自动回滚。


#分支之间真的完全隔离吗?

从活动上下文看,分叉后的消息不会自动出现在另一个分支中。

例如:

Text
共同背景
├── A:使用 Redis
└── B:使用进程内缓存

你在 B 分支里补充:

Text
部署环境不允许增加 Redis。

这条消息不会凭空改写 A 分支已有的历史。

但“分支完全互不相知”也不够准确。

Pi 的 Session Format 定义了 branch_summary​。当你通过 /tree 离开当前路径时,Pi 可以为被放弃的路线生成摘要,把那条路径直到共同祖先之间的关键上下文带到新分支中。[2]

它可能记录:

Text
旧分支尝试了复合索引,但评估发现写入放大明显。

这样,新路线不需要继承旧分支的全部消息和工具输出,仍然可以知道“已经试过什么、为什么放弃”。

因此,更准确的说法是:

分支默认沿各自活动路径构建上下文,但 Pi 可以用分支摘要保留旧路线的关键经验。

这比简单的绝对隔离更适合调试:我们想丢掉错误路线带来的上下文负担,却不想再次踩进已经验证过的坑。


#分支多了,会不会浪费很多 Token

树里分支多,不等于每次请求都会把整棵树发给模型。

Pi 构建上下文时沿当前叶子节点回溯活动路径。旧分支仍保存在 JSONL 文件中,但不会因为存在就全部占用当前上下文。[2]

真正影响上下文长度的通常是:

  • 当前活动路径本身太长;
  • 工具输出和日志过多;
  • 长时间在同一分支持续追加;
  • 分支摘要或压缩摘要本身需要生成;
  • 频繁切换后继续在多条路径上分别调用模型。

当活动路径接近模型上下文上限时,Pi 支持 /compact​ 手动压缩,也会在接近上限或溢出时自动压缩。压缩是有损的,但完整 Session 历史仍保存在 JSONL 中,可以继续通过 /tree 查看。[1][2]

因此,“定期清理废弃分支来节省 Token”不是最准确的建议。

更实用的做法是:

  1. 给关键分叉点和结论添加标签;
  2. 长期发展的方向用 /fork 拆成独立 Session;
  3. 高风险实验用 /clone 保留现场;
  4. 当前活动路径过长时使用 /compact
  5. 不必删除仍有复盘价值的失败路线。

历史保存在磁盘上并不可怕。需要控制的是当前送进模型的上下文。


#三个最适合树状 Session 的编码场景

#1. 修 Bug:同时调查多个假设

线上接口偶发超时,你有三个怀疑:

Text
缓存雪崩
数据库锁等待
第三方 API 抖动

可以从同一个问题节点分别调查。每条路线保存自己的日志、工具调用和判断,最后再比较证据。

这比在一条对话里反复说“先忽略缓存,现在查数据库”更干净。

#2. 架构选择:让方案真正可比较

例如在以下方案之间选择:

Text
继续在单体中重构
拆分成独立服务
引入消息队列解耦

三条路径共享业务背景和约束条件,但分别分析改造范围、风险和迁移成本。

树状结构保留的是决策过程,而不只是最终选择。

#3. 高风险重构:先保存现场,再大胆实验

当前实现可以运行,但结构已经很难维护。

这时先使用 /clone 保留活动分支,再在新 Session 中尝试大规模重构;同时配合 Git commit 或 worktree 隔离文件变化。

会话和代码都有恢复点,试错成本才真正降下来。


#Pi 真正改变的是“走错以后怎么办”

/tree 看起来只是一个聊天历史界面。

但它背后的产品判断很明确:编程过程中的分叉不是异常,而是常态。

传统线性对话的思路是:

Text
走错
→ 删除或覆盖旧回答
→ 尽量把对话重新拉回正轨

Pi 的思路则是:

Text
走错
→ 回到真正的分叉点
→ 保留失败路径
→ 换一个方向继续

这两种设计对“历史”的态度完全不同。

在线性聊天里,历史主要用于让模型记住前文。

在 Pi 里,历史还承担了另一层作用:记录技术决策是如何产生的。

为什么没采用中间件?

为什么放弃复合索引?

哪次测试证明缓存不是根因?

哪个节点之前的上下文仍然可信?

这些信息往往比最后那段可运行代码更难重建。

所以,Pi 保存的不只是一串用户消息和模型回答。

它保存的是你在一次编码任务里走过的路线,包括那些最终没有被采用、但已经帮你排除错误答案的路线。

/tree 不是为了把聊天记录画得更酷,而是把“走错了怎么办”变成一等能力。

当 Session 树与 Git commit、worktree 配合起来,AI 编程才真正拥有了低成本的“存档读档”:既能回到当时的技术上下文,也能回到对应的代码状态。

这才是树状会话最有价值的地方。

#Sources

[1] https://github.com/earendil-works/pi — Pi official repository
[2] https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/session-format.md — Pi session format

返回顶部

评论

还没有评论,来说点什么吧。

评论经发布者审核后公开