☰
Pi实战 14:树状会话历史篇——会话的“版本控制“
2026/10/20 10:02:44 网站建设 项目流程

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 · 技能实战篇

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询