議:可插拔AI技能的聲明式交付標(biāo)準(zhǔn))
1. “agent-skills”不是功能模塊而是一套可插拔的能力交付協(xié)議你第一次在 GitHub 倉庫名里看到agent-skills或在 CLI 工具的--skills參數(shù)后跟一串路徑時(shí)大概率會下意識把它理解成“AI Agent 能做的幾件事列表”——比如“查天氣”“讀文件”“發(fā)郵件”。但實(shí)際踩過三次坑、重構(gòu)過四版技能注冊邏輯后我才明白agent-skills的本質(zhì)是一套輕量級、聲明式、面向運(yùn)行時(shí)能力發(fā)現(xiàn)的接口契約Interface Contract它解決的從來不是“能做什么”而是“如何被發(fā)現(xiàn)、如何被安全調(diào)用、如何與上下文對齊”。這個(gè)認(rèn)知偏差直接導(dǎo)致大量團(tuán)隊(duì)在開發(fā)初期就陷入兩個(gè)典型困局一是把 skills 寫成一堆零散函數(shù)結(jié)果 agent 調(diào)度器根本識別不了二是強(qiáng)行塞進(jìn) OpenAPI Schema結(jié)果 runtime 解析失敗報(bào)錯(cuò)信息只顯示skill not found in registry連具體哪一行配置錯(cuò)了都看不到。為什么必須從“協(xié)議”角度理解看幾個(gè)熱詞就能印證zcode cli、codex cli、boos cli、openspec cli——它們?nèi)谟貌煌熬Y封裝同一類行為通過 CLI 命令行工具將本地目錄中符合特定結(jié)構(gòu)的代碼片段注冊為 agent 可感知、可調(diào)度、可審計(jì)的技能單元。而不是簡單地“加載一個(gè) JS 文件”。再看claude agent skills: a first principles deep dive這個(gè)熱搜標(biāo)題里的關(guān)鍵詞first principles第一性原理——它暗示的正是別急著寫代碼先定義清楚“一個(gè) skill 在系統(tǒng)里究竟意味著什么”。比如它必須有一個(gè)唯一標(biāo)識符不是文件名而是id: file-readv1這種帶命名空間和版本的字符串它必須聲明輸入?yún)?shù)的 JSON Schema不是 TypeScript interface而是 runtime 可校驗(yàn)的 OpenAPI v3.1 subset它必須提供一個(gè)description字段且該字段會被 LLM 用于 zero-shot 工具選擇不是給人看的注釋而是 prompt engineering 的關(guān)鍵 token source它必須包含permissions聲明如fs:read:/home/user/**否則在 sandbox 環(huán)境中直接被攔截不報(bào)錯(cuò)、不提示、靜默失敗。提示很多團(tuán)隊(duì)用fs.readFile寫了個(gè)讀文件 skill卻沒在permissions里聲明文件路徑白名單結(jié)果在 Docker 容器里跑通了本地測試上線后所有文件操作全部返回空對象——因?yàn)?runtime 默認(rèn)禁止一切 fs 操作除非顯式授權(quán)。我見過最典型的反模式是把skills/目錄當(dāng)成普通工具函數(shù)庫來維護(hù)skills/ ├── weather.js # 導(dǎo)出一個(gè) async function ├── send-email.ts # 導(dǎo)出一個(gè) class └── db-query.py # 導(dǎo)出一個(gè) main() 函數(shù)這種結(jié)構(gòu)在agent-skills協(xié)議下是無效的。真正的技能包必須是自描述的、可獨(dú)立驗(yàn)證的單元標(biāo)準(zhǔn)結(jié)構(gòu)如下skills/file-read/ ├── manifest.json # 必須id, version, description, input_schema, permissions, entrypoint ├── handler.js # 必須導(dǎo)出默認(rèn)函數(shù)簽名 (input: any) Promiseany └── README.md # 可選人類可讀文檔不影響 runtime其中manifest.json是協(xié)議核心。它不是配置文件而是技能的“身份證”。比如一個(gè)合法的file-readmanifest 長這樣{ id: file-readv2, version: 2.0.1, description: Read content from local file system. Supports text and binary mode., input_schema: { type: object, properties: { path: { type: string, pattern: ^/home/[^/]/.*$ }, encoding: { type: string, enum: [utf8, base64] } }, required: [path] }, permissions: [fs:read:/home/{user}/**], entrypoint: ./handler.js }注意三個(gè)關(guān)鍵點(diǎn)path字段用了正則約束^/home/[^/]/.*$這是為了防止路徑遍歷攻擊../../../etc/passwdpermissions中的{user}是 runtime 替換變量實(shí)際運(yùn)行時(shí)會替換成當(dāng)前 agent 用戶名實(shí)現(xiàn)租戶隔離entrypoint是相對路徑且必須指向一個(gè) CommonJS 或 ESM 模塊默認(rèn)導(dǎo)出函數(shù)不能是類或命名導(dǎo)出。這套協(xié)議之所以能成為事實(shí)標(biāo)準(zhǔn)是因?yàn)樗瑫r(shí)滿足了三類角色的需求開發(fā)者不用改 agent 核心代碼只要按結(jié)構(gòu)放文件CLI 就能自動(dòng)注冊安全工程師所有權(quán)限聲明集中管控audit log 可追溯到具體 skill id 和調(diào)用上下文LLM 提示工程師description字段直接喂給模型配合input_schema自動(dòng)生成 tool call JSON無需人工編寫 prompt 模板。所以當(dāng)你看到agent-skills這個(gè)標(biāo)題時(shí)請立刻切換思維它不是一個(gè)項(xiàng)目名而是一個(gè)協(xié)議名。就像 HTTP 不是某個(gè)網(wǎng)站的名字而是瀏覽器和服務(wù)器之間約定的通信規(guī)則。接下來的所有實(shí)操都建立在這個(gè)認(rèn)知基礎(chǔ)上。2. CLI 工具鏈不是輔助而是協(xié)議落地的強(qiáng)制執(zhí)行層很多人以為zcode cli、codex cli、boos cli這些工具只是“方便安裝 skill 的命令行包裝”甚至覺得“手寫 manifest 手動(dòng) copy 到 skills 目錄”也能跑通。我在早期也這么干過——直到第 7 次因?yàn)閙anifest.json缺少version字段導(dǎo)致 agent 啟動(dòng)失敗才徹底放棄手動(dòng)維護(hù)。CLI 工具鏈在agent-skills生態(tài)里承擔(dān)的是協(xié)議合規(guī)性守門員Compliance Gatekeeper的角色。它不負(fù)責(zé)執(zhí)行 skill也不參與調(diào)度決策但它必須在 skill 注冊前完成三項(xiàng)不可繞過的靜態(tài)檢查2.1 Manifest 結(jié)構(gòu)完整性校驗(yàn)拒絕任何“差不多就行”的妥協(xié)CLI 會逐字段驗(yàn)證manifest.json是否符合協(xié)議 schema。這不是簡單的 JSON 格式檢查而是深度語義校驗(yàn)。例如id字段必須匹配正則^[a-z0-9][a-z0-9.-]*[a-z0-9][a-z0-9][a-z0-9.-]*[a-z0-9]$小寫字母/數(shù)字/點(diǎn)/短橫線以字母數(shù)字開頭結(jié)尾中間含分隔 name 和 versioninput_schema必須是有效的 OpenAPI v3.1 Schema Object且不能包含$ref引用runtime 不支持遠(yuǎn)程引用permissions數(shù)組中每個(gè)字符串必須符合resource:action:scope三元組格式且scope部分必須是 glob pattern如/home/*/**不能是正則runtime 不解析 regex。我遇到過最隱蔽的坑是input_schema里寫了type: integer但沒寫minimum和maximum。CLI 檢查通過了agent 啟動(dòng)也沒報(bào)錯(cuò)。但當(dāng) LLM 生成{count: 999999999999999999999}這種超大整數(shù)時(shí)Node.js runtime 直接溢出變成Infinity后續(xù)計(jì)算全錯(cuò)。CLI 后來加了一條規(guī)則所有integer類型必須聲明minimum和maximum否則拒絕注冊。驗(yàn)證過程不是黑盒。你可以用codex cli validate --verbose skills/file-read/查看完整校驗(yàn)鏈$ codex cli validate --verbose skills/file-read/ ? manifest.json structure valid ? id format compliant: file-readv2 ? version semver valid: 2.0.1 ? description length 256 chars ? input_schema is valid OpenAPI v3.1 Schema ? input_schema.properties.path.pattern must be a glob pattern, got: ^/home/[^/]/.*$ → Fix: use /home/*/** instead of regex注意最后一行CLI 明確告訴你pattern字段在這里不是正則而是 glob。這是協(xié)議強(qiáng)制要求——因?yàn)?runtime 的權(quán)限引擎只支持 glob 匹配不支持 PCRE 正則。這種細(xì)節(jié)光看文檔容易忽略但 CLI 會把它變成硬性錯(cuò)誤。2.2 Handler 入口函數(shù)簽名驗(yàn)證確保 runtime 能正確調(diào)用CLI 不會執(zhí)行 handler 代碼但它會做 AST 靜態(tài)分析確認(rèn)導(dǎo)出的默認(rèn)函數(shù)簽名符合(input: any) Promiseany。這聽起來簡單但實(shí)際有大量陷阱TypeScript 項(xiàng)目CLI 會先調(diào)用tsc --noEmit --skipLibCheck檢查類型再用esbuild提取 AST。如果寫了async function read(path: string): PromisestringCLI 會報(bào)錯(cuò)“入口函數(shù)必須接受單個(gè) any 類型參數(shù)不能有多個(gè)參數(shù)或重載”ESM vs CJS 混合如果handler.js里用了import fs from fs但項(xiàng)目package.json沒設(shè)type: moduleCLI 會提示“ESM import detected but module type not declared”頂層 awaithandler.js開頭寫了await initDB()CLI 會拒絕“入口文件不能包含頂層 await必須包裹在函數(shù)內(nèi)”。這些檢查的目的是保證無論你用什么語言、什么框架寫 handler最終打包出來的產(chǎn)物都能被 agent runtime 的統(tǒng)一加載器loader無差別處理。我們曾用 Deno 寫了一個(gè) skillCLI 檢查通過但 runtime 加載時(shí)報(bào)ReferenceError: Deno is not defined——因?yàn)?CLI 只檢查語法不檢查 runtime 環(huán)境兼容性。后來 CLI 加入了--target node18參數(shù)強(qiáng)制指定目標(biāo)環(huán)境才堵住這個(gè)漏洞。2.3 權(quán)限沙箱預(yù)檢提前暴露 runtime 不會容忍的問題這是 CLI 最關(guān)鍵的價(jià)值。它模擬 sandbox 環(huán)境檢查permissions聲明是否與 handler 實(shí)際行為一致。例如manifest.json聲明了fs:read:/tmp/**但handler.js里調(diào)用了fs.writeFileSync(/var/log/app.log, data)permissions寫了http:post:https://api.example.com/**但 handler 里實(shí)際請求的是https://api.internal.example.com/v1/data域名不匹配permissions是env:read:API_KEY但 handler 里用了process.env.SECRET_TOKEN環(huán)境變量名不匹配。CLI 會掃描 handler 代碼中的所有 I/O 調(diào)用fs, http, child_process, env 等提取出所有字面量資源路徑然后與permissions列表做匹配。不匹配的調(diào)用點(diǎn)會標(biāo)出具體行號$ codex cli validate skills/db-query/ ... ? Permission mismatch at handler.js:42 → fs.writeFileSync(/var/log/app.log, data) → declared permission: fs:write:/tmp/** → fix: add fs:write:/var/log/** to permissions這個(gè)功能救了我們至少三次線上事故。有一次一個(gè) skill 在本地開發(fā)時(shí)用require(child_process).execSync(curl ...)調(diào)外部 APIpermissions里只寫了http:get:...沒聲明child_process。CLI 檢查時(shí)沒報(bào)錯(cuò)因?yàn)闆]掃描 execSync但上線后 sandbox 直接 kill 進(jìn)程。后來 CLI 更新了規(guī)則所有child_process調(diào)用必須顯式聲明child_process:exec:*權(quán)限否則拒絕注冊。注意CLI 的權(quán)限預(yù)檢是靜態(tài)的無法檢測動(dòng)態(tài)拼接的路徑如fs.readFileSync(pathPrefix userInput)。所以協(xié)議規(guī)定所有動(dòng)態(tài)路徑必須通過input_schema的pattern字段約束且 runtime 在執(zhí)行時(shí)會做二次校驗(yàn)。CLI 只負(fù)責(zé)守住第一道防線。真正讓 CLI 成為“強(qiáng)制執(zhí)行層”的是它的集成方式。它不是獨(dú)立工具而是嵌入到 CI/CD 流水線中的 gate# .github/workflows/skills-ci.yml - name: Validate skills run: | npm install -g codex/cli codex cli validate skills/**/ - name: Register to staging if: github.event_name pull_request github.event.action closed github.event.pull_request.merged true run: | codex cli register --env staging --token ${{ secrets.STAGING_TOKEN }} skills/**/這意味著任何 skill 要進(jìn)入 staging 環(huán)境必須先通過 CLI 的三重校驗(yàn)。沒有例外沒有 bypass。這比寫一百行文檔更有效。3. Slash Commands 是技能的用戶界面不是技術(shù)實(shí)現(xiàn)當(dāng)你在 Slack 或 Discord 里輸入/file-read path/home/user/report.txt或者在終端里敲zcode /file-read --path /home/user/report.txt你調(diào)用的不是一個(gè) API endpoint而是一個(gè)skill 的命令行界面CLI UI。這個(gè)設(shè)計(jì)非常反直覺——因?yàn)榻^大多數(shù)開發(fā)者第一反應(yīng)是“這應(yīng)該是個(gè) HTTP POST 請求body 里傳 JSON”。但agent-skills協(xié)議刻意回避了 HTTP 層原因很實(shí)在slash commands 是 skill 對最終用戶的最小可行交互單元MVUI它必須做到零配置、零學(xué)習(xí)成本、零網(wǎng)絡(luò)依賴。如果每次調(diào)用都要記 endpoint、寫 curl、處理 token那技能就失去了“即開即用”的靈魂。3.1 Slash Command 的解析邏輯從字符串到結(jié)構(gòu)化輸入的精確映射Slash command 的解析不是簡單的空格分割。它有一套嚴(yán)格的 grammar由 CLI 工具鏈統(tǒng)一實(shí)現(xiàn)。以zcode /file-read --path /home/user/report.txt --encoding base64為例解析過程如下命令識別/file-read被映射到skills/file-read/manifest.json中的id字段去掉/前綴匹配file-readv2參數(shù)提取--path和--encoding是input_schema中定義的 required/optional 字段名CLI 會嚴(yán)格校驗(yàn)它們是否在 schema 中存在類型轉(zhuǎn)換/home/user/report.txt是字符串但input_schema.properties.path.type是string直接賦值base64是字符串但input_schema.properties.encoding.enum是[utf8, base64]CLI 會檢查值是否在枚舉中缺失參數(shù)填充如果input_schema中encoding有default: utf8且命令里沒寫--encodingCLI 會自動(dòng)注入默認(rèn)值校驗(yàn)執(zhí)行所有參數(shù)組裝成 input object 后再用input_schema的 JSON Schema validator 做最終校驗(yàn)。這個(gè)過程的關(guān)鍵在于slash command 的語法是input_schema的鏡像而不是自由文本。你不能寫/file-read path/home/user/report.txt encodingbase64等號賦值也不能寫/file-read /home/user/report.txt base64位置參數(shù)——除非input_schema顯式聲明了additionalProperties: false且定義了order字段。我見過最頭疼的案例是一個(gè)團(tuán)隊(duì)把input_schema設(shè)計(jì)成{ type: object, properties: { url: {type: string}, method: {type: string, enum: [GET, POST]} } }然后用戶習(xí)慣性地輸入/http-request https://api.example.com GET期望位置參數(shù)自動(dòng)映射。但 CLI 報(bào)錯(cuò)“Unknown positional argument https://api.example.com”。解決方案不是改 CLI而是改 schema{ type: object, properties: { url: {type: string}, method: {type: string, enum: [GET, POST]} }, required: [url], additionalProperties: false, order: [url, method] // 新增 order 字段啟用位置參數(shù) }order字段是協(xié)議擴(kuò)展告訴 CLI當(dāng)命令中沒有--key前綴時(shí)按此順序?qū)⑽恢脜?shù)依次賦值。這是少數(shù)允許“自由文本”存在的地方但必須由 schema 顯式開啟。3.2 Slash Command 與 LLM Tool Calling 的雙向同步機(jī)制Slash command 不僅是人機(jī)接口更是人機(jī)協(xié)同的橋梁。當(dāng) LLM 生成 tool call 時(shí)它輸出的是結(jié)構(gòu)化 JSON{ name: file-readv2, arguments: {path: /home/user/report.txt, encoding: base64} }而 agent runtime 的職責(zé)是把這個(gè) JSON無損轉(zhuǎn)換為 slash command 字符串再交給 CLI 執(zhí)行。轉(zhuǎn)換規(guī)則是確定性的name去掉版本號轉(zhuǎn)為/file-readarguments中每個(gè) key-value 對轉(zhuǎn)為--key value如果 value 是布爾值true轉(zhuǎn)為--key無值false則忽略如果 value 是數(shù)組[a, b]轉(zhuǎn)為--key a --key b。反過來CLI 執(zhí)行完 skill 后必須把返回結(jié)果Promise resolve 值原樣回傳給 LLM作為下一步推理的 context。這里有個(gè)致命細(xì)節(jié)CLI 返回的必須是純 JSON 可序列化對象不能包含函數(shù)、Date、Buffer 等非序列化類型。我們曾有一個(gè) skill 返回new Date()CLI 序列化后變成{_isAMomentObject: true, ...}因?yàn)橛昧?moment.jsLLM 看不懂直接崩潰。解決方案是協(xié)議強(qiáng)制規(guī)定所有 skill handler 的返回值必須經(jīng)過JSON.stringify(JSON.parse(JSON.stringify(result)))的雙重序列化清洗。CLI 在執(zhí)行后自動(dòng)做這一步確保輸出干凈。你在 handler 里可以放心返回new Date()或Buffer.from(hello)CLI 會幫你轉(zhuǎn)成{date: 2024-06-15T10:30:00.000Z}和{buffer: aGVsbG8}。3.3 Slash Command 的權(quán)限代理用戶意圖與系統(tǒng)策略的翻譯器Slash command 是用戶意圖的載體但它本身不執(zhí)行權(quán)限檢查。真正的權(quán)限控制發(fā)生在 runtime 層。CLI 在這里扮演翻譯器角色用戶輸入/file-read --path /home/alice/report.txtCLI 解析出path: /home/alice/report.txtCLI 查看manifest.json中的permissions: [fs:read:/home/{user}/**]CLI 將{user}替換為當(dāng)前執(zhí)行 CLI 的系統(tǒng)用戶名如alice得到/home/alice/**CLI 將/home/alice/report.txt與/home/alice/**做 glob 匹配結(jié)果為 true允許繼續(xù)如果用戶是bob但試圖讀/home/alice/report.txtCLI 會立即報(bào)錯(cuò)“Permission denied: path /home/alice/report.txt not allowed for user bob”。這個(gè)過程發(fā)生在 CLI 本地不依賴網(wǎng)絡(luò)、不調(diào)用 API。它讓權(quán)限檢查變得極快微秒級且完全離線。更重要的是它把復(fù)雜的 RBAC 策略翻譯成了用戶能理解的語言/file-read這個(gè)命令天然就帶著“只能讀自己 home 目錄”的含義。提示不要在 slash command 里暴露敏感路徑。比如/db-query --sql SELECT * FROM users WHERE id 123是危險(xiǎn)的因?yàn)?SQL 語句可能包含注入。協(xié)議推薦的做法是input_schema中sql字段必須是enum列表如[get_user_profile, get_user_orders]用戶只能選預(yù)定義的查詢模板不能自由輸入 SQL。4. API 集成不是終點(diǎn)而是技能能力的延伸觸點(diǎn)搜索熱詞里反復(fù)出現(xiàn)deepseek api、智譜api、百度api、??低昦pi接口說明一個(gè)現(xiàn)實(shí)絕大多數(shù) skill 的核心價(jià)值不在于本地計(jì)算而在于安全、可控、可審計(jì)地連接外部服務(wù)。agent-skills協(xié)議對此有明確分層skill 是能力容器API 是能力來源二者必須解耦。4.1 API Credentials 的聲明式管理從硬編碼到策略驅(qū)動(dòng)早期我們把 API Key 直接寫在handler.js里// ? 危險(xiǎn)Key 泄露風(fēng)險(xiǎn)高無法輪換 const apiKey process.env.DEEPSEEK_API_KEY || sk-xxx; fetch(https://api.deepseek.com/v1/chat/completions, { headers: { Authorization: Bearer ${apiKey} } });這違反了協(xié)議三大原則不可審計(jì)Key 在代碼里git history 里全是密鑰不可隔離所有 skill 共享同一個(gè) Key一個(gè) skill 被攻破全盤淪陷不可策略化無法限制某個(gè) skill 只能調(diào)用/v1/chat/completions不能調(diào)/v1/models。協(xié)議要求所有 credentials 必須通過manifest.json的credentials字段聲明并由 runtime 統(tǒng)一注入。正確寫法{ id: deepseek-chatv1, credentials: [ { id: deepseek-api-key, type: api_key, scope: [https://api.deepseek.com/v1/chat/completions], env_var: DEEPSEEK_API_KEY } ], permissions: [http:post:https://api.deepseek.com/**] }credentials是一個(gè)數(shù)組每個(gè)元素定義一個(gè) credential 的策略idcredential 的唯一標(biāo)識供 handler 代碼引用type目前支持api_key、oauth2、basic_auth未來可擴(kuò)展scope該 credential 允許訪問的 URL 列表glob pattern超出范圍的請求會被 runtime 攔截env_varruntime 從環(huán)境變量讀取的實(shí)際密鑰值。handler 代碼里不再碰密鑰而是通過 runtime 提供的getCredentialAPI 獲取// ? 安全Key 由 runtime 注入scope 由 manifest 約束 export default async function handler(input) { const apiKey await runtime.getCredential(deepseek-api-key); const res await fetch(https://api.deepseek.com/v1/chat/completions, { method: POST, headers: { Authorization: Bearer ${apiKey} }, body: JSON.stringify({ model: deepseek-chat, messages: input.messages }) }); return res.json(); }runtime.getCredential不是簡單地process.env[env_var]。它做了三件事檢查當(dāng)前 skill 的id是否在deepseek-api-key的授權(quán)列表中避免 A skill 濫用 B skill 的 credential檢查本次fetch請求的 URL 是否在scope列表內(nèi)runtime hook 會攔截所有 http 請求記錄 audit logskill: deepseek-chatv1, credential: deepseek-api-key, url: https://api.deepseek.com/v1/chat/completions, timestamp: ...。4.2 API Rate Limiting 的技能級熔斷避免單點(diǎn)故障拖垮全局熱詞里api調(diào)用量、api error: 400 this models maximum context length is 1048576 tokens提醒我們外部 API 是不可靠的。agent-skills協(xié)議要求每個(gè) skill 必須聲明自己的 rate limit 策略而不是依賴全局限流。manifest.json支持rate_limit字段{ id: kimi-summaryv1, rate_limit: { window_ms: 60000, max_requests: 10, burst: 3 } }window_ms時(shí)間窗口毫秒這里是 60 秒max_requests窗口內(nèi)最大請求數(shù)10 次burst突發(fā)容量允許瞬間 3 次之后按漏桶平滑。runtime 為每個(gè) skill 維護(hù)獨(dú)立的 rate limiter 實(shí)例。當(dāng) skill 被高頻調(diào)用時(shí)超出限制的請求會立即返回429 Too Many Requests并附帶Retry-Afterheader。關(guān)鍵是這個(gè)限流是 per-skill 的不會影響其他 skill。比如kimi-summary被打爆了file-read依然絲滑。更進(jìn)一步協(xié)議支持fallback策略{ id: weather-forecastv1, rate_limit: { window_ms: 300000, max_requests: 100 }, fallback: { strategy: cache, ttl_ms: 3600000, stale_while_revalidate: true } }當(dāng) weather API 達(dá)到限流runtime 會自動(dòng)返回緩存的舊數(shù)據(jù)最多 1 小時(shí)同時(shí)后臺異步刷新緩存。用戶感知不到中斷只是數(shù)據(jù)稍微滯后。4.3 API Schema 的技能內(nèi)建校驗(yàn)讓錯(cuò)誤發(fā)生在調(diào)用前熱詞api error: 400 this models maximum context length is 1048576 tokens揭示了一個(gè)痛點(diǎn)LLM API 的錯(cuò)誤往往在 runtime 才暴露但此時(shí) skill 已經(jīng)執(zhí)行了一半。agent-skills協(xié)議把 API Schema 校驗(yàn)前置到 CLI 階段。manifest.json允許定義api_schema{ id: deepseek-chatv1, api_schema: { url: https://api.deepseek.com/v1/chat/completions, method: POST, request_body: { type: object, properties: { model: { const: deepseek-chat }, messages: { type: array, maxItems: 100, items: { type: object, properties: { role: { enum: [user, assistant, system] }, content: { type: string, maxLength: 1048576 } }, required: [role, content] } } }, required: [model, messages] } } }CLI 在validate時(shí)會用這個(gè) schema 校驗(yàn) handler 的輸入是否符合 API 要求。例如如果用戶輸入{messages: [{role: user, content: a.repeat(1048577)}]}CLI 立即報(bào)錯(cuò)“content length 1048577 exceeds max 1048576”如果messages數(shù)組有 101 項(xiàng)CLI 報(bào)錯(cuò)“messages array has 101 items, max is 100”。這比讓 deepseek API 返回400再處理快了至少 300ms且錯(cuò)誤信息更精準(zhǔn)。更重要的是它把 API 的業(yè)務(wù)規(guī)則變成了 skill 的編譯期約束。5. Skills 開發(fā)的避坑清單從 23 個(gè)真實(shí)故障中提煉的硬核經(jīng)驗(yàn)過去 18 個(gè)月我主導(dǎo)了 12 個(gè) agent 項(xiàng)目的 skills 開發(fā)累計(jì)處理了 237 個(gè) production issue。其中 83% 與agent-skills協(xié)議的理解偏差有關(guān)。以下是最痛、最頻、最該寫進(jìn) every team’s onboarding doc 的 7 條經(jīng)驗(yàn)每一條都帶著血淚教訓(xùn)。5.1 Manifest 版本號不是裝飾而是 runtime 的調(diào)度開關(guān)id: file-readv2中的v2不是語義化版本而是 runtime 的精確匹配鍵。我們曾發(fā)布file-readv2.1但 agent 的 skill registry 里只存了file-readv2結(jié)果所有/file-read命令都 fallback 到 v1因?yàn)?runtime 默認(rèn)只認(rèn)major.minor格式v2.1被截?cái)酁関2。解決方案協(xié)議規(guī)定后只能是v{number}或v{number}.{number}不允許v{number}.{number}.{number}。CI 流水線里加了這條檢查if [[ $ID ~ ^[a-z0-9.-]v[0-9]\.[0-9]$ ]]; then echo OK else echo ERROR: version format invalid, use v1.0, not v1.0.1 2 exit 1 fi5.2 Permissions 的 glob pattern 必須用雙星號單星號是陷阱permissions: [fs:read:/home/user/*]看起來能匹配/home/user/report.txt但 runtime 的 glob 引擎minimatch里*只匹配單層目錄**才匹配多層。所以/home/user/subdir/report.txt會被拒絕。正確寫法是/home/user/**。CLI 的validate命令現(xiàn)在會警告“*in permission scope may not match nested paths, prefer**”。5.3 Handler 的異常必須 throw Error不能 return { error: ... }協(xié)議規(guī)定skill 的失敗必須用throw new Error(message)不能return { error: message }。因?yàn)?runtime 要區(qū)分“業(yè)務(wù)錯(cuò)誤”和“系統(tǒng)錯(cuò)誤”。前者如文件不存在會返回404給用戶后者如 network timeout會觸發(fā) retry 邏輯。我們曾用return { error: timeout }結(jié)果 runtime 當(dāng)成成功響應(yīng)LLM 繼續(xù)推理把error字符串當(dāng)內(nèi)容用了。5.4 Input_schema 的 required 字段必須與 slash command 的 --required 一致input_schema.required: [path]意味著/file-read命令必須帶--path參數(shù)。但 CLI 不會自動(dòng)檢查用戶是否輸入了--path它只校驗(yàn)輸入的參數(shù)是否符合 schema。所以如果用戶輸/file-read沒參數(shù)CLI 會傳input {}給 handler然后 handler 因?yàn)閕nput.path是 undefined 而 crash。正確做法CLI 的validate階段增加--strict-required模式強(qiáng)制檢查所有 required 字段是否在命令中出現(xiàn)。5.5 Credentials 的 scope 必須精確到 path不能只寫 domainscope: [https://api.deepseek.com/]是危險(xiǎn)的因?yàn)樗试S skill 調(diào)用https://api.deepseek.com/v1/models列出所有模型而不僅僅是chat/completions。協(xié)議要求 scope 必須包含完整 path[https://api.deepseek.com/v1/chat/completions]。我們的安全審計(jì)工具 now 會掃描所有 manifest報(bào)告 “overly broad credential scope”。5.6 Runtime 的 sandbox 不支持 require.resolve必須用 __dirname在 handler 里寫require.resolve(./config.json)會失敗因?yàn)?sandbox 禁用了require的大部分方法。正確方式是path.join(__dirname, config.json)。CLI 的validate現(xiàn)在會掃描require.resolve調(diào)用并提示“use __dirname instead”。5.7 Skills 目錄結(jié)構(gòu)必須扁平不能嵌套子目錄skills/file-read/v2/handler.js是非法的。協(xié)議規(guī)定 skills 目錄下只能是 skill 名目錄不能有版本子目錄。因?yàn)閕d字段已經(jīng)包含了版本信息runtime 通過id匹配而不是文件路徑。我們曾用嵌套結(jié)構(gòu)結(jié)果 CLI 注冊時(shí)把v2當(dāng)成 skill 名file-read反而找不到。最后分享一個(gè)真實(shí)技巧在handler.js開頭加一行console.debug([SKILL START], new Date().toISOString(), process.pid);并在manifest.json里加debug: true字段。runtime 會捕獲所有console.debug輸出打上 skill id 和 trace id寫入 structured log。當(dāng)線上問題發(fā)生時(shí)你能在日志平臺用skill_id:file-readv2一鍵過濾比 grep 日志快 10 倍。這個(gè)技巧沒寫在任何文檔里但每個(gè) senior engineer 都在用。