Claude Code 的 skills 原始碼解析

古茗前端團隊 team icon

avatar

古茗前端團隊 @古茗科技

作者:王君生

前言

skills(以下文中統稱「技能」)是個很難定義的東西,從翻譯上看叫技能,但它不只是代表技能。本文先從歷史脈絡梳理 LLM 的幾個發展階段,再看 skills 能做什麼,給出簡單定義,最後結合碼解析,提出一些對 skill 的不成熟思考。

刀耕火種

2022 年底 — ChatGPT 爆紅,大家熱情高漲,都在跟 ChatGPT 對話、聊天。那時對我們而言,與 ChatGPT 交流的核心是「怎麼說才能讓它聽話」。大家討論 prompt(提示詞)如何寫、如何定義 role,如何把「需求 + 指令 + 約束」全部塞進一句話,甚至出現了「prompt 工程師」一職。那個階段知識高度碎片化、難以復用,但這個過程改變了程式的邏輯,從「程式邏輯」轉為「自然語言」。

開始規模化

2023 年初 — Anthropic 發表 Constitutional AI、OpenAI 出了 prompt engineering 官方指南,開始把有效的 prompt 沉澱成範本庫、系統提示(system prompt)規範。在這個階段,也誕生了像 Prompt-Engineering、awesome-chatgpt-prompts-zh 等著名的 prompt 倉庫。但問題也顯現:

  • 模板難以維護
  • 跨任務遷移困難
  • 模型更新後需要大量重新調試

此階段 prompt 開始函式化,但工程角度來看仍然薄弱,prompt 的管理依舊是一大難題。

有標準

2023 年 6 月 — OpenAI 正式推出 Function Calling(可稱為「函數調用」),模型第一次有了結構化呼叫外部系統的標準介面。2024 年,Anthropic 提出 MCP(Model Context Protocol,模型上下文協議),嘗試統一工具呼叫的協議層,生態開始標準化。當模型具備了呼叫外部工具的能力後,「讓模型做什麼」和「模型怎麼做」開始分離。Prompt 不再需要硬編碼所有邏輯,而是描述意圖,執行交由工具處理。這打開了更結構化的思路,從「語言生成」走向「決策 + 調度」。

推出 skill

2025 年 10 月中旬,Anthropic 正式發布 Claude Skills。Skills 本質上是可重用、有文件的能力單元。它把「如何完成某類任務的最佳實踐」封裝起來(例如如何生成 docx、如何讀取 PDF),讓模型在需要時查閱並遵循,而不是靠 prompt 裡的臨時指令。這帶來幾個優勢:

  • 知識可維護:最佳實踐集中在 SKILL.md 與相關資料夾內,更新即可生效。
  • 按需載入:模型判斷需要時才讀取,不污染上下文。
  • 人機協作:人負責打磨 skill 文件,模型負責執行。
  • 可重用:他人只需拿到寫好的 skills,就能獲得穩定結果。

可以把 Skills 理解為「公司規章制度 + 工具箱」的組合。規章告訴 AI:遇到某類任務時怎麼做、分幾步、每步用什麼工具;工具箱則放腳本和參考資料。展開來說,一個 Skill 是一個資料夾,內含三樣東西:

  1. SKILL.md 文件:以自然語言寫的「指令」,說明 Skill 的用途、使用情境與注意事項。
  2. 腳本:可為 Python、JavaScript 或其他語言,當 AI 需要「動手」時執行。
  3. 資源檔:例如參考文件、範本、設定檔,AI 執行時可查閱。

所以 skills 結合了高階 prompt + 工具呼叫,再配合像 clawhub 等發布平台,便具備了 skills 的發布、查詢、安裝、版本管理等功能,之前的許多問題得以解決。

比方說:函數呼叫像給你鍋鏟、一個鍋和調味料,你仍得知道何時倒油、何時下菜、如何翻鍋。Skills 則像給 AI 一本菜譜 + 十八般廚具,菜譜不只寫步驟,還說明各階段要用的工具。AI 只要照著做,就能做出成品。不論使用者是否有經驗,給定同一 Skill,成品應該是可重現的。

