pi agent:又一个少即是多的典范
pi agent:又一个少即是多的典范
0 · 折腾一圈,停在"自己拼"
我到现在还记得第一次按下 Tab 时的那种震惊。当时的我还把"自动补全"等同于"裸代码提示、补个变量名",可 Cursor 的 tab coding 完全不是一回事——它像是住在我编辑器里的预言家:我还没打下半句,它已经把我接下来的整段逻辑续写出来了,而且用的是我刚起的变量名、我那套命名习惯、配合旁边开着的那些文件一起想。那感觉不是"帮你补字",而是"它居然知道我想干什么"。
那是我生产力第一次"肉眼可见"地跳了一大截。当年写胶水代码、样板代码、把差不多的函数翻来覆去抄三遍的日子,一下被它接管了大半。我开始相信:只要自然语言描述得够清楚,剩下的它都能替我补齐。
也是从那时候起,我进入了对 AI 技术的第一阶段幻想——觉得"以后写代码 = 跟机器说人话",觉得程序员很快就要被解放,觉得 AI 会一路带我走向"想要什么就有什么"的终点。回头看,那会儿的幻想纯真得可爱,但确实是它,把我引上了后面这条一路升级的路。可升级得越远,我心里那个"不对劲"的声音也越来越响。
那种"不对劲"在我进入 Vibe Coding 之后非但没消失,反而被放得更大。Cursor 之后是 Codex 和 Claude Code。到它们这一代,连最开始的框架搭建我都省了——只要描述清楚意图,Agent 就开始"酷酷地干活",一个项目从骨架到功能唰唰地长出来,那段时间是真的爽。可爽完之后问题就来了:质量。功能多了、复杂了之后,它产出的东西常常有各种各样的问题,有时候甚至完全不可用,返工的成本高得吓人。
但我依然敏锐地注意到了这些工具里那些设计精妙的地方:plan 模式、security passthrough、子代理、用户画像……每一样单拎出来都很聪明,我很受启发。可遗憾也随之而来:这些工具各做各的,彼此之间不成体系;而且它们还经常发生巨幅的版本变更——今天还能用的配置,下个版本就推倒重来。于是始终没有一种"稳定、顺手、属于我"的使用体验。
中间还有一段噩梦般的黑暗时刻。某个版本的 Claude Code,你随便问一个"1+1"级别的问题,它都可能先烧掉几十 k 的预填充上下文,然后还给你一塌糊涂的回答。上下文被一次次的预填充白白吃掉,等待很久,出来的东西却不尽如人意,那滋味实在不好受。
就在这个黑暗时刻,hermes 出现了。它主打一个"白手起家"——不预设我什么,而是在我不断使用的过程中研究我、摸清我的习惯,然后一步步成长。那段时间它真的完全替代了 Codex 和 Claude Code 在我心里的位置,因为它真的能越来越懂我,这对我来说比什么都要重要。可惜好景不长:后来 hermes 的研发团队走远了,hermes 也进入了一启动就塞一堆预填充上下文的时代,并且逐渐向通用领域靠拢——离编程开发,越来越远。
再后来,我甚至一度返璞归真,重新回到 opencode 的怀抱。它不像前面那些家伙一样,由专家们精心设计一堆机制和 harness;opencode 就专注于开发功能,"除了开发能力啥都没用"。说来也妙——经过我长期用 AI 开发的经验,我的每一个指令都早已自带 harness,不再需要工具替我操心。于是 opencode 反而成了那个最听话、最得心应手的小弟。但用久了,还是觉得它有时候过于简陋了。
绕了这么一大圈,我终于想明白了一件事:没有一个成品能 100% 满足我所有的需求。不是它们不聪明,而是"需求"是我自己的、是流动的--而成品,永远是"别人替我定义好的一整套"。所以我开始去找一个不替我定义一切、只给我地基和积木的东西。就在这时,遇到了 pi——而我想要的,正是被这一块块积木搭起来的、属于自己的数字分身。
1 · 只给地基,不给答案
得先交代一句 pi 是什么——后面这些“积木”,都是从它的设计哲学里长出来的。
pi 是一个极简的终端编码 harness。默认情况下,它只给模型四把工具:read、write、edit、bash。就这些。没有花哨的内建功能,没有一堆默认行为。
它最信的一句话,是 “Aggressively extensible”(极度可扩展)——内核刻意不做内置的 plan mode、不做 todos、不做 sub-agents、不做权限弹窗。官方原话大意是:"别人家工具硬塞进来的功能,你都可以用 Extension / Skill / Prompt Template / Theme 自己搭。"
这其实是它的选择:内核保持最简,把"塑造权"交还给你。它不想替你定义"一个 AI 助手应该长什么样",而是给你一块地基和一堆积木接口,让你决定它长什么样。
要搭积木,pi 给了四条路:
- Extensions:写 TypeScript 代码,能注册工具、命令、钩子、UI——最有力量的一条;
- Skills:给模型写"操作手册",教它特定技能;
- Prompt Templates:可复用的提示模板;
- Themes:改界面观感;
- 以及把以上任意组合打包成 Pi Packages,用
pi install分享给别人。
正因为它"什么都不强加",它才像一盒积木,而不是一个成品。
2 · 一盒乐高
把 pi 想成一盒乐高:
- 那个极简内核就是底座——它本身不漂亮,但提供了标准插槽;
- 每个扩展就是一块积木——可独立搭、可换、可拆、可分享;
- 积木可以热更:改完代码在 pi 里跑
/reload,下一轮就生效,不用重启、不用等发布。
那“一块积木”到底能插在哪?这样看:
| 插槽 | API | 说明 |
|---|---|---|
| 能力积木 | pi.registerTool(...) |
给模型加一把新"工具",比如自定义的写文件、调接口 |
| 控制积木 | pi.registerCommand(...) |
加一个 /command,比如 /plan、/mood |
| 观感积木 | ctx.ui.setStatus(...) / 主题 |
改页脚状态栏、提示、整体外观 |
| 生命周期积木 | pi.on(...) 钩子 |
挂进 agent 的"心跳",如 before_agent_start、message_start/update/end、input、session_start |
一块最小的积木大概长这样(注册一个命令 + 一个状态栏项):
export default function (pi: ExtensionAPI) {
// 一个 /hello 命令
pi.registerCommand("hello", {
description: "Say hello. Usage: /hello",
handler: (_args, ctx) => {
ctx.ui.notify("你好,我是你的一块积木。", "info");
},
});
// 一个常驻状态栏项
pi.on("session_start", (_e, ctx) => {
ctx.ui.setStatus("hello-block", ctx.ui.theme.fg("accent", "🧱 hello block"));
});
}
就这几行,你已经搭好一块了。难的不是写,是想清楚要搭一块什么样的。
我更着迷的是后半句:积木之间由 pi 的插槽接起来,我只管打磨每一块的内容和边界。下面就是我给自己搭的四块。
3 · 四块积木
这四块,我都按同一个节奏讲:为什么做、做了啥、怎么做的、踩过什么坑、用起来什么感觉。
3.1 · 红线(pi-better-develop)
我想用一句我特别认同的话给这块积木开场:“最好的代码,就是没有写的代码。”
这句话放到“以项目开发为主”的场景里,尤其成立。对一个已经成形的项目来说,绝大多数"改动"都意味着风险--每写下一行,都是给系统增加一个潜在出错、需要维护、还可能和其他部分打架的点。所以很多时候,Agent 最该做的不是"马上动手",而是先不写、先想清楚:这个需求到底要不要改?改哪里?会不会波及别处?
正因如此,coding agent 的写权限就不该默认全开。给了它,等于默认“它可以随时改变你的项目”;而项目开发最在意的恰恰是确定性——你时刻得知道它手里握着多长的刀。把它默认锁成只读,让它必须先“证明自己值得动手”(通过 /plan 写计划、必要时再进 /dev 才落盘),既保护了代码,也逼着它先想后做。这,就是为什么我把“默认只读”做成第一块积木。
Why:用 AI 编码助手最怕的一件事——我随口一句话,它 write/edit/bash 直接把我整盘文件改得面目全非。我需要“写权限按需开放”,而不是默认全开。
What:三种互斥工作模式,命令 /chat /plan /dev:
| 模式 | 写权限 | 用途 |
|---|---|---|
chat(默认) |
无(只读) | 讨论、审阅;edit/write 禁用 |
plan |
仅 ./.pi/plans/ |
写计划文件;通过专属 write_plan 工具 |
dev |
全盘 | 完整开发,恢复内置 edit/write |
How(几个关键设计):
- 每种模式是一个统一的 union 状态
"chat" | "plan" | "dev"。切换时只改一个状态,简单不易错。 before_agent_start每轮做两件事:强制setActiveTools与当前模式对齐(保证工具集一直对),并把一个"当轮权威模式标记"追加进 system prompt(比如[MODE: DEV — FULL WRITE ACCESS])。因为 system prompt 每轮重建,旧模式的标记不会残留——模型不会在一轮被切回 chat 后还自以为是 dev。这是踩过坑之后的设计:早期用"消息注入 + 上下文去重",后来发现问题,改成了"每轮重建 + 单一权威标记",干净得多。- plan 模式的写权限用路径锁死:自定义
write_plan工具,后端做一次 containment 校验:
const target = resolve(plansRoot, rel);
// 必须落在 ./.pi/plans/ 之内,防止 ../ 越界
const relCheck = relative(plansRoot, target);
if (isAbsolute(relCheck) || relCheck.startsWith("..") || relCheck.startsWith("\u0000")) {
throw new Error(`write_plan: path escapes ./.pi/plans/ (blocked): ${rel}`);
}
- 一个设计取舍:
bash我不做代码级拦截。chat/plan 模式里 bash 仍然可用,但我通过 system prompt 里的软约束(CHAT_NOTE/PLAN_NOTE)提醒"别用 bash 写盘"——把写盘交给专门的write_plan/edit/write去管,而不是维护一份"允许的命令白名单"。
迭代:这个包其实合并了早期两个包(dev-mode 和 pi-plan-mode);拦截策略也从"命令白名单硬 block"演变成"系统提示软约束"——因为白名单根本管不全,还不如把责任交给模型、用清晰的状态标记约束它。
体感:这块积木守住了“读 / 想 / 做”的节奏。我可以先 /chat 随便聊、宁可让模型大胆说想法,想动手了再 /plan 写个计划、确认后 /dev 放它改——每一刻我心里都清楚它当下能动的边界有多大。
3.2 · 转速表(pi-token-speed)
Why:几秒甚至几十秒没有输出的时候,我最常想的一句话是——它到底是在想,还是卡死了?
What:在页脚状态栏实时显示当前生成速度(tok/s),让我看到它"在动"。
How:
- 监听
message_update流式事件,把每个增量块(text/thinking/toolcall)换算成 token 数。因为流式 delta 没有精确 token 计数,我用一个字符启发式估算:CJK 字约等于 1 token/字,拉丁字母约 1/4 token/字符:
function estTokens(text: string): number {
let n = 0;
for (const ch of text) {
const cp = ch.codePointAt(0)!;
if ((cp >= 0x4e00 && cp <= 0x9fff) || (cp >= 0x2e80 && cp <= 0x2eff)) n += 1; // CJK
else n += 0.25; // Latin / other
}
return Math.max(1, Math.round(n));
}
- 语义定为全回合移动平均(累计 tokens ÷ 已耗时)。这样实时值会平缓收敛到最终值,不会因为某一段瞬时快慢而剧烈跳变。
- 首个 token 到达前隐藏状态(避免显示误导性的 "…");模型答完时用消息里精确的
usage.output计算最终值,并置灰"冻结"住,直到下一轮生成才更新。
迭代:"实时语义"(用全回合平均还是瞬时速度)和"结束后保留末值"这两处,都是明确权衡过的取舍——要的就是"稳、不误导"。
体感:它像给 AI 装了个仪表盘。看到 tok/s 跳动,我就知道"它没卡死,只是在想"。这个小信息大大消解了等待期的焦虑。
3.3 · 嘴替(pi-mood)
Why:工具可以冷冰冰,但我想让助手有点"人味儿"。更进一步——我希望有个角色能吐槽我的 AI 同事,缓解一下被它绕来绕去的牢骚。
What:一个"情绪管家"。它会定时随机对"用户"或"模型"吐槽一句(页脚展示),也可 /mood user|model 指定对象立刻来一句。间隔还能挂机自适应:你有动作保持 30s,长时间没动作就自动延长(30s → 1m → 10m → 30m → 1h),你一有动静立刻回到 30s。
How(三个有意思的设计):
- 身份化上下文。吐槽之前,它每次都被喂进「用户最近 3 条发言 + 用户最近 3 条命令 + Agent 最新回复 + 它自己历史上说过的吐槽(分角色)」,并在模板里明确标明"谁是谁"——用户的话是用户说的,模型的话是模型说的,防止 LLM 把两者搞混。这是这块积木最微妙的地方。
- 独立记忆。吐槽役要说过的每句话记进
pi.appendEntry("pi-mood-memory")(custom entry,不进入 LLM 上下文),按目标分两类(对用户 toUser / 对模型 toModel)去重,防止它反复炒冷饭。 - 会话隔离。生成吐槽走
ctx.modelRegistry.complete()旁路调用,且每次用一个全新的uuidv7sessionId +cacheRetention:none——也就是说它完全在"主对话之外"完成,不写进 transcript、不占主对话上下文、不污染 provider 缓存:
const resp = await ctx.modelRegistry.complete(ctx.model, {
systemPrompt, // 身份化后的一段独立提示
messages: [{ role: "user", content: [{ type: "text", text: "来一句。" }], timestamp: Date.now() }],
}, {
signal: ctx.signal,
sessionId: uuidv7(), // 全新会话,避免与主对话共享缓存
cacheRetention: "none",
});
迭代:现在的身份化上下文和独立记忆是 v3 才加上的——早期版本没有"自我记忆",会说重复的话、也容易把用户和模型的话混起来。都是一轮轮用出来的问题倒逼出来的改进。
体感:这块积木给了分身"灵魂"。它既能在你连写三小时时调侃你一句,也会用自嘲的方式帮你把对模型的牢骚说出来——有人味儿,却从不抢主对话的戏。
3.4 · 记性(pi-user-profile)
Why:跟任何 AI 协作,最烦的莫过于"每次都从零跟你打交道"——我的偏好、办事风格、沟通习惯,每次都要重新交代一遍。我希望 AI 越来越懂"我是怎么干活的"。
What:一个全局持久化的用户画像。它自动归纳你的偏好/办事风格/沟通习惯,并在你每次提问前把画像注入到 system prompt——让 AI 越来越贴合"你"。
How:
- 节流自动总结:收集你的发言与反馈进缓冲区,攒够一定量或超时后,用旁路 LLM 增量更新画像并持久化到
~/.pi/agent/extensions_data/pi-user-profile/(本地目录,受PI_CODING_AGENT_DIR覆盖)。 - 每轮注入:每回合把画像追加进 system prompt(每轮重建、无残留,跟 pi-better-develop 同一种手法)。
- 自动 lint 质量守卫:三层——
- 结构校验(JSON/schema/条目合法性,违规自动修复或记录);
- 冲突调和(检测矛盾、近重复,优先用 LLM 调和,LLM 不可用时用确定性规则兜底:去重合并 + 新取胜);
- 长度硬上限:注入默认上限 700 tokens,超限自动压缩,实在超了就在本回合禁用注入并提示。
- 隐私:全部数据只存本机,可随时
/profile view看、/profile reset清空、/profile off关闭。
迭代:2.0 起数据归入 extensions_data/pi-user-profile/ 子目录,不再读写早期散落在 agent 根目录的旧文件——避免跟遗留文件打架、数据更规范。
体感:这块积木是分身的"记忆"。用得越久,它越知道你常用中文、喜欢先给计划再动手、在意可维护性……协作里那些"我再说一遍"的重复逐渐变少,它开始真的"懂你"。
4 · 拼出另一个我
四块积木单独看,是四个互不相干的扩展。但把它们拼起来,"数字分身"就浮现了:
| 积木 | 它给分身的那一面 |
|---|---|
| pi-better-develop | 原则 / 分寸——知道什么时候只读、什么时候动手 |
| pi-token-speed | 透明度——让你看到它在想、在动 |
| pi-mood | 灵魂 / 人情味——会陪伴、会自嘲 |
| pi-user-profile | 记忆 / 懂你——越用越贴合你的习惯 |
这就是我整篇反复想讲的那句:积木是手段,分身是结果。
分开看,它们只是工具链上的一些小零件;合起来,共同构成一个"属于你"的协作伙伴——一个知道分寸、看得见、有性格、记得住你的 AI。
但我也不想把"数字分身"说得太玄。它不是能全权代理你的"数字人",而是在"工具链 + 性格 + 记忆"这三个维度上,越来越像"你顺手的样子"。它的聪明程度受模型本身限制,它也不可能替你下所有决定——它只是贴合你,而不是取代你。
这恰好闭环了开头那圈折腾:你不再从市场上挑一个成品来迁就,而是亲手拼了一个可以不断调整的、属于你的存在。
效果展示:

