拆开 Pi Agent:一个能把 Agent 当“系统“搭的架构,到底好在哪?

发布时间:2026/8/30 16:48:31
拆开 Pi Agent:一个能把 Agent 当“系统“搭的架构,到底好在哪? 最近我把 Pi Agent 认认真真过了一遍。从外层状态、内层循环、生命周期钩子一条一条拆开看一个能长期干活、能被打断、能记得住来龙去脉的 Agent本质上不是一段脚本而是一套有状态与无状态分层、有明确生命周期约束的系统工程。这篇不是教程是我学习过程中的一份拆解。我把它的整体流程、四大拆分、四层生命周期都整理进来也顺便补一些我自己这两年做 Agent 时踩过的坑。所有要点我都保留只做展开不改动原意。一句话看懂整体流程有状态外层 无状态内层Pi Agent 最核心的分层就是有状态的外层和无状态的内层这两段代码。有状态外层负责Agent 的记忆和调度——它知道现在进行到哪了、用户想干什么、上下文要不要压缩、要不要换个分支。这些都需要持久化状态所以它是有状态的。无状态底层负责每一条消息真正跑起来的 agent-loop——它每次进来都是干净的不记住上一轮的事只按传入的上下文执行。这很重要因为只有当内层无状态时你才能安全地重试、回滚、并行而不用担心状态被污染。外层把上下文准备妥当后调用内层内层跑完把结果交还外层。这个“外层管状态、内层管执行”的分工是理解整个架构的钥匙。先分清两种输入Steering 和 FollowUp外层要处理的第一个判断就是你这句消息到底是想插嘴干预还是等结果。Steering引导用户主动插进来的一句话比如别用 python 了改用 node、先别读那个文件。这种消息是要打断当前执行、改变 agent 正在做的事所以它被放进一个专门的 steering 队列在内层循环的开头被检查。FollowUp追问用户在当前任务结束之后的下一条消息比如好的那再帮我总结一下。这种消息是在外层 while 循环里等着等 agent-loop 跑完一轮之后再处理。外层维护两个消息队列 steering 和 followUp。关键点在于steering 是实时干预所以它在内层每轮循环开头就去抢答followUp 是排队后续所以它在外层等 agent 真正停下来。补充一句我自己的体会很多 Agent 被诟病失控就是因为缺少 steering 这种通道——用户想打断只能硬等它跑完或者干脆杀进程。有专门的消息队列去接住用户的干预意图体验是完全不一样的。上下文加载动态加载在真正调用模型之前外层会把这次运行的上下文拼装出来。它加载的东西有这么几类系统提示词base prompt模型行为的底层设定。记忆长期记忆比如跨会话要记住的偏好和事实。tools 列表这次会话暴露给模型的所有工具。渐进式加载Skills这是重点——Skills 不是一次全塞进去而是用到才加载。Agent.md专门写给 Agent 的任务说明书。短期记忆的消息历史当前会话窗口内最近的一串消息。渐进式加载 Skills这点值得单独说。最开始我习惯把所有能力都一股脑塞进系统提示词结果就是上下文越来越大、模型越来越分心。改成懒加载之后Skills 只在真正匹配到任务时才被注入既省 token也让模型把注意力集中在当下真正需要的能力上。配合 Agent.md 这种面向 Agent 的说明文件上下文就变成按需组装而不是全部常驻。上下文变换为下一轮清出 20000 token这里写死了装完上下文还不够紧接着要判断上下文是不是太大了需不需要压缩。Pi Agent 的压缩策略很清晰判断是否需要 compact目标是空出 20000 token。具体做法是从最近的消息开始向上积累一直累计到空出这 20000 token 为止。靠近现在的消息原样保留因为它们是接下来最可能被用到的而远离现在的早期消息压缩成一段结构化摘要——而且只花一次 LLM 调用就搞定。这个设计很聪明它体现了两个取舍近的保真远的浓缩。新近的上下文含金量最高细节必须保留老旧的对话大都是为什么走到这一步的背景概括成摘要就够了。一次调用一次性摘要。不是逐条滚动压缩而是把要丢的那一大段一次性喂给模型得到一份结构化的摘要顶替。既省调用次数也避免反复压缩带来的信息累积失真。我补充一点工程层面的背景压缩永远是有损的你摘掉的细节模型在后面就再也看不到原话了。所以近的保留、远的摘要这个倾斜是合理的——它把损失集中在最不可能被重新用到的那部分而不是平均地牺牲所有历史。无状态 agent-loop外层管消息内层管工具把上下文准备好之后就进入无状态底层的 agent-loop。外层循环负责处理用户的每一条消息——for遍历每一个消息每一条都进入内层处理。FollowUp 就是在外层 while 循环里等待着 agent-loop 跑完一轮的结果。内层循环才是真正的 agent-loop。它的终止条件有两个没有 tool_calls 了——模型停下来不再要求调用工具说明这一轮的任务处理完了可以正常结束。Erroe——发生了异常走硬停止立即中断任然可以继续调用工具。在内层循环开头会先看一眼有没有 Steering 消息。有就调整当下正在做的事没有就继续按原计划跑。这正好呼应了前面说的 steering 队列——它是在这个每轮循环开头的位置被消费的。这个内层循环“跑到 agent 不再调用工具为止”的设计是整个 Agent 的发动机只要模型还想调工具就继续一旦它给出最终文本回复、不再要工具了循环就停。用户不会看到模型“思考到一半”的中间态看到的是一轮完成的、确定的结果。会话不是列表是一棵树这一块是我这次收获最大的部分。Pi Agent 的短期记忆就是一个窗口内的所有 session但它对会话的定义和我以为的很不一样会话不是一排列表对话是一棵树。每条消息是一个节点。回退、分支不删数据只是移动指针。这在工程上意味着你想回到某个历史节点重新来一遍或者从某个点分出一条新分支继续聊历史数据一条都不会删改的只是一个当前指针指向哪里。回退和分叉都是指针的移动不是数据的销毁。它用jsonl 文件把整棵树持久化到用户目录下的~/.pi/agent/session里而且节点之间带父子级关系这样能完整重建这棵对话树。这个设计的价值很实在可回溯、可审计。你随时能看回之前的任何一条分支因为数据都在。安全的分叉探索。你想在同一个会话里尝试两种不同方案不用复制粘贴上下文直接开一条分支就行。复盘成本低。跑错了一条分支指针拉回来就好不用重头再来。而且——当用户进入一个新的会话分支时会把旧的会话先摘要再注入到新的上下文里。这样新的分支不是从零开始而是带着前面聊到哪了的浓缩记忆继续走。让分支既独立又有继承这是它做得漂亮的地方。上下文工程的四个关键动作除了上面说的压缩Pi Agent 的上下文工程还有几个配套动作一起看更完整。系统提示词的动态组装系统提示词不是一份写死的文本而是动态拼出来的多层 CLAUDE.md 递归加载再加上Skills 懒加载加载tool_list会话记忆。CLAUDE.md 可以分层递归一层层把约定叠进去Skills 按需加载Claude.md 补充任务级的指引。这几者合起来构成这次运行真正生效的那份系统提示词。工具输出的截断这个点非常容易被忽略但对 token 的消耗影响极大。像read文件、grep、bash这类工具一个输出就可能吃掉几千甚至几万 token。如果不设上限一个不小心读了一个超大的日志文件整个上下文就爆了。所以工具输出要做截断行数字节限制双重限制 、截头截尾两种策略比如读文件截断留下前面的部分bash命令截断留下后面的内容后续如果需要再继续读。这相当于给工具输出加了渐进式披露——模型每次只看到它请求的那一小段想要更多再显式地读下一段。既省了 token也避免一次性塞太多噪声导致模型抓不住重点。上下文的压缩这个前面讲过在 trace 之后从最近消息向上积累直到剩余 20000 token近的保留、远的用一次 LLM 调用做结构化摘要。它发生在一次完整的执行trace结束之后是一个事后整理的动作。分支的摘要这个也在会话树里讲过用户进入新的会话分支把旧的会话摘要之后注入新的上下文。它解决的是分支之间怎么共享记忆的问题。这四招——组装、截断、压缩、分支摘要——合起来就是一套完整的用最少、最相关、最新鲜的 token去支撑当前这一轮运行。上下文工程不是玄学它就是在有限的上下文窗口里做最有价值的取舍。模型路由一套规则调用多种模型Pi Agent 的模型调用也很有意思它的做法是一种规则调用多个模型。不是每个任务都硬绑死一个模型而是由一套路由规则去决定这次该用哪个模型。这样可以在能力强的贵模型和便宜快的小模型之间做选择简单任务走便宜模型省成本复杂推理走贵模型保质量。规则可以是基于任务类型、上下文长度、还是工具复杂度来决定——它是一套统一的调度逻辑底层却可以对接多款模型。工具执行用 schema 和钩子兜底Agent 的手脚就是工具而工具执行这里Pi Agent 用了两道保险钩子验证在工具执行的turn 前后用钩子做验证确认这次调用该不该发生、参数是不是合理。schema 验证对工具调用做 schema 校验确保参数的结构和类型的合法性防止模型吐出一个格式对不上的调用。还有一点出错也算一条消息。工具执行失败不会让整个 agent 崩溃而是把报错作为一条普通消息喂回给模型让它自己判断该怎么继续处理。这非常符合Agent 要能自愈的思路——错误不是终点而是需要被模型理解的下一份上下文。四层生命周期10 个钩子节点pi agent围绕四层生命周期10 个钩子节点搭建agent这就是pi agent的 harness最后把整个架构的骨骼串起来。Pi Agent 定义了四层生命周期trace从会话开始到会话结束是最外层的大生命周期。turn一个 trace 里会发生多次模型调用每一次模型调用 它这次调用所触发的工具执行合起来算一个turn。在这四层其实对应着 trace 和 turn 两级的前后以及更细的节点上一共插了10 个干预点trace 开始前、trace 结束后turn 开始前、turn 结束后模型调用的开始、运行、结束工具调用的执行开始、执行运行、执行结束。这 10 个节点就是harness运行时骨架用来干预、观察、验证、hook 的地方。你在每个节点上挂上钩子就能做到模型调用前做输入校验工具执行后做结果审核turn 结束做状态记录trace 结束做上下文压缩——所有横切的能力都挂在这个生命周期的节点上而不用写死在 agent 的逻辑里。这也是我最认同的一点把干预做成生命周期上的钩子而不是嵌在 agent 主流程里的 if-else。这样 agent 的核心逻辑保持干净、可测试而安全、审计、压缩、观测这些关注点都被优雅地分离到 harness 的节点上。这就是把 Agent 当系统搭的具象体现。总结把 Pi Agent 从头到尾拆完我的核心感受是它真正厉害的地方不是某一个具体的 trick而是一整套分层和生命周期的设计取舍。有状态外层 / 无状态内层让记忆和执行解耦可回滚、可并行、可重试。对话是一棵树而不是列表让分支和回退变成指针移动而不是数据摧毁。上下文工程是一套渐进式组装 截断 压缩 分支摘要的组合拳在有限的窗口里做最有价值的取舍。生命周期上的 10 个钩子把安全、观测、压缩这些横切逻辑干净地塞进 harness而不是写死进主流程。它不是一句 prompt 加一堆工具而是一套知道现在在哪、要去哪、能怎么打断、该怎么收官的系统。这四层骨架就是让 Agent 从 demo 走向真正能长期干活的关键。如果你也在做自己的 Agent我强烈建议先不要急着堆工具而是先把这套有状态外层 无状态内层 生命周期钩子的骨架搭明白——它才是决定你的 Agent 能不能变得可靠的根本。