所以,根據上面敘述,簡單定義 Skill 為:可被語義觸發的能力包,包含領域知識、執行步驟、輸出規範與約束條件。

Skills 是如何實作的

Skill 的本質,是把磁碟上一段人類可讀的 markdown(SKILL.md),在呼叫瞬間編譯成模型能消化的 prompt blocks,然後注入對話上下文。因此我們可以將 skill 分為兩個大階段:一是 loading(載入)階段,另一是注入/呼叫階段。下面從 Claude Code 原始碼角度看這兩個階段如何實作,Claude Code 做了哪些事來實現這個設計。

加載

一、啟動入口:從命令列到 main()

當我們在命令列輸入 claude 時,下列流程會啟動。

關鍵程式位置:src/main.tsx:1918-1932

程式片段(已簡化):

// 同步註冊:必須在 getCommands 之前完成
if (process.env.CLAUDE_CODE_ENTRYPOINT !== 'local-agent') {
  initBuiltinPlugins();
  initBundledSkills();
}
// 並行啟動
const setupPromise = setup(preSetupCwd, ...);
const commandsPromise = worktreeEnabled ? null : getCommands(preSetupCwd);

為什麼要這樣設計:initBundledSkills() 是純記憶體的 push 操作(bundledSkills.push(skill)),必須在 getCommands() 啟動前完成,否則 getBundledSkills() 會回傳空陣列,導致內嵌技能遺失。

二、技能載入

當我們在 .claude/skills/ 目錄或專案目錄放入 skills 時,系統會透過 IO 讀取技能。整體流程如下:

載入順序(合併優先度由高到低):

  • bundledSkills(內嵌技能)
  • builtinPluginSkills(內建插件技能)
  • skillDirCommands(磁碟上的技能:使用者 / 專案 / 管理)
  • workflowCommands(工作流命令)
  • pluginCommands(插件命令)
  • pluginSkills(插件技能)
  • COMMANDS()(內建命令,非技能)

如果有重複,會做去重處理。

三、磁碟技能載入:getSkillDirCommands

關鍵程式位置:getSkillDirCommands

這是最核心的技能載入邏輯,負責從檔案系統讀取 SKILL.md。

3.1 目錄搜尋範圍

getSkillDirCommands(cwd) 會搜尋以下目錄:

  • Managed Skills ← /etc/claude/.claude/skills/(企業策略管理)
  • User Skills ← ~/.claude/skills/(使用者全域技能)
  • Project Skills ← .claude/skills/(專案級技能,向上搜尋目錄樹)
  • Additional Skills ← --add-dir 指定的目錄/.claude/skills/
  • Legacy Commands ← .claude/commands/(舊格式,向後相容)

3.2 並行載入策略

所有目錄的載入是並行的(使用 Promise.all),因為它們互不依賴:

程式片段(已簡化):

const [managedSkills, userSkills, projectSkills, additionalSkills, legacyCommands] =
  await Promise.all([
    loadSkillsFromSkillsDir(managedSkillsDir, 'policySettings'),
    loadSkillsFromSkillsDir(userSkillsDir, 'userSettings'),
    Promise.all(projectSkillsDirs.map(dir =>
      loadSkillsFromSkillsDir(dir, 'projectSettings'))),
    Promise.all(additionalDirs.map(dir =>
      loadSkillsFromSkillsDir(join(dir, '.claude/skills'), ...))),
    loadSkillsFromCommandsDir(cwd),
  ]);

3.3 單一技能目錄的載入流程

