AI Agnet的会话实现原理-Pi 的Session工作流程全解析(二)
2026/8/18 10:12:22 网站建设 项目流程

上一篇我们了解了Pi的Session的整体架构设计、Session的树形数据结构、三种队列管理等等.

本篇我们继续探索Pi的Session工作原理

5.2 持久化时机:Save Point

Session 不是"每改一个字符就 flush 一次磁盘",而是按"回合"批量写:

  • message_end

    :单条消息收尾时写一次(user、assistant、toolResult 各写一次)

  • turn_end

    :整个 turn 结束时 flush 所有"待写"配置变更(model_change、thinking_level_change、active_tools_change)

  • agent_end

    :整个 Agent 运行结束时发"settled"事件

这种设计的取舍:批量写减少 IO 次数,但若程序在中途崩溃,最后一小段配置变更可能丢失——但用户消息和 AI 回复都已经写盘了,不会丢对话内容

5.2.1 三个事件代表什么:三层嵌套的"生命周期"

message_end / turn_end / agent_end 不是三个并列的事件,而是三层嵌套的"生命周期边界"。一次 Agent 运行可以包含多个 turn,一个 turn 可以包含多条 message。从外到内:

一句话总结agent_end是"外层天花板",turn_end是"中段里程碑",message_end是"内层原子"。三者是包含关系,不是并列出三次

三个事件分别代表什么:

事件

生命周期含义

每次 AgentLoop 触发次数

携带数据

典型用途

agent_start / agent_end

一次完整的runAgentLoop调用的开始和结束

1 次(agent_end 会发settled事件,让 TUI 知道可以解锁输入框)

agent_end 带messages: AgentMessage[],即这次 run 新产生的所有消息

TUI 知道"AI 这轮干完了,可以给我看结果 / 让我继续打字"了

turn_start / turn_end

一个 turn 的开始和结束。一个 turn = 一次 assistant 回复 + 它所触发的所有工具调用 + 所有 toolResult

可能多次。如果 assistant 调用了工具,AgentLoop 会再开新 turn 把工具结果喂回去,直到 assistant 给出stop

turn_end 带message(本轮的 assistant 消息)+toolResults(本轮产生的 toolResult 列表)

在 turn 边界批量 flush 配置变更(model_change 等);也是prepareNextTurn钩子的触发点,让 harness 决定下一轮要不要换模型/改 thinking

message_start / message_update* / message_end

单条消息(user / assistant / toolResult)的开始、更新(仅 assistant 流式期间)、结束

每个 turn 内至少 2 条(user + assistant),如有工具则更多

始终带message: AgentMessagemessage_update额外带assistantMessageEvent(增量流式片段)

Session 落盘的最小单位

。message_end 一触发,那条消息就立刻appendEntry写进 JSONL

实际触发的时序(看 agent-loop.ts:109-198):
emit({ type: "agent_start" }) // 整个 loop 启动 1 次 emit({ type: "turn_start" }) // 第一个 turn 开始 emit({ type: "message_start", prompt }) // user prompt emit({ type: "message_end", prompt }) // user prompt 立刻结束(无流式) emit({ type: "message_start", assistantPartial }) // assistant 开始流式 emit({ type: "message_update", ... }) // 流式过程中触发 N 次 emit({ type: "message_end", assistantFinal }) // assistant 流完 // 如果有工具调用: emit({ type: "tool_execution_start", ... }) emit({ type: "tool_execution_end", ... }) emit({ type: "message_start", toolResult }) emit({ type: "message_end", toolResult }) emit({ type: "turn_end", message, toolResults }) // turn 边界 // 如果需要继续(assistant 还要看 toolResult 再回话): emit({ type: "turn_start" }) // 开新 turn // ... 再次流式 assistant ... emit({ type: "turn_end", ... }) // 直到 assistant stopReason === "stop" emit({ type: "agent_end", messages }) // 整个 loop 收尾
为什么是三层而不是两层?
  • message 层

    保证"AI 一句话讲完就落盘"——断电也不丢用户已看到的字。

  • turn 层

    是真正的"工作单元"——一个 turn 结束后 harness 才会去检查"要不要换模型/压缩/插入 steer 消息",这些批量配置变更在 turn_end 时统一落盘,避免每改一个就写一次。

  • agent 层

    是"是否还活着"的信号——agent_end 一发,TUI 立刻解锁输入框;如果长时间没收到,TUI 知道 Agent 还在忙,可以显示"AI 正在输入..."。