5 · 从一块积木开始
如果你也想试试,上手比想象中简单:
- 先写一块最小的:从
registerCommand+ui.setStatus开始,一个/hello就够了; - 临时验证:
pi -e ./index.ts,单跑一个扩展,不写入任何配置; - 日常开发(改动即热更):把
index.ts放到~/.pi/agent/extensions/(全局)或项目.pi/extensions/,然后/reload,之后改代码都能热加载; - 长期/分享:把它做成 pi 包,
pi install <路径>或pi install npm:...,别人也能装; - 看参考:
docs/extensions.md+examples/extensions/,里面有针对registerTool、registerCommand、setActiveTools、before_agent_start的现成示例。
⚠️ 安全提示:扩展以你的完整系统权限运行、能执行任意代码。装任何第三方扩展前,先审一遍源码,别盲信。
6 · 结语
绕了这么大一圈,我最想说其实就一句:
工具不该由厂商替你定死,而该由你拼出来。
用 Cursor、Codex、Claude Code,再到 openclaw、hermes,它们都很强,但都是"别人替我定义好的一整套"——我总在迁就它们。直到 pi,这个只给我地基和积木的东西,让我第一次能按我的工作方式把它搭成我要的样子。
试试把 pi 当成一盒积木。你不需要一次搭四块——先从一块最小的开始,慢慢地,你会搭出那个属于你的数字分身。
如果你对我的做法感兴趣,四块积木的完整源码都在 egod-pi-extensions 仓库里:
| 扩展 | 说明 | 核心命令 |
|---|---|---|
pi-better-develop |
三种工作模式:/chat(只读)、/plan(仅写 plans)、/dev(完全写) |
/chat /plan /dev |
pi-token-speed |
页脚状态栏实时显示 tok/s | 自动,无需操作 |
pi-mood |
情绪陪伴:定时随机吐槽/鼓励 | /mood [user|model|on|off|memory] |
pi-user-profile |
全局持久化用户画像 + 问卷 | /figureme /profile |
祝你也搭出自己趁手的那一套。
7 · 题外话
这篇博客本身,就是用这篇文章里介绍的那个 pi-better-develop 拓展的 plan + dev 两个模式写出来的——写完脱稿,用时不到一个小时。
从头到尾,我几乎没在“打字”,而是在“讲意图”:我做的只是不断交代核心写作意图(主线是“积木是手段,分身是结果”,读者定位是泛技术开发者),以及补充一些局部的写作细节(这句话要加、那段要有哲理、这里留占位、这段口语一点、那章再展开一点)——剩下的组织、起承转合、成句成段,都交给 agent 替我把脑子里的话“打”出来。
这不就是我要讲的那种后时代的智能输入法吗?从前输入法把“拼音”联想成“字、词、句”;而现在,我把“意图 + 局部细节”联想成“一整篇文章”。你依然需要想清楚自己到底要说什么,但不再需要为“怎么把它写出来”费神。第 0 章里那个 Vibe Coding 的“爽感”,这次终于被我亲手搭出来的工具正面兑现了一次。
这大概就是“积木”真正打动我的地方:不是它有多强,而是当我把它们拼成自己顺手的样子时,我才真正用它,而不只是“体验”它。

浙公网安备 33010602011771号