资源发现、项目信任与系统提示词
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 和测试常用),再决定何时加载。
各资源的去向并不相同,这张表是理解整个模块的钥匙:
| 资源 | 本质 | 最终影响谁 | 受项目信任约束 |
|---|---|---|---|
| Extensions | TS/JS 模块,加载即执行代码 | 工具、命令、事件处理、provider、UI | 项目内的是 |
| Skills | SKILL.md 指令文档加配套文件 | system prompt 里的 skills 列表,按需被模型读取 | 项目内的是 |
| Prompt templates | 带参数的 Markdown 文本 | 用户输入在进入 agent 前被展开 | 项目内的是 |
| Themes | 主题 JSON | 交互界面配色,不进模型上下文 | 项目内的是 |
| Context files | AGENTS.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 目录不算。
决定的来源按顺序:
- CLI 的
--approve/--no-approve,一次性覆盖; - 用户级和 CLI 扩展处理
project_trust事件,第一个回答 yes/no 的说了算,可以选择记住; ~/.pi/agent/trust.json里为当前目录或最近的祖先目录保存的决定(/trust写入这里);- 全局设置
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 的规则是:
- 先看全局
~/.pi/agent/; - 再从 cwd 一路向上走到文件系统根,每一级目录取一个文件,候选顺序是
AGENTS.override.md→AGENTS.md→CLAUDE.md(大小写两种写法都认),找到第一个就停; - 最终顺序是“全局在前,祖先目录从远到近”,全部按顺序拼接,不互相覆盖。
~/.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,最后是“简洁”“清楚给出文件路径” | 工具与扩展 |
docs | Pi 自身文档和示例的本地路径,只在用户问到 harness 本身时去读 | 内置 |
addendum | APPEND_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() 的处理顺序是:
- 先看是不是扩展注册的命令,是则直接执行,不进入对话;
- 触发扩展的
input事件,扩展可以改写或拦截原始输入; /skill:name args展开成 skill 全文(见 Skills);/name args匹配 prompt template 就展开成正文;- 展开后的文本才作为 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