终端编程 Agent 选型五条线:以 pi 这个 Agent 框架为样本

2026-07-29

本文基于 pi v0.82.1(仓库 commit 027a584,2026-07-28)梳理,该项目仍在快速迭代,具体行为以官方文档 https://pi.dev/docs/latest 与仓库最新代码为准。

**开源终端 Agent 的选型,绝大部分风险不在”它能不能写代码”,而在你能不能在半小时内、不问任何人,把它的许可证、隔离边界、嵌入方式、模型接法、会话留痕这五件事从仓库里翻出来、逐条读明白。**能翻出来的项目,你哪怕最后不选它,也知道自己在拒绝什么;翻不出来的项目,你选了也只是把不确定性搬进了生产环境。

这五条线是把”引入一个会在你机器上执行命令的程序”拆开之后剩下的最小集合:法务能不能过(许可证)、它能碰到什么(隔离)、能不能塞进你已有的流水线(可嵌入性)、模型这条命脉握在谁手里(模型自由度)、出事之后能不能复原现场(可审计性)。

站内已经有几篇讲通用方法论的:Agent 框架横向对比 讲维度怎么拆,多 Agent 框架选型 讲多角色场景下的取舍,Agent SDK 与框架之争 讲该用薄 SDK 还是完整框架。这一篇不重复那些结论,而是反过来做一件事:拿一个真实存在、你现在就能 clone 下来打开的开源项目当样本,把五条线落到具体文件、具体字段、具体那句话上。方法论看多了容易空转,走一遍真实仓库才知道每条线的证据长什么样。

样本选的是 pi,一个 TypeScript 写的终端编程 Agent,主仓库 https://github.com/earendil-works/pi-mono ,文档站 https://pi.dev/docs/latest ,截至 2026 年 7 月在 GitHub 上约有 8 万 star。选它不是因为它最好,而是因为它的文档把几个通常被含糊带过的问题写得异常直白,正好适合演示”怎么查”。

先把这个项目的骨架摆出来,后面每条线都会回到这张表:

组成部分它负责什么对应仓库位置你什么时候会碰到它
@earendil-works/pi-coding-agent交互式 CLI,同时是 SDK 的入口包packages/coding-agent全局装的就是这个包
@earendil-works/pi-agent-coreAgent 运行时,负责工具调用与状态管理packages/agent想搞清楚一条消息在内存里长什么样时
@earendil-works/pi-ai多家模型 API 的统一层packages/ai接自建模型或代理端点时
@earendil-works/pi-tui终端 UI 库,做差分渲染packages/tui给扩展写自定义界面时
安全与容器化文档写明信任模型和三种隔离模式packages/coding-agent/docs/security.mdcontainerization.md决定它能跑在什么机器上之前
会话格式文档JSONL 条目类型与 SessionManager 接口packages/coding-agent/docs/session-format.md要做审计、回放、成本归因时

一、许可证与供应链:能不能用,以及依赖是谁在把关

许可证这条线看着最简单,实际最容易查漏。很多人只在仓库首页看一眼徽章就结案了,但单仓库多包(monorepo)项目里,根目录的 LICENSE 和每个发布包各自 package.json 里的 license 字段完全可能不一致——发布到 npm 的是包,不是仓库。

