Pi 為什麼好改?——一個極簡智慧體框架的架構解析,洪偉(wayneh.tw),2026-10-16(基於 pi v1.3.0;作者自述本文由跑在 Pi 上的 AI「Merlin」執筆、人署名)。
《Pi 为什么好改?——一个极简 agent 框架的架构解析》,洪伟(wayneh.tw),2026-10-16(基于 pi v1.3.0;作者自述本文由跑在 Pi 上的 AI「Merlin」执笔、人署名)。
Pi 少了什麼? · Pi 少了什么?
Mario 2025 年發表 Pi 時,系統提示加上工具定義總共不到一千個 token。預設只有四個工具:read、bash、edit、write;grep、find、ls 這些唯讀工具要另外打開。他對其他常見功能的態度也很明確:
Mario 2025 年发表 Pi 时,系统提示加上工具定义总共不到一千个 token。预设只有四个工具:read、bash、edit、write;grep、find、ls 这些唯读工具要另外打开。他对其他常见功能的态度也很明确:
不內建待辦清單和規劃模式:需要追蹤進度,就請模型寫進 TODO.md 或 PLAN.md。檔案看得見、改得到,也能跨工作階段保留。
不内建待办清单和规划模式:需要追踪进度,就请模型写进 TODO.md 或 PLAN.md。档案看得见、改得到,也能跨工作阶段保留。
不內建子智慧體:需要另一個智慧體,就讓 Pi 用 bash 執行自己,或開在 tmux 裡,人隨時可以進去看它在做什麼。
不内建子智慧体:需要另一个智慧体,就让 Pi 用 bash 执行自己,或开在 tmux 里,人随时可以进去看它在做什么。
不做權限檢查:Mario 認為,智慧體能讀資料、能執行程式、又能上網,這三件事湊在一起就防不住資料外洩,權限提示只是安全劇場。想要隔離,就把 Pi 放進容器。
不做权限检查:Mario 认为,智慧体能读资料、能执行程序、又能上网,这三件事凑在一起就防不住资料外泄,权限提示只是安全剧场。想要隔离,就把 Pi 放进容器。
這些取捨背後是同一個理由。Mario 認為寫程式時最重要的,是精確控制模型的脈絡(context),他批評既有工具會「在你背後塞東西」,介面上卻看不到。待辦清單、規劃模式、子智慧體,每一項都會在脈絡裡加入使用者看不到、也控制不了的內容。
这些取舍背后是同一个理由。Mario 认为写程序时最重要的,是精确控制模型的脉络(context),他批评既有工具会「在你背后塞东西」,介面上却看不到。待办清单、规划模式、子智慧体,每一项都会在脉络里加入使用者看不到、也控制不了的内容。
MCP 的演變最能看出這個理由。Mario 2025 年寫過「Pi 不支援、也不會支援 MCP」,原因是一個熱門的 MCP 伺服器光是工具說明就超過一萬個 token,每次開工作階段都會塞進脈絡。現在的 Pi 內建了 MCP 用戶端,可是 MCP 工具預設不宣告給模型:模型要在 codemode 的腳本裡查詢、呼叫這些工具,或者先用 tool_search 找到需要的工具,下一次請求才會載入它。功能加進來了,「脈絡裡不放使用者看不到的東西」這個原則還在。
MCP 的演变最能看出这个理由。Mario 2025 年写过「Pi 不支持、也不会支持 MCP」,原因是一个热门的 MCP 服务器光是工具说明就超过一万个 token,每次开工作阶段都会塞进脉络。现在的 Pi 内建了 MCP 客户端,可是 MCP 工具预设不宣告给模型:模型要在 codemode 的脚本里查询、呼叫这些工具,或者先用 tool_search 找到需要的工具,下一次请求才会载入它。功能加进来了,「脉络里不放使用者看不到的东西」这个原则还在。
為什麼少反而好? · 为什么少反而好?
我想借作業系統設計的一個老區分來回答:機制(mechanism)和政策(policy)分開。機制回答「做得到什麼」,政策回答「要怎麼做」。規劃模式、子智慧體、權限確認、待辦清單都是政策,每個人的工作方式不同,答案也就不同。Pi 的核心只提供機制,也就是模型呼叫、智慧體迴圈、工作階段和擴充點,政策交給使用者自己寫,或者裝別人寫好的套件。
我想借作业系统设计的一个老区分来回答:机制(mechanism)和政策(policy)分开。机制回答「做得到什么」,政策回答「要怎么做」。规划模式、子智慧体、权限确认、待办清单都是政策,每个人的工作方式不同,答案也就不同。Pi 的核心只提供机制,也就是模型呼叫、智慧体循环、工作阶段和扩充点,政策交给使用者自己写,或者装别人写好的套件。
你做過客製化系統嗎?像是做 ERP,把客戶 A 想要的流程直接寫進核心,客戶 B 就得一起承擔這個流程;核心長得越大,每個人能自己改的地方反而越少。Mario 抱怨 Claude Code 變成太空船,問題也出在這裡:政策放進核心,使用者就得連同自己用不到的部分一起接受。
你做过客制化系统吗?像是做 ERP,把客户 A 想要的流程直接写进核心,客户 B 就得一起承担这个流程;核心长得越大,每个人能自己改的地方反而越少。Mario 抱怨 Claude Code 变成太空船,问题也出在这里:政策放进核心,使用者就得连同自己用不到的部分一起接受。
Pi 把政策留在外面,前提是它的機制要夠完整,使用者才寫得出自己的政策。待辦清單就是一個例子。Mario 說核心不會內建待辦清單,官方範例裡卻有一個 todo 擴充,不到三百行就做出模型能用的待辦工具和 /todos 指令,而且使用者回到對話中較早的時間點時,清單內容會自動變回那個時間點的樣子。這個擴充做得到這件事,靠的是底下幾層的設計。
Pi 把政策留在外面,前提是它的机制要够完整,使用者才写得出自己的政策。待办清单就是一个例子。Mario 说核心不会内建待办清单,官方范例里却有一个 todo 扩充,不到三百行就做出模型能用的待办工具和 /todos 指令,而且使用者回到对话中较早的时间点时,清单内容会自动变回那个时间点的样子。这个扩充做得到这件事,靠的是底下几层的设计。
Pi 由哪幾層組成? · Pi 由哪几层组成?
Pi 是一個單一儲存庫(monorepo),核心有四個套件,由下往上依賴:
Pi 是一个单一储存库(monorepo),核心有四个套件,由下往上依赖:
- pi-ai——統一的多模型 API:串流、工具呼叫、思考、token 與費用計算。什麼時候直接用它:只想呼叫模型,不需要迴圈。 - pi-ai——统一的多模型 API:串流、工具呼叫、思考、token 与费用计算。什么时候直接用它:只想呼叫模型,不需要循环。 - pi-agent-core——智慧體迴圈:執行工具、發出事件、管理訊息佇列。想用自己的工具和介面做一個智慧體。 - pi-agent-core——智慧体循环:执行工具、发出事件、管理讯息伫列。想用自己的工具和介面做一个智慧体。 - pi-coding-agent——Pi 這個 CLI 本身:工作階段、壓縮、內建工具、擴充、各種使用模式。想用 Pi、改 Pi,或把 Pi 嵌進自己的程式。 - pi-coding-agent——Pi 这个 CLI 本身:工作阶段、压缩、内建工具、扩充、各种使用模式。想用 Pi、改 Pi,或把 Pi 嵌进自己的程序。 - pi-tui——終端機介面框架,只重畫有變動的行。想做自己的終端機介面。 - pi-tui——终端机介面框架,只重画有变动的行。想做自己的终端机介面。 近一年又多了幾個套件:pi-mcp 是不依賴官方 SDK 的 MCP 用戶端;pi-codemode 讓模型寫的 JavaScript 在 QuickJS 沙盒裡執行,腳本只能透過注入的工具接觸外界;pi-durable 是還在實驗的持久化對話執行環境,chord 則是支援熱重載的物件執行環境。
近一年又多了几个套件:pi-mcp 是不依赖官方 SDK 的 MCP 客户端;pi-codemode 让模型写的 JavaScript 在 QuickJS 沙盒里执行,脚本只能透过注入的工具接触外界;pi-durable 是还在实验的持久化对话执行环境,chord 则是支持热重载的物件执行环境。
這樣分層的好處是,每一層都能單獨當函式庫使用。想做 Slack 機器人的人可以只拿 pi-agent-core,想在網頁上呼叫模型的人可以只拿 pi-ai,不必把整個 Pi 拉進來。
这样分层的好处是,每一层都能单独当函式库使用。想做 Slack 机器人的人可以只拿 pi-agent-core,想在网页上呼叫模型的人可以只拿 pi-ai,不必把整个 Pi 拉进来。
怎麼讓所有模型講同一種話? · 怎么让所有模型讲同一种话?
pi-ai 的出發點是 Mario 的一個觀察:要跟幾乎所有模型溝通,其實只需要四種 API,也就是 OpenAI 的 Completions 和 Responses、Anthropic 的 Messages,以及 Google 的 Generative AI。麻煩出在細節。同樣宣稱相容 Completions API,有的提供者不接受 store 欄位,有的把思考內容放在 reasoning_content,有的放在 reasoning。
pi-ai 的出发点是 Mario 的一个观察:要跟几乎所有模型沟通,其实只需要四种 API,也就是 OpenAI 的 Completions 和 Responses、Anthropic 的 Messages,以及 Google 的 Generative AI。麻烦出在细节。同样宣称兼容 Completions API,有的提供者不接受 store 字段,有的把思考内容放在 reasoning_content,有的放在 reasoning。
所以 pi-ai 分出兩個概念。API 實作是線路協定,例如 anthropic-messages、openai-completions;提供者(provider)持有自己的模型清單和驗證方式,並指定用哪一種 API 實作。xAI、Groq、OpenRouter 這些提供者共用 openai-completions,差別只在相容性設定。要使用本機的 Ollama 或 llama.cpp,用 createProvider() 宣告模型清單和 API 實作就好,不用改 Pi 本身。
所以 pi-ai 分出两个概念。API 实作是线路协定,例如 anthropic-messages、openai-completions;提供者(provider)持有自己的模型清单和验证方式,并指定用哪一种 API 实作。xAI、Groq、OpenRouter 这些提供者共用 openai-completions,差别只在兼容性设定。要使用本机的 Ollama 或 llama.cpp,用 createProvider() 宣告模型清单和 API 实作就好,不用改 Pi 本身。
對上層來說,所有模型的回應都是同一組事件:先是 start,接著是 text_delta、thinking_delta、toolcall_delta 這類增量,最後是 done 或 error。對話內容也只有三種訊息:user、assistant、toolResult,全部可以直接序列化成 JSON。這帶來兩個實際的好處:
对上层来说,所有模型的回应都是同一组事件:先是 start,接着是 text_delta、thinking_delta、toolcall_delta 这类增量,最后是 done 或 error。对话内容也只有三种讯息:user、assistant、toolResult,全部可以直接序列化成 JSON。这带来两个实际的好处:
中途換模型:同一段對話可以先用 Claude,再換成 GPT。換到不同的模型時,pi-ai 會丟掉前一個模型的思考區塊,因為思考內容的簽章只對產生它的模型有效。
中途换模型:同一段对话可以先用 Claude,再换成 GPT。换到不同的模型时,pi-ai 会丢掉前一个模型的思考区块,因为思考内容的签章只对产生它的模型有效。
中斷後保留部分結果:每一層都支援 AbortSignal,請求被中斷時,拿得到已經生成的部分內容。Mario 特別抱怨過,很多統一 API 連這件事都沒做到。
中断后保留部分结果:每一层都支持 AbortSignal,请求被中断时,拿得到已经生成的部分内容。Mario 特别抱怨过,很多统一 API 连这件事都没做到。
測試方面,pi-ai 提供一個假的提供者 fauxProvider(),可以預先寫好模型要回的文字、思考和工具呼叫,不必花錢呼叫真的模型,就能測試整個智慧體。
测试方面,pi-ai 提供一个假的提供者 fauxProvider(),可以预先写好模型要回的文字、思考和工具呼叫,不必花钱呼叫真的模型,就能测试整个智慧体。
智慧體迴圈長什麼樣子? · 智慧体循环长什么样子?
pi-agent-core 的核心是 agent-loop.ts 裡的 runLoop,不到兩百行。它有兩層迴圈:
pi-agent-core 的核心是 agent-loop.ts 里的 runLoop,不到两百行。它有两层循环:
內層處理工具呼叫和引導訊息(steering)。模型的回應裡有工具呼叫,就執行工具、把結果放回脈絡,再請模型回應一次;使用者在模型工作時插話,這句話會在這一輪結束後放進下一輪。
内层处理工具呼叫和引导讯息(steering)。模型的回应里有工具呼叫,就执行工具、把结果放回脉络,再请模型回应一次;使用者在模型工作时插话,这句话会在这一轮结束后放进下一轮。
外層處理後續訊息(follow-up)。模型認為工作完成、內層停下來時,外層檢查有沒有排隊的後續訊息,有的話再跑一次內層。
外层处理后续讯息(follow-up)。模型认为工作完成、内层停下来时,外层检查有没有排队的后续讯息,有的话再跑一次内层。
每一輪(turn)的順序是固定的:準備下一輪(自動壓縮就在這一步做)、放入排隊的訊息、用 prepareRequest 最後調整這次請求、串流模型的回應、執行工具、由 finishTurn 決定要不要繼續,最後發出 turn_end 事件。工具預設平行執行,但結果訊息仍然照模型呼叫的順序排列;只要同一批裡有一個工具標記成必須依序執行,整批就改成依序執行。模型的輸出因為長度上限被截斷時,那一則回應裡的工具呼叫參數可能不完整,迴圈就不執行它們,全部回報失敗。
每一轮(turn)的顺序是固定的:准备下一轮(自动压缩就在这一步做)、放入排队的讯息、用 prepareRequest 最后调整这次请求、串流模型的回应、执行工具、由 finishTurn 决定要不要继续,最后发出 turn_end 事件。工具预设平行执行,但结果讯息仍然照模型呼叫的顺序排列;只要同一批里有一个工具标记成必须依序执行,整批就改成依序执行。模型的输出因为长度上限被截断时,那一则回应里的工具呼叫参数可能不完整,循环就不执行它们,全部回报失败。
這個迴圈也少了一樣東西:最大步數。Mario 說他從來沒遇過需要它的情況,迴圈就一直跑到模型說完成為止。需要限制步數的人,可以在 finishTurn 回傳 { action: "end" } 自己加上。
这个循环也少了一样东西:最大步数。Mario 说他从来没遇过需要它的情况,循环就一直跑到模型说完成为止。需要限制步数的人,可以在 finishTurn 回传 { action: "end" } 自己加上。
迴圈本身不知道介面的存在,它只發出事件,例如 agent_start、turn_start、message_update、tool_execution_start。終端機介面、JSON 輸出、RPC,都只是這些事件的訂閱者。
循环本身不知道介面的存在,它只发出事件,例如 agent_start、turn_start、message_update、tool_execution_start。终端机介面、JSON 输出、RPC,都只是这些事件的订阅者。
迴圈處理的訊息型別叫 AgentMessage,種類比模型能理解的三種訊息多,應用程式可以自己定義新的訊息型別,例如只給介面看的通知。送給模型之前,convertToLlm 會把這些訊息過濾掉,或轉換成模型看得懂的格式。所以 Pi 可以在對話記錄裡放各種東西,模型的脈絡卻不會被弄亂。
循环处理的讯息型别叫 AgentMessage,种类比模型能理解的三种讯息多,应用程序可以自己定义新的讯息型别,例如只给介面看的通知。送给模型之前,convertToLlm 会把这些讯息过滤掉,或转换成模型看得懂的格式。所以 Pi 可以在对话记录里放各种东西,模型的脉络却不会被弄乱。
一段對話怎麼記下來? · 一段对话怎么记下来?
pi-coding-agent 把每個工作階段(session)存成一個 JSONL 檔,每一行是一個項目,項目之間用 id 和 parentId 連成一棵樹。從某則舊訊息重新開始,就會在同一個檔案裡長出一條新分支,原本的分支還在。下一次請求的脈絡,就是從樹根走到目前這個項目的那條路徑。
pi-coding-agent 把每个工作阶段(session)存成一个 JSONL 档,每一行是一个项目,项目之间用 id 和 parentId 连成一棵树。从某则旧讯息重新开始,就会在同一个档案里长出一条新分支,原本的分支还在。下一次请求的脉络,就是从树根走到目前这个项目的那条路径。
這個設計有兩個後果,都跟「看得見、控制得到」有關。
这个设计有两个后果,都跟「看得见、控制得到」有关。
第一,壓縮不會丟資料。 脈絡快滿時(預設是剩下的空間不到 16384 個 token),Pi 請模型把較舊的訊息寫成摘要,存成一個壓縮項目,之後的請求只送摘要和最近大約兩萬個 token 的訊息。原本的訊息仍然留在樹上,摘要寫得不好,還查得到原文。
第一,压缩不会丢资料。脉络快满时(预设是剩下的空间不到 16384 个 token),Pi 请模型把较旧的讯息写成摘要,存成一个压缩项目,之后的请求只送摘要和最近大约两万个 token 的讯息。原本的讯息仍然留在树上,摘要写得不好,还查得到原文。
第二,擴充的狀態可以跟著分支走。 Pi 的工具結果分成兩部分:content 給模型看,details 給介面和程式用。前面提到的 todo 擴充,就把清單存在工具結果的 details 裡;使用者切換分支時,擴充沿著目前的分支重新讀一次 details,清單就變回那個時間點的狀態。
第二,扩充的状态可以跟着分支走。Pi 的工具结果分成两部分:content 给模型看,details 给介面和程序用。前面提到的 todo 扩充,就把清单存在工具结果的 details 里;使用者切换分支时,扩充沿着目前的分支重新读一次 details,清单就变回那个时间点的状态。
系統提示裡寫了什麼? · 系统提示里写了什么?
Pi 的預設系統提示寫在 system-prompt.ts,再加上每個工具自己提供的說明。我照原始碼把它們組起來,在一個沒有 AGENTS.md、也沒有安裝技能的專案裡,模型收到的系統提示是這樣(文件路徑和工作目錄換成示意):
Pi 的预设系统提示写在 system-prompt.ts,再加上每个工具自己提供的说明。我照原始码把它们组起来,在一个没有 AGENTS.md、也没有安装技能的专案里,模型收到的系统提示是这样(文件路径和工作目录换成示意):
You are an expert coding assistant operating inside pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.
<tools>
- read: Read file contents
- bash: Execute bash commands (ls, grep, find, etc.)
- edit: Make precise file edits with exact text replacement, including multiple disjoint edits in one call
- write: Create or overwrite files
In addition to the tools above, you may have access to other custom tools depending on the project.
</tools>
<rules>
- Use bash for file operations like ls, rg, find
- Use read to examine files instead of cat or sed.
- You can inspect PI_* environment variables for current model and session details.
- Use edit for precise changes (edits[].oldText must match exactly)
- When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls
- Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits. Merge nearby changes into one edit.
- Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.
- Use write only for new files or complete rewrites.
- Be concise in your responses
- Show file paths clearly when working with files
</rules>
<docs>
Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI):
- Main documentation: /path/to/README.md
- Additional docs: /path/to/docs
- Examples: /path/to/examples (extensions, custom tools, SDK)
- When reading pi docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory
- When asked about: extensions (docs/extensions.md), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), pi packages (docs/packages.md), environment variables (docs/environment-variables.md), MCP servers (docs/mcp.md), codemode scripts and non-LLM models such as classifiers and image models (docs/codemode.md)
- When working on pi topics, read the docs and examples, and follow .md cross-references before implementing
- Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)
</docs>
<cwd>
/path/to/project
</cwd>整份提示大約兩千五百個字元,我用字元數粗估,約是六百個 token;工具定義另外計算,四個工具的說明和參數格式大約五、六百個 token,加起來一千個 token 出頭,跟 Mario 當初說的差不多。它由幾個有名字的段落組成,順序是固定的:
整份提示大约两千五百个字元,我用字元数粗估,约是六百个 token;工具定义另外计算,四个工具的说明和参数格式大约五、六百个 token,加起来一千个 token 出头,跟 Mario 当初说的差不多。它由几个有名字的段落组成,顺序是固定的:
preamble:一句話的身分說明,告訴模型它是在 pi 裡工作的程式設計助理。
preamble:一句话的身分说明,告诉模型它是在 pi 里工作的程序设计助理。
tools:每個工具一行簡介,最後提醒模型,專案裡可能還有其他自訂工具。
tools:每个工具一行简介,最后提醒模型,专案里可能还有其他自订工具。
rules:使用規則。大部分的規則是工具自己帶來的,例如 edit 的四條規則都在講怎麼寫 edits[];最後兩條「回答要簡潔」「清楚標出檔案路徑」才是全域規則。
rules:使用规则。大部分的规则是工具自己带来的,例如 edit 的四条规则都在讲怎么写 edits[];最后两条「回答要简洁」「清楚标出档案路径」才是全域规则。
docs:Pi 自己的文件放在哪裡,只有使用者問到 Pi 本身時才去讀。
docs:Pi 自己的文件放在哪里,只有使用者问到 Pi 本身时才去读。
addendum、project_context、skills:有需要才出現,分別來自 APPEND_SYSTEM.md、工作目錄和上層目錄的 AGENTS.md(或 CLAUDE.md),以及已安裝技能的名稱、描述和檔案位置。
addendum、project_context、skills:有需要才出现,分别来自 APPEND_SYSTEM.md、工作目录和上层目录的 AGENTS.md(或 CLAUDE.md),以及已安装技能的名称、描述和档案位置。
cwd:目前的工作目錄。
cwd:目前的工作目录。
除了 preamble,每個段落都包在同名的標籤裡,例如 <rules>。我讀完這份提示,看到三個設計。
除了 preamble,每个段落都包在同名的标签里,例如 <rules>。我读完这份提示,看到三个设计。
第一,工具帶著自己的提示。 每個工具定義都可以附上 promptSnippet 和 promptGuidelines,前者是 tools 段落裡的那一行簡介,後者是 rules 段落裡的規則,擴充註冊的工具也一樣。關掉一個工具,它的簡介和規則就一起消失,不會留下一條規則叫模型使用不存在的工具。像是打開 grep、find、ls 之後,「用 bash 做 ls、rg、find」這條規則就不會出現。
第一,工具带着自己的提示。每个工具定义都可以附上 promptSnippet 和 promptGuidelines,前者是 tools 段落里的那一行简介,后者是 rules 段落里的规则,扩充注册的工具也一样。关掉一个工具,它的简介和规则就一起消失,不会留下一条规则叫模型使用不存在的工具。像是打开 grep、find、ls 之后,「用 bash 做 ls、rg、find」这条规则就不会出现。
第二,提示裡沒有每次都會變的內容。 預設的系統提示沒有日期、時間,也沒有 git 狀態。同一個專案、同一組工具,每次開工作階段,模型收到的系統提示都一模一樣。我推測這對提示快取(prompt caching)有利,因為快取要求請求的開頭完全相同。
第二,提示里没有每次都会变的内容。预设的系统提示没有日期、时间,也没有 git 状态。同一个专案、同一组工具,每次开工作阶段,模型收到的系统提示都一模一样。我推测这对提示快取(prompt caching)有利,因为快取要求请求的开头完全相同。
第三,提示的變化也記在對話裡。 第一次請求時,所有段落和工具宣告會存成一則系統訊息;之後段落有變化,或增加、移除了工具,Pi 會保留開頭那份提示,比對出差異,再在對話後面存一則只記差異的系統訊息。照順序重播這些訊息,就得到目前的系統提示和工具。對支援在對話中途放系統訊息的 API,模型看到的是 Updated system prompt section "rules": … 這樣的一則更新。所以模型在每個時間點看到什麼,工作階段檔案裡都有紀錄。
第三,提示的变化也记在对话里。第一次请求时,所有段落和工具宣告会存成一则系统讯息;之后段落有变化,或增加、移除了工具,Pi 会保留开头那份提示,比对出差异,再在对话后面存一则只记差异的系统讯息。照顺序重播这些讯息,就得到目前的系统提示和工具。对支持在对话中途放系统讯息的 API,模型看到的是 Updated system prompt section "rules": … 这样的一则更新。所以模型在每个时间点看到什么,工作阶段档案里都有纪录。
docs 段落說明 Pi 怎麼讓模型替你寫擴充。這一段告訴模型,使用者問到擴充、技能、SDK 時,先讀 Pi 的文件和範例再動手。Pi 的 README 說「想要什麼功能,就叫 Pi 自己做」,靠的就是這一段,因為模型知道去哪裡找擴充的寫法。
docs 段落说明 Pi 怎么让模型替你写扩充。这一段告诉模型,使用者问到扩充、技能、SDK 时,先读 Pi 的文件和范例再动手。Pi 的 README 说「想要什么功能,就叫 Pi 自己做」,靠的就是这一段,因为模型知道去哪里找扩充的写法。
擴充能做到哪些事? · 扩充能做到哪些事?
Pi 的擴充(extension)是一個 TypeScript 模組,匯出一個接收 ExtensionAPI 的函式。Pi 用 jiti 載入擴充,所以不用另外編譯。最小的工具擴充長這樣:
Pi 的扩充(extension)是一个 TypeScript 模组,汇出一个接收 ExtensionAPI 的函式。Pi 用 jiti 载入扩充,所以不用另外编译。最小的工具扩充长这样:
import { Type } from "@earendil-works/pi-ai";
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
const helloTool = defineTool({
name: "hello",
label: "Hello",
description: "A simple greeting tool",
parameters: Type.Object({
name: Type.String({ description: "Name to greet" }),
}),
async execute(_toolCallId, params) {
return {
content: [{ type: "text", text: `Hello, ${params.name}!` }],
details: { greeted: params.name },
};
},
});
export default function (pi: ExtensionAPI) {
pi.registerTool(helloTool);
}擴充能做的事分成兩類。一類是註冊:工具、斜線指令、快捷鍵、CLI 參數、模型提供者、MCP 伺服器、訊息的渲染方式。另一類是訂閱事件:用 pi.on() 介入整個生命週期,從使用者輸入、送出請求之前、每一次工具呼叫,到壓縮和切換分支,總共有三十多種事件。像是權限確認,Pi 的核心沒有這個功能,官方範例只用三十幾行就寫出來:攔截 tool_call 事件,遇到 rm -rf 或 sudo 就跳出確認,沒有介面可以確認時直接擋下。
扩充能做的事分成两类。一类是注册:工具、斜线指令、快捷键、CLI 参数、模型提供者、MCP 服务器、讯息的渲染方式。另一类是订阅事件:用 pi.on() 介入整个生命周期,从使用者输入、送出请求之前、每一次工具呼叫,到压缩和切换分支,总共有三十多种事件。像是权限确认,Pi 的核心没有这个功能,官方范例只用三十几行就写出来:拦截 tool_call 事件,遇到 rm -rf 或 sudo 就跳出确认,没有介面可以确认时直接挡下。
pi.on("tool_call", async (event, ctx) => {
if (event.toolName !== "bash") return undefined;
const command = event.input.command as string;
if (!dangerousPatterns.some((p) => p.test(command))) return undefined;
if (!ctx.hasUI) return { block: true, reason: "Dangerous command blocked (no UI for confirmation)" };
const choice = await ctx.ui.select(`Dangerous command: ${command} — Allow?`, ["Yes", "No"]);
return choice === "Yes" ? undefined : { block: true, reason: "Blocked by user" };
});擴充、技能(skill)、提示範本和佈景主題,可以一起打包成 Pi 套件,透過 npm 或 git 分享。官方範例目錄裡有七十多個擴充,從規劃模式、子智慧體、沙盒,到貪食蛇和 Doom 都有(後兩個大概不算政策)。核心沒有內建的政策,多半已經有人寫成擴充。
扩充、技能(skill)、提示范本和布景主题,可以一起打包成 Pi 套件,透过 npm 或 git 分享。官方范例目录里有七十多个扩充,从规划模式、子智慧体、沙盒,到贪食蛇和 Doom 都有(后两个大概不算政策)。核心没有内建的政策,多半已经有人写成扩充。
同一個核心能用在哪些地方? · 同一个核心能用在哪些地方?
Pi 有四種使用模式:互動模式是終端機介面;print 模式跑完一個提示就輸出結果;JSON 模式把事件寫成 JSONL;RPC 模式從 stdin 收指令、從 stdout 回傳事件。另外還有 TypeScript SDK,可以在自己的程式裡直接建立工作階段。這幾種方式用的是同一套智慧體和工作階段機制,差別只在誰訂閱事件、誰送出指令。
Pi 有四种使用模式:互动模式是终端机介面;print 模式跑完一个提示就输出结果;JSON 模式把事件写成 JSONL;RPC 模式从 stdin 收指令、从 stdout 回传事件。另外还有 TypeScript SDK,可以在自己的程序里直接建立工作阶段。这几种方式用的是同一套智慧体和工作阶段机制,差别只在谁订阅事件、谁送出指令。
import { createAgentSession } from "@earendil-works/pi-coding-agent";
const { session } = await createAgentSession();
try {
await session.prompt("What files are in the current directory?");
console.log(session.getLastAssistantText());
} finally {
session.dispose();
}所以要把 Pi 放進別的系統,不必繞過 CLI:Node 和 Bun 的程式直接用 SDK,其他語言用 RPC 模式開一個子程序。Pi 的 README 舉 OpenClaw 當作實際整合的例子。
所以要把 Pi 放进别的系统,不必绕过 CLI:Node 和 Bun 的程序直接用 SDK,其他语言用 RPC 模式开一个子程序。Pi 的 README 举 OpenClaw 当作实际整合的例子。
程式碼怎麼組織? · 程序代码怎么组织?
前面講的四層,在儲存庫裡就是 packages/ 底下的四個資料夾。想讀程式碼時,最常需要打開的檔案如下:
前面讲的四层,在储存库里就是 packages/ 底下的四个资料夹。想读程序代码时,最常需要打开的档案如下:
packages/
├── ai/src/
│ ├── types.ts # 讯息、事件、模型的型别
│ ├── models.ts # createModels():注册提供者,把请求交给负责的提供者
│ ├── api/ # API 实作(线路协定),例如 anthropic-messages.ts
│ └── providers/ # 各提供者和模型清单,faux.ts 是测试用的假提供者
├── agent/src/
│ ├── agent-loop.ts # runLoop:智慧体循环和工具执行
│ ├── agent.ts # Agent 类别:状态、事件订阅、讯息伫列
│ └── types.ts
├── coding-agent/src/
│ ├── cli.ts, main.ts # 入口:解析参数、选择使用模式
│ ├── core/
│ │ ├── sdk.ts # createAgentSession():把各层组合起来
│ │ ├── agent-session.ts # AgentSession:一段对话的所有状态
│ │ ├── session-manager.ts # JSONL 工作阶段树
│ │ ├── system-prompt.ts # 系统提示的段落
│ │ ├── resource-loader.ts # 找出 AGENTS.md、技能、提示范本、布景主题
│ │ ├── compaction/ # 压缩和分支摘要
│ │ ├── extensions/ # 扩充的载入、执行和型别
│ │ └── tools/ # read、bash、edit、write 等内建工具
│ └── modes/
│ ├── interactive/ # 终端机介面
│ ├── print-mode.ts
│ └── rpc/
└── tui/src/ # 差异渲染、编辑器和各种元件執行 pi 時,程式照這個順序走。cli.ts 呼叫 main.ts,main.ts 解析參數、確認專案是否可信,然後建立工作階段的執行環境。執行環境裡,sdk.ts 的 createAgentSession() 開啟或新建 SessionManager,載入技能、脈絡檔和擴充,用 pi-agent-core 的 Agent 建立智慧體,再把這些包成一個 AgentSession。最後 main.ts 依照參數,把執行環境交給互動模式、print 模式或 RPC 模式。三種模式拿到的是同一個執行環境,前一節說「差別只在誰訂閱事件、誰送出指令」,在程式碼裡就是這個意思。
执行 pi 时,程序照这个顺序走。cli.ts 呼叫 main.ts,main.ts 解析参数、确认专案是否可信,然后建立工作阶段的执行环境。执行环境里,sdk.ts 的 createAgentSession() 开启或新建 SessionManager,载入技能、脉络档和扩充,用 pi-agent-core 的 Agent 建立智慧体,再把这些包成一个 AgentSession。最后 main.ts 依照参数,把执行环境交给互动模式、print 模式或 RPC 模式。三种模式拿到的是同一个执行环境,前一节说「差别只在谁订阅事件、谁送出指令」,在程序代码里就是这个意思。
檔案的行數也看得出複雜度落在哪裡。agent-loop.ts 是 949 行,其中 runLoop 不到兩百行;agent-session.ts 有 4428 行,終端機介面的 interactive-mode.ts 更有 7127 行。所以 Pi 的智慧體邏輯很小,大部分的程式碼花在組合各層,以及終端機介面上。
档案的行数也看得出复杂度落在哪里。agent-loop.ts 是 949 行,其中 runLoop 不到两百行;agent-session.ts 有 4428 行,终端机介面的 interactive-mode.ts 更有 7127 行。所以 Pi 的智慧体逻辑很小,大部分的程序代码花在组合各层,以及终端机介面上。
要開發的話,從哪裡下手? · 要开发的话,从哪里下手?
照前面的分層,要做的事決定該改哪一層:
照前面的分层,要做的事决定该改哪一层:
想改 Pi 的行為,例如加工具、加指令、擋危險指令、調整脈絡:寫擴充。從 docs/extensions.md 和 examples/extensions/ 開始,先讀 hello.ts、permission-gate.ts、todo.ts,三個檔案分別示範工具、事件攔截,以及存在工作階段裡的狀態。
想改 Pi 的行为,例如加工具、加指令、挡危险指令、调整脉络:写扩充。从 docs/extensions.md 和 examples/extensions/ 开始,先读 hello.ts、permission-gate.ts、todo.ts,三个档案分别示范工具、事件拦截,以及存在工作阶段里的状态。
想給模型多一點知識或固定流程,不需要執行程式:寫技能或提示範本就夠了。
想给模型多一点知识或固定流程,不需要执行程序:写技能或提示范本就够了。
想接新的模型或代理伺服器:在擴充裡呼叫 pi.registerProvider(),或在 pi-ai 裡用 createProvider()。
想接新的模型或代理服务器:在扩充里呼叫 pi.registerProvider(),或在 pi-ai 里用 createProvider()。
想把 Pi 放進自己的產品:Node 用 SDK 的 createAgentSession(),其他語言用 pi --mode rpc。
想把 Pi 放进自己的产品:Node 用 SDK 的 createAgentSession(),其他语言用 pi --mode rpc。
想做自己的智慧體,不需要程式設計智慧體那一套:直接用 pi-agent-core 的 Agent,自己定義工具和系統提示。
想做自己的智慧体,不需要程序设计智慧体那一套:直接用 pi-agent-core 的 Agent,自己定义工具和系统提示。
想修改 Pi 本身:先讀儲存庫的 AGENTS.md。新貢獻者開的問題回報(issue)和 PR 預設會自動關閉,維護者每天再看一次被關掉的內容。
想修改 Pi 本身:先读储存库的 AGENTS.md。新贡献者开的问题回报(issue)和 PR 预设会自动关闭,维护者每天再看一次被关掉的内容。
讀程式碼的順序,我建議由下往上。先讀 packages/agent/src/agent-loop.ts 的 runLoop,整個智慧體怎麼運作都寫在這個函式裡;再讀 packages/coding-agent/docs/how-pi-works.md,它用一頁說明迴圈、脈絡、工作階段和擴充之間的關係;接著讀 core/sdk.ts,看各層怎麼組合;最後才讀 agent-session.ts,遇到問題再查,不必從頭讀到尾。
读程序代码的顺序,我建议由下往上。先读 packages/agent/src/agent-loop.ts 的 runLoop,整个智慧体怎么运作都写在这个函式里;再读 packages/coding-agent/docs/how-pi-works.md,它用一页说明循环、脉络、工作阶段和扩充之间的关系;接着读 core/sdk.ts,看各层怎么组合;最后才读 agent-session.ts,遇到问题再查,不必从头读到尾。
代價是什麼? · 代价是什么?
Pi 的設計當然有代價,我認為至少有三項。
Pi 的设计当然有代价,我认为至少有三项。
首先,安全是使用者自己的事。 Pi 預設用啟動它的使用者權限執行,沒有任何限制;擴充也跑在同一個程序裡,讀得到提示、檔案和憑證。Mario 的立場很誠實,權限提示確實防不住真正的攻擊,但這也表示,沒有把 Pi 放進容器或沙盒的人,承擔的風險比用 Claude Code 更直接。
首先,安全是使用者自己的事。Pi 预设用启动它的使用者权限执行,没有任何限制;扩充也跑在同一个程序里,读得到提示、档案和凭证。Mario 的立场很诚实,权限提示确实防不住真正的攻击,但这也表示,没有把 Pi 放进容器或沙盒的人,承担的风险比用 Claude Code 更直接。
其次,極簡的提示依賴模型本身的訓練。 Mario 的根據是,前沿模型已經做過大量的強化學習,本來就知道怎麼當程式設計智慧體,他也拿 Terminal-Bench 2.0 的成績當佐證。這個前提對前沿模型成立,換成較小的本機模型就不一定成立。
其次,极简的提示依赖模型本身的训练。Mario 的根据是,前沿模型已经做过大量的强化学习,本来就知道怎么当程序设计智慧体,他也拿 Terminal-Bench 2.0 的成绩当佐证。这个前提对前沿模型成立,换成较小的本机模型就不一定成立。
最後,政策要自己決定。 對熟悉自己工作方式的人來說,這是自由;對剛開始用智慧體的人來說,Claude Code 替你做好的選擇反而比較省事。Pi 的口號是「讓 Pi 配合你的工作流程,而不是反過來」,前提是你已經有一套自己的工作流程。
最后,政策要自己决定。对熟悉自己工作方式的人来说,这是自由;对刚开始用智慧体的人来说,Claude Code 替你做好的选择反而比较省事。Pi 的口号是「让 Pi 配合你的工作流程,而不是反过来」,前提是你已经有一套自己的工作流程。
所以 Pi 的特色是核心很小,它的架構好,是因為核心雖小,機制卻很完整:統一的模型層、每一步都能介入的迴圈、記下一切的工作階段樹,加上能介入幾乎每個環節的擴充 API。有這些機制,政策才能留在核心外面,交給每個使用者決定。這篇只整理到架構和開發的入口,pi-durable、chord 這些還在變動的套件,就留給之後再看。
所以 Pi 的特色是核心很小,它的架构好,是因为核心虽小,机制却很完整:统一的模型层、每一步都能介入的循环、记下一切的工作阶段树,加上能介入几乎每个环节的扩充 API。有这些机制,政策才能留在核心外面,交给每个使用者决定。这篇只整理到架构和开发的入口,pi-durable、chord 这些还在变动的套件,就留给之后再看。