返回课程首页

18

Skill 是如何被发现和加载的

走读目录扫描、Frontmatter 解析与资源冲突诊断。

源码基线:Pi v0.82.0 · commit 518855d

上一章创建了:

examples/skills/error-log-analysis/SKILL.md

但仅仅把文件放在磁盘上,并不代表桌面 Agent 一定能看到它。本章沿着真实源码回答:

Pi 从哪些位置找 Skill,怎样解析,重名时谁赢,失败后又怎样报告?

1. 加载链路总览

flowchart LR
    SRC["Settings / Auto / 配置 Package"] --> PM["PackageManager.resolve"]
    EP["additionalExtensionPaths"] --> EPS["resolveExtensionSources"]
    ADD["--skill / SDK Additional"] --> PATHS["Resource Loader 有序路径"]
    DISC["resources_discover"] --> EXT["extendResources"]
    PM --> PATHS
    EPS --> PATHS
    PATHS --> RL["ResourceLoader.updateSkillsFromPaths"]
    EXT --> RL
    RL --> LS["loadSkills"]
    LS --> FILE["loadSkillFromFile"]
    FILE --> META["Skill Metadata + Diagnostics"]
    META --> PROMPT["System Prompt 的可见目录"]

这条链可分为两类职责:

  • Package Manager:决定候选路径、启用状态、来源和优先级;
  • Skill Loader:扫描文件、解析 Frontmatter、去重名称并生成诊断。

把它们混成一个“扫描目录”会看不懂 Settings、Package 和 CLI 为什么能参与。

2. 默认 Skill 来源

Pi 文档列出:

用户级

~/.pi/agent/skills/
~/.agents/skills/

项目级

<cwd>/.pi/skills/
<cwd或祖先>/.agents/skills/

其他来源

  • Package 的 skills/package.json 中的 pi.skills
  • 用户/项目 Settings 的 skills 数组;
  • 重复使用的 --skill <path>
  • SDK 的 additionalSkillPaths
  • Extension 的 resources_discover

这些来源最终都被规范化成文件或目录路径,不是分别实现六套加载器。

3. 项目 Trust 的作用

项目自动发现只有在项目受信任时加入:

  • <cwd>/.pi/skills
  • cwd 和祖先的 .agents/skills
  • 项目级 Settings / Package 来源。

项目 .agents/skills 的祖先搜索:

  1. cwd 开始;
  2. 每层加入 .agents/skills
  3. 如果发现 Git Repo Root,到 Repo Root 为止;
  4. 如果不在 Git Repo 中,一直走到文件系统根。
flowchart TD
    C["cwd/.agents/skills"] --> P["parent/.agents/skills"]
    P --> G{"到 Git Root?"}
    G -->|否| P2["继续向上"]
    G -->|是| STOP["停止"]

注意这个顺序是从当前目录向祖先排列,较具体的路径先进入候选。

4. .pi/skills.agents/skills 的扫描差异

源码有两种 Discovery Mode:

type SkillDiscoveryMode =
  | "pi"
  | "agents";

共同规则:

  • 递归查找目录中的 SKILL.md
  • 跳过点开头目录;
  • 跳过 node_modules
  • 跟随可用的文件/目录 Symbolic Link;
  • 遵守 .gitignore.ignore.fdignore

自动发现时的差异在 Root Markdown:

目录模式 根目录直接 .md 文件
~/.pi/agent/skills、项目 .pi/skills 作为独立 Skill 候选
~/.agents/skills、项目 .agents/skills 忽略,要求目录中的 SKILL.md

这对应文档对跨 Harness 共享目录的兼容约定。

这个规则属于 Package Manager 的自动发现 Mode,不是物理目录的永久属性。若 Settings、 --skill 或 SDK Additional 显式传入同一个 .agents/skills 目录,路径可能走 Pi Mode 或 loadSkillsFromDirInternal(..., true),此时根 .md 可以被接受。

5. 遇到 SKILL.md 后为什么不再向下

collectSkillEntries() 会先检查当前目录是否存在 SKILL.md。一旦找到,就把它加入并立即 返回,不再递归扫描这个目录的子目录。

parent/
├── SKILL.md          ← parent 自己是一个 Skill
└── nested/
    └── SKILL.md      ← 这次不会作为独立 Skill 发现

原因是带入口文件的目录已经被视为一个自包含能力包;其中的子目录应是它自己的资源。

如果要发布两个 Skill,应使用同级的两个 Skill Root。

6. Ignore 规则怎样传播

扫描从 Root 建立 Ignore Matcher。进入子目录时,会读取该层:

.gitignore
.ignore
.fdignore