pi 的查法是:先读根目录 LICENSE,是标准 MIT 全文,署名 Copyright (c) 2025 Mario Zechner;再逐个打开 packages/*/package.json,确认 pi-coding-agentpi-aipi-agent-corepi-tui 四个包的 license 字段都是 MIT,版本号也随 monorepo 统一递进。两处对上了,这条线才算过。MIT 意味着允许商用、允许修改、允许闭源分发,条件只有一条:保留版权声明和许可声明。对多数公司法务来说这是最省事的一档。

许可证过了只是及格线。真正吃亏的地方在依赖链——你审的是主仓库,装进来的是几百个传递依赖。这一点上 pi 的 README 有一整节写供应链加固,值得当成检查清单来读:直接外部依赖锁死到精确版本,工作区内部包才用范围版本;.npmrc 里设了 save-exact=truemin-release-age=2,避免解析到当天刚发布的依赖;package-lock.json 是依赖的唯一事实来源,预提交钩子会拦住误提交的 lockfile;发布出去的 CLI 包里带一份 packages/coding-agent/npm-shrinkwrap.json,把传递依赖也钉住;CI 用 npm ci --ignore-scripts 安装,另有定时任务跑 npm audit --omit=devnpm audit signatures --omit=dev;依赖的生命周期脚本走显式白名单,新增带生命周期脚本的依赖会让检查失败,直到有人评审过。

这些机制值不值钱见仁见智,但它给了你一个可核对的动作清单:打开 .npmrc 看那两个字段在不在,打开 package-lock.json 看直接依赖是不是精确版本。能核对的承诺才叫承诺,README 里写”我们非常重视安全”而没有对应文件的,就当没写。

二、隔离能力:一个明说自己没有内置沙箱的项目该怎么评

这条线是分歧最大的。pi 的 docs/security.md 开头就把话说死了:它是本地 Agent,以启动它的用户账号权限运行,并且把该用户可写的文件都视为同一个本地信任边界之内。README 里的措辞更直接——pi 不包含用于限制文件系统、进程、网络、凭证访问的内置权限系统。

文档里给出的理由是:部分的进程内沙箱很容易被误当成安全边界,而它实际上仍然依赖宿主的 shell、文件系统、包管理器、凭证和扩展代码;真正的隔离得来自操作系统或者虚拟化/容器边界。你可以不同意这个判断,但它至少是一个明确写下来、能被引用、能被反驳的判断。选型时最怕的恰恰是相反的情况:文档暗示自己有防护,出事之后才发现那只是个提示框。

那 pi 里那个叫”项目信任”的东西管什么?security.md 说得很清楚——它控制的是是否加载项目本地的设置、资源、包和扩展,不是沙箱,也不限制模型在你开始工作之后能让工具做什么。触发信任判断的条件被逐项列了出来:.pi/settings.json.pi/extensions.pi/skills.pi/prompts.pi/themes.pi/SYSTEM.md.pi/APPEND_SYSTEM.md,以及当前目录或祖先目录里的 .agents/skills。空的 .pi 目录不算。决定来自全局设置里的 defaultProjectTrust,默认值是 ask,已保存的决定按规范化目录记在 ~/.pi/agent/trust.json。有一个细节特别值得记住:AGENTS.mdCLAUDE.md 这类上下文文件不受信任门控,只要没关掉上下文加载就会被读进去。也就是说,仓库里的一段文字仍然能影响模型行为——文档自己把这归入”预期的本地 Agent 风险”,并明说提示注入无法由 pi 可靠地阻止。

真要隔离,docs/containerization.md 给了三种模式,取向差别很清楚:

  • Gondolin 扩展:pi 和凭证留在宿主机,把内置工具和用户敲的 ! 命令路由进一个本地 Linux 微虚拟机。扩展会把宿主当前目录挂到虚拟机的 /workspace,并覆盖 readwriteeditbashgrepfindls 这几个工具。前提是较新的 Node.js(文档里写明了具体的最低要求),外加自行用系统包管理器装 QEMU。
  • 普通 Docker:整个 pi 进程跑在容器里,最简单,代价是供应商 API key 得进容器。
  • OpenShell:整个 pi 进程跑在带策略控制的沙箱里,需要一个 gateway。文档提到在配置了推理路由的情况下,沙箱内的代码调用 https://inference.local,由 gateway 在上游注入凭证,原始 API key 可以留在沙箱外——这对”不想把长期密钥交给 Agent”的团队是个有意思的路子。

同一份文档还提醒了一句常被忽略的事:扩展跑在 pi 进程所在的地方。如果你用宿主 pi 加工具路由扩展,其它自定义扩展的工具仍然在宿主机上执行,除非它们自己也做了委派。隔离的粒度到底在哪一层,这句话说得比任何架构图都准。关于工作区级隔离的通用做法,可以对照 Agent 工作区隔离 那篇一起看。

三、可嵌入性与模型自由度:入口有几个,出口锁不锁死

可嵌入性看的是一件事:除了人坐在终端前敲,还有没有第二条、第三条路把它接进系统。只有交互式 TUI 的项目,注定停留在个人工具阶段。

pi 的 docs/index.md 把编程式用法单列了一栏,四个入口:SDK(在 Node.js 应用里嵌入)、RPC 模式(stdin/stdout 上跑 JSONL)、JSON 事件流模式(打印模式输出结构化事件)、TUI 组件(给扩展写终端界面)。docs/sdk.md 里的最小例子长这样:

import { createAgentSession, ModelRuntime, SessionManager } from "@earendil-works/pi-coding-agent";

const modelRuntime = await ModelRuntime.create();
const { session } = await createAgentSession({
  sessionManager: SessionManager.inMemory(),
  modelRuntime,
});

session.subscribe((event) => {
  if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
    process.stdout.write(event.assistantMessageEvent.delta);
  }
});

await session.prompt("What files are in the current directory?");

判断嵌入深度时,重点看它开放到哪一层。这里能看到的是:工具集合可以按名字白名单(内置工具名是 readbasheditwritegrepfindls,默认只开前四个),noTools: "all" 全关,noTools: "builtin" 只关内置而保留扩展与自定义工具,excludeTools 在白名单之后再排除具体名字。会话可以完全不落盘(SessionManager.inMemory()),设置可以完全不读文件(SettingsManager.inMemory())。这些是判断”能不能在无状态服务里跑”的硬指标,比任何宣传语都可靠。

跨语言集成走 RPC,文档给的命令是 pi --mode rpc --no-session。这里有一处容易翻车的细节值得单独记住:RPC 模式用严格 JSONL 语义,只以 LF 作为记录分隔符,文档明确指出 Node 的 readline 不符合该协议,因为它还会在 U+2028U+2029 处断行,而这两个字符在 JSON 字符串里是合法的。这种级别的说明写不写,某种程度上就是文档质量的分水岭。

模型自由度这条线,问的是”哪天某家供应商涨价、限流或者不能用了,你要改多少东西”。pi 把这块拆成了三层,都可以自己查:

第一层是订阅登录,/login 之后可选 ChatGPT Plus/Pro(Codex)、Claude Pro/Max、GitHub Copilot、xAI、OpenRouter、Radius。第二层是 API key,docs/providers.md 里列了一张相当长的表,逐行给出环境变量名和 auth.json 里的键名,从 ANTHROPIC_API_KEYOPENAI_API_KEYDEEPSEEK_API_KEY 到各家网关。凭证解析顺序也写了:CLI 的 --api-keyauth.json、环境变量、models.json 里的自定义供应商密钥,依次生效;auth.json0600 权限创建。第三层是自定义供应商,通过 ~/.pi/agent/models.json 接入本地或自建服务,支持四种 API 形态:openai-completionsopenai-responsesanthropic-messagesgoogle-generative-ai。文档里给的最小配置只需要每个模型一个 id

{
  "providers": {
    "ollama": {
      "baseUrl": "http://localhost:11434/v1",
      "api": "openai-completions",
      "apiKey": "ollama",
      "models": [
        { "id": "llama3.1:8b" },
        { "id": "qwen2.5-coder:7b" }
      ]
    }
  }
}

还有一个对国内团队更实际的能力:models.json 里可以只给内置供应商写一个 baseUrl,把它整体路由到代理端点,而内置模型列表继续可用。这就是”退出成本”的量化答案——换供应商的改动量,等于一个 JSON 文件里的几行。

必须说明的是,海外几家模型服务商官方对中国大陆存在区域限制,不支持直连;市面上存在第三方中转,但那属于你自己承担合规与稳定性风险的选择,这里不背书也不给具体渠道。计费规则、额度与限流各家不同且会调整,以官方最新说明为准。

四、可审计性:出事之后你能复原多少

这条线在选型清单里常年排最后,在事故复盘里常年排第一。判断标准很朴素:Agent 跑完之后,留下的东西够不够你回答”它当时到底做了什么、花了多少、为什么这么做”。

pi 的会话是 JSONL 文件,路径规则写在 docs/session-format.md 里:~/.pi/agent/sessions/--<path>--/<timestamp>_<uuid>.jsonl,其中 <path> 是工作目录把 / 换成 -。这里有两个信息量很大的设计。

一是条目组成的是树而不是线性列表。每条记录带 idparentIdid 是 8 位十六进制),分支就是从较早的条目上长出新的子节点,不用另建文件。文件头一行是会话头,格式文档里直接给了样例:

{"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project"}

会话头里带一个版本字段,文档把历代格式的差别逐条列了出来(从线性序列到 id/parentId 树,再到角色命名的统一),旧格式在加载时自动迁移到当前版本。条目类型是枚举好的:messagemodel_changethinking_level_changecompactionbranch_summarycustomcustom_messagelabelsession_info。也就是说,“中途换了模型”、“改了思考等级”、“做过一次上下文压缩”这些平时最难还原的动作,都是文件里独立的一行,而不是消失在日志噪音里。压缩条目上还带 tokensBefore,新版本还会把压缩后保留的上下文直接内嵌在 retainedTail 字段里,让这个条目成为一个自包含的检查点。

二是成本和终止原因是结构化字段而不是文本。助手消息里带 providermodelstopReason,以及一个 usage 对象,里面是 inputoutputcacheReadcacheWritetotalTokens 和一个 cost 子对象。工具结果消息带 toolNameisError,bash 执行消息带 commandoutputexitCodetruncated。这些字段决定了你能不能不写解析器就把成本按项目、按人、按任务分摊出去。想做回放和复现的,可以对照 Agent 复现与回放 里的通用做法。

还有一条平时不起眼、排查时很好用的通道:docs/environment-variables.md 说,由模型调用的 bash 工具会拿到一组描述当前会话状态的变量——PI_SESSION_IDPI_SESSION_FILEPI_PROVIDERPI_MODELPI_REASONING_LEVEL,值在每条命令启动时解析。文档还特意提了一句:被问到当前跑的是哪个模型时,去读这些变量,别从系统提示词里推断。

printf '%s/%s\n' "$PI_PROVIDER" "$PI_MODEL"
printf 'reasoning=%s session=%s\n' "$PI_REASONING_LEVEL" "$PI_SESSION_ID"

注意这些变量只注入给模型可调用的 bash 工具,不注入给用户自己敲的 !!! 命令;通过 SDK 嵌入时,那个表示”运行在 pi 内部”的进程标记 PI_CODING_AGENT 也不会自动设置。这类边界条件如果不读文档,八成会踩。

五、边界与代价:这套取向放弃了什么

任何设计都是取舍,把取舍列出来比列功能更有用。

首先,无内置沙箱意味着安全责任整体外移。文档把这一点摆到了台面上:缺少内置沙箱、来自不可信内容的提示注入、以及用户自己安装的扩展和技能的行为,一般都在安全边界之外,除非报告能证明存在真实的权限边界绕过。翻译成人话——如果你需要”开箱即用的权限护栏”,这个项目本身不提供,你得自己搭容器或虚拟机。团队里没人负责这一层的话,选它就是把责任悬空。

其次,扩展是与主进程同权限的 TypeScript 模块。这带来了极高的可扩展性,代价是扩展的审查责任落在你身上,装一个扩展等同于在自己账号权限下跑一段别人的代码。

第三,容器化本身也有代价。文档提醒:把宿主工作区以读写方式绑定挂载进去,容器或虚拟机里的写入照样会改宿主文件,想要更强的保护得用只读挂载或者进出复制;而如果 OpenShell 用的是远程 gateway,项目文件不会从宿主绑定挂载,沙箱里的写入不会反映到你的机器上,得靠上传下载命令搬运。前者是”以为隔离了其实没有”,后者是”以为改了其实没改”,两个方向都会咬人。

第四,供应链上的保守是有摩擦的。min-release-age=2 意味着你拿不到当天发布的依赖版本;预提交钩子拦 lockfile,意味着改依赖要走额外流程。安全和便利在这里没有免费午餐。

最后一条与代码无关但影响协作:README 开头写明,新贡献者提交的 issue 和 PR 默认会被自动关闭,维护者每天审阅这些被自动关闭的条目。如果你的团队计划靠上游合并补丁来长期跟进,这条规则会实实在在影响你的排期,得提前算进去。

六、上手与避坑清单

下面每条都写清楚为什么会踩,以及怎么绕开。

把项目信任当成权限系统。 会踩是因为交互式启动时会弹一个信任提示,视觉上很像权限确认框。但文档说得明白,它只是输入加载的守卫,防的是仓库悄悄改掉设置和扩展,不能让不可信代码、不可信提示或不可信模型输出变安全。做法:把它当成”要不要加载这个仓库的配置”来理解,真正的权限边界另外用容器或虚拟机划。

在自动化里忽略非交互模式的信任行为。 会踩是因为 -p--mode json--mode rpc 这三种模式不会显示信任提示。没有适用的已保存决定时,asknever 会直接忽略需要信任的资源,而 always 会直接信任。做法:给 CI 单独确认 defaultProjectTrust 的取值,需要按次覆盖时用 --approve/-a--no-approve/-na

Docker 隔离时顺手把宿主配置目录挂进去。 会踩是因为挂 ~/.pi/agent 能让容器复用你已经登录好的凭证,很省事。代价是容器同时拿到了宿主的认证信息和历史会话。做法:文档建议用命名卷做容器本地的设置与会话存储,只传这次任务必需的密钥,最好用短期凭证。

以为用了 Gondolin 就全隔离了。 会踩是因为它确实覆盖了那七个内置工具和 ! 命令,看起来很彻底。但扩展仍然跑在 pi 进程所在的地方,其它自定义扩展的工具照旧在宿主机执行。做法:清点你装的每一个扩展,确认它是否也做了委派;不确定就整个进程进容器。

接本地模型时不填 apiKey,结果模型不出现在 /model 里。 会踩是因为 Ollama 这类服务本来就不校验密钥,直觉上没必要填。但 pi 在模型出现在选择器之前会要求配置了认证。做法:按文档的写法留一个占位值,或者用 /login 给该供应商存一个密钥,或者在选模型时用 --api-key 传。

用通用行读取器解析 RPC 输出。 会踩是因为绝大多数语言的按行读取默认就能用,直到某次模型输出里带了 U+2028。做法:只按 \n 切分记录,输入端容忍并剥掉行尾的 \r,别用会把 Unicode 分隔符当换行的读取器。

安装时跑了依赖的生命周期脚本。 会踩是因为默认的 npm install -g 会执行它们。文档给的安装命令是带 --ignore-scripts 的,并说明 pi 在正常 npm 安装下不需要安装脚本:

npm install -g --ignore-scripts @earendil-works/pi-coding-agent

做法:照抄文档给的命令,别自己省参数。

收个尾。这五条线的价值不在于给某个项目打分,而在于它们都能落到”打开某个文件、看某个字段”的动作上——许可证看根目录 LICENSE 加每个包的 license 字段,隔离看 docs/security.mddocs/containerization.md,可嵌入性看 docs/sdk.mddocs/rpc.md,模型自由度看 docs/providers.mddocs/models.md,可审计性看 docs/session-format.mddocs/environment-variables.md。换成任何一个别的开源 Agent,文件名会变,但要找的东西不变:一句明确写下来的信任边界声明、一份可核对的依赖策略、一个不需要人坐在终端前的入口、一个能改 baseUrl 的地方、一份带结构化字段的会话记录。

如果你打算真去评估 pi,建议的阅读顺序是:先读 packages/coding-agent/docs/security.md 判断它配不配跑在你的机器上,再读 containerization.md 决定用哪种隔离,最后读 session-format.md 想清楚留痕怎么进你自己的审计系统。这三份读完,剩下的功能细节大多可以边用边查。

本篇属于一个把开源编程 Agent 项目 pi 逐层拆开讲的系列,整体地图见开源编程 Agent pi 是什么;沿着这条线往下,还可以看 开源编程 Agent pi 的终端界面为什么不闪开源编程 Agent pi 接入国产模型

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。