loadSkillsFromSkillsDir(basePath, source) 的流程:

  1. fs.readdir(basePath) — 讀取目錄列表
  2. 對每個 entry 並行處理:
    • 跳過非目錄項(目前只支援 skill-name/SKILL.md 格式)
    • 讀取 skill-name/SKILL.md 的內容
    • parseFrontmatter(content) — 解析 YAML frontmatter
      • 輸入示例: "---\ndescription: ...\n---\n# Skill body"
      • 輸出: { frontmatter: {...}, content: "# Skill body" }
    • parseSkillFrontmatterFields(...) — 提取結構化欄位(如 description, allowedTools, model, hooks, paths, effort...)
    • createSkillCommand(...) — 建構 Command 物件(閉包會捕獲 markdownContent,延遲到呼叫時才編譯)

3.4 去重機制

載入完成後,使用 realpath 解析檔案實際路徑來去重(以偵測符號連結或重複父目錄):

程式片段(已簡化):

const fileIds = await Promise.all(
  allSkillsWithPaths.map(({ filePath }) => getFileIdentity(filePath))
);

// 先到先得:優先順序由合併順序決定
// managed > user > project > additional > legacy
for (entry of allSkillsWithPaths) {
  if (seenFileIds.has(fileId)) continue;  // 跳過重複
  seenFileIds.set(fileId, skill.source);
  deduplicatedSkills.push(skill);
}

3.5 條件技能(Conditional Skills)

帶有 paths frontmatter 的技能不會立即啟用,而是存儲在 conditionalSkills Map 中。例如 frontmatter:

---
description: React 元件開發助手
paths: src/components/**, src/pages/**
---

當使用者操作的檔案路徑匹配 paths 模式時,技能會被激活並加入動態技能列表。激活流程使用 activateConditionalSkillsForPaths(filePaths, cwd) 並搭配 ignore 套件做 gitignore 式的匹配。

四、SKILL.md 檔案解析

4.1 Frontmatter 欄位格式

SKILL.md 的 frontmatter 範例(YAML):

---
# 基本資訊
name: 顯示名稱(可選,預設取目錄名)
description: 技能描述
argument-hint:
arguments: [arg1, arg2]

# 模型與行為控制
model: claude-sonnet-4-6       # 指定使用的模型
effort: high                  # low | medium | high | 整數
context: fork                 # fork = 獨立子進程執行,inline = 主執行緒
agent: agent-name             # 指定 agent 定義

# 權限控制
allowed-tools: [Bash, Read, Write]
user-invocable: true          # 使用者是否可透過 /name 呼叫
disable-model-invocation: false # 模型是否可透過 SkillTool 呼叫

# 條件啟動
paths: src/**/*.tsx           # 匹配檔案路徑時自動啟動

# 鉤子
hooks:
  PreToolUse:
    - command: "eslint $FILE"
      matcher: "Write|Edit"

# Shell 執行環境
shell: bash

# 版本
version: "1.0"
---

4.2 解析為 Command 物件

parseSkillFrontmatterFields() 會把 YAML 映射為結構化欄位,接著 createSkillCommand() 組裝成 Command 物件。示例(已簡化):

{
  type: 'prompt',              // 技能都是 prompt 類型
  name: 'skill-name',          // 目錄名(唯一識別)
  description: '...',          // 從 frontmatter 或正文第一行擷取
  source: 'projectSettings',   // 來源:userSettings / projectSettings / policySettings
  loadedFrom: 'skills',        // 載入方式:skills / bundled / plugin / mcp
  allowedTools: ['Bash'],      // 額外允許的工具
  model: 'claude-sonnet-4-6',  // 模型覆蓋
  effort: 'high',              // 努力程度
  userInvocable: true,         // 使用者可呼叫
  context: 'fork',             // 執行上下文
  hooks: {...},                // 鉤子設定
  paths: ['src/**/*.tsx'],     // 條件路徑
  contentLength: 1234,         // SKILL.md 內容長度
  skillRoot: '/path/to/skill', // 技能目錄路徑

  // 核心:延遲載入閉包
  getPromptForCommand: async (args, toolUseContext) => { ... }
}

五、延遲載入機制:getPromptForCommand

技能內容在啟動時只解析 frontmatter,SKILL.md 的正文內容則透過閉包捕獲,僅在使用者呼叫 /skill-name 時才執行完整的「編譯」過程。

