AIGC标识 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。默认情况下,它只给模型四把工具:readwriteeditbash。就这些。没有花哨的内建功能,没有一堆默认行为。

它最信的一句话,是 “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_startmessage_start/update/endinputsession_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(几个关键设计)

  1. 每种模式是一个统一的 union 状态 "chat" | "plan" | "dev"。切换时只改一个状态,简单不易错。
  2. before_agent_start 每轮做两件事:强制 setActiveTools 与当前模式对齐(保证工具集一直对),并把一个"当轮权威模式标记"追加进 system prompt(比如 [MODE: DEV — FULL WRITE ACCESS])。因为 system prompt 每轮重建,旧模式的标记不会残留——模型不会在一轮被切回 chat 后还自以为是 dev。这是踩过坑之后的设计:早期用"消息注入 + 上下文去重",后来发现问题,改成了"每轮重建 + 单一权威标记",干净得多。
  3. 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}`);
}
  1. 一个设计取舍bash 我不做代码级拦截。chat/plan 模式里 bash 仍然可用,但我通过 system prompt 里的软约束(CHAT_NOTE/PLAN_NOTE)提醒"别用 bash 写盘"——把写盘交给专门的 write_plan/edit/write 去管,而不是维护一份"允许的命令白名单"。

迭代:这个包其实合并了早期两个包(dev-modepi-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(三个有意思的设计)

  1. 身份化上下文。吐槽之前,它每次都被喂进「用户最近 3 条发言 + 用户最近 3 条命令 + Agent 最新回复 + 它自己历史上说过的吐槽(分角色)」,并在模板里明确标明"谁是谁"——用户的话是用户说的,模型的话是模型说的,防止 LLM 把两者搞混。这是这块积木最微妙的地方。
  2. 独立记忆。吐槽役要说过的每句话记进 pi.appendEntry("pi-mood-memory")(custom entry,不进入 LLM 上下文),按目标分两类(对用户 toUser / 对模型 toModel)去重,防止它反复炒冷饭。
  3. 会话隔离。生成吐槽走 ctx.modelRegistry.complete() 旁路调用,且每次用一个全新的 uuidv7 sessionId + 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。

但我也不想把"数字分身"说得太玄。它不是能全权代理你的"数字人",而是在"工具链 + 性格 + 记忆"这三个维度上,越来越像"你顺手的样子"。它的聪明程度受模型本身限制,它也不可能替你下所有决定——它只是贴合你,而不是取代你

这恰好闭环了开头那圈折腾:你不再从市场上挑一个成品来迁就,而是亲手拼了一个可以不断调整的、属于你的存在

效果展示:

image

5 · 从一块积木开始

如果你也想试试,上手比想象中简单:

  1. 先写一块最小的:从 registerCommand + ui.setStatus 开始,一个 /hello 就够了;
  2. 临时验证pi -e ./index.ts,单跑一个扩展,不写入任何配置;
  3. 日常开发(改动即热更):把 index.ts 放到 ~/.pi/agent/extensions/(全局)或项目 .pi/extensions/,然后 /reload,之后改代码都能热加载;
  4. 长期/分享:把它做成 pi 包,pi install <路径>pi install npm:...,别人也能装;
  5. 看参考docs/extensions.md + examples/extensions/,里面有针对 registerToolregisterCommandsetActiveToolsbefore_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 的“爽感”,这次终于被我亲手搭出来的工具正面兑现了一次。

这大概就是“积木”真正打动我的地方:不是它有多强,而是当我把它们拼成自己顺手的样子时,我才真正用它,而不只是“体验”它。

posted @ 2026-08-13 14:20  凉快李某  阅读(47)  评论(0)    收藏  举报