Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

资源发现、项目信任与系统提示词

TL;DR:Pi 的“外部资源”有好几种:会执行代码的 extensions、给模型读的 skills、展开成用户输入的 prompt templates、只管界面的 themes,以及进入 system prompt 的 context files(AGENTS.md / CLAUDE.md)和 SYSTEM.md / APPEND_SYSTEM.md。它们来自全局目录、项目 .pi/、Pi 包和 CLI 参数四类来源,由 ResourceLoader 在 reload() 时统一发现、去重、缓存,同名冲突按“CLI > 项目 > 用户 > 包”先到先得。项目目录可能来自陌生仓库,所以 Pi 先只加载用户级和 CLI 扩展,在加载任何项目设置和项目资源之前解决项目信任,未信任就跳过它们。system prompt 不是写死的字符串,而是按名字分段(基础说明、工具、规则、文档、追加内容、项目上下文、skills 列表、cwd)每次运行前重建,与会话里已有的版本做 diff,只把变化的段追加成 system 消息。

它解决什么问题

Pi 的定位是“内核小、靠扩展长大”,大部分个性化都通过外部资源完成:团队在仓库里放 AGENTS.md 写约定,用户在 ~/.pi/agent/skills 放自己的 skill,装一个 Pi 包带进来一组扩展和模板,临时用 pi -e ./my-ext.ts 试一个扩展。如果每种资源各写一套“去哪找、谁覆盖谁、出错怎么办”,就会有四五套发现逻辑和冲突规则。

ResourceLoader 把这件事收口:给定 cwd,把所有来源、所有类型的资源装配成一份可查询的缓存,再交给 AgentSession(注册扩展、建 system prompt、展开模板)和 TUI(主题、命令补全、启动时的资源列表)使用。它在 Pi 进程内运行,不常驻扫描磁盘:启动时 reload() 一次,之后各方读缓存;用户 /reload 时整体重建一次。构造函数本身不做 I/O,调用方可以先注入 override 钩子(SDK 和测试常用),再决定何时加载。

各资源的去向并不相同,这张表是理解整个模块的钥匙:

资源本质最终影响谁受项目信任约束
ExtensionsTS/JS 模块,加载即执行代码工具、命令、事件处理、provider、UI项目内的是
SkillsSKILL.md 指令文档加配套文件system prompt 里的 skills 列表,按需被模型读取项目内的是
Prompt templates带参数的 Markdown 文本用户输入在进入 agent 前被展开项目内的是
Themes主题 JSON交互界面配色,不进模型上下文项目内的是
Context filesAGENTS.md / CLAUDE.md 等目录级规则system prompt 的 project_context 段否,始终加载
SYSTEM.md / APPEND_SYSTEM.md替换 / 追加 system prompt 的文本system prompt 的基础段 / 追加段项目内的是

一句话:extension 改系统的行为,skill 和 context file 改模型的行为,prompt template 改用户的输入,theme 只改界面。

资源从哪里来、谁覆盖谁

每类资源(extensions、skills、prompts、themes)都有四类来源:

  • 用户级:~/.pi/agent/ 下的约定目录(extensions/、skills/、prompts/、themes/),以及用户 settings.json 里显式列出的路径;skills 还额外支持 Agent Skills 标准的 ~/.agents/skills/。
  • 项目级:<cwd>/.pi/ 下的同名目录和项目 settings.json 里的路径;skills 还会从 cwd 向上逐级找 .agents/skills/,到仓库根为止。
  • Pi 包:settings 里 packages 声明的 npm / git / 本地包,包清单决定它贡献哪些资源(详见 Pi Packages 与包管理)。
  • CLI:-e/--extension、--skill 等临时路径,只对这一次运行有效。

路径的发现和启用由 package manager 统一解析(settings 里的数组支持 !glob 排除、+path / -path 精确开关),然后按优先级排序:CLI 临时路径最先,其次是项目的显式配置、项目的自动发现、用户的显式配置、用户的自动发现,再是包资源,内置扩展最后。加载时同名资源“先到先得”,后来者被丢弃并生成一条 collision 诊断(技能名、模板名、主题名都如此;同一文件经由符号链接出现两次会被静默去重)。所以项目里的同名 skill 会覆盖你全局的那个,而一个包里的同名 skill 覆盖不了你本地的。--no-extensions、--no-skills、--no-prompt-templates、--no-themes 可以关掉自动发现,只保留 CLI 显式给的。

