青雲的博客
深入浅出 Pi 第六部:工具与扩展怎样进入运行时 第 34 章

AGENTS.md、Skills 与 Prompt 模板怎样被发现

沿 DefaultResourceLoader、loadProjectContextFiles、loadSkills 与 loadPromptTemplates,拆清三类 Markdown 资源的发现顺序、去重规则和最终消费位置。

源码版本
v0.83.0
验证日期
Commit
845d6ff1f6643aba440341cce877ce1c43ebbc39

第 33 章处理工具执行后的输出。现在回到请求发出之前:项目目录里同样都是 Markdown,AGENTS.mdSKILL.md.pi/prompts/review.md 为什么不会被当成一类文件直接拼起来?答案藏在 ResourceLoader 的返回接口里。它分别暴露 getAgentsFiles()getSkills()getPrompts(),因为三者的消费时点完全不同。

AGENTS.mdCLAUDE.md 是项目上下文,内容随 system prompt 构建进入 <project_context>。Skill 在 system prompt 中只列名称、描述和绝对位置,模型匹配任务后再用 read 读取正文。Prompt template 则不进入 system prompt;用户输入 /review src/a.ts 时,AgentSession.prompt() 才把模板内容与参数替换成普通文本。

flowchart LR
  accTitle: 三类 Markdown 资源的消费路径
  accDescr: ResourceLoader 分别产出 context files、skills 与 prompt templates;context 全文进入 system prompt,skills 只注入索引,prompt template 在 slash command 命中后展开。
  FS["user / project / package paths"] --> LOADER["DefaultResourceLoader"]
  LOADER --> CTX["context files"] --> SYSTEM["system prompt full text"]
  LOADER --> SKILL["skill metadata"] --> INDEX["available_skills index"]
  INDEX -->|"model calls read"| BODY["SKILL.md body"]
  LOADER --> TEMPLATE["prompt templates"] -->|"/name args"| EXPAND["expanded user text"]

AGENTS.md 从外向内排列

loadContextFileFromDir() 在每个目录按 AGENTS.mdAGENTS.MDCLAUDE.mdCLAUDE.MD 的顺序寻找第一个普通文件。一个目录若同时存在前两种,只取靠前的候选;读取失败打印 warning 后继续候选。这里没有递归扫描文件名,也不会加载任意 instructions.md

loadProjectContextFiles() 先读取 agentDir 的全局 context,再从 cwd 一直向文件系统根逐级查找。遍历时用 unshift 收集祖先,所以最终顺序是 global、最外层祖先、逐层靠近 cwd。seenPaths 防止同一路径重复,另有 worktree 特例,避免主 worktree 的同一份 tracked context 被嵌套 linked worktree 再继承一次。

这些内容进入 prompt 时保留绝对 path 属性,每份全文放在独立 <project_instructions> 中。它们是给模型看的上下文,不是由 Pi 解释执行的配置语言。多层指令冲突怎样处理,最终仍依赖模型;源码只提供稳定顺序和来源标记,没有实现一套“最内层字段覆盖外层字段”的 Markdown merge。

Skill 启动时加载的是目录卡片

Skill discovery 遇到目录根的 SKILL.md 就把该目录视为一个 skill root,并停止向下递归;若根没有 SKILL.md,才继续找子目录,同时允许 discovery root 的直接 .md 文件作为兼容形式。隐藏目录、node_modules 和 ignore 规则会被跳过,symlink 会跟随到实际文件或目录。

解析出的 skill 至少有 name、description、filePath 等元数据。同名 collision 采用 first wins:第一份进入 skillMap,后来的不替换,只产生包含 winner/loser path 的 diagnostic;同一真实文件经 symlink 重复发现则静默跳过。ResourceLoader 在 package manager 已经排好 precedence 的路径上调用 loadSkills(... includeDefaults: false),所以 first 的含义来自上游资源顺序,不是文件系统偶然顺序可以随便忽略。

formatSkillsForPrompt() 过滤 disableModelInvocation 的 Skill,只输出 XML 索引,并明确提示模型用 read 加载 file。于是“加载了 40 个 Skills”不等于 40 份正文已经占满 context window。若当前 active tools 没有 read,system prompt 连这份索引也不附加;显式 /skill:name 仍是 AgentSession 输入层的另一条路径,不能与模型自主调用混为一谈。

Prompt template 要等输入命中才展开

Prompt template 的文件名就是 command name,frontmatter 可提供 description 与 argument-hint,正文保存为 template content。目录扫描是非递归的,只接纳直接 .md 文件;显式路径可以指文件或目录。无法读取或解析的文件返回 null,这一层不把失败升级成进程崩溃。

输入以 / 开头并匹配模板名时,expandPromptTemplate() 解析带引号参数,再替换 $1$@${N:-default} 与 slice 语法。参数值本身不会递归做第二轮 substitution,避免用户传入 $1 后被意外当占位符继续展开。

用临时目录验证三类资源时,最稳妥的是直接调用各自纯 loader;如果当前 checkout 没安装依赖,可以先做静态定位:

repo="${PI_SOURCE_DIR:-/tmp/pi-handbook-qbQTcA}"
git -C "$repo" grep -n 'loadProjectContextFiles\|formatSkillsForPrompt\|expandPromptTemplate' \
  v0.83.0 -- packages/coding-agent/src/core
git -C "$repo" grep -n 'collision' v0.83.0 -- \
  packages/coding-agent/src/core/skills.ts \
  packages/coding-agent/src/core/resource-loader.ts

这组命令证明三条实现路径存在,不证明你机器上的项目资源已获 trust 或实际被选中。要验证一个具体 cwd,应再检查 ResourceLoader diagnostics 与 sourceInfo,而不是只列目录。

最后划清执行边界:AGENTS context 和 Skill/Prompt 文本都会影响模型输入,文本中的命令不会由 loader 自动执行;extension 文件则会被模块 loader import,并调用它导出的 factory。第 35 章进入这条可执行路径,解释 Pi 为什么先以 untrusted 状态做一次 bootstrap,以及 trust 决定后哪些项目资源才会出现。