规则被加上相对 Root 的 Prefix,再继续向下传递。空行和普通注释被忽略,否定规则保留。

这意味着可以让 Skill 仓库排除:

  • 生成目录;
  • 大型 Fixture;
  • 私密本地文件;
  • 不想暴露给 Agent 的实验 Skill。

Ignore 只是发现过滤,不是访问控制。若模型通过其他路径知道文件位置,Tool Policy 仍决定 能否读取。

7. Package Manager 先处理来源

DefaultPackageManager.resolve() 的大致顺序:

  1. 收集项目和用户 Packages;
  2. 解析 Package Resources;
  3. 处理项目 Settings 中的本地资源;
  4. 处理用户 Settings 中的本地资源;
  5. 加入自动发现资源;
  6. 按 Precedence Rank 排序;
  7. 对规范化后的相同物理路径去重。

结果不是简单 string[],每项还有:

type ResolvedResource = {
  path: string;
  enabled: boolean;
  metadata: PathMetadata;
};

Metadata 记录 Scope、Source、Origin 和 Base Directory,后续用于冲突和来源展示。

8. 正常资源优先级

源码的 Rank 从高到低:

Rank 来源
0 项目 Settings 本地条目
1 项目自动发现
2 用户 Settings 本地条目
3 用户自动发现
4 Package Resource

排序的目的不是直接覆盖文件内容,而是配合后面的“同名 Skill 第一个获胜”。

Resource Loader 中名为 cliEnabledSkills 的变量容易误导:它来自 additionalExtensionPaths 所解析的临时 Extension/Package,其中可以携带 Skill;它不是 命令行 --skill

真正的 --skillmain.ts 中被解析成 additionalSkillPaths,与 SDK additionalSkillPaths 走同一条尾部列表。正常模式的组合顺序是:

临时 Extension/Package 携带的 Skill
→ Normal Skill
→ --skill / SDK additionalSkillPaths

由于同名采用 first-wins,显式 --skill 并不天然覆盖普通资源。

9. 路径去重与名称去重不是一回事

路径去重

Package Manager 和 Resource Loader 会规范化、Canonicalize 路径。同一个文件通过 Symbolic Link 或不同文字路径重复出现时,只保留一份。

名称去重

两个不同文件都可以声明:

name: error-log-analysis

loadSkills()Map<name, Skill>,第一个成功加载的 Skill 获胜;后一个产生 Collision Diagnostic。

flowchart LR
    A["project/error/SKILL.md\nname=error-log-analysis"] --> M{"Skill Map"}
    B["package/error/SKILL.md\nname=error-log-analysis"] --> M
    M --> W["保留排在前面的 Skill"]
    M --> D["为后者记录 collision"]

10. Resource Loader 怎样组合路径

DefaultResourceLoader.reload()

  1. 调用 Package Manager 解析正常资源;
  2. 单独解析 additionalExtensionPaths 的临时 Extension/Package Sources;
  3. 只取 Enabled Resources;
  4. 如果未禁用 Skills,组合 Extension/Package Skills + Normal + additionalSkillPaths
  5. 交给 updateSkillsFromPaths()
  6. 保存 Skill 与 Diagnostics。

合并过程按 Canonical Path 去重,保留第一次出现的位置。

Extension 后续通过 extendResources() 添加 Skill Path 时,会把新路径合并到已加载路径尾部, 再重新执行 Skill Loading。若与已有 Skill 同名,仍是前者获胜。

11. --no-skills 到底禁用什么

设置 noSkills 后,正常自动/配置资源不会进入组合列表。

但仍有例外:

  • additionalExtensionPaths 显式加载的临时 Extension/Package 所携带的 Skill;
  • CLI --skill 和 SDK additionalSkillPaths
  • 后续 Extension resources_discover 返回的 Skill Paths。

源码在 noSkills 分支仍组合 Additional Paths;extendResources() 也不因 noSkills 拒绝 Extension Skill,只要最终路径数组非空,updateSkillsFromPaths() 仍会加载。因此它更接近:

禁用普通 Skill Discovery,但保留显式 Additional 和 Extension 运行期扩展。

这与文档中“--skill 即使配合 --no-skills 仍是 Additive”的说明一致。

12. loadSkills 接收什么

Resource Loader 调用:

loadSkills({
  cwd,
  agentDir,
  skillPaths,
  includeDefaults: false,
});

这里 includeDefaults: false 很关键:默认目录已经由 Package Manager 解析过,不应在 loadSkills() 中再次无条件扫描。

loadSkills() 对每个 Path:

  • 不存在:产生 Warning;
  • 是目录:递归扫描;
  • .md 文件:直接尝试解析;
  • 是其他文件:产生 Warning;
  • 读取/Stat 失败:产生 Warning。

