拆解 Pi 的 harness 设计
Pi(npm 包 @earendil-works/pi-coding-agent)自称 "minimal agent harness"——极简的 agent harness。这句话值得拆开读:它没有自称"最强的 coding agent",也没有自称"最好用的 AI 编程工具",而是把自己的定位钉死在 harness 这个词上。
这一篇我们用课程的五子系统框架(指令、工具、环境、状态、反馈)来拆 Pi,看它的设计哲学和 Claude Code、Codex 有什么根本不同。答案先给出来:Pi 的哲学是"内核最小化 + 扩展可编程化",把上下文工程做进系统提示之外,让使用者(甚至 Pi 自己)去改 harness,而不是让 Pi 替你决定 harness。
一句话定位
Pi 是极简内核:官方定位刻意把内核做小、把决定权交还给你——pi.dev 首页的原话是 "Ask Pi to build what you want, or install a package that does it your way"。它把 harness 拆成四层可定制物:
- 扩展(Extensions):挂在 Pi 生命周期事件上的 TypeScript 钩子,运行时(runtime)级的可编程面。
- 技能(Skills):按需加载的能力包,包含指令和工具,渐进式披露(progressive disclosure)。
- 提示模板(Prompt templates):可复用的 Markdown 提示,输入
/name即可展开。 - 主题(Themes):TUI 外观。
这个分层思路本身就是一种 harness 设计:把"模型能看到什么、什么时候能看到"完全交给规则和扩展,而不是写死在内核里。
核心循环
Pi 和所有 coding agent 一样,本质是"推理 → 工具执行 → 观察 → 再推理"的 while 循环。值得注意的不是这个循环本身,而是 Pi 对循环外层的处理:它把上下文管理这件事,从循环内部的"压缩"扩展到了循环之外的"控制"。
Pi 的运行时对外暴露可编程接口——源码 README 的 Programmatic Usage 里,除了交互式 TUI,还支持脚本化的打印/JSON 模式、RPC 协议与 SDK 嵌入。这决定了同一个 harness 既能被人逐条驱动,也能被 CI/CD 或别的程序自动驱动。它对应课程第十三讲"循环工程"里"从手动驱动到自动循环"的前提:一个 harness 如果只能被人类交互驱动,就永远进不了自动循环。
指令子系统:AGENTS.md 与 SYSTEM.md
Pi 对"指令"的处理很克制,但层级清晰:
- AGENTS.md:源码 README 的 Project Context Files 明确写了加载顺序——全局
~/.pi/agent/AGENTS.md→ 逐级向上遍历父目录 → 当前目录./AGENTS.md(也兼容 CLAUDE.md)。这就是"仓库即事实来源"的贯彻——指令是文件,不是聊天框里的叮嘱。 - SYSTEM.md:pi.dev 官方文档 里说可按项目替换(replace)或追加(append)默认系统提示。这是 Pi 允许你动"系统提示"的唯一正式入口,也是它的"环境自描述"层。
Pi 官方强调它的系统提示本身是极简的。这背后是一个明确的取舍:内核不往里塞"如果……那么……"的长篇规则,而是留出扩展点,让规则以技能和扩展的形式在需要时才出现。这直接呼应课程第四讲"为什么单个巨型指令文件会失败"——Pi 用"内核极简 + 文件拆分 + 按需加载"天然避开了巨型指令的问题。
状态与上下文:Pi 拆得最细的地方
Pi 的上下文工程值得重点拆,因为它把课程里"上下文连续性""防止上下文腐化"这些概念落成了具体机制:
1. 压缩(Compaction)可编程化。 接近上下文上限时自动把旧消息摘要化——pi.dev 官方文档里讲压缩策略本身是可定制的:你可以用扩展实现基于话题的压缩、代码感知的摘要,甚至换一个不同的模型来干摘要。源码 README 里也能看到默认机制的细节:自动压缩在两种情况下触发(上下文溢出恢复 / 超过保留阈值),切割点保留最近约 2 万 token,之前的消息被摘要成 "context handoff" 并逐级链式压缩。也就是说,Pi 不把"怎么压缩"当做一个不可改的常数,而是当作 harness 的一部分。
2. 动态上下文(Dynamic context)。 pi.dev 官方文档里说扩展可以在每一轮推理前注入消息、过滤消息历史、实现 RAG、构建长期记忆。这比"上下文满了再压缩"更进一步:它让你在上下文进入窗口之前就决定放什么、不放什么。对应课程里"让 agent 的运行过程可观测、可调试"和"保持上下文连续性",Pi 把这两件事下沉到了扩展面。
3. 会话树(Session tree)。 pi.dev 首页明确写"会话以树状结构存储(sessions are stored as trees),/tree 可以回到任意历史节点继续,所有分支都保存在同一个文件里"。这解决了课程反复强调的"跨会话上下文断裂"问题——不是靠摘要硬接,而是靠结构化的历史重放。分支可以导出为 HTML,或上传为 gist 分享,可观测性也顺带解决了。
工具子系统:技能与扩展
Pi 的"工具"分两层:
- 技能(Skills):源码 README 的 Skills 一节定义得很明确——"self-contained capability packages that the agent loads on-demand",即按需加载的能力包,包含指令和工具,遵循 Agent Skills 标准。渐进式披露让技能详情只在被触发时才进入上下文,不打爆提示缓存(prompt cache)。这是成本角度的 harness 设计:上下文里每多一个 token,每次推理都要为它付费;把技能做成按需加载,就是"给地图,不给说明书"的另一种表达。
- 扩展(Extensions):挂在内置生命周期事件上的 TypeScript 钩子——源码 README 的 Hooks 一节给了四个官方示例用途:拦截危险命令(权限门)、任务切换时 checkpoint 代码状态、保护路径(禁止写
.env等)、修改工具输出后再交给模型、以及从外部(文件监听/Webhook/CI)注入消息唤醒 agent。这些钩子 API 也在@mariozechner/pi-coding-agent/hooks里导出。社区 harness(pi-agent-harness)则把钩子面进一步封装成 skill-router、session-summary、extract-patterns、telemetry 等现成扩展。
扩展是 Pi 最重要的设计决定:它不是"给用户几个开关",而是把运行时内部的事件面全部暴露出来。 想加记忆?在 agent/pre-step 注入。想记录行为?订阅 session 事件。想改模型请求?挂钩 agent/request。你可以让 Pi 自己改自己的 harness——这比任何"配置项"都更接近"可编程 harness"的定义。
反馈与验证:把"学习"也做成 harness
Pi 本身不自带强制的测试门禁(那是使用者要在 AGENTS.md 里写验证命令的),但社区 harness(pi-agent-harness)用扩展把"反馈回路"结构化了,官方 README 的 Hooks 一节也给出了类似的机制基础:
- session-summary(pi-agent-harness 扩展):维护滚动的
PROGRESS.md条目——这就是课程里的状态子系统,长任务进度跟踪。 - extract-patterns(pi-agent-harness 扩展):从会话中收集经验教训候选,沉淀到
LESSONS.md——把"每次会话结束前做好交接"从约定变成了机制。 - telemetry(pi-agent-harness 扩展):记录 token 用量、成本等——可观测性。
同一个社区仓库进一步验证了这个模式:VISION.md(目标)、PROGRESS.md(进度)、LESSONS.md(经验)、STANDARDS.md(标准),全部是 Markdown 文件,跨会话持久化。这跟课程推荐的"仓库即事实来源 + 进度文件 + 交接机制"是一模一样的套路,只是被 Pi 的扩展机制变成了开箱即用的层。
映射到课程框架
用课程五子系统给 Pi 打分(主观,供对照):
| 子系统 | Pi 的实现 | 评价 |
|---|---|---|
| 指令 | AGENTS.md 分级加载 + SYSTEM.md | 层级清晰,但规则本身要靠用户写 |
| 工具 | 技能按需加载 + 扩展全生命周期钩子 | 极强,把工具系统做成了可编程面 |
| 环境 | SYSTEM.md 做环境自描述;运行时环境靠用户在 AGENTS.md 里声明 | 机制是开放的,但可复现性依赖用户自述 |
| 状态 | 会话树 + 压缩可定制 + PROGRESS.md | 极强,跨会话与可恢复性是其核心 |
| 反馈 | 验证命令靠用户定义;session-summary / extract-patterns 机制化 | 机制提供,内容靠用户 |
Pi 的取舍和 Claude Code / Codex 形成鲜明对照:Claude Code 把"记忆、权限、子智能体"都做进内核,开箱即用;Codex 把"仓库规范、环境隔离"做成默认;Pi 选择什么都不替你决定——它把决定权做成扩展点。代价是你要么自己写扩展,要么装别人写好的包。
值得借鉴的设计
- 压缩策略做成可插拔。你的 harness 里"上下文怎么压缩"不应该是一个写死的参数,而应该是一个可以替换的策略接口。
- 用会话树替代硬摘要。跨会话恢复不一定要靠"上一轮总结",结构化重放历史往往是更可靠的状态子系统。
- 提示缓存友好。按需加载技能、别把全部规则一次性塞进系统提示,既是上下文工程,也是成本工程。
- 让 agent 能改自己的 harness。如果 harness 的扩展面够开放,"优化 agent 行为"这件事本身就能由 agent 半自动完成。
参考来源(原文 / 源码)
每条论断都能回溯到下面的原文或源码,避免凭印象转述:
- pi.dev 官网:定位原话 "Ask Pi to build what you want, or install a package that does it your way"、四层可定制物、会话树("sessions are stored as trees",
/tree、单文件保存、导出 HTML / 分享 gist)。
https://pi.dev/ - pi.dev 官方文档 · Sessions:压缩可插拔(topic-based / code-aware / 换摘要模型)、自动压缩与动态上下文注入的机制描述。
https://pi.dev/docs/usage/sessions - pi.dev 官方文档 · Extensions:扩展可在每轮推理前注入消息、过滤历史、做 RAG、构建长期记忆。
https://pi.dev/docs/usage/extensions - pi.dev 官方文档 · Project Context:SYSTEM.md 的 replace / append 语义。
https://pi.dev/docs/usage/project-context - Pi Coding Agent 源码 README(badlogic/pi-mono):AGENTS.md 三级加载顺序(全局 → 父目录 → 当前目录)、
/compact与自动压缩的触发条件与 2 万 token 切割点、Skills 按需加载与 Agent Skills 标准、Hooks 生命周期与四个官方示例用途、Programmatic Usage(JSON / RPC / SDK)。
https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/README.md - pi-agent-harness 社区仓库:skill-router / session-summary / extract-patterns / telemetry 扩展,VISION.md / PROGRESS.md / LESSONS.md / STANDARDS.md 文件体系。
https://github.com/LabidySabidy/pi-agent-harness
相关讲义:第二讲 · Harness 到底是什么 | 第五讲 · 让跨会话的任务保持上下文连续 | 第十三讲 · 从手动驱动到自动循环