单个资源坏掉不会阻塞启动:skill 的 frontmatter 不合法、主题 schema 不对、扩展加载抛错,都只记一条诊断,启动时在资源列表里展示,其余资源照常可用。

context files 和 system prompt 文件走的是另一套简单得多的规则,后面单独讲。

项目信任:在加载项目资源之前做决定

问题

你 git clone 了一个陌生仓库,cd 进去运行 pi。如果仓库里有 .pi/extensions/evil.ts,而 Pi 启动就加载它,那么在你说第一句话之前,任意代码已经以你的权限执行了。项目 settings 也可能配置包让 Pi 去安装,同样危险。

做法

Pi 在加载项目设置和项目资源之前先确定“这个目录是否可信”。只要从 cwd 发现了下面任意一种,就需要一个信任决定:.pi/settings.json、.pi/mcp.json、.pi/extensions|skills|prompts|themes、.pi/SYSTEM.md 或 .pi/APPEND_SYSTEM.md、当前目录或祖先目录里的项目 .agents/skills。一个空的 .pi 目录不算。

决定的来源按顺序:

  1. CLI 的 --approve / --no-approve,一次性覆盖;
  2. 用户级和 CLI 扩展处理 project_trust 事件,第一个回答 yes/no 的说了算,可以选择记住;
  3. ~/.pi/agent/trust.json 里为当前目录或最近的祖先目录保存的决定(/trust 写入这里);
  4. 全局设置 defaultProjectTrust:always / never / ask(默认)。ask 在交互模式下弹窗;print、JSON、RPC 模式没法弹窗,ask 和 never 都按不信任处理。

如果项目里根本没有需要信任的资源,就直接取已保存的决定,没有则视为信任,不打扰用户。

实现上这是 ResourceLoader.reload() 的一个“两遍加载”:第一遍强制把项目视为不可信,只加载用户级和 CLI 扩展(它们需要先存在,才能处理 project_trust 事件);拿到信任结果后写回 SettingsManager,按这个状态重新读取设置,再进行正式的全量加载。信任为假时,项目 settings、项目 MCP 配置、.pi 下的扩展/skills/模板/主题/系统提示词文件、项目配置的包都被跳过。cwd 在运行中切换时也会重新走这套流程。

边界

面试时要能说清楚项目信任不是什么:

  • 它不是沙箱。信任之后,工具调用照样以 Pi 进程的系统权限读写、执行;扩展在同一个进程里跑。真正的隔离要靠容器、虚拟机或单独的系统用户。
  • 它不覆盖 context files。AGENTS.md、CLAUDE.md 无论是否信任都会加载(除非用 --no-context-files 关掉),因为它们只是文本,不会执行;但文本仍可能是 prompt injection,应当当作不可信输入看待。
  • 它不是完全的启动边界。项目 settings 里的 sessionDir 在选择/创建会话时就被读取了,早于信任决定。

一句话:项目信任防的是“打开一个目录就静默执行它的代码”,不防模型被目录里的内容带偏,也不限制工具能做什么。

一次启动的完整顺序

把上面串起来,pi 在一个仓库里启动时:

读取全局设置 → 选择/创建会话(此时会读项目 sessionDir)
→ ResourceLoader.reload()
    第一遍:项目视为不可信,只加载用户级 + CLI 扩展
    解决项目信任(CLI 覆盖 → project_trust 扩展 → trust.json → defaultProjectTrust/弹窗)
    按信任状态重读设置,package manager 解析全部来源并排序
    加载扩展 → skills → prompt templates → themes(各自去重、记诊断)
    加载 context files,发现 SYSTEM.md / APPEND_SYSTEM.md
→ 扩展的 resources_discover 事件可再追加 skill / 模板 / 主题路径
→ AgentSession 用这些结果构建 system prompt、注册工具和命令

最后一步的“追加”走的是 extendResources():扩展在启动或 reload 时声明额外的资源路径,loader 在现有缓存上增量加载,不触发完整 reload。这样一个扩展可以顺带“带进来”一组 skill。