設計權衡:在啟動時只解析 frontmatter(用於技能列表展示),正文編譯延遲到呼叫時。這使啟動速度變快,同時支援動態內容(例如 shell 指令在每次呼叫時執行以取得最新結果)。

六、動態技能發現

除了啟動時載入,系統也支援在對話或操作過程中動態發現新技能。

6.1 檔案操作觸發發現

當使用者讀寫檔案時,系統會沿檔案路徑向上搜尋 .claude/skills/ 目錄:

discoverSkillDirsForPaths(filePaths, cwd) 流程:

  • 對每個 filePath,從檔案父目錄開始,向上遍歷到 cwd(不含 cwd)
  • 每級檢查是否存在 .claude/skills/ 目錄,記錄到 dynamicSkillDirs(並去重)
  • 回傳新發現的目錄列表(按深度降序排列)

6.2 啟用流程

addSkillDirectories(dirs)

  • 對每個目錄調用 loadSkillsFromSkillsDir()
  • 深層路徑覆蓋淺層路徑(同名技能)
  • 存入 dynamicSkills Map
  • 發出 skillsLoaded.emit() → 通知快取失效

6.3 快取失效

動態技能載入後,需要清除相關的 memoization 快取:

clearCommandMemoizationCaches() {
  loadAllCommands.cache?.clear?.()
  getSkillToolCommands.cache?.clear?.()
  getSlashCommandToolSkills.cache?.clear?.()
  clearSkillIndexCache?.() // 技能搜尋索引
}

註:getCommands() 本身不被快取(因為每次要重新檢查 availability 與 isEnabled),但內部的 loadAllCommands 有 memoize,所以只要清除內層快取即可。

七、技能優先級總覽

優先級從高到低:

  1. managed skills(企業策略目錄 /etc/claude/.claude/skills/)
  2. user skills(使用者全域 ~/.claude/skills/)
  3. project skills(專案目錄 .claude/skills/,最近的優先)
  4. additional skills(--add-dir 指定目錄)
  5. legacy commands(舊格式 .claude/commands/)
  6. bundled skills(程式碼內嵌技能)
  7. builtin plugin skills(內建插件技能)
  8. plugin skills(第三方插件技能)

同名技能:先註冊者勝出(由合併順序決定)。檔案去重:realpath 相同的檔案只保留第一個。

八、關鍵資料流圖

(原文章有示意圖,請參閱原始連結)

調用

載入階段會把所有 skills 載入成 Command[] 陣列,等待被呼叫。從 Claude Code 源碼來看,使用者總共有 9 個入口可以呼叫 skills,9 種入口摘要如下:

  1. 使用者斜線命令:使用者輸入 /skill-name(執行模式:inline / fork) — 關鍵檔案:processSlashCommand.tsx
  2. immediate 命令(local-jsx):像 /config 等可直接執行(local)
  3. SkillTool.call():模型呼叫 Skill 工具(執行模式:inline / fork / remote)
  4. MCP Skill:透過 SkillTool 或 /server:skill(強制 fork)
  5. Cron / 定時任務:由 scheduled_tasks.json 或 /loop 觸發,進入隊列再執行
  6. Agent 預載入:Agent 定義中的 skills: 字段會預注入初始訊息
  7. Ultraplan 關鍵字輸入:若包含魔法關鍵字會重寫為 /ultraplan
  8. 初始 prompt:例如 -p "/skill ..." 或 agent initialPrompt 在提交時會觸發

(原文列出 9 種方式,這裡合併為重點條列以利閱讀。若需完整對應來源檔案位置,可參考原文內的連結。)

本文篇幅有限,後續以「使用者從斜線命令(/skill-name)呼叫 skills」的場景來詳述呼叫流程。

程式碼的呼叫流程圖如下

(原文章有流程圖,請參閱原始連結)

詳細程式碼呼叫流程

第 1 層:REPL.onSubmit() — 入口把關

[程式位置:REPL.tsx:3142]

