
原文:Han Lee – Claude Agent Skills: A First Principles Deep Dive
上篇我們理解了 Skill 的結構與撰寫方式;篇將以工程視角拆解 Skill 作為「Meta-tool」 的設計、兩段訊息注入機制、API 請求結構,以及從「發現 → 授權 → 注入 → 執行」的完整生命週期。讀完後,你將能以第一性原理理解為何 Skills 能做到「不執行程式碼、卻改變模型行為」。
內部架構:Skill 作為 Meta-tool
Skill(大寫 S) 是 Claude 的「元工具(Meta-tool)」。
它的任務是動態生成所有可用技能清單,再交給模型自行匹配。
核心特徵
- prompt 為動態生成:列出所有 skill 名稱與描述。
- 無外部分類器:由模型自行推論何時使用。
- Token 預算限制:每次清單上限約 15,000 字元,逼迫 skill 描述精簡。
- 獨立於 system prompt:存在於 tools 陣列中,而非全域系統指令。
Skills 物件設計:不是「直接執行」,而是「注入與改寫」
傳統工具(如 Read、Bash、Write)會直接執行並回傳結果;Skills 則不直接執行,而是做兩件事:
- 將專用指示注入對話歷史(conversation context),透過完整提示改寫模型接下來的推理方式。
- 動態修改執行環境(execution context),例如開放工具權限、切換模型、調整思考 token 參數等。
這個過程透過兩則使用者訊息完成:
- 一則是給使用者看的中繼資訊(可見、極短)。
- 另一則是給模型看的完整 skill 提示(隱藏於 UI)。

