Skip to main content

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-invocationfalsetrue 时不自动出现在 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.tsloadSkills() L428
目录扫描算法skills.tsloadSkillsFromDirInternal() L197
文件解析skills.tsloadSkillFromFile() L293
System Prompt 格式化skills.tsformatSkillsForPrompt() L359
frontmatter 解析frontmatter.tsparseFrontmatter()
frontmatter 去除frontmatter.tsstripFrontmatter()
资源加载器resource-loader.tsupdateSkillsFromPaths() L471
技能命令展开agent-session.ts_expandSkillCommand() L1061
斜杠命令注册agent-session.tsL2113-2118
System Prompt 构建system-prompt.tsbuildSystemPrompt() L31

翻译自 pi-mono/skills.tsdocs/skills.md