當我們在 Claude Code 中輸入 /commit 並按下 Enter,PromptInput 會呼叫 onSubmit 回呼。這裡是整條鏈路的入口。

程式片段(已簡化):

// 1. 檢測是否為 immediate 命令(immediate: true 的 local-jsx 命令)
//    這些命令可以在 AI 正在處理時立即執行,不用排隊
if (!speculationAccept && input.trim().startsWith('/')) {
  const commandName = /* 從 input 擷取命令名 */
  const matchingCommand = commands.find(...)
  const shouldTreatAsImmediate = queryGuard.isActive &&
    (matchingCommand?.immediate || options?.fromKeybinding)

  if (matchingCommand && shouldTreatAsImmediate && matchingCommand.type === 'local-jsx') {
    // 直接執行,跳過隊列 — return early
  }
}

對於大多數 /skill-name(type 為 prompt),不會命中 immediate 快速通道,而是繼續向下處理。接著會清空輸入框、加入歷史紀錄,然後呼叫 handlePromptSubmit() 並載入 frontmatter。

第 2 層:handlePromptSubmit() — 隊列化

[程式位置:handlePromptSubmit.ts:120]

這個模組的核心職責是決定輸入是立即執行還是排隊等待。

程式片段(已簡化):

// 如果 AI 正在處理中(queryGuard.isActive),新的輸入進入隊列
if (queryGuard.isActive || isExternalLoading) {
  enqueue({ value: finalInput.trim(), mode, pastedContents })
  return // 不執行,等 AI 空閒後 dequeue
}
// 否則立即執行
await executeUserInput({ queuedCommands: [cmd], ... })

executeUserInput 是實際執行的核心函數,內部會呼叫 processUserInput()。

第 3 層:processUserInput() — 模式路由

[程式位置:processUserInput.ts:533]

這個函式是大型的路由器,根據輸入模式分發到不同處理器:

// Bash 模式 → processBashCommand()
if (mode === 'bash') { ... }

// Slash command → processSlashCommand()
if (inputString !== null && !effectiveSkipSlash && inputString.startsWith('/')) {
  const { processSlashCommand } = await import('./processSlashCommand.js')
  const slashResult = await processSlashCommand(inputString, ...)
  return slashResult
}

// 其他普通文本 → processTextPrompt()

由於 /commit 以 / 開頭,會命中 slash command 分支。

第 4 層:processSlashCommand() — 命令解析與分發

[程式位置:processSlashCommand.tsx:309]

這是整條鏈路中最關鍵的分發層。

Step 4.1:解析命令名

const parsed = parseSlashCommand(inputString)
// 若輸入為 "/commit fix: 修復bug"
// parseSlashCommand 會輸出:{ commandName: 'commit', args: 'fix: 修復bug', isMcp: false }

parseSlashCommand 的解析規則:

  • 去掉 / 前綴
  • 第一個空格前為命令名
  • 支援 MCP 命令格式:/mcp:tool (MCP) args

Step 4.2:查找命令登記表

if (!hasCommand(commandName, context.options.commands)) {
  // 命令不存在 → 判斷是檔案路徑還是未知命令
  if (looksLikeCommand(commandName) && !isFilePath) {
    return { messages: [...], shouldQuery: false, resultText: 'Unknown skill: xxx' }
  }
  // 可能是檔案路徑(如 /var/log)→ 當作普通文本發給模型
  return { messages: [...], shouldQuery: true }
}

Step 4.3:按 type 分發

Claude Code 有三種命令類型(types/command.ts),不同類型會執行不同動作:

  • prompt:展開為文本,送給 AI 模型(例如 /commit, skill 類命令)
  • local:在本地執行,返回文本結果
  • compact / local-jsx:渲染互動式 UI 組件(例如 /config, /model)

對於 /skill-name(type = prompt),會進入 getMessagesForPromptSlashCommand()。

第 5 層:getMessagesForPromptSlashCommand() — 技能內容載入

[processSlashCommand.tsx:827]