Normal Tool vs Skill Tool:設計差異
| Feature | Normal Tool | Skill Tool |
|---|---|---|
| 本質 | 直接動作執行器 | 提示注入 + 上下文修改器 |
| 訊息流 | assistant → tool_use → user → tool_result | assistant → tool_use(Skill)→ user → skill prompt 注入 |
| 複雜度 | 簡單(3–4 則訊息) | 較複雜(5–10+ 則訊息) |
| 上下文 | 靜態 | 動態(每回合可變) |
| 持久性 | 僅工具互動 | 工具互動 + skill 指令 |
| Token 成本 | 低(~100 tokens) | 高(~1,500+ tokens/回合) |
| 適用 | 單一步驟任務 | 複雜、需指導的工作流 |
Skill 工具結構與動態描述生成
Pd = {
name: "Skill", // 工具名稱常數
inputSchema: {
command: string // 例如 "pdf", "skill-creator"
},
outputSchema: {
success: boolean,
commandName: string
},
// 🔑 關鍵:動態產生 skills 列表(而非固定字串)
prompt: async () => fN2(),
// 驗證與執行
validateInput: async (input, context) => { /* 五種錯誤碼 */ },
checkPermissions: async (input, context) => { /* allow/deny/ask */ },
call: async *(input, context) => { /* 產出訊息 + 修改執行上下文 */ }
}
為什麼要動態 prompt?
與固定描述的工具不同,Skill 工具會在執行時聚合所有 skills 的「名稱+描述」,組成 <available_skills> 清單供模型判斷是否要呼叫某個 skill。
這就是漸進式揭露(Progressive Disclosure):先提供最少的技能中繼資料,只有當模型選擇某個 skill 後,才載入其完整提示,避免上下文暴漲、同時維持可探索性。
async function fN2() {
let A = await atA(),
{ modeCommands: B, limitedRegularCommands: Q } = vN2(A),
G = [...B, ...Q].map((W) => W.userFacingName()).join(", ");
l(`Skills and commands included in Skill tool: ${G}`);
let Z = A.length - B.length,
Y = nS6(B),
J = aS6(Q, Z);
return `Execute a skill within the main conversation
<skills_instructions>
...(使用說明與注意事項)
</skills_instructions>
<available_skills>
${Y}${J}
</available_skills>
`;
}為何不放在 System Prompt?
有些系統(如某些助理)會把工具定義放在 system prompt。但 Claude 的 Skills 不這麼做,理由是:
- system prompt 具全域與持久影響,一旦放入就會「長駐」整段對話。
- Skills 需要暫時、具任務範疇(scoped)的行為。
因此,Skills 以 tools 陣列中的 Skill 工具描述出現;個別 skill 的名稱則是 Skill 的 input_schema.command 的取值。
API 請求實例
{
"model": "claude-sonnet-4-5-20250929",
"system": "You are Claude Code, Anthropic's official CLI...",
"messages": [
{"role": "user", "content": "Help me create a new skill"}
],
"tools": [
{
"name": "Skill",
"description": "Execute a skill...\n\n<skills_instructions>...\n\n<available_skills>\n...",
"input_schema": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The skill name (no arguments)"
}
}
}
},
{ "name": "Bash", "description": "Execute bash commands..." },
{ "name": "Read" }
]
}
<available_skills> 每次請求都重新生成,通常有約 15,000 字元的預算上限,迫使技能描述精煉、避免擠爆上下文。
兩段訊息注入設計:透明度 × 可讀性
為什麼需要兩段訊息?
- 若全部可見,UI 會被上千字的內部指令淹沒。
- 若全部隱藏,使用者又看不到系統正在做什麼。
解法:注入兩則訊息,各司其職——
- 可見的中繼訊息(短、機器可解析的 XML),2) 不可見的完整提示(長、只送 API)。
可見的中繼訊息(isMeta: false)
let metadata = [
`<command-message>${statusMessage}</command-message>`,
`<command-name>${skillName}</command-name>`,
args ? `<command-args>${args}</command-args>` : null
].filter(Boolean).join('\n');
messages.push({
content: metadata,
autocheckpoint: checkpointFlag
});
使用者會看到類似:
<command-message>The "pdf" skill is loading</command-message> <command-name>pdf</command-name> <command-args>report.pdf</command-args>
通常 50–200 字元,讓前端以特殊樣式呈現與審計紀錄。
隱藏的完整提示(isMeta: true)
let skillPrompt = await skill.getPromptForCommand(args, context);
let fullPrompt = prependContent.length || appendContent.length
? [...prependContent, ...appendContent, ...skillPrompt]
: skillPrompt;
messages.push({
content: fullPrompt,
isMeta: true // 隱藏於 UI,僅送 API
});
典型內容(500–5,000 字):任務背景、流程步驟、可用工具、輸出格式、環境路徑等(如 PDF 專家流程)。
👉 圖片建議:左右對照圖「左=簡短 XML 狀態」「右=Markdown 長提示(標註 isMeta:true)」。
為何不能合併成一則?
兩者受眾不同、目的不同、處理管線不同:
- 可見訊息:給人看、要極簡、要能被 UI 解析。
- 隱藏訊息:給模型看、可冗長、免經 UI 驗證。
合併會違反單一職責原則並汙染 UI/上下文。
訊息組合的擴充:附件與權限
除了兩段核心訊息,有時還會加入**附件(attachments)與權限設定(command_permissions)**訊息:
let allMessages = [
createMessage({ content: metadata, autocheckpoint: flag }), // 1. 可見中繼
createMessage({ content: skillPrompt, isMeta: true }), // 2. 隱藏長提示
...attachmentMessages, // 3.(條件式)附件
...(allowedTools.length || skill.model ? [
createPermissionsMessage({ // 4.(條件式)權限/模型
type: "command_permissions",
allowedTools: allowedTools,
model: skill.useSmallFastModel ? getFastModel() : skill.model
})
] : [])
];
- 附件:診斷資訊、檔案參考或補充上下文。
- 權限:當 frontmatter 指定
allowed-tools或要求切換model時,才加入。 - 可視性:依需求決定是否顯示於 UI,但通常屬於機制性訊息。
案例:技能執行生命週期(以 pdf skill 為例)

