Skills 技能系统(完整版)
概述
Skills(技能)是自包含的能力包,Agent 按需加载。每个技能提供:
- 专业化的工作流程
- 安装说明
- 辅助脚本
- 参考文档
Pi 实现的是 Agent Skills 标准,会对违规行为发出警告但保持宽容。
一、存放位置
Pi 从以下位置加载技能:
1.1 三个默认来源
| 来源 | 路径 | Source 标识 |
|---|---|---|
| 用户级(全局) | ~/.pi/agent/skills/ | user |
| 项目级 | ./.pi/skills/ | project |
| 扩展指定 | 通过 Extension 运行时动态传入 | path |
1.2 其他兼容路径
Pi 还兼容 Claude Code 和 OpenAI Codex 的技能路径:
// settings.json 或 .pi/settings.json
{
"skills": [
"~/.claude/skills",
"~/.codex/skills"
]
}
1.3 CLI 参数指定
pi --skill /path/to/skill-dir # 加载指定目录
pi --skill /path/to/skill.md # 加载单个技能文件
pi --no-skills # 禁用默认技能加载(但显式路径仍有效)
1.4 package.json 声明
{
"pi": {
"skills": ["/path/to/skill1", "/path/to/skill2"]
}
}
⚠️ 安全注意:技能可以指示模型执行任何操作,可能包含可执行代码。使用前请审查技能内容。
二、发现机制(Discovery Rules)
2.1 核心算法:SKILL.md 优先
scan(dir)
│
├─ 【第一步】查找 SKILL.md
│ └─ 找到 → 加载为 skill,STOP(不递归子目录)
│
├─ 【第二步】遍历所有条目
│ ├─ .开头文件 → 跳过
│ ├─ node_modules → 跳过
│ ├─ 普通子目录 → 递归(回到第一步)
│ └─ 普通 .md 文件 → 加载为 skill
关键点:
- 有 SKILL.md 的目录 → 只加载自己,不递归
- 无 SKILL.md 的目录 → 递归扫描子目录
- 跳过 node_modules — 避免扫描依赖
2.2 忽略规则
支持三种忽略文件:
.gitignore.ignore.fdignore
扫描目录时会自动应用这些文件中的忽略模式。
2.3 软链接(Symlink)处理
// 对于软链接,会尝试解析真实路径
try {
realPath = realpathSync(skill.filePath);
} catch {
realPath = skill.filePath; // 解析失败用原路径
}
// 通过真实路径实现去重
if (realPathSet.has(realPath)) {
continue; // 同一真实文件,跳过
}
三、Skill 文件格式
3.1 完整 SKILL.md 结构
---
name: my-skill # 必需:技能名称
description: 这个技能做什么... # 必需:用途说明
disable-model-invocation: false # 可选:是否在 system prompt 中隐藏
license: MIT # 可选:许可证
compatibility: Node.js 18+ # 可选:环境要求
metadata: # 可选:任意键值对
version: 1.0
author: example
---
# My Skill
正文内容,包含:
- 详细说明
- 使用示例
- 辅助脚本调用
3.2 Frontmatter 字段详解
| 字段 | 必需 | 默认值 | 说明 |
|---|---|---|---|
name | 是 | 父目录名 | 技能标识,≤64 字符 |
description | 是 | — | 用途说明,≤1024 字符 |
disable-model-invocation | 否 | false | 为 true 时不自动出现在 system prompt |
license | 否 | — | 许可证名称 |
compatibility | 否 | — | 环境要求,≤500 字符 |
metadata | 否 | — | 任意键值对 |
allowed-tools | 否 | — | 预批准工具列表(实验性) |
3.3 name 字段规则
规则:
✓ 1-64 字符
✓ 小写字母 a-z、数字 0-9、连字符 -
✓ 不以连字符开头或结尾
✓ 不含连续连字符 --
✓ 必须与父目录名一致
有效示例:pdf-processing, data-analysis, code-review-2
无效示例:PDF-Processing, -pdf, pdf--processing, pdf_
3.4 description 最佳实践
description 决定了 LLM 何时决定加载此技能。要具体!
# ✅ 好的描述
description: 从 PDF 文件中提取文本和表格,填写 PDF 表单,合并多个 PDF。当处理 PDF 文档时使用。
# ❌ 差的描述
description: 帮助处理 PDF。
四、校验规则(Validation)
Pi 在加载时会进行以下校验:
| 检查项 | 结果 |
|---|---|
| name 与父目录名不匹配 | ⚠️ 警告(仍加载) |
| name 超过 64 字符 | ⚠️ 警告(仍加载) |
| name 含非法字符 | ⚠️ 警告(仍加载) |
| name 以连字符开头/结尾 | ⚠️ 警告(仍加载) |
| description 超过 1024 字符 | ⚠️ 警告(仍加载) |
| description 完全缺失 | ❌ 不加载 |
| 同名冲突 | ⚠️ 警告,保留先加载的 |
未知 frontmatter 字段会被忽略。
五、System Prompt 注入机制
5.1 注入流程
buildSystemPrompt()
├─ 拼接工具列表
├─ 拼接 Guidelines
├─ 拼接项目上下文
└─ 追加 formatSkillsForPrompt(skills)
5.2 格式化为 XML
formatSkillsForPrompt() 将技能格式化为 XML 注入 system prompt:
export function formatSkillsForPrompt(skills: Skill[]): string {
// 过滤 disableModelInvocation=true 的 skill
const visibleSkills = skills.filter((s) => !s.disableModelInvocation);
const lines = [
"\n\nThe following skills provide specialized instructions for specific tasks.",
"Use the read tool to load a skill's file when the task matches its description.",
"When a skill file references a relative path, resolve it against the skill directory...",
"",
"<available_skills>",
];
for (const skill of visibleSkills) {
lines.push(" <skill>");
lines.push(` <name>${escapeXml(skill.name)}</name>`);
lines.push(` <description>${escapeXml(skill.description)}</description>`);
lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
lines.push(" </skill>");
}
lines.push("</available_skills>");
return lines.join("\n");
}
5.3 最终 System Prompt 中的效果
The following skills provide specialized instructions for specific tasks.
Use the read tool to load a skill's file when the task matches its description.
...
<available_skills>
<skill>
<name>github</name>
<description>在 GitHub 仓库中搜索问题、拉取请求、代码和文件</description>
<location>/Users/pengshengjie/.pi/agent/skills/github/SKILL.md</location>
</skill>
<skill>
<name>weather</name>
<description>获取天气预报和温度信息</description>
<location>/Users/pengshengjie/.pi/agent/skills/weather/SKILL.md</location>
</skill>
</available_skills>
5.4 注入条件
| 条件 | 结果 |
|---|---|
read 工具可用 | ✅ 注入 |
read 工具不可用 | ❌ 不注入 |
disable-model-invocation: true | ❌ 不注入 |
六、调用机制
6.1 两种调用方式对比
| 自动调用(Implicit) | 显式调用(Explicit) | |
|---|---|---|
| 触发 | LLM 自主决定 | 用户通过 /skill: 命令 |
| 读取方式 | LLM 主动用 read 工具 | AgentSession 同步读取文件 |
| 走 Tool Call | ✅ 是 | ❌ 否(直接读文件) |
| 格式 | LLM 自己解析返回的原始内容 | 包装成 <skill> XML block |
6.2 自动调用(Implicit)
用户消息 → LLM 看到 <available_skills>
└─ 判断任务匹配某个 Skill
└─ 主动调用 read 工具读取 <location> 路径
└─ 用 Skill 内容指导执行
6.3 显式调用(Explicit)
用户: /skill:github 帮我查个 issue
AgentSession._expandSkillCommand("/skill:github 帮我查个 issue")
│
├─ 检测 /skill: 前缀
├─ 解析 skillName = "github"
├─ 从 skillMap 查找同名 Skill
├─ readFileSync(skill.filePath) ← 同步读取,不走工具调用
├─ stripFrontmatter(content) ← 去除 frontmatter
├─ 包装为 <skill> XML block
└─ 返回:
<skill name="github" location="/path/to/SKILL.md">
References are relative to /path/to/skills/github.
# GitHub Skill
当用户需要与 GitHub 交互时使用...
</skill>
帮我查个 issue
6.4 展开流程源码解析
// agent-session.ts 第 1061-1095 行
private _expandSkillCommand(text: string): string {
if (!text.startsWith("/skill:")) return text;
// 解析 skillName 和参数
const spaceIndex = text.indexOf(" ");
const skillName = spaceIndex === -1
? text.slice(7)
: text.slice(7, spaceIndex);
const args = spaceIndex === -1
? ""
: text.slice(spaceIndex + 1).trim();
// 从已加载的 skills 中查找
const skill = this.resourceLoader.getSkills().skills
.find((s) => s.name === skillName);
if (!skill) return text; // 未找到,原样传递
try {
// 同步读取文件
const content = readFileSync(skill.filePath, "utf-8");
// 去除 frontmatter
const body = stripFrontmatter(content).trim();
// 包装为 XML block
const skillBlock = `<skill name="${skill.name}" location="${skill.filePath}">
References are relative to ${skill.baseDir}.
${body}
</skill>`;
// 拼接参数
return args ? `${skillBlock}\n\n${args}` : skillBlock;
} catch (err) {
// 错误时发出扩展事件
this._extensionRunner?.emitError({
extensionPath: skill.filePath,
event: "skill_expansion",
error: err instanceof Error ? err.message : String(err),
});
return text; // 出错时返回原文
}
}
6.5 注册为斜杠命令
每个 Skill 会自动注册为 /skill:name 斜杠命令:
const skills: SlashCommandInfo[] =
this._resourceLoader.getSkills().skills.map((skill) => ({
name: `skill:${skill.name}`,
description: skill.description,
source: "skill",
sourceInfo: skill.sourceInfo,
}));
用户输入 /skill: 时可查看所有可用技能。
七、完整生命周期
┌─────────────────────────────────────────────────────────┐
│ 阶段一:加载(Initialization) │
│ │
│ AgentSession 初始化 │
│ └─ ResourceLoader.updateSkillsFromPaths() │
│ ├─ loadSkills({ │
│ │ cwd, agentDir, skillPaths, includeDefaults │
│ │ }) │
│ │ ├─ 扫描 ~/.pi/agent/skills/ (user) │
│ │ ├─ 扫描 ./.pi/skills/ (project) │
│ │ └─ 扫描 skillPaths (extensions) │
│ │ ├─ loadSkillsFromDirInternal() │
│ │ └─ loadSkillFromFile() │
│ │ └─ parse frontmatter → Skill │
│ │ │
│ │ 真实路径去重(realpath) │
│ │ 冲突检测(collision) │
│ └─ skillsOverride?.() (extension hook) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 阶段二:注入 System Prompt │
│ │
│ buildSystemPrompt({ skills }) │
│ └─ formatSkillsForPrompt(skills) │
│ ├─ 过滤 disableModelInvocation=true │
│ └─ 格式化为 <available_skills> XML │
│ 注入 System Prompt │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 阶段三:触发 │
│ │
│ 方式 A:自动触发(Implicit) │
│ LLM 看到 <available_skills> │
│ └─ 判断任务匹配 → 调用 read(SKILL.md 路径) │
│ └─ read 工具返回文件内容 │
│ └─ LLM 用内容指导执行 │
│ │
│ 方式 B:显式触发(Explicit) │
│ 用户输入 "/skill:name args" │
│ └─ _expandSkillCommand() │
│ ├─ 同步读取文件(不走工具调用) │
│ ├─ 包装为 <skill> XML block │
│ └─ 拼接到消息 │
│ └─ LLM 直接看到展开后的内容 │
└─────────────────────────────────────────────────────────┘
八、冲突处理与优先级
8.1 去重机制
// 通过真实路径(解析 symlink)实现去重
let realPath: string;
try {
realPath = realpathSync(skill.filePath);
} catch {
realPath = skill.filePath;
}
// 同一真实路径的 symlink → 跳过(静默)
if (realPathSet.has(realPath)) {
continue;
}
8.2 同名冲突
const existing = skillMap.get(skill.name);
if (existing) {
// 冲突诊断:保留先加载的,报告冲突
collisionDiagnostics.push({
type: "collision",
message: `name "${skill.name}" collision`,
path: skill.filePath,
collision: {
winnerPath: existing.filePath, // 保留先加载的
loserPath: skill.filePath, // 丢弃后加载的
},
});
}
8.3 加载优先级
~/.pi/agent/skills/ (user) → 优先级最高
./.pi/skills/ (project)
skillPaths (extensions) → 优先级最低
九、禁用技能
9.1 完全禁用默认技能
pi --no-skills
注意:显式指定的
--skill路径仍然有效。
9.2 单个技能隐藏(不自动出现在 System Prompt)
---
name: internal-tool
description: 内部工具,仅供指定用户使用
disable-model-invocation: true
---
# Internal Tool
此工具不自动暴露,需通过 /skill:internal-tool 显式调用。
效果:
- ❌ 不出现在
<available_skills>中 - ❌ LLM 不会自动发现和使用
- ✅ 仍可通过
/skill:internal-tool显式调用
十、技能目录结构示例
my-project/
├── .pi/
│ └── skills/ # 项目级技能
│ ├── github/
│ │ └── SKILL.md
│ └── weather/
│ └── SKILL.md
└── .git/
~/.pi/agent/skills/ # 用户级技能
├── code-review/
│ ├── SKILL.md
│ ├── scripts/
│ │ └── run-review.sh
│ └── references/
│ └── rules.md
└── blog-writer/
├── SKILL.md
└── templates/
└── post-template.md
十一、相关文件索引
| 功能 | 文件 | 关键函数 |
|---|---|---|
| 技能加载主逻辑 | skills.ts | loadSkills() L428 |
| 目录扫描算法 | skills.ts | loadSkillsFromDirInternal() L197 |
| 文件解析 | skills.ts | loadSkillFromFile() L293 |
| System Prompt 格式化 | skills.ts | formatSkillsForPrompt() L359 |
| frontmatter 解析 | frontmatter.ts | parseFrontmatter() |
| frontmatter 去除 | frontmatter.ts | stripFrontmatter() |
| 资源加载器 | resource-loader.ts | updateSkillsFromPaths() L471 |
| 技能命令展开 | agent-session.ts | _expandSkillCommand() L1061 |
| 斜杠命令注册 | agent-session.ts | L2113-2118 |
| System Prompt 构建 | system-prompt.ts | buildSystemPrompt() L31 |