這是 skill 的核心 —— 將 SKILL.md 的內容載入為 prompt。

Step 5.1:檢查 context === 'fork'

if (command.context === 'fork') {
  return await executeForkedSlashCommand(...)
  // 在獨立子 agent 中執行,有自己的上下文與 token 預算,預設不是 fork
}

Step 5.2:載入技能內容

const result = await command.getPromptForCommand(args, context)

getPromptForCommand 在技能註冊時已定義(見 loadSkillsDir.ts:344),會做以下事:

  • 變數替換:把 $ARGUMENTS, ${CLAUDE_SKILL_DIR}, ${CLAUDE_SESSION_ID}` 替換為實際值
  • Shell 執行:若 SKILL.md 中有 !command 格式的 shell 注入,會先執行
  • 回傳 ContentBlockParam[]:包含最終展開後的文本內容

Step 5.3:構造訊息列表

const messages = [
  createUserMessage({ content: metadata }),        // 命令元資料:名稱、參數
  createUserMessage({ content: skillContent, isMeta: true }),  // SKILL.md 內容(對使用者隱藏)
  ...attachmentMessages,                           // 附件訊息
  createAttachmentMessage({                       // 權限宣告:allowedTools
    type: 'command_permissions',
    allowedTools: additionalAllowedTools,
  }),
]
return {
  messages,
  shouldQuery: true,    // ← 關鍵:告訴上層需要呼叫 AI 模型
  allowedTools: additionalAllowedTools,
  model: command.model,
  effort: command.effort,
  command,
}

第 6 層:onQuery() — 發送給 AI

[程式位置:handlePromptSubmit.ts:560]

回到 executeUserInput():

await onQuery(
  newMessages,          // 包含 skill 內容的訊息列表
  abortController,
  shouldQuery,          // true → 需要呼叫模型
  allowedTools ?? [],   // skill 宣告的額外工具權限
  model ?? mainLoopModel,
  onBeforeQuery,
  primaryInput,
  effort,
)

onQuery 會把這些訊息追加到對話歷史,然後呼叫 Claude API。此時 SKILL.md 的全部內容作為一條 user message 發送給模型,模型會依此指示執行。

不成熟的思考

一、Skill 在解決什麼問題?

目前 LLM 的三個缺陷構成了 Skill 系統存在的全部理由:

  • 輸出不一致性(同一輸入產生不同輸出)
  • 結構漂移(長對話中偏離初始意圖)
  • 當缺乏上下文時會「瞎猜」或產生幻覺

Skill 的對抗手段摘要:

  • 輸出不一致性 → Prompt 範本 + 參數化注入 → 固定行為邊界
  • 結構漂移 → Frontmatter 約束 + hooks 校驗 → 結構護欄
  • 瞎猜問題 → when_to_use + paths 條件啟動 → 精確觸發域

更深層的問題:為什麼 Prompt 範本能讓機率模型收斂?答案在程式碼中。看 createSkillCommandgetPromptForCommand 閉包(程式片段已簡化):

async getPromptForCommand(args, toolUseContext) {
  let finalContent = baseDir
    ? `Base directory for this skill: ${baseDir}\n\n${markdownContent}`
    : markdownContent

  finalContent = substituteArguments(finalContent, args, true, argumentNames)
  finalContent = finalContent.replace(/\$\{CLAUDE_SKILL_DIR\}/g, skillDir)
  finalContent = finalContent.replace(/\$\{CLAUDE_SESSION_ID\}/g, getSessionId())
  finalContent = await executeShellCommandsInPrompt(finalContent, ...)
  return [{ type: 'text', text: finalContent }]
}

這不是簡單的「給 LLM 一個範本」。這是一條「編譯管線」——把宣告式的 Markdown 編譯成確定性的執行時上下文。每一層轉換都在縮小 LLM 的決策空間:

  1. Base directory 注入:錨定檔案系統上下文,消除路徑猜測
  2. 參數替換:將使用者輸入映射到預定義的槽位,限制輸入域
  3. 環境變數注入:執行時狀態的確定性綁定
  4. Shell 指令執行:動態注入即時資料,避免 LLM 憑記憶猜測
  5. 返回 ContentBlockParam[]:結構化輸出,消除格式不確定性

因此 Skill 的本質不是「範本」,而是一個 Prompt 編譯器 —— 把高熵的人類意圖,經由多層轉換,編譯成低熵的結構化指令。

二、它不是一個,而是三個系統

從程式碼可以辨識出三個正交的子系統,各自有不同設計目標與權衡:

2.1 宣告層:SKILL.md 作為 DSL

SKILL.md 不只是 Markdown,而是一個領域特定語言(DSL)。frontmatter 每一欄位都在回答一個問題:這個 Skill 需要什麼運行時保證?

範例欄位與設計意圖:

  • paths:何時啟動?(延遲載入,減少上下文污染)
  • allowed-tools:能做什麼?(最小權限原則)
  • model:用哪個模型?(成本-品質權衡)
  • effort:要多深的推理?(推理預算控制)
  • context: fork:是否隔離執行?(故障半徑控制)
  • hooks:誰來校驗?(外部護欄)

但目前 DSL 缺少像 depends_oncomposes 這樣的欄位。Skill 之間的組合目前只能透過 SkillTool 在 prompt 層面隱式實現,沒有宣告式依賴關係。若加入 depends_on,有可能帶來類似 node_modules 的依賴地獄;此外,開放的 skills 生態已出現各種「有害」或「帶毒」的 skills,宣告式依賴可能讓這些問題更難發現。

2.2 編譯層:getPromptForCommand 作為編譯管線

延遲編譯是個巧妙設計。啟動時只解析 frontmatter(estimateSkillFrontmatterTokens 只估算元資訊的 token),正文編譯延遲到呼叫時。這帶來幾點好處:

  • 啟動速度不受 Skill 內容大小影響
  • Shell 指令在每次呼叫時獲取最新結果(非啟動時快照)
  • 代價是首次呼叫時會有延遲(需要執行編譯管線)

這是經典的延遲求值(Lazy Evaluation)策略,在 prompt 工程中的應用很有趣。

2.3 執行時層:Command 物件作為執行時表示

createSkillCommand 回傳的 Command 物件是一個閉包 —— 捕獲了 markdownContent 與所有 frontmatter 欄位,但不立即執行。直到 getPromptForCommand 被呼叫,編譯管線才啟動。此設計的推論:

  • Skill 的內容在記憶體中只有一份拷貝(閉包捕獲引用)
  • 每次呼叫會產生新的編譯結果
  • 記憶體效率高:N 個 Skill 只佔 N 份 Markdown
  • CPU 效率:只有被呼叫的 Skill 才消耗編譯時間
  • 一致性代價:若同一 Skill 的兩次呼叫中,間隔執行了 shell 命令且結果改變,則輸出也會不同(這是可測試且有趣的行為)

三、Skill 系統無法解決的問題

3.1 組合爆炸問題

目前 Skill 的組合是隱式的 —— 一個 Skill 可以透過 SkillTool 呼叫另一個 Skill,但:

  • 沒有宣告式的組合關係(例如 A composes B, C
  • 沒有組合後的 token 預算管理
  • 沒有組合衝突檢測(兩個 Skill 對同一檔案可能給出矛盾指示)
  • 沒有 DAG(有向無環圖)調度(Skill A 的輸出作為 Skill B 的輸入)

若引入宣告式組合,Skill 系統會從「Prompt 範本庫」進化為「Prompt 計算圖」。每個 Skill 為節點,composes 定義邊,執行時按拓撲順序執行,層層輸出作為下一層輸入。這能解決:

  • Token 預算:DAG 調度可精算每層預算
  • 衝突檢測:編譯期可靜態分析輸出衝突
  • 可觀測性:每層的輸入輸出可獨立審查

但複雜度也會上升。是否引入,取決於這類痛點對用戶的實際影響。

3.2 驗證閉環缺失

從程式碼看,Skill 的執行路徑是單向的:

SKILL.md → parseFrontmatter → createSkillCommand → getPromptForCommand → LLM → 輸出

沒有回饋迴路。如果 LLM 的輸出偏離 Skill 預期,系統無法自動:

  • 偵測偏離
  • 回滾到上個檢查點
  • 動態調整 Prompt 參數

雖然 hooks 可在工具調用後做部分校驗(例如 PostToolUse),但它只能攔截工具呼叫,無法攔截 LLM 的純文字輸出。理想的結構應包含驗證與回滾:

SKILL.md → 編譯 → 執行 → 校驗 → { 通過 → 輸出 | 失敗 → 回退 + 重新編譯 }

要達成此目標,需要為 Skill 引入 output_schemavalidation 欄位,明確定義預期輸出結構。這也能讓 Skill 編寫者撰寫可自動化的測試,而非只能邊寫邊調。

3.3 版本與演進問題

frontmatter 有 version 欄位,但程式碼中並未利用它實作任何版本控制邏輯,它僅是個標籤。當 Skill A 依賴 Skill B 的 v1 行為,B 升級到 v2 時,沒有任何機制保證相容性。在團隊協作中,這尤其危險 —— 一人的 Skill 升級可能悄然破壞其他人的工作流程。

四、從產研流程視角重新審視 Skill

4.1 當前現實

目前 Skill 自動化的多半是原子操作:/commit, /review, /test 等。這些是產研流程中的「葉節點」——它們多半不依賴其他操作的輸出。

流程示意:

需求 → 技術方案 → 編碼 → 測試 → 上線
│ │ │ │
│ │ ├─ /commit ├─ /deploy
│ │ ├─ /review │
│ │ └─ /test │

中間很多節點尚未被 Skill 深度介入。

4.2 Skill 理論上能覆蓋的範圍(最大值)

若以產研流程每個階段來映射 Skill 能力,理論可能如下(摘要):

  • 需求:從 PRD 抽取關鍵需求、識別模糊點、生成問題清單 → 需要結構化需求輸入與歷史需求庫
  • 技術方案:/ultraplan 類技術方案建議、架構依賴分析、風險評估 → 需專家級知識庫與依賴圖
  • 編碼:/commit、/review、TDD skill 協助自動編碼與增量校驗 → 需要方案到代碼的映射規則
  • 測試:自動生成測試、覆蓋率分析、邊界測試策略 → 需測試基礎設施支援
  • 上線:無變更影響分析、回滾方案、灰度策略、CI/CD 整合 → 需部署與監控整合

也就是說,若以領域內專家來編寫對應的 skills,並以自動化或人工方式串起,每個產研節點都可能被 Skill 覆蓋。

4.3 關鍵約束:Skill ≠ 自動化(完全替代)

在把 skills 用於產研流程時,必須放棄一個直覺性的誤解:使用 skills 並不等於完全自動化或直接節省時間。相反,Skill 的核心價值在於「與 AI 交互標準化」——讓不同人能產出同等品質的產出。標準化帶來:

  1. 降低隨機性:在沒有 Skill 時,程式碼審查品質取決於審查者經驗;有 Skill 時,審查流程由 Skill 保證最低品質。
  2. 知識傳承:資深工程師的方法論可以被編碼為 Skill,而不是只存在腦中。
  3. 可稽核性:Skill 的執行路徑是確定性且可回溯的。

但這也有隱含前提:Skill 編寫者的方法論本身必須正確。若方法論有誤,Skill 將以工業化速度擴散這些錯誤。因此,關鍵不是「是否要讓 AI 貫穿整個產研流程」,也不是「是否要使用 skill」,而是團隊或編寫 skill 的人所採用的方法論是否正確、是否適合。否則整個方向可能越走越偏。


原文出處:https://juejin.cn/post/7625838952655912994


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝9   💬11   ❤️3
564
🥈
我愛JS
📝2   💬7   ❤️2
149
🥉
💬1  
4
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登