尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

AI Agnet的会话实现原理-Pi 的Session工作流程全解析(一)

AI Agnet的会话实现原理-Pi 的Session工作流程全解析(一) 作为龙虾(OpenClaw)早期的实现基座, 开源AI Agent项目“pi”,正被越来越多人所提及. 其优雅简洁的设计以及便捷的扩展能力让你可以轻松的基于它进行二次创作而快速地得到自己独有的Agent. 尤其在各路主流Coding Agent内置了各种臃肿上下文的情况下(比如在claude code输入一个hello则动辄携带上万token的提示词), 清爽简洁的pi则看起来别具一格.而从agent设计入门和借鉴的角度, pi也是再好不过的一个参考项目. 本系列主要解析pi核心模块的工作原理.今天继续我们的Pi Agent (https://github.com/earendil-works/pi)探索之旅.今天我们来对Session的实现原理一探究竟.由于本模块篇幅较长, 我们分两篇来阐述.本篇为第一部分.大模型本身是没有记忆的. 本质上其就是根据你提供的一段文本来预测下一个输出. 而让我们有“一直在跟AI对话”这种体验的, 正是通过Agent的Session机制实现的.0. 一个生活化的比喻把 Session 想象成你和 AI 的对话笔记本在你和一个普通的 AI 聊天工具对话时AI 似乎记住了你之前说过的话。但实际上AI 本身是无状态的——它每收到一次请求就把到目前为止的全部对话重新看一遍然后给出一个回答。Session会话就是负责管理这份全部对话的中间层。把 Pi 的 Session 想象成一本带特殊功能的笔记本生活中的操作对应到 Session你说一句话AI 回一句往笔记本上追加两行一行你说的话一行 AI 的话第二天打开应用想接着聊从硬盘把笔记本整本读回找到最后一页从那里继续聊到一半你说我们换个方向吧翻到之前那一页插一根书签分支从那里开新分支笔记本越来越厚AI 看不过来了把前面 N 页总结成一张便利贴贴在最前面压缩AI 切换了一个更聪明的模型在笔记本上记一条小字批注从这一页起改用新模型你打字打到一半AI 正在回话你的草稿进入一个侧栏便签AI 这一轮先不回等这轮结束再插入整个 Pi 的 Session 子系统就是这样一本不断变厚、可以翻页、可以插书签、能贴便利贴的AI 对话笔记本。1. Session 在 Pi 整体架构中的位置Pi 项目分四层自下而上可以看到Session 横跨运行时和持久化两层。它是运行时数据和磁盘数据之间的桥梁运行时视角Session 是 AgentHarness 用来读取上下文、记录状态、追踪分支的 API 接口持久化视角Session 是 JSONL 文件提供断电可恢复、分支可追溯的存储模型2. Session 的核心职责用一句话讲清楚Session 的核心职责把用户输入、AI 回复、工具调用结果、配置变更、压缩快照、分支导航等所有与一次对话相关的事件当作一份按时间顺序追加、有父子关系的可恢复历史并提供读出当前叶子路径上的消息这一基本能力。换句话说Session 既是账本记录发生了什么也是指针指向当前在哪儿还是压缩机当历史太长时折叠旧内容。3. 从用户敲下回车到看到 AI 回复的完整流程这一节是本文最核心的部分。我们沿着一次完整的对话把 Session 的角色拆开来看。3.1 整体流程鸟瞰3.2 拆解每一步 Session 在干嘛步骤谁负责Session 在干嘛1. 用户敲下回车TUI / InteractiveMode无操作2. 把 prompt 交给 AgentSessionAgentSession校验 phase 必须是 idle3. AgentSession → AgentHarnessAgentHarness从 Session 读出当前上下文组装 systemPrompt / model / tools / 队列里的 steer 消息4. 启动一次 TurnAgentLoopAgentLoopSession.appendMessage 写一条 user 消息到 JSONL5. 调用 LLMAgentLoop 模型层Session.buildContext 取出当前叶子到根路径上的所有消息转成 LLM 看得懂的格式6. LLM 流式返回模型层流式事件 → Session 不参与仅由 AgentLoop 缓存7. 收到完整 assistant 消息AgentLoopSession.appendMessage 写一条 assistant 消息到 JSONL8. 包含工具调用时AgentLoop Tool每执行完一个工具Session.appendMessage 写一条 toolResult 到 JSONL9. 回合结束AgentHarness所有待写的条目 flush 到 JSONLsave point发出 settled 事件10. TUI 重新渲染InteractiveMode无操作已订阅事件关键观察Session 在这个流程里被读了一次步骤 5被写了多次步骤 4、7、8。读和写的边界正是 LLM 和持久化的边界——Session 是它们的契约。3.3 一个具体例子假设你输入帮我写一个 hello world 的 Python 文件。整个 Session 文件大致是这样追加的{type:session,version:3,id:...,timestamp:...,cwd:/Users/you/proj} {type:model_change,id:a1,parentId:null,timestamp:...,provider:anthropic,modelId:claude-sonnet} {type:message,id:b2,parentId:a1,timestamp:...,message:{role:user,content:帮我写一个 hello world 的 Python 文件}} {type:message,id:c3,parentId:b2,timestamp:...,message:{role:assistant,content:[{...}],provider:anthropic,model:claude-sonnet,stopReason:toolUse}} {type:message,id:d4,parentId:c3,timestamp:...,message:{role:toolResult,toolCallId:...,toolName:write,content:[{...}],isError:false}} {type:message,id:e5,parentId:d4,timestamp:...,message:{role:assistant,content:[{...}],provider:anthropic,model:claude-sonnet,stopReason:stop}}从这张图你能看出 Session 的几个设计取舍线性追加每条新条目都指向上一条parentId形链表消息是平等的user、assistant、toolResult 都是message类型不存在会话主题这种更高层的概念模型切换留痕在 user prompt 之前先记一条 model_change下次恢复对话就知道当时用的是什么模型同一文件可被文本编辑器查看因为是 JSONLcat 就能看懂4. Session 的骨架数据结构和树形模型4.1 树形 vs 线性为什么不是简单的一行一条消息很多 Agent 框架比如 OpenAI Assistants 早期版本用线性追加。但 Pi 选择了树形原因是要支持分支——你聊到一半说换个方向试试老的路径不会被删除只是叶子指针移到了另一条路径上。当前叶子currentLeaf指向 M5A 还是 M5B决定了下一次 LLM 看到的是哪条路径上的消息。4.2 条目类型SessionTreeEntrySession 里不止有消息还有配置变更、压缩快照、分支摘要等十几种条目它们用 type 字段区分为什么这么设计把所有和这次对话有关的事件都当成同一种条目存到同一棵树里Session 就成了对话的全部真相——不仅是聊天内容还包括配置变更、压缩记录、分支信息。这样恢复时能 100% 还原当时的现场。4.3 一次压缩Compaction在树里长什么样压缩不是把老消息删掉而是插入一个摘要节点并记下我从哪条开始还保留原文下次 LLM 看到的消息流是[compactionSummary, user 任务2, assistant 步骤3]原来的 U1→T2 全部折叠成了一段摘要文字。物理上它们还在 JSONL 里只是不会喂给 LLM。5.1 三种队列Steer / FollowUp / NextTurn你在 LLM 还没回完话时输入新消息新消息有三种去向对应不同的插入时机这三种队列的差别本质上是插入点离当前时刻有多远Steer打断当前轮插入到本轮 LLM 调用前FollowUp等当前轮结束开新一轮NextTurn等当前轮结束但允许 idle 状态时立刻触发5.1.1 决策指南到底该用哪个队列选错队列不会丢消息但会让 AI 在错误的时间点读到你的输入体验上很奇怪。决策的核心其实只有两个问题当前 Agent 在不在干活phase 是不是 idle你希望消息多快被 AI 看到打断本轮 / 等本轮完 / 不着急简化版决策表你的场景应该用为什么Agent 空闲我想直接和它说话prompt不是队列steer / followUp 在 idle 下会抛invalid_state应该走 executeTurnAI 正在写代码方向错了立刻喊停steersteer 消息在下一次 LLM 调用之前就被注入本轮 LLM 输出的剩余部分还是会流完但 AI 接下来看到的上下文已经包含你的纠正AI 正在干我想等它这一轮干完再说followUpfollowUp 消息在turn_end事件之后被消费不会打断当前轮避免污染正在生成的内容扩展/工具想提前留个话但不立即触发 LLMnextTurnnextTurn 消息会作为下次 prompt 的前缀可以被合并写入而且它在 idle 状态下也能正常入队这是和 steer/followUp 最大的不同实操例子例 1正在流式返回时按了 Esc 输入停TUI 的处理检测到isStreaming为 truemessage.mode不为 followUp于是调用session.steer(...)。这就是 steer 的典型来源用户希望立刻纠正方向。例 2流式返回中输入等下顺便看看 README用户在 TUI 里把这条消息标记为mode: followUp不是默认的 steer 模式TUI 走session.followUp(...)分支。这条消息不会打断当前 turn而是在 turn_end 后被取出。例 3扩展extension在 Agent 空闲时插入提醒扩展调用session.sendCustomMessageAppMessage时AppMessage 进_pendingNextTurnMessages下一次 prompt 时被作为前缀加进去。不会突然启动 LLM但保证被记住。补充细节steer 和 followUp 都会在 idle 时抛AgentHarnessError(invalid_state, ...)所以 TUI 在 idle 状态下根本不显示steer按钮只显示prompt输入框。agent-harness.ts:657-667steeringMode / followUpMode 决定队列怎么排空默认是one-at-a-time一次只取一条避免多条 steer 互相打架也可以设成all让所有排队消息一次性灌进上下文。agent-harness.ts:204-205nextTurn 的前缀语义如果当前是 idlenextTurn 消息会拼到下一次 prompt 的前面顺序在 [user prompt] 之前如果当前是 busy则要等当前 turn 结束后、下一次 prompt 时一起处理。换句话说nextTurn 永远是下一轮才生效这是它和 steer/followUp 最大的语义差异。优先级关系同一时刻只能选一个TUI 里用户在 streaming 时的输入会优先选 steer 或 followUp 二选一扩展在 idle 时插入会优先选 nextTurn。不同来源互不干扰因为它们入的是不同的队列。第一部分先阐述到这里. 下篇我们继续.
返回列表