Skip to content

技能系统

源码版本v0.73.1

skills.ts 实现 Agent Skills 规范(见 https://agentskills.io/integrate-skills)。技能是 markdown 文件,带 frontmatter 描述技能用途,LLM 在系统提示里看到技能列表后,通过 read 工具读取对应 SKILL.md 获取详细指令。这个文件负责技能的发现、加载、校验、去重、格式化为 prompt 段落。

职责

  1. 发现规则:loadSkillsFromDirInternal 递归扫目录——遇到 SKILL.md 视为技能根不再递归,否则扫子目录和根 .md 文件。见 packages/coding-agent/src/core/skills.ts:173-280
  2. frontmatter 校验:validateName 检查名称(a-z0-9-、不超 64 字符、不连字符开头/结尾、无 --、与父目录名一致),validateDescription 检查描述必填且不超 1024 字符。见 packages/coding-agent/src/core/skills.ts:93-117packages/coding-agent/src/core/skills.ts:122-132
  3. 多源加载:loadSkills 从用户全局(~/.pi/agent/skills)、项目(./.pi/skills)、显式 skillPaths 三个来源加载,冲突时保留先注册的并生成 collision 诊断。见 packages/coding-agent/src/core/skills.ts:405-504
  4. 去重:用 realpath 解析符号链接,同一文件通过不同路径注册只算一次;同名技能先到先得。见 packages/coding-agent/src/core/skills.ts:416-445
  5. prompt 格式化:formatSkillsForPrompt 生成 <available_skills> XML 段落,每个技能 <name>/<description>/<location> 三元组,disableModelInvocation: true 的技能不进 prompt(只能通过 /skill:name 显式调用)。见 packages/coding-agent/src/core/skills.ts:340-366
  6. ignore 规则:尊重 .gitignore / .ignore / .fdignore,递归扫描时按目录前缀拼路径模式。见 packages/coding-agent/src/core/skills.ts:48-66packages/coding-agent/src/core/skills.ts:25-46

设计动机

为什么用 SKILL.md 而不是直接扫所有 .md?因为一个技能可能包含多个文件(脚本、子文档),SKILL.md 是入口标志。遇到 SKILL.md 后停止递归,把整个目录当成技能包,让技能作者可以组织多个文件而不被当作多个独立技能。

为什么用 frontmatter 而不是注释?因为 frontmatter 是 YAML,结构化、可解析、可校验。名称、描述、disable-model-invocation 这些字段需要 LLM 和程序都读得懂。validateName 强制名称与父目录名一致(name "x" does not match parent directory "y"),避免技能文件被移动后名称漂移。

为什么需要 disableModelInvocation?有些技能是用户自用的提示模板,不应该被 LLM 自动触发(比如「写代码时用这个风格」),只在用户显式 /skill:name 时才加载。formatSkillsForPrompt 过滤掉这类技能,prompt 里只暴露允许 LLM 自动调用的。

为什么用 realpath 去重?因为用户可能通过 --skills ./my-skills 同时又把 ./my-skills 软链到 ~/.pi/agent/skills/my-skills,两次扫描会拿到同一文件的不同路径。canonicalizePath 解析符号链接后比较,避免重复加载同一技能。

关键文件

formatSkillsForPrompt 用 XML 标签包裹技能列表,转义 XML 特殊字符:

typescript
// packages/coding-agent/src/core/skills.ts:340-366
export function formatSkillsForPrompt(skills: Skill[]): string {
	const visibleSkills = skills.filter((s) => !s.disableModelInvocation);

	if (visibleSkills.length === 0) {
		return "";
	}

	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 (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
		"",
		"<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");
}

loadSkillsFromDirInternal 遇到 SKILL.md 立即返回,不再递归:

typescript
// packages/coding-agent/src/core/skills.ts:199-226
for (const entry of entries) {
	if (entry.name !== "SKILL.md") {
		continue;
	}
	const fullPath = join(dir, entry.name);
	let isFile = entry.isFile();
	if (entry.isSymbolicLink()) {
		try {
			isFile = statSync(fullPath).isFile();
		} catch {
			continue;
		}
	}
	const relPath = toPosixPath(relative(root, fullPath));
	if (!isFile || ig.ignores(relPath)) {
		continue;
	}
	const result = loadSkillFromFile(fullPath, source);
	if (result.skill) {
		skills.push(result.skill);
	}
	diagnostics.push(...result.diagnostics);
	return { skills, diagnostics };
}

数据流

技能从磁盘到 prompt:

边界与失败

小结

skills.ts 实现 Agent Skills 规范,三源加载(用户全局、项目、显式路径),frontmatter 校验,realpath 去重,XML 格式化注入 prompt。技能被系统提示引用后通过 read 工具读取,看 系统提示构建工具集 read/bash/edit/write/grep/find/ls。装配技能路径的入口看 CLI 入口与分发