我是安徽最忧郁程序员无隅

很多人第一次接触编程 Agent,容易把它理解成“给大模型加几个读文件、执行命令的工具”。但真正决定 Agent 是否可扩展、可观察、可长期运行的,不是工具数量,而是模型、工具、上下文、会话和 UI 之间有没有清晰边界。
Pi 是一个很适合拿来拆解的案例:它把核心运行时做得很小,再把终端交互、会话、扩展、Skills 等能力放到上层。这篇文章不把 Pi 当作一个命令行产品来介绍,而是借它回答一个更通用的问题:一个生产级编程 Agent 到底由哪些机制组成?
文中架构结论基于 Pi 官方开源仓库与文档整理;具体默认值、配置项和目录可能随版本变化,落地时请以官方文档为准。
一、先抓住本质:Pi 不是一个“大 Prompt”,而是一套可分层的运行时
从官方仓库的包划分看,Pi 至少可以被理解成四层能力:
| Provider 适配层 | pi-ai | 统一不同模型提供商的流式调用接口 |
| Agent 运行时 | pi-agent-core | 维护状态、协调模型调用与工具调用 |
| 终端渲染层 | pi-tui | 负责终端组件与差量渲染 |
| 编程 Agent 应用层 | pi-coding-agent | 负责会话、提示词、工具、扩展、Skills 和交互模式 |
最重要的边界是:TUI 不等于 Agent。
终端 UI 只是事件的一个消费者。只要核心运行时把“模型开始流式输出”“工具开始执行”“工具产生进度”“当前 Turn 结束”等过程暴露为事件,同一套 Agent Core 就可以被终端、脚本、JSON 客户端,甚至其他应用复用。
这也是我们自己写 Agent 时值得保留的设计原则:
- 把 Provider 的差异压在最底层,业务层面对统一的消息和流。
- 把“推理 + 调用工具”的反馈循环放在核心层。
- 把会话、界面、项目约定、扩展放到外围,而不是塞进循环。
- 把每次状态变化做成可观察事件,避免核心逻辑直接操作 UI。
Pi 官方仓库对这些包的职责有明确说明;SDK 文档也展示了 Agent 的状态中包含消息、模型、工具、系统提示词和流式中的消息等信息。Pi Monorepo Pi SDK 文档
二、核心链路:模型不是直接“做事”,而是在反馈循环中决定下一步
工具型 Agent 的核心,不是一次 LLM 调用,而是一个不断接收反馈的循环。它的抽象可以写成下面这样:
while (true) {
const assistantMessage = await streamModel(context)
const toolCalls = findToolCalls(assistantMessage)
if (toolCalls.length === 0) {
return assistantMessage
}
const results = await executeTools(toolCalls)
context.messages.push(…results)
}

这段伪代码只有几行,但里面有四个不能混淆的角色:
这里最容易忽略的是“工具结果”不是给用户看的日志,而是下一次模型推理的输入。比如模型先调用 read 读取文件,拿到内容后才可能决定调用 edit;执行测试获得报错后,才可能形成新的修复假设。工具把外部世界变成模型可消费的证据。
生产实现还会补充几个工程约束:
- 相互独立的工具调用可以并行,以降低总等待时间;
- 即使并行完成,写回消息时仍按模型原始调用顺序排列,避免上下文语义混乱;
- 模型流、工具执行与会话操作共享取消信号,用户中断时能一致停止;
- 事件流只描述“发生了什么”,不决定“终端怎么画出来”。
因此,真正可复用的不是这段 while,而是它的 Contract:模型产出结构化意图,运行时执行意图,再把环境反馈交还给模型。
三、最关键的状态设计:Session 保存历史,Context 服务下一步决策
长任务一定会遇到一个矛盾:我们既希望保留完整历史,方便回溯和分支探索;又不能把全部记录无差别塞给模型,否则上下文窗口很快耗尽。
Pi 的解决思路是把两个概念刻意分开:
- Session 回答“此前发生过什么”。
- Model Context 回答“模型下一步需要知道什么”。