Phase 1|發現與載入(啟動時)
系統啟動會掃描所有來源的 skills,解析 SKILL.md 的 frontmatter 與內容,建立技能物件。
async function getAllCommands() {
let [userCommands, skillsAndPlugins, pluginCommands, builtins] =
await Promise.all([
loadUserCommands(),
loadSkills(),
loadPluginCommands(),
getBuiltinCommands()
]);
return [...userCommands, ...skillsAndPlugins, ...pluginCommands, ...builtins]
.filter(cmd => cmd.isEnabled());
}
針對外掛型 skills 的載入(略)後,會形成類似:
{
"type": "prompt",
"name": "pdf",
"description": "Extract text from PDF documents (plugin:document-tools)",
"whenToUse": "When user wants to extract or process text from PDF files",
"allowedTools": ["Bash(pdftotext:*)", "Read", "Write"],
"model": null,
"isSkill": true,
"disableModelInvocation": false,
"promptContent": "You are a PDF processing specialist..."
}
Phase 2|第 1 回合:使用者請求與 skill 選擇
使用者:「Extract text from report.pdf」。
系統先過濾可供 Skill 工具列出的 skills(需有 description 或 when_to_use):
async function getSkillsForSkillTool() {
const all = await getAllCommands();
return all.filter(cmd =>
cmd.type === "prompt" &&
cmd.isSkill === true &&
!cmd.disableModelInvocation &&
(cmd.source !== "builtin" || cmd.isModeCommand === true) &&
(cmd.hasUserSpecifiedDescription || cmd.whenToUse)
);
}
接著格式化清單為 <available_skills>,模型讀取後以語義推論判斷 pdf 符合需求,於是呼叫 Skill 工具:
{
"type": "tool_use",
"name": "Skill",
"input": { "command": "pdf" }
}
關鍵:沒有任何外部演算法在做分類/意圖偵測;純 LLM 推論。
Phase 3|Skill 工具執行
- 輸入驗證(是否存在、可否自動啟動、型別是否為 prompt 等)。
- 權限檢查(deny/allow 規則;預設
ask由使用者確認)。 - 載入 Skill 檔並產生兩段訊息;必要時加入權限/模型覆寫。
- 回傳 contextModifier,在後續回合自動預核准工具或切換模型。
yield {
type: "result",
data: { success: true, commandName: skillName },
newMessages: messages,
contextModifier(context) {
// 預先開放 allowedTools
// 覆蓋 mainLoopModel(若指定)
return modified;
}
};
Phase 4|送交 API(第 1 回合完成)
{
"model": "claude-sonnet-4-5-20250929",
"messages": [
{ "role": "user", "content": "Extract text from report.pdf" },
{ "role": "assistant", "content": [{ "type": "tool_use", "name": "Skill", "input": { "command": "pdf" } }]},
{ "role": "user", "content": "<command-message>The \"pdf\" skill is loading</command-message>\n<command-name>pdf</command-name>" },
{ "role": "user", "content": "You are a PDF processing specialist...\n## Process\n1. Validate PDF exists\n2. Run pdftotext...", "isMeta": true },
{ "role": "user", "content": { "type": "command_permissions", "allowedTools": ["Bash(pdftotext:*)", "Read", "Write"], "model": null } }
]
}
此時,對話上下文(長提示)與執行上下文(工具白名單/模型)都已就緒,但尚未真正做事。

Phase 5|工具執行(在 skill 上下文中)
Claude 於下一回合依據已注入的提示執行:
- 檢查
report.pdf是否存在 - 呼叫 Bash:
pdftotext report.pdf output.txt - 用 Read 讀取
output.txt - 將結果呈現給使用者
{
"type": "tool_use",
"name": "Bash",
"input": {
"command": "pdftotext report.pdf output.txt",
"description": "Extract text from PDF using pdftotext"
}
}
差別在於:Bash、Read、Write 已被預先允許,不需再次徵求同意,流程順暢完成。
心智模型回顧(Key Takeaways)
- Skills 是
SKILL.md的提示模板,不是可執行程式。 - Skill(大寫 S)是 tools 陣列裡的 Meta-tool,不是 system prompt 的一部分。
- 對話上下文透過 isMeta:true 的訊息注入;執行上下文則調整工具許可與模型選擇。
- 選擇使用哪個 skill,由 LLM 讀描述後自行推論,非演算法匹配。
- 工具權限具範疇性:在技能執行期間預先允許,任務後環境恢復。
- 兩段訊息同時滿足透明度(人類可見)與可操作性(模型可用)。
結語:以「提示×權限」取代「程式×函式」
將專業知識「提示化」並以一則 Meta-tool 管理其範疇化的權限與模型,讓 Claude 得以在安全、可控、可組合的框架下完成複雜任務。
這種設計避免了工具爆炸與系統提示臃腫,同時保留 LLM 的語義彈性——這,就是 Agent Skills 的優雅之處。