13. loadSkillFromFile 怎样生成 Metadata

读取完整文件后:

flowchart TD
    F["Markdown"] --> FM["parseFrontmatter"]
    FM --> D{"description 有效?"}
    D -->|否| SKIP["Diagnostic + Skip"]
    D -->|是| N["name 或父目录回退"]
    N --> V["名称/长度校验"]
    V --> S["Skill Metadata"]

生成的核心对象包含:

interface Skill {
  name: string;
  description: string;
  filePath: string;
  baseDir: string;
  sourceInfo: SourceInfo;
  disableModelInvocation: boolean;
}

正文没有保存在这个 Metadata 对象中。文件虽为了解析 Frontmatter 被读取过,正文要进入 模型 Context 仍需后续 read 或显式 Command 展开。

14. 诊断分为哪些类型

课程可以按产品展示分成:

类型 示例 是否一定阻止加载
Warning 名称非法、描述过长、文件读取失败 否;取决于问题
Error 某些 Resource Loader/Package 级硬错误 对应资源不可用
Collision 同名 Skill 后者失败 Winner 仍可用

普通本地 additionalSkillPaths 不存在时,loadSkills() 已先生成 warning: skill path does not exist;Resource Loader 看到同路径已有诊断后不会再追加 Error。因此当前这类缺失路径的实际诊断是 Warning。

源码的 ResourceDiagnostic 还携带 Path;Collision 包含 Winner 和 Loser Path。桌面 Skill 管理页应把来源和冲突双方都展示出来。

只显示“加载了 12 个 Skill”会隐藏第 13 个为什么消失。

15. 多数校验为何宽容

Pi 对 Agent Skills Standard 的多数违反只警告,仍然加载:

  • 名称格式;
  • 名称长度;
  • Description 过长。

缺失 Description 则跳过,因为模型可见目录失去了路由信息。无效 YAML 也无法安全得到 Frontmatter,因此跳过。

宽容加载有利于兼容旧 Skill,但宿主可以在团队策略中把 Warning 升级为发布阻断。

16. SourceInfo 有什么用

每个成功 Skill 会带来源信息,例如:

  • local / project;
  • local / user;
  • Package Source;
  • CLI 或 Extension 临时来源;
  • Base Directory。

Resource Loader 优先使用路径解析阶段保存的 Metadata,找不到时才根据路径推断默认来源。

但当前 v0.82.0 有一个动态扩展边界:metadataByPathreload() 的局部 Map。 extendResources() 加入新路径后会重新加载全部 Skill,却没有把旧 Map 再传给 updateSkillsFromPaths()。结果是:

  • 新增 Extension Path 可从 extensionSkillSourceInfos 恢复正确来源;
  • 先前 Package、.agents 或临时 Extension/Package Skill 可能退化成 source: "path"、Temporary Scope。

所以 SourceInfo 是重要诊断信息,但桌面 UI 不能假定运行期扩展后所有旧来源都保持稳定。

SourceInfo 不直接改变模型执行逻辑,但对桌面产品很重要:

  • 告诉用户能力从哪里来;
  • 判断是否属于项目;
  • 显示 Package 或 Extension 名称;
  • 为禁用、冲突和安全审查提供证据。

17. 渐进式加载的准确含义

磁盘扫描阶段:读取文件以解析 Frontmatter
运行时 Metadata:保留名称、描述、路径等
System Prompt:仅放模型可见 Skill 的轻量目录
任务匹配:模型 read 正文,或 /skill:name 显式展开
引用资源:再按需读取

“按需加载正文”不是说启动时从未读取文件字节,而是说正文不会在启动时常驻模型 Context。

同时还必须满足:

  • read 是 Active Tool;
  • Skill 没有 disable-model-invocation

否则 Metadata 可以在宿主中已加载,但不会进入模型的自动发现目录。

18. Reload 与 Extension 资源

DefaultResourceLoader.reload() 会重新:

  • 解析 Settings;
  • 解析 Package 和本地资源;
  • 扫描 Skill;
  • 重建 Diagnostics;
  • 重建 Skill/Diagnostics;Session 随后的装配才负责 Prompt。

同时它会清空 Extension SourceInfo Map,并从普通来源重新建立路径;上一次 resources_discover 加入的路径不会仅靠 Loader Reload 自动恢复。

启动时,宿主调用 session.bindExtensions(...) 会触发 session_startresources_discover。Extension 返回的 Skill Paths 由 extendResources() 加入并立即 重新加载,随后 Session 重建 Base Prompt。

