Pi 的 Session、Trace、Turn 是什么关系?Steer 和 Follow-up 怎么注入?
Pi 在不同尺度上各有名字:Session 是磁盘上的工作单元,Trace 是一次完整的 agent 循环,Turn 是其中一轮模型响应,Steer 和 Follow-up 是两个不同时机的消息注入口。
- 三个时间尺度:Session(持久化)包含 Trace(agent_start→agent_end),Trace 包含 Turn(turn_start→turn_end)
- Session:一个 JSONL 文件,跨多次 Trace 复用,Pi 的 Session 是什么? 里讲过存储和分支
- Trace:一次用户 prompt 触发的完整运行。模型自己开多轮 不会 拆出多条 Trace
- Turn:一次 assistant 响应 + 它的 tool 批,是事件层最细的颗粒度
- Steer:每个 Turn 开头都会去取一次,抢下一轮 LLM 调用的发言权
- Follow-up:只有 Trace 想结束时才去取,本质是续命,不是打断
- Queue Mode:
one-at-a-time一条一条发,all攒齐一次性发 - 实战取舍:边跑边改用 steer,结束后追问题用 follow-up
三个时间尺度,从大到小嵌套
Pi 区分三个时间尺度,理解它们的边界就不会再把"会话""运行""轮次"混着用。
Session是最大单位:用户在某工作目录里持续工作的一段历史,落盘成一个 JSONL 文件。它跨越多次 Trace、跨模型切换、跨 /tree 回退。具体结构在 Pi 的 Session 是什么? 已经拆过,这里只关心它和另外两个的层级关系。
Trace对应一次 agent_start → agent_end 事件对——一次用户输入触发、模型跑完所有内部轮次、最终停下。Trace 内部可能有很多 Turn,但模型自己决定再调一轮工具不会拆出新 Trace:只要还是同一次 prompt 触发的连续运行,就还在同一条 Trace 里。
Turn是最细的颗粒度:一次 assistant 响应 + 这次响应带出的所有 tool 调用和它们的 result。事件上是 turn_start / turn_end 一对。
三者关系:Session 包含 Trace,Trace 包含 Turn。一次 Session 可以包含很多 Trace(每次新 prompt 一条),一条 Trace 包含很多 Turn(一次 prompt 内模型会跑多轮)。
agent loop 里两套队列轮询
runAgentLoop 是个双层循环,外层 while-true,内层 while-hasMoreToolCalls。Steer 和 Follow-up 就在两层循环的不同位置被轮询。
关键点:
- Steer 队列在每个 Turn 开头被轮询(
getSteeringMessages)。模型正在跑当前 Turn 时,用户在 TUI 里再敲一句话,不会打断当前轮——它只是排在下一轮 LLM 调用之前,等当前 turn_end 之后再注入。 - Follow-up 队列只在内层循环退出之后才被检查——也就是"模型想停下来了"那一刻。Follow-up 不是引导下一轮,是把本来要结束的 Trace 续上一条新的 pendingMessages,让外层循环再跑一圈。
所以 Steer 和 Follow-up 的差异不是"哪种消息更重要",是注入的时间点不同:前者是 Turn 边界,后者是 Trace 边界。
Steer:抢下一轮 LLM 调用
Agent 跑任务时,用户在 TUI 里发现方向错了,敲一句话。Pi 的默认行为是 steer:把这条消息塞进 steering 队列。
// 包了扩展命令等处理后
if (this.isStreaming) {
if (options.streamingBehavior === "followUp") {
await this._queueFollowUp(expandedText, currentImages);
} else {
await this._queueSteer(expandedText, currentImages);
}
return;
}
队列是 PendingMessageQueue,有 enqueue / drain / hasItems 三个动作。steer 调 enqueue 推入。等当前 Turn 跑完、下一个 Turn 开头,循环去 getSteeringMessages 取出来注入到 context 里。用户敲"等等,先别写测试",这条消息会在下一轮模型响应之前被插入 LLM 上下文,模型下一轮就能看到。
队列还有两种 mode:
| Mode | 行为 | 适用 |
|---|---|---|
one-at-a-time | 每轮只发一条 | 默认;多次微调要分轮次消化 |
all | 攒齐一次性发 | 想把多个补充点合并成一条 user msg |
Follow-up:给 Trace 续命
Follow-up 是另一个队列,只在 Trace 准备结束时才被检查。具体在 agent-loop.ts 的第 261 行:
// Agent would stop here. Check for follow-up messages.
const followUpMessages = (await config.getFollowUpMessages?.()) || [];
if (followUpMessages.length > 0) {
pendingMessages = followUpMessages;
continue; // 重新进入外层循环,不发 agent_end
}
await emit({ type: "agent_end", messages: newMessages });
如果 follow-up 队列里有消息,外层循环不退出,把 follow-up 当作 pendingMessages 重新跑内层循环,整个过程不发新的 agent_start / agent_end——所以从事件流看还是同一条 Trace,但用户视角看就是"Agent 接着做下一件事"。
典型场景:Agent 跑完 git status,你说"顺便帮我提交一下"。如果 Agent 还没停(还在跑别的),用 steer;如果 Agent 已经停下来了(trace 已结束或正想结束),TUI 默认就用 follow-up 把它当成"下一段任务"接着跑。
Follow-up 也有
one-at-a-time和all两种 mode,含义和 steer 一样。
实战怎么选
| 场景 | 用什么 | 为什么 |
|---|---|---|
| Agent 还在跑,用户改主意 | steer | 抢下一轮注入,立即纠正 |
| Agent 跑完了想加任务 | follow-up | 等 Trace 结束后自动续上 |
| 想合并多条补充成一个上下文 | follow-up + mode all | 一次性发,模型看到的是完整意图 |
| 想让模型分轮消化 | steer + mode one-at-a-time | 默认;逐条处理 |
| 用户敲完才发现没发出去 | 已经被 isStreaming 抛错 | Pi 会要求显式选 streamingBehavior,不允许静默吞消息 |
streamingBehavior 是 streaming 时的强制要求:跑着的时候再发消息必须显式选 steer 还是 followUp。这是有意的设计——用户和 Agent 协作时,分清楚"打断式注入"和"接力式注入"是决策的一部分。
一句话总结
- Session 是磁盘上的工作单元,跨多次 Trace 复用
- Trace 是一次 prompt 触发的完整
agent_start → agent_end,模型内部多轮不出 Trace - Turn 是一次 assistant 响应 + tool 批,事件层最细颗粒
- Steer 在每个 Turn 开头注入,抢下一轮 LLM 调用
- Follow-up 在 Trace 想结束时注入,续命而不是打断
知道这五个名词各管什么,写扩展、做 TUI、查 bug 的时候就不会把"消息流"和"任务生命周期"混在一起处理。
References
- Pi Session 管理:/new /tree /fork /clone /compact —— Kimi Gao, 2026-08-19
- Pi 的 Session 是什么?JSONL 树形结构撑起分支、回滚、压缩 —— Kimi Gao, 2026-07-04
- Pi 的 Compaction 是什么?自动压缩 + 分支摘要共享一套摘要格式 —— Kimi Gao, 2026-07-07