Pi实战 14:树状会话历史篇——会话的"版本控制"
来源:Pi 官方《Sessions》文档、社区关于
/tree上下文管理的实践(StackToHeap、developersdigest)、以及 Pi 会话的内部数据结构分析。
第 04 篇把"会话树"当作多代理经验之一提了一句。这篇专门深挖它——因为树状会话历史很可能是 Pi 最被低估的能力:它让你像用 git 一样对待对话,可分支、可回退、可分享,是管理上下文窗口的核心手段。
本文按你要的三块展开:使用场景 → 实现原理 → 实战例子,中间穿插命令速查。
一、使用场景:什么时候该用它
普通聊天是一条直线,说错一句只能/undo一步步回,或者/new全丢。Pi 的会话是一棵树,这带来几个真实痛点的全部解法:
场景 1:Agent 跑偏进死胡同。它改着一改着把你带沟里了,越改越乱。你不需要/undo连按十几次,直接/tree跳回最后一个"还算正常"的点,从那儿重走。
场景 2:同一套上下文,试多个方案。重构一个模块,想分别试"保守重构 / 激进重写 / 只抽函数"三种。每种都开新会话要重新喂一遍上下文;用树的话,在同一个点分三个叉,互不污染,最后挑一个。
场景 3:一系列相似任务共享同一上下文。先加载一个技能 + 需求,跑一次/grill-me把意图对齐,之后每个子任务都从这点对齐好的状态分支出去执行(“Do A”、“Do B”、“Do C”),不用重复前置对话。
场景 4:噪声探索不污染主线。你想让 Agent 放手去试(翻日志、跑实验、走死胡同),但又不想让这些噪声塞满主线的上下文。分支出去探索,把结论落成文件,然后跳回主线继续——主线始终干净。
场景 5:一个会话内模拟"角色分离"。开一个"分析分支"做调研、一个"实现分支"写代码,类似子代理的分工,但全在一个会话文件里,切换零成本。
场景 6:分享 / 另起。把整条会话回退到某点、分支、分享给别人;或从旧会话的某个状态fork出一个全新任务。
一句话心智模型(StackToHeap 总结得很准):Pi 的会话是一棵树,不是一条时间线。想清楚这点,用法就通了。
二、实现原理:它凭什么能"版本控制"
1. 会话存成一个 JSONL 文件,每条记录是一棵树的节点。
路径在~/.pi/agent/sessions/<org>/<repo>/_.jsonl(按工作目录组织,一个会话一个文件)。文件里每条 entry 有id和parentId——这就是"树"的来源:追加式(append-only)树,不是线性列表。
2. 一个"叶子指针"(leafId)跟踪你当前在树的哪个位置。
- 追加一条消息 = 给当前 leaf 生一个子节点。
- 分支= 把 leaf 指针移到更早的某个节点上,但不改动任何历史。你接着发的下一条消息,就成了那个旧节点的一个新子节点——于是旧的延续和新的延续变成兄弟分支。
3. 组装模型上下文时,从 leaf 一路走回 root。
buildSessionContext()从当前 leaf 回溯到根,把路径上的消息拼成数组喂给模型。结果:只有你当前所在的激活分支在上下文里,但所有分支都完整保留在文件中。这就是"切换分支 = 切换上下文"的机制。
4. 分支摘要(branch_summary)——带走要点,不带 token。
用/tree从一条分支切到另一条时,Pi 可以把离开的那条分支总结成一段摘要,附到新位置上。你带走的是"我刚才试过啥、结论是啥",而不是整条分支的 token 细节。
5. Compaction(压缩)也是非破坏的。
压缩时 Pi 不是删旧消息,而是append 一个 summary entry,只在"组装未来上下文"时才用它替换掉那段老历史。原历史仍在文件里。
6. 节点类型不止对话。
一条 entry 可能是:user/assistant/tool(工具调用)/model_change/thinking_level_change/compaction/branch_summary/label(你打的书签)/custom(扩展状态,不进模型上下文)/custom_message(扩展注入、会进模型上下文)/session_info(会话名)。
7. 版本演进。v1 是线性的,v2 引入树,v3 把hookMessage改名custom;旧会话加载时自动迁移。
一张图看懂分支(在某个点/tree后试了 approach B):
session root └── user: implement feature └── assistant: approach A ├── user: continue with A ← 原始分支 │ └── assistant: ... └── user: try approach B ← /tree 回退后新建的分支 └── assistant: ...你在try approach B这条线上时,continue with A那条完全不受影响;随时/tree跳回approach A继续。
三、命令速查
| 命令 | 作用 |
|---|---|
/tree | 导航当前会话树,跳到任意节点继续 |
/fork | 从某个历史点建新会话文件 |
/clone | 把当前激活分支复制成一个新会话文件 |
/resume(或启动pi -r) | 浏览并选择历史会话 |
/new | 开新会话 |
/name <名字> | 给会话起名(好找,pi --name "..."启动时设) |
/session | 看当前会话:文件路径、ID、消息数、token、花费 |
/compact [prompt] | 压缩旧上下文 |
/export [file] | 导出会话 |
/share | 分享会话 |
CLI 侧:
| 命令 | 作用 |
|---|---|
pi -c | 继续最近一次会话 |
pi -r | 启动即浏览历史会话 |
pi --session <path|id> | 指定会话文件/部分 ID |
pi --fork <path|id> | 从某会话 fork 出新会话 |
pi --no-session | 临时模式,不保存 |
/treevs/forkvs/clone怎么选:
| 输出 | 视图 | 典型用途 | |
|---|---|---|---|
/tree | 同一会话文件 | 整棵树 | 在一个工作流内快速跳分支 |
/fork | 新会话文件 | 历史消息选择器 | 从更早的某句话另起一个新任务 |
/clone | 新会话文件 | 当前激活分支 | 继续前先"复制一份"当前工作 |
原则:想就地试 alternatif 用
/tree;某分支变成独立任务/交付物时用/fork或/clone拆成单独文件。
四、/tree里怎么操作
敲/tree(或连按两次Esc)打开会话树导航器:
- 方向键上下移动选择节点。
- Shift+L给当前节点打 label(书签);Shift+T切换 label 上是否显示时间戳。
- Ctrl+O循环过滤模式:default / no-tools(隐藏工具调用)/ user-only / labeled-only(只看打了书签的)/ all。
- 选中一个user 消息:leaf 移到它的父节点,并把这句话丢进输入框让你改——你一改重发,就新建了一个分支。
- 选中assistant / tool 条目:leaf 移到那,从该点继续。
关键细节:在
/tree里选 user 消息时,你是回到"它发出之前",所以重发会生成分支;选 assistant/tool 则是"接着它的结果"继续。两者都是新建分支,原历史一字不改。
五、实战例子
实战 1:最小场景——跑偏了跳回去
你: 把这个函数的错误处理重写下 Agent: (改了一堆,引入了新问题,来回扯皮三轮) 你: /tree → 方向键选到 "把这个函数的错误处理重写下" 的上一句正常状态 → 回车,输入框已载入那句话,改成:"重写错误处理,但保留原有返回值结构,只加日志" → 重发重发的瞬间,一条新分支诞生;原来那三轮混乱留在旁边,随时能回看。
实战 2:同一上下文试 3 个重构方案
你: 重构 auth 模块,先别动代码,给三个方案 Agent: 方案A 保守、方案B 抽层、方案C 重写 你: /tree → 选到 Agent 给出三方案那条 assistant 消息 → 继续发:"按方案A实现" ← 分支1 (看完方案A结果,/tree 回退到同一点) → "按方案B实现" ← 分支2(与分支1兄弟,互不影响) (再 /tree 回退) → "按方案C实现" ← 分支3 你: /tree → 三个方案结果并排都在,挑一个继续深入比起开三个新会话各自重新喂"auth 模块背景 + 三方案",这里前置对话只发生一次。
实战 3:噪声探索不污染主线(推荐工作流)
想大胆试错但保持主线干净,照 StackToHeap 这套 5 步:
1. /tree 到(或就在)主线某点,分支出去 2. 噪声探索:翻日志、跑实验、走死胡同 3. 把有用结论落盘:让 Agent "把结论写进 notes.md" 4. /tree 跳回主线 5. 继续主线,读 notes.md 消费探索结果主线上下文里永远只有"干净的决策 + 结论文件路径",没有大段试错过程。
实战 4:分支摘要 + 分享会话
长会话切分支时,Pi 提示要不要给离开的分支做摘要,选是:
你: /tree → 切到另一分支 → 提示 "Summarize the branch you're leaving?" 选 Yes → 生成 branch_summary 附在新位置之后把整条会话/share给别人;对方拿到的是带分支结构、且关键岔路有摘要的完整历史,不是流水账。
实战 5:命令行 fork——从旧会话另起任务
不必进交互模式,直接从一个历史会话 fork:
# 先看有哪些会话(带名字/消息数/目录)pi-r# 从某个会话文件(或部分 ID)fork 出一个新会话接着干pi--fork~/.pi/agent/sessions/acme/shop/2026-09-20_14-03.jsonl适合"上次那个会话的状态我还想要,但要朝新方向走、且不想动原会话"。
六、避坑
- 分支 ≠ 改历史。跳回旧节点再发消息,建的是新分支,原分支一字不改。别指望它"覆盖"过去。
- 当前上下文只含激活分支。
/tree跳走后,上一条分支的内容不在模型上下文里了——需要的话靠 branch_summary 带过来,或探索前先落盘。 - 探索结论一定要落成文件。否则跳回主线后,那条分支的产出就"看不见"了(它还在文件里,但不在上下文里,Agent 也不会主动读)。
/fork和/clone别混。/fork从历史某点起新文件;/clone复制当前这条激活分支。- 会话文件就是 JSONL。删文件 = 删会话;在
/resume里按Ctrl+D删除,Pi 会优先用trashCLI(如果装了)而不是直接永久删。 - 给会话起名。用
/name或pi --name,否则/resume里一堆无标题会话很难找。
参考资料
- Pi 官方《Sessions》:https://pi.dev/docs/latest/sessions
- StackToHeap:Managing Context Windows with pi /tree — https://stacktoheap.com/blog/2026/02/26/pi-tree-context-window-management
- developersdigest:Hands-On With Pi: Run Modes and JSONL Session Trees — https://www.developersdigest.tech/blog/pi-hands-on-run-modes-session-trees-guide
- GitHub 讨论:What is the point of the tree functionality? — https://github.com/earendil-works/pi/discussions/5205
- Pi 会话内部结构分析(Inside Pi Agent):https://edward40.com/p/pi-agent-internal
- pi-session-tree-browser(可视化浏览会话树):https://pi.dev/packages/pi-session-tree-browser
上一篇:13 · 技能实战篇