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 入口與分派