/reload 会重做整个 reload():清掉扩展缓存、重读所有资源,并重新发出 resources_discover。它是全量重建,而不是增量更新,对几十个资源的规模来说开销只有几十毫秒,换来的是不需要维护文件监听和失效逻辑。重建后 skills 列表或 context 变了,下一次请求的 system prompt 自然跟着变(见下文的 diff 机制)。

Context files:沿目录树向上叠加

AGENTS.md 解决的是“项目约定怎么告诉 agent”。Pi 的规则是:

  1. 先看全局 ~/.pi/agent/;
  2. 再从 cwd 一路向上走到文件系统根,每一级目录取一个文件,候选顺序是 AGENTS.override.md → AGENTS.md → CLAUDE.md(大小写两种写法都认),找到第一个就停;
  3. 最终顺序是“全局在前,祖先目录从远到近”,全部按顺序拼接,不互相覆盖。
~/.pi/agent/AGENTS.md            全局规则
/home/me/work/AGENTS.md          上层目录
/home/me/work/repo/AGENTS.md     仓库根
/home/me/work/repo/pkg/CLAUDE.md 当前目录(离得最近,排在最后)

拼接而不是覆盖,是因为目录级规则不应该消灭上层规则,而是逐层细化;离 cwd 最近的放在最后,在 prompt 里也最“新鲜”。AGENTS.override.md 提供了一个“只在这一级目录里压过同级 AGENTS.md”的口子。对嵌套在主仓库里的 git worktree,Pi 会识别出主仓库的同名文件与 worktree 自己的副本是同一份规则,只加载一次。

SYSTEM.md 与 APPEND_SYSTEM.md:替换与追加

  • SYSTEM.md(项目 .pi/ 优先,需要信任;否则 ~/.pi/agent/)替换 Pi 默认的基础说明。一旦替换,默认的工具清单、规则、文档指引这几段都不再生成,只保留你的文本,后面再接项目上下文、skills 和 cwd。用户自定义 system prompt 时通常想完全掌控内容,所以是替换语义。CLI 的 --system-prompt 同理。
  • APPEND_SYSTEM.md(同样项目优先)追加一段内容,不动默认部分。适合“默认的挺好,我只想多加几条要求”。CLI 对应 --append-system-prompt。

System prompt 是怎么拼出来的

分段结构

system prompt 由一组有名字的 section 依次组成:

section内容来源
preamble默认是一句角色说明:“你是运行在 pi 里的编码助手……”;有 SYSTEM.md 时就是它的全文内置 / SYSTEM.md
tools当前启用工具的一行说明,加“可能还有其他工具”各工具的 prompt snippet
rules去重后的行为规则:比如只有 bash 没有 grep/find 时提示用 bash 搜文件、各工具贡献的 guideline、扩展追加的 guideline,最后是“简洁”“清楚给出文件路径”工具与扩展
docsPi 自身文档和示例的本地路径,只在用户问到 harness 本身时去读内置
addendumAPPEND_SYSTEM.md 的内容用户 / 项目
project_context所有 context files,每个包在 <project_instructions path=...> 里AGENTS.md 等
skills<available_skills> 列表:每个 skill 的名字、描述、SKILL.md 路径,并告诉模型“任务匹配时用 read 读取”Skills
cwd当前工作目录运行时
自定义扩展添加的命名段扩展

除 preamble 外,每段都用同名 XML 标签包起来(<rules>...</rules>),方便模型和后续的“补丁”对上号。tools/rules/docs 三段只在没有自定义 prompt 时生成。工具的 JSON schema 不在 prompt 文本里,而是作为工具声明随请求发送;被扩展隐藏声明的工具也不会出现在工具清单和规则里,保证 prompt 与实际声明一致。skills 段只有在模型能读文件(有 read 或 bash 工具)时才加入,否则列出来也用不了。

注意 1.x 的默认 prompt 不含当前日期:日期每天变,会让 prompt 前缀的 provider 缓存隔天就失效,所以被去掉了。

何时重建、怎么生效