会话为什么适合做成树,而不是线性聊天记录
一次编程任务经常会出现这样的过程:先采用方案 A,后来发现不合适,再回到早期决策点尝试方案 B。如果会话只有线性消息列表,回退往往意味着丢弃后续内容。
用 id 和 parentId 把记录组织为树后,可以从旧节点继续新增子分支,原分支仍然保留:
用户:实现登录
└── Assistant:方案 A
├── 用户:继续方案 A
└── 用户:改用方案 B
这份树是“完整事实记录”。它适合追加、检查和回放,也能让用户比较不同路径。Pi 的 SDK 文档允许直接替换 Agent 的消息数组,用于恢复或分支类场景;这也说明对话状态是核心运行时里一个独立、可管理的对象。Pi SDK 文档
压缩为什么不是“删聊天记录”
压缩解决的是模型上下文有限,而不是历史记录无用。
当活跃路径过长时,运行时可以把较早消息总结成结构化 Checkpoint,并保留最近、最具体的消息尾部。一个合格的 Checkpoint 至少应包含:
- 当前目标与约束;
- 已完成、进行中、阻塞中的工作;
- 关键技术决策;
- 重要文件、函数名和报错;
- 下一步行动。
之后模型看到的是“Checkpoint + 最近上下文”,而不是从第一条消息开始的全部记录。压缩改变的是模型的视野,不是已经发生过的历史。
这条经验很适合迁移到自己的 Agent 项目中:持久化层优先保证完整性;上下文层优先保证相关性。把两者混成同一个数组,后面会很难同时做好回放、分支和 Token 控制。
四、扩展、Skills 与终端 UI:把变化放在稳定边界之外
如果把所有能力都写进 Agent Core,核心会很快变成一个难以维护的“全能框架”。Pi 更偏向于提供稳定边界,让变化从边界进入。
Extension:改变 Agent “能做什么”
扩展更适合放可执行能力和运行时 Hook,例如:
- 注册新的工具、命令和快捷键;
- 在工具执行前做审批、审计或路径保护;
- 注入模型 Provider;
- 增加消息 Renderer 或终端组件;
- 改写压缩、会话或工作流策略。
以自定义工具为例,核心循环并不需要知道“发布预览环境”是什么;它只需要面对统一的工具 Contract:
pi.registerTool({
name: "deploy_preview",
description: "部署当前分支的预览环境",
parameters: schema,
async execute(toolCallId, params, signal, onUpdate) {
// 执行任务,并通过 onUpdate 报告进度
return { content: [{ type: "text", text: "Preview ready" }] }
},
})
安全策略也应该放在这个边界:例如 Shell 命令执行前审批、敏感路径写入前拦截、工具输出进入模型前脱敏。这样核心循环保持通用,环境策略则可以按项目替换。扩展相关 API 和示例可从官方仓库的 coding-agent 文档与示例目录继续追踪。Pi Monorepo
Skill:改变 Agent “何时、如何做”
Skill 更像一份可复用的工作说明书。它通常以 SKILL.md 为入口,告诉 Agent:
- 这个任务适用于什么场景;
- 需要先读哪些文件;
- 应执行哪些命令;
- 哪些操作需要批准;
- 最终如何验证和交付。
它和 Extension 的分工可以一句话记住:
| Extension | 新增能力或拦截生命周期 | 新工具、Hook、UI、Provider 集成 |
| Skill | 复用操作方法 | 发布流程、CI 排错、代码审查规范 |
Skill 的价值还在于渐进加载:启动时只向模型暴露名称、描述和位置这类“目录信息”;真正匹配任务后,再读取完整说明。这样技能库变大时,不会把所有细节永久占满系统提示词。官方 SDK 示例展示了 Skill 的发现、筛选和自定义注入方式。Skills SDK 示例
TUI:它展示运行时,但不拥有运行时
终端 UI 的难点是模型文本、工具进度和用户输入会同时变化。Pi 的 pi-tui 使用差量渲染:比较新旧帧后,尽量只追加或刷新变化的尾部,从而减少整屏重绘和闪烁。
这个设计再次强调了边界:UI 订阅事件并呈现状态,而不是直接接管工具循环。于是交互式终端、一次性命令、JSON 事件输出或 SDK 嵌入,都能共享同一个 Agent Harness。
五、从 Pi 可以复用的 Agent 构建顺序
如果从零实现一个编程 Agent,建议按下面的顺序推进,而不是一开始就堆 Prompt、子 Agent 和复杂 UI:
Pi 最值得学习的并不是某一个命令或某一个默认配置,而是这种架构取舍:每层都足够小,边界足够清楚,复杂能力通过扩展和组合获得。
对于正在做 Python Agent 应用的同学,这套思路可以直接映射到自己的项目:用状态对象管理对话,用工具协议隔离外部操作,用事件或回调隔离界面,用摘要控制长上下文,用 Skill 把重复流程沉淀成可执行规范。这样做出来的 Agent 才不只是“能调用模型”,而是一个能持续工作、可维护、可扩展的应用运行时。
参考资料
- Pi Agent Harness 官方仓库
- Pi Coding Agent SDK 文档
- Pi Settings 文档
- Pi Skills SDK 示例