AgentSession.reload() 只有在 UI Context、Command Context Actions、Shutdown Handler 或 Error Listener 至少一种 Binding 被保留时,才再次触发 session_start(reason="reload") 和 资源发现。只调用 bindExtensions({}) 虽能完成启动发现,但 Reload 时 hasBindings 为假, Extension Skill 不会自动恢复。

因此桌面 UI 的 Skill 列表应订阅资源更新,而不是假设启动时永远固定。

19. 用上一章示例走一遍

假设宿主把:

examples/skills/error-log-analysis

作为 additionalSkillPaths

  1. Resource Loader 解析成绝对路径;
  2. Path 存在且是目录;
  3. 扫描器在根找到 SKILL.md 后停止向下递归;
  4. Frontmatter Parser 得到 Name 和 Description;
  5. 名称与 Description 校验通过;
  6. 生成 Skill,Base Directory 是示例目录;
  7. 没有同名 Winner,所以加入结果;
  8. read Active 时进入 <available_skills>
  9. 模型匹配任务后读取正文;
  10. 正文再引导读取 Reference 和运行 Script。
sequenceDiagram
    participant H as Desktop Host
    participant R as ResourceLoader
    participant L as Skill Loader
    participant P as System Prompt
    participant M as Model

    H->>R: additionalSkillPaths
    R->>L: normalized path
    L-->>R: Skill + diagnostics
    R-->>P: visible metadata
    P-->>M: name / description / location
    M->>L: read SKILL.md when matched

20. 桌面 Skill 管理页

建议至少显示:

type SkillView = {
  name: string;
  description: string;
  filePath: string;
  source: string;
  scope: string;
  modelVisible: boolean;
  diagnostics: string[];
};

这是产品侧视图,不是 Pi 原样类型。

modelVisible 不能只看 Skill 本身,还要结合:

  • Active Tools 中有没有 read
  • disableModelInvocation
  • 当前 Base Prompt 是否已重建。

21. 常见误区

“Pi 只扫描一个固定 Skills 目录”

错误。来源包括用户、项目、Settings、Package、CLI、SDK 与 Extension。

“越靠近 cwd 的 .agents Skill 一定覆盖”

只有在同名且路径顺序确实排在前面时才由 first-wins 生效;不要脱离完整来源优先级推断。

“同一路径去重等于同名去重”

错误。它们发生在不同阶段。

“有 Warning 就一定没加载”

错误。多数规范 Warning 仍加载,缺失 Description 等情况才跳过。

“noSkills 会屏蔽所有 Skill”

错误。Additional Extension/Package 携带的 Skill、--skill / SDK Additional,以及后续 Extension resources_discover 都仍可加载。

“渐进式加载表示启动时完全不读 SKILL.md”

错误。启动时读取 Frontmatter;渐进的是正文进入模型 Context。

22. 本章小结

  • Package Manager 先解析来源、启用状态、优先级与路径,Skill Loader 再解析文件;
  • 默认来源包括用户、受信任项目、Package、Settings、CLI、SDK 和 Extension;
  • 自动发现的 .pi Mode 接受根 .md.agents Mode 不接受;显式目录可能走不同模式;
  • 当前目录有 SKILL.md 时,该目录成为 Skill Root,不继续发现嵌套 Skill;
  • 扫描遵守三类 Ignore File,但 Ignore 不是权限控制;
  • 路径去重使用 Canonical Path,名称冲突使用 first-wins 并产生 Collision;
  • 正常优先级是项目 Settings、项目 Auto、用户 Settings、用户 Auto、Package;
  • --skill 与 SDK Additional 位于普通路径之后,不天然覆盖同名 Skill;
  • noSkills 不屏蔽 Additional Extension/Package、显式 Skill Additional 或后续 Extension Discovery;
  • 缺失 Description 或解析失败会跳过,多数格式问题只 Warning;
  • 运行时 Metadata 不保留正文,正文按需进入模型 Context;
  • Loader Reload 会丢弃旧 Extension Discovery Path;Session Reload 是否重新发现取决于 四类保留 Binding。
  • extendResources 重载全部 Skill 时,旧 metadataByPath 不会重传,部分既有 SourceInfo 可能退化。

23. 自测

  1. Package Manager 与 Skill Loader 分别负责什么?
  2. .pi/skills.agents/skills 对根 .md 的规则有何不同?
  3. 目录自身含 SKILL.md 时,为什么不继续发现子目录 Skill?
  4. 路径重复与名称冲突分别怎样处理?
  5. 正常资源的五级 Precedence 是什么?
  6. noSkills 后哪些 Additional/Extension 来源仍能加载?
  7. 为什么“启动时读取文件”不违背渐进式披露?
  8. 桌面管理页怎样判断一个 Skill 是否对模型可见?