第 3 讲
安装与使用初体验
从 /login 到多模式运行:先把 Pi 当工具用起来,拿到「现象层」的第一手体感,再进源码。
图 3-0 · Pi 的首次启动链路:安装(三种方式任选其一)→ 进入项目目录启动 pi → /login 配置凭据 → /model 选模型 → /thinking 调思考等级;随后四种接口共享同一套会话与模型配置。
学习目标
- 掌握 Pi 的三种安装方式(官方脚本 / npm / Nix)及各自的更新机制与适用场景;
- 走完首次配置闭环:
/login登录厂商 →/model选模型 →/thinking调思考等级,并知道凭据落在~/.pi/agent/auth.json; - 理解交互模式的完整工作流:提交 prompt、观察工具调用、在正确时机使用 steering(中途插话)与 follow-up(追加任务),以及 abort 取消;
- 记住七个会话命令:
/tree/fork/clone/compact/export/resume/session; - 对比四种运行接口(interactive / print / JSON / RPC)的输入输出形态与适用边界;
- 建立
~/.pi/agent/用户级配置目录与项目级.pi/的心智地图。
§3.1安装:三种方式对比
Pi 是一个跑在终端里的编码代理(coding agent)。动手读源码之前,先用官方推荐的方式把它装好——能跑起来的系统才值得读。三种安装方式的核心差异不在命令本身,而在依赖是否被锁定与由谁负责更新。
| 方式 | 命令 | 依赖处理 | 更新机制 | 适合谁 |
|---|---|---|---|---|
| 官方安装脚本 macOS / Linux / Windows |
curl -fsSL https://pi.dev/install.sh | shpowershell -c "irm https://pi.dev/install.ps1 | iex" |
锁定全部依赖 | pi update 自更新;重跑脚本可卸载 |
默认推荐:日常使用者 |
| npm 全局安装 | npm install -g --ignore-scripts @earendil-works/pi-coding-agent |
不锁定传递依赖;要求 Node.js ≥ 22.19 | 由 npm 管理;正常安装不需要依赖生命周期脚本 | 已有 Node 工具链、想统一管理全局包的开发者 |
| Nix macOS / Linux |
nix profile add github:earendil-works/pi/stable |
从源码构建,可按 tag 锁定版本 | nix profile upgrade pi;pi update 不可用 |
Nix 用户;需要可复现安装 |
# macOS / Linux 官方安装脚本
curl -fsSL https://pi.dev/install.sh | sh
# Windows 官方安装脚本
powershell -c "irm https://pi.dev/install.ps1 | iex"
# npm 方式(不锁定传递依赖,要求 Node.js 22.19 或更新)
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
# Nix 方式(源码构建;旧版本 Nix 用 nix profile install)
nix profile add github:earendil-works/pi/stable
# 验证安装
pi --version
三个容易忽略的细节:第一,官方脚本同时承担安装与更新——重跑 install.sh 会进入菜单,可选择卸载;第二,npm 安装特意加了 --ignore-scripts,因为 Pi 正常安装不需要依赖包的生命周期脚本,关掉它反而更安全;第三,三种方式互不清理——卸载任何一种都不会删除 ~/.pi/agent/ 下的配置、凭据、会话与已安装的 Pi 包。
§3.2首次配置:/login → /model → /thinking
安装完成后的第一次 pi 启动还什么都不能做——Pi 只是个「挽具」(harness),它需要一个可访问的大模型。先分清两个词:模型(model)生成 Pi 的回复;厂商(provider)是你访问该模型所用的服务或账号。
# 1. 进入 Pi 要工作的目录(目录决定文件发现、指令加载与会话分组)
cd /path/to/folder
pi
# 2. 在 Pi 交互界面内依次执行:
/login # 选择厂商,按提示完成订阅账号登录或存储 API Key
/model # (可选)切换到其他可用模型
/thinking # 调整当前模型的思考(reasoning)强度
/login 支持两类凭据:走 OAuth 订阅账号(打开浏览器授权),或直接存 API Key。无论哪种,凭据都保存在 ~/.pi/agent/auth.json——这也是后续安全话题里最敏感的一个文件。不想进会话也可以在终端里完成:pi auth login <provider>(--api-key 存密钥,--no-browser 走设备码等非浏览器方式,适合 SSH 环境)。
/thinking 的等级从低到高是 off、minimal、low、medium、high、xhigh、max,但会被钳制(clamp)在模型能力范围内——给不支持的模型设 max 不会报错,只会取它支持的上限。交互模式里按 Shift+Tab 循环切换;编辑器边框即当前思考等级的指示器。命令行同样能预设:pi --model sonnet:high 里的 :high 后缀,或 --thinking <level>(后者会覆盖模型后缀)。
/model:打开模型选择器;快捷键Ctrl+L打开同一选择器。/thinking:选择当前模型使用多少推理能力;Shift+Tab循环切换。/login与/logout:管理厂商凭据的存取。
配置完成后给 Pi 一个真实任务即可。Quickstart 文档给的三个示例任务值得照抄一遍——它们分别覆盖「读文件 + 写文件」「仓库结构分析」「多文件对比」三种典型工作流:
Summarize @meeting-notes.md and save the action items to action-items.md.
Explain how this repository is structured and how to run its checks.
Compare @previous.csv with @current.csv and summarize the important changes.
§3.3交互工作流:prompt、steering、follow-up、abort
交互模式(interactive TUI)的界面由三块组成:transcript(对话区,显示你的 prompt、Pi 的回复、每次工具调用、结果与错误)、editor(输入区)与 footer(当前目录、会话、模型、上下文用量与累计费用)。工作流的心脏是一个问题:模型正在干活时,你说的话什么时候生效?
输入侧:prompt 怎么写
Enter发送;Shift+Enter换行;Ctrl+G在配置的外部编辑器里写长 prompt;- 输入
@搜索并引用文件(Tab补全路径);粘贴或拖入图片; !git status:执行终端命令并把输出计入对话;!!cmd:执行但不把输出发给模型。
观察侧:Pi 怎么干活
Pi 会可视化每一次工具调用与结果——读了哪个文件、跑了什么命令、改了哪段代码,全部实时显示。Ctrl+O 展开/收起工具输出,Ctrl+T 显示/隐藏思考块。注意官方文档的明确声明:Pi 不会在每次工具调用前征求确认,所以重要工作要靠版本控制或备份兜底,不可信任务要进沙箱。
控制侧:三种「插话」的进入时机
这是本讲最值得画图的地方。你输入的消息有三种去向,官方 usage.md 的表述分别是:以 Enter 发送的消息「等待当前响应及其工具调用结束后,指导下一次响应」(steering);以 Alt+Enter 发送的 follow-up「等待 Pi 完成当前任务」;Escape 则停止当前任务,且排队中的消息会退回编辑器。
agent_end 只关一个底层运行,agent_settled 才表示不会再自动继续。| 意图 | 操作 |
|---|---|
| 调整当前任务 | 输入消息并按 Enter(steering) |
| 在当前任务之后追加工作 | 输入消息并按 Alt+Enter(follow-up) |
| 把排队消息取回编辑器 | Alt+Up(Windows Terminal 有保留键位,见 terminal-setup.md) |
| 停止当前任务 | Escape(abort;排队消息自动返回编辑器) |
§3.4会话命令:树、分叉、克隆与压缩
Pi 会话不是一条直线,而是一棵条目树(entry tree):回到历史某点提问,不会抹掉你离开的那个分支。理解这一点后,七个命令的分工就清晰了。
| 命令 | 作用 | 何时用 |
|---|---|---|
/tree | 在当前会话文件内的树上移动;选中 user 消息会把文本放回编辑器,改写后提交即产生新分支;离开分支时可生成摘要挂在进入的分支上 | 相关备选方案希望留在同一会话里 |
/fork | 从更早的某条 user 消息创建新会话,原会话不动 | 备选方案要成为独立工作 |
/clone | 把当前活动分支复制成一个新会话 | 想要当前状态的独立副本 |
/compact | 手动压缩:生成摘要并保留近期消息,不删除原始条目;可附指示要求保留特定主题 | 上下文接近模型上限前主动瘦身 |
/export | 把当前会话导出为 HTML 或 JSONL | 归档、评审、离线分享 |
/resume | 打开会话选择器(可搜索、重命名、删除);会回到会话自己的工作目录执行 | 换回任意历史会话 |
/session | 显示当前会话文件、ID、消息数、token 用量与费用 | 排查「我到底在哪个会话里」 |
配套的 CLI 入口:pi --continue 恢复当前目录最近一次会话;pi --resume 打开选择器;pi --fork <path|id> 直接分叉。会话默认持久化在 ~/.pi/agent/sessions/,按工作目录分组——每个会话记录创建时的工作目录且永不更改,恢复时 Pi 会在那个目录里运行,工具与 @path 引用都从那里解析。--no-session 则得到一个退出即消失、无法恢复的内存会话。
§3.5四种接口:interactive / print / JSON / RPC
同一套会话与模型配置,有四种消费方式。选型只看两个维度:谁来读输出(人还是程序),以及谁控制生命周期(一次跑完还是长期驻留)。源码层面它们就在 src/modes/ 下各占一个模块。
| 接口 | 启动 | 输入 | 输出 | 生命周期 | 典型场景 |
|---|---|---|---|---|---|
| interactive | pi(stdin/stdout 均为终端时) |
TUI 编辑器、斜杠命令、@ 文件 |
transcript + 工具调用可视化 + footer | 长期会话,人工驱动 | 日常编码、调试、探索 |
pi --print "...";任一流被重定向且未选 JSON/RPC 时自动进入 |
参数 prompt、管道 stdin(前置到首条 prompt) | 最终 assistant 文本 → stdout | 跑完即退 | 脚本、CI、一次性问答 | |
| JSON | pi --mode json "..." > events.jsonl |
同 print | 严格 JSONL:先 session header,后事件流 | 跑完即退 | 程序消费完整事件、观测管道 |
| RPC | pi --mode rpc |
stdin 逐行 JSON 命令(拒绝 @file 参数) |
stdout:命令 response + 会话事件(无 session header) | 长期驻留子进程,直到 shutdown | IDE、自定义 UI、跨语言集成 |
print 与 JSON:同一 prompt 的两种输出
pi
pi --print "Summarize this repository"
git diff | pi --print "Review this change"
pi --mode json "Inspect this repository" > events.jsonl
print 模式适合「只要答案」:pi --print "Summarize this repository" 只把最终回复写到 stdout;管道场景 git diff | pi --print "Review this change" 会把 stdin 内容前置到 prompt。要看过程就换 JSON 模式——每一行是一个以 LF 结尾的 JSON 对象,首行是 session header,随后是 agent/turn/message/tool 事件:
{"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path"}
{"type":"agent_start"}
{"type":"turn_start"}
{"type":"message_start","message":{"role":"user","content":"Review this repository","timestamp":1733234401000}}
{"type":"message_end","message":{"role":"user","content":"Review this repository","timestamp":1733234401000}}
{"type":"message_start","message":{"role":"assistant","content":[],"stopReason":"pending","...":"..."}}
{"type":"message_update","usage":{"...":"..."},"assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":"Hello"}}
{"type":"message_end","message":{"role":"assistant","...":"..."}}
{"type":"turn_end","message":{"role":"assistant","...":"..."},"toolResults":[]}
{"type":"agent_end","messages":[{"...":"..."}],"willRetry":false}
{"type":"agent_settled","aborted":false}
两个关键语义:agent_end 只表示一个底层运行结束——自动重试、溢出恢复、压缩重试、steering、follow-up 仍可能继续;agent_settled 才表示 Pi 对这次会话级运行没有剩余的自动工作。文档还给出一个即用的过滤示例:
# 打印一次性运行里已完成的消息
pi --mode json "List files" 2>/dev/null | jq -c 'select(.type == "message_end")'
RPC:把 Pi 变成受控子进程
RPC 模式通过 stdin/stdout 的 JSONL 记录实现双向控制:stdin 写命令,stdout 收response 与会话事件。命令可带字符串 id,响应原样回带用于关联——因为命令处理是异步的,不能按响应顺序对号入座。
# 启动
pi --mode rpc --no-session
# stdin:查询状态 → stdout:带相同 id 的 response
{"id":"req-1","type":"get_state"}
{"id":"req-1","type":"response","command":"get_state","success":true,"data":{"...":"..."}}
# stdin:发送 prompt;success=true 只表示「已接受」,不代表模型跑完
{"id":"req-2","type":"prompt","message":"Review this repository"}
{"id":"req-2","type":"response","command":"prompt","success":true,"data":{"disposition":"started"}}
# 之后继续消费事件流,直到 agent_settled
注意 prompt 的响应语义:success:true 只说明 prompt 被接受、排队或处理(disposition:"started" 表示已启动运行),模型何时跑完要看后续事件;若 disposition:"handled" 则该 prompt 没有启动运行,不必等 agent_settled。帧解析也有一条官方警告:不要用 Node.js 的 readline——它会把 JSON 字符串内合法的 U+2028/U+2029 也当行边界,必须按字节/LF 切分,并持续读取 stdout 以免管道缓冲填满卡住进程。
源码速览:modes/ 目录
四种接口在仓库里各有落点。现在只需记住地图,第 8 讲与第 13 讲会深入:
modes/
├── index.ts # 模式分发入口
├── interactive/ # 交互 TUI(interactive-mode.ts、tui-renderer.ts)
│ ├── components/ # footer / model-selector / thinking-selector /
│ │ # tree-selector / login-dialog / tool-execution …
│ └── theme/ # dark.json / light.json / theme-controller
├── print-mode.ts # print 模式
├── json-event.ts # JSON 事件流的线格式转换
└── rpc/ # rpc-mode.ts / rpc-client.ts / rpc-types.ts / jsonl.ts
§3.6配置地图:~/.pi/agent/ 与项目级 .pi/
用户级配置全部住在agent 目录(默认 ~/.pi/agent,可用环境变量 PI_CODING_AGENT_DIR 重定位);项目级配置住在工作目录的 .pi/ 下,且在项目获得信任后才加载。第 3 讲先建立地图,每个文件在后续讲次逐一展开。
图 3-2 · agent 目录结构(依据 configuration.md 的 Agent directory 与 Project .pi directory 两表)。注意 sessionDir 是唯一在信任判定前读取的例外。
§3.7动手实践
- 完整工具调用链观察:完成安装与
/login后,找一个有真实 bug 的小仓库,让 Pi 修复它。全程不要打断,观察 transcript 里每一次read/bash/edit的参数与结果,用Ctrl+O展开输出,记录「一次任务里 Pi 自己发起了几轮工具调用」。 - print 对 JSON:用同一个 prompt 分别执行
pi --print "Explain how this repository is structured"与pi --mode json "Explain how this repository is structured" > events.jsonl,对比 stdout 差异;在 JSONL 里数一数turn_start与tool_execution_start的次数。 - 分支体验:在会话中问两个问题后执行
/tree,选中第一条 user 消息、改写提问并提交,观察新分支的诞生;再用/fork从同一节点分叉,最后/resume对比两个会话文件。
§3.8常见误区
误区一:npm 安装「失败」是因为禁用了 scripts
--ignore-scripts 不是妥协,而是官方推荐写法——Pi 的正常 npm 安装不需要依赖包的生命周期脚本。真正要注意的是它不锁定传递依赖,且要求 Node ≥ 22.19;追求确定性请用官方安装脚本或 Nix。
误区二:以为 steering 会立刻打断模型
steering 不是中断,是排队:消息等到当前响应及其工具调用都结束后才进入,用于指导下一次响应。想立刻停手是 Escape(abort)的职责;想在整个任务后追加是 Alt+Enter(follow-up)的职责。
误区三:以为 /fork 会改动原会话,或 /compact 会删历史
/fork 与 /clone 都是新建会话,原会话文件不动;/tree 才是同一文件内的分支移动。/compact 生成摘要并保留近期消息,但从不删除原始条目——它改变的是发给模型的视图,不是持久化日志。
误区四:用 Node readline 解析 JSON/RPC 流
JSONL 帧只按 LF 切分,而 readline 会把 JSON 字符串里合法的 U+2028/U+2029 也当边界。正确做法是按字节读、按 LF 切、可选去掉前置 CR;同时要持续读取 stdout,否则管道缓冲填满会卡住 Pi。
§3.9自测问答
1. steering 与 follow-up 的进入时机有何差异?
steering(Enter 发送)等到当前响应及其工具调用结束后进入,用于指导下一次响应——边界是一个 turn。follow-up(Alt+Enter 发送)等到 Pi 完成整个当前任务后进入——边界是整个运行(可能含多个 turn、自动重试与压缩恢复)。
2. JSON 模式输出的每一行是什么?首行和结束判断有什么讲究?
每行是一个以 LF 结尾的 JSON 对象(严格 JSONL 帧)。首行是 session header({"type":"session","version":3,...}),随后是 agent/turn/message/tool 等事件。结束判断看 agent_settled 而非 agent_end:前者表示没有剩余自动工作(含重试、压缩恢复、排队消息),后者只结束一个底层运行。
3. 会话默认存在哪里?按什么分组?恢复会话时在哪个目录运行?
默认在 ~/.pi/agent/sessions/,按工作目录分组。每个会话记录创建时的工作目录且永不更改;pi --continue 恢复当前目录的最近会话,而 pi --resume / --session / /import 会在会话自己的工作目录里运行(可能与当前目录不同)。
4. 三种安装方式各自的更新机制是什么?
官方安装脚本:依赖全部锁定,用 pi update 自更新,重跑安装脚本可选择卸载。npm:由 npm 管理全局包,要求 Node ≥ 22.19,不锁定传递依赖。Nix:nix profile upgrade pi 更新(可按 tag 锁定),pi update 对 Nix 安装不可用。
5. /tree、/fork、/clone 三者的边界差异?
/tree 在同一个会话文件内的条目树上移动,适合相关的备选方案共存;/fork 从更早的 user 消息创建新会话,适合备选方案独立成工作;/clone 把当前活动分支复制成新会话,适合对当前状态做独立副本。三者都不破坏被离开的分支。
6. RPC 模式里 prompt 命令返回 success:true 意味着什么?@file 参数能用吗?
只表示 prompt 被接受、排队或处理(看 data.disposition,如 "started"),不代表模型工作完成,还要继续消费事件直到 agent_settled;若 disposition 为 "handled" 则没有启动运行。RPC 模式拒绝 @file 参数,文件内容应通过 prompt 命令传递。
— 第 3 讲完 · 下一讲进入模型层 —