prompt 不是会话开始时拼一次就固定的。每次运行开始(before_agent_start 扩展处理之后)以及运行中每一轮之间,AgentSession 都会按当前状态重建一遍 section,然后和会话里已经记录的版本做 diff:

  • 第一次请求:追加一条完整的 system 消息(全部 section + 全部工具声明);
  • 之后有变化(/reload 后 skills 变了、扩展换了工具集):只追加一条补丁 system 消息,写变化的 section(删掉的写 null)和 toolsAdded/toolsRemoved;
  • 没变化:什么都不追加。

这些 system 消息存在会话文件里(见 会话树与上下文压缩),重放即可得到任意时刻的完整 prompt。这样设计有两个直接收益:prompt 前缀尽量稳定,provider 缓存不被无谓打破;会话能如实导出“模型当时看到的 prompt 和工具”。扩展若在 before_agent_start 里返回一整段 systemPrompt 强制替换,Pi 不会把它写进会话,而是在请求时把它投影为 provider 的首条 system prompt。

一句话:资源决定“有哪些段”,AgentSession 在每次运行和每轮之间重建这些段,只把差异以 system 消息的形式追加到会话里。

Prompt templates:在进入 agent 之前展开

prompt template 是 prompts/ 目录下的 Markdown 文件,文件名就是命令名:~/.pi/agent/prompts/review.md 对应 /review。frontmatter 里的 description 用于命令补全,argument-hint 提示参数。正文支持 $1、$@/$ARGUMENTS、${1:-默认值}、${@:N} 等 shell 风格的参数替换,参数按 shell 规则切分引号。

用户输入一行以 / 开头的文本时,AgentSession.prompt() 的处理顺序是:

  1. 先看是不是扩展注册的命令,是则直接执行,不进入对话;
  2. 触发扩展的 input 事件,扩展可以改写或拦截原始输入;
  3. /skill:name args 展开成 skill 全文(见 Skills);
  4. /name args 匹配 prompt template 就展开成正文;
  5. 展开后的文本才作为 user 消息进入 agent(运行中则排入 steering / follow-up 队列)。

所以模板是纯文本层面的复用,模型看到的只是一条普通的 user 消息,不知道它来自模板。

取舍

得到的:所有资源一套优先级、一套诊断,用户学会一次规则就能预测结果;单个资源失败不拖垮启动;资源带来源信息(哪个包、用户级还是项目级),TUI 能展示、冲突报告有意义;override 钩子和 --no-* 开关让 SDK 与测试能精确控制加载结果;项目信任把“打开目录”和“执行目录里的代码”拆开。

放弃的:资源类型差异很大(扩展要执行、skill 只读文件、主题要校验),统一入口不得不按最复杂的扩展来设计(例如异步的 reload);来源多、规则多,同名资源的最终归属需要理解完整排序才能预测;reload() 是全量重建,缓存的生命周期就是两次 reload 之间,外部修改在 reload 前不会被感知。

常见追问

为什么 context files 不受项目信任约束? 它们只是注入 prompt 的文本,不会执行代码;跳过它们会让大量只放了 AGENTS.md 的正常仓库体验变差。代价是目录里的文本可能做 prompt injection,所以文档明确要求把它当不可信输入。

为什么要两遍加载扩展? 信任决定本身可以交给扩展(比如公司统一的信任策略),而做决定的扩展只能来自用户级或 CLI,不能来自尚未被信任的项目。第一遍只加载这些“可信来源”的扩展,第二遍才加载项目的。

system prompt 每轮都变会不会打破缓存? 正因为怕这个,Pi 按 section 做 diff,没变化就不追加,变化也只追加补丁段;日期也从默认 prompt 里移除了。只有真正的变化(比如 reload 后 skills 变了)才会改变后续请求。

项目的 skill 和全局的同名,用哪个? 项目的。排序是 CLI > 项目 > 用户 > 包,同名先到先得,后者产生 collision 诊断。

源码入口(v1.5):packages/coding-agent/src/core/resource-loader.ts、packages/coding-agent/src/core/project-trust.ts、packages/coding-agent/src/core/trust-manager.ts、packages/coding-agent/src/core/system-prompt.ts、packages/coding-agent/src/core/package-manager.ts、packages/coding-agent/docs/security.md