简单说:message_end 关心"内容不能丢",turn_end 关心"配置可以批量",agent_end 关心"什么时候让人继续打字"。三者职责分明、各管一段。

常见误解:agent_start / agent_end ≠ 一次 Session

关键区别:Session 是整本对话笔记本(可能跨多个工作日、几百条消息),而 agent_start / agent_end 只是"AI 响应一次用户输入"的完整流程。一次 Session 里会有很多次agent_start / agent_end。

概念

范围

触发时机

持续多久

Session

整本对话笔记

用户开/关应用、加载历史

可能跨小时、天、甚至周

agent_start / agent_end

一次prompt()的完整执行

用户敲一次回车 / 扩展主动prompt

几秒到几分钟,取决于工具调用

turn_start / turn_end

一次 assistant 回复 + 它的工具结果

工具调用会开新 turn(直到 assistantstop

一次 LLM 调用 + 工具执行

message_start / message_end

单条消息

每条 user/assistant/toolResult 都有

毫秒到秒(流式)

用代码佐证:AgentHarness.prompt()是用户每发一条消息就会调一次的方法(agent-harness.ts:608),它内部executeTurnrunAgentLoop→ 触发一次完整的agent_start...agent_end。所以一次agent_start严格对应"用户的一次输入 + AI 完成这次输入处理的全过程",而不是"一次完整的会话"。

算账式理解:

  • 一次 Session =N 次agent_start / agent_end(你每说一句话算一次)

  • 一次 agent_start / agent_end = 1~M 次 turn_start / turn_end(AI 调一次工具就多一个 turn)

  • 一次 turn_start / turn_end = 2~K 条消息(user + assistant + 0~N 个 toolResult)

所以三层事件和 Session 之间的关系是:Session 是"账本",三层事件是"这次记账里具体写哪几行、什么时候结算"。把 agent_start / agent_end 误当成 Session 是常见误解——前者是"这次响应",后者是"整本历史"。

5.3 上下文构建:buildSessionContext

从磁盘的整棵树,到 LLM 看到的一维消息流,中间有一个关键的"翻译"步骤:

这里的关键洞见:树的形状由用户和工具决定(分支、压缩),但 LLM 看到的永远是一条线性消息流。实际干这件"扁平化"工作的是buildSessionContext这个纯函数(session.ts:22)——它接收"从根到当前叶子的全部条目",吐出SessionContext。Session 类本身只负责提供路径(getBranch()),Session.buildContext()是把这两步粘在一起的胶水方法。

6. 架构设计:分层与数据流

6.1 分层架构

分层的好处

  • 存储可替换

    :默认是 JSONL 文件,但通过 SessionStorage 接口,可以换成 SQLite、内存、远程存储等。AgentHarness 不用改一行代码

  • 运行时和持久化解耦

    :AgentHarness 只跟 Session 这个抽象打交道,不关心 JSONL 怎么写

  • 易于测试

    :用 MemorySessionStorage 就能跑 AgentHarness 单元测试,不用碰磁盘

6.2 关键数据流(一次 prompt)

注意一个微妙之处:同一回合内,buildContext 被调用了两次——一次在 prepareNextTurn(AgentHarness 准备上下文),一次在 runAgentLoop 内部(AgentLoop 真正调 LLM 前)。这是因为 AgentHarness 的 prepareNextTurn 会先注入 steer 消息,再让 AgentLoop 拿到最新上下文。

7. 设计优点和缺点

7.1 优点

优点

说明

断电可恢复

JSONL 追加写,每条 message_end 都已落盘。程序崩溃后重开能完整恢复

分支可追溯

树形结构天然支持"换个方向试试"。旧分支不删除,只是叶子指针移走

压缩可回滚

compaction 是插入式节点,不删除老消息。需要时可以"反压缩"看到原文

配置变更留痕

模型切换、thinking level 变更、工具集变更都是 SessionTreeEntry。下次恢复时知道当时用的是什么

可文本编辑器查看

JSONL 一行一条 cat 就能读。开发者和用户都能用熟悉的工具排查问题

存储后端可插拔

SessionStorage 接口让 JSONL / 内存 / 远程存储都能用同一套代码

扩展可注入消息

CustomMessageEntry 让扩展往 LLM 上下文里塞东西,不用改核心

7.2 缺点 / 取舍

缺点

说明

无并发写保护

JSONL 追加写假设只有单一进程。多个 Agent 共享同一 session 会冲突

文件会无限增长

每条消息、每次配置变更都追加。没有"老条目归档"机制。Compaction 减少喂给 LLM 的内容,但 JSONL 文件本身仍然在变长

压缩会损失细粒度

compaction 摘要由 LLM 生成,可能丢失关键细节。一旦压缩,老内容必须"反摘要"才能找回

不支持流式读

JSONL 是文本格式,10MB 以上的 session 启动会比较慢。重启时要把整本读进来

没有内置索引

找"包含某关键字的消息"必须全量扫一遍

压缩时机需要手动或启发式

何时触发 compaction 由上层决定,Session 本身不主动判断

7.3 设计哲学总结

Pi Session 的核心哲学可以归纳为一句话:

"把对话和它发生的所有上下文都当成可追加的事件日志,而不只是聊天内容。"

这与传统的"聊天历史"概念有本质区别:传统的 IM 系统存的是消息内容,Pi Session 存的是"对话这台状态机的完整演化轨迹"。这种设计让"恢复"、"分支"、"压缩"都变成了"在树上操作"而不是"在聊天记录上操作",从而获得了前面列出的所有优点。

8. 与著名 Agent 实现的对比

这一节挑几个有代表性的 Agent 框架,看它们的"会话/状态"设计,对比 Pi 的方案。

8.1 对比表

实现

状态模型

持久化

分支

压缩

断电恢复

Pi Session

JSONL 条目树

本地文件

原生支持

原生支持

原生支持

LangGraph

StateGraph, 显式节点和边

Checkpointer 抽象, 默认内存

通过多图分支

不内置

需要 Postgres 等后端

OpenAI Assistants API

Thread + Message + Run

服务端托管

不支持

服务端自动

服务端自动

Anthropic Claude SDK

messages 数组, 客户端管理

无, 客户端自行实现

客户端实现

客户端实现

客户端实现

AutoGPT / BabyAGI

任务列表 + 记忆

本地文件 / 向量库

不支持

不内置

部分

Mastra

类似 LangGraph 的工作流 + Memory 抽象

可插拔存储后端

通过多工作流

通过 working memory

通过 checkpointer

8.2 几个有代表性的设计差异

(1) Pi vs OpenAI Assistants:客户端 vs 服务端

OpenAI Assistants 把 Thread 状态完全托管在服务端,你只需要调 API。好处是简单,坏处是你看不见状态、不能改格式、不能跑本地模型。Pi 反过来:状态全在客户端 JSONL 里,你拥有全部数据,可以换存储、换模型、换 UI。

(2) Pi vs LangGraph:事件日志 vs 状态机

LangGraph 的核心是"图"——开发者显式定义节点(函数)和边(条件),状态在节点之间流动。Pi 的核心是"事件日志"——没有显式定义流程,所有流程都从事件序列中浮现。LangGraph 适合"流程固定、状态复杂"的场景,Pi 适合"流程自由、事件丰富"的场景(尤其是 Coding Agent 这种"用户问什么就做什么"的场景)。

(3) Pi vs Anthropic SDK:自己实现 vs 自己实现

Anthropic 官方的 Python/TS SDK 把 messages 数组完全交给开发者自己管理,连持久化都没有。Pi 实际上是"把 Anthropic SDK 应该做但没做的事做了"——给你一个完整的、持久化的、可分支的会话层。两者是互补关系,Pi 在 SDK 之上又建了一层。

(4) Pi 的独特之处

Pi Session 在几个维度上有自己的特色:

  • JSONL 条目树 + parentId 链表

    :同时支持线性追加(最近路径)和树形分支(历史路径)

  • 配置变更也是一类条目

    :模型切换、thinking level 变更都被记到 Session 里,这是少有的设计

  • compaction 不删除原文

    :只插入摘要节点,原文物理保留

  • 三种队列

    :steer / followUp / nextTurn 覆盖了"打断当前轮"、"等本轮结束后开新轮"、"idle 时立即开新轮"三种典型场景

9. 一图总结

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

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

立即咨询