計(jì):從工具到可復(fù)用技能的實(shí)戰(zhàn)指南)
AI Agent 火了這兩年我見過太多 demo 跑得飛起、一上真實(shí)業(yè)務(wù)就拉胯的案例。問題多半不是模型不夠強(qiáng)而是 Agent 的“手”太短——模型再聰明沒有一套組織良好的技能庫它也只能在對(duì)話里打轉(zhuǎn)做不了實(shí)事。我去年花了不少時(shí)間整理了一套自己的 agent-skills 工程實(shí)踐把高頻能力沉淀成標(biāo)準(zhǔn)化技能包后來好幾個(gè)項(xiàng)目都靠這套東西把“演示級(jí) Agent”變成了“能上生產(chǎn)的 Agent”。這篇文章就把這套實(shí)踐從頭到尾拆開講一遍從技能庫怎么設(shè)計(jì)、技能文件怎么寫到怎么評(píng)測(cè)、怎么維護(hù)、怎么排查一次說清楚。這篇文章適合誰如果你正在做 Agent 應(yīng)用發(fā)現(xiàn)模型老是亂調(diào)工具、輸出不穩(wěn)定、換一個(gè)場(chǎng)景就得重新寫一遍邏輯那這篇就是給你準(zhǔn)備的。哪怕你完全沒接觸過 Agent 開發(fā)我會(huì)把 skill、tool、workflow 這些概念掰開揉碎講明白你照著做也能搭出一套自己的技能庫。1. 先想清楚agent-skills 到底解決什么問題1.1 Agent 的“玩具感”從哪來現(xiàn)在很多 Agent 項(xiàng)目本質(zhì)就是“模型 一堆函數(shù)”。模型負(fù)責(zé)理解用戶意圖函數(shù)負(fù)責(zé)干活。聽起來很簡(jiǎn)單但實(shí)際一跑就露餡第一工具是零散的。今天給 Agent 掛一個(gè)查天氣的函數(shù)明天加一個(gè)發(fā)郵件的函數(shù)每個(gè)函數(shù)的輸入輸出風(fēng)格都不一樣模型調(diào)用的時(shí)候經(jīng)常張冠李戴。第二調(diào)用是碰運(yùn)氣的。同一個(gè)意圖模型這次傳對(duì)參數(shù)了下次可能就傳錯(cuò)這次按你期望的格式返回下次可能給你一段廢話解析邏輯直接被干碎。第三邏輯是沒法復(fù)用的。A 項(xiàng)目里寫了一個(gè)“抓取網(wǎng)頁正文”的函數(shù)B 項(xiàng)目要用得改半天才能接上幾乎等于重寫。這三件事疊在一起就造成了 Agent 的“玩具感”演示的時(shí)候很驚艷真讓它在生產(chǎn)環(huán)境里連續(xù)跑幾百次就開始花式報(bào)錯(cuò)。agent-skills 的思路就是把“工具”升級(jí)成“技能”。技能不是單個(gè)函數(shù)而是一個(gè)完整的、自包含的能力單元它有清晰的名字和描述有結(jié)構(gòu)化的輸入輸出協(xié)議有示例有校驗(yàn)邏輯甚至有獨(dú)立的測(cè)試。模型用技能的時(shí)候不需要“猜”怎么調(diào)用只需要照著技能說明填參數(shù)。這樣 Agent 的穩(wěn)定性就會(huì)從“碰運(yùn)氣”變成“有保障”。1.2 Skill、Tool、Workflow 的邊界這里必須先劃清三個(gè)概念不然后面設(shè)計(jì)會(huì)亂套。Tool、Skill、Workflow 經(jīng)常被混著說但它們的粒度完全不一樣概念粒度典型例子是否可獨(dú)立完成一個(gè)任務(wù)Tool最小操作單元發(fā)一個(gè) HTTP 請(qǐng)求、讀一個(gè)文件否通常只是動(dòng)作Skill完成一個(gè)小任務(wù)的能力提取網(wǎng)頁正文、生成周報(bào)草稿是包含邏輯和校驗(yàn)Workflow一串技能的組合搜集資料→總結(jié)→生成報(bào)告是編排層面的事我見過的很多失敗項(xiàng)目問題就出在把 Tool 當(dāng) Skill 用。你只給模型一個(gè)fetch_url工具模型拿到 HTML 之后不知道怎么辦是直接返回還是解析還是提取正文模型只能自己亂猜輸出自然一團(tuán)糟。而一個(gè)標(biāo)準(zhǔn)的 Skill最少要包含四樣?xùn)|西明確的功能定義、輸入輸出協(xié)議、調(diào)用示例、兜底校驗(yàn)。它把“工具能干什么”和“工具怎么用才對(duì)”都焊死在了一起。模型不需要理解底層邏輯它只需要按照協(xié)議調(diào)用就行這就像你不需要懂發(fā)動(dòng)機(jī)原理只要會(huì)踩油門剎車就能開車一樣。Workflow 則是在 Skill 之上做編排它是另一層的事。agent-skills 的核心關(guān)注點(diǎn)就是把中間這層“技能”做到位。你可以用 LangChain、Coze 這類框架做上層編排但技能層的質(zhì)量決定了下限。2. 技能庫的整體設(shè)計(jì)目錄、接口與元數(shù)據(jù)2.1 目錄結(jié)構(gòu)按業(yè)務(wù)域分還是按技術(shù)域分技能庫的目錄怎么組織看起來是小事實(shí)際上決定了后續(xù)的擴(kuò)展成本。我踩過坑之后最終的目錄結(jié)構(gòu)長(zhǎng)這樣agent-skills/ ├── skills/ │ ├── web/ │ │ ├── extract_content/ │ │ │ ├── SKILL.md │ │ │ ├── schema.yaml │ │ │ ├── run.py │ │ │ └── examples.json │ │ └── search_web/ │ │ ├── SKILL.md │ │ ├── schema.yaml │ │ ├── run.py │ │ └── examples.json │ ├── data/ │ │ └── csv_preview/ │ │ ├── SKILL.md │ │ ├── schema.yaml │ │ └── run.py │ └── office/ │ └── gen_meeting_minutes/ │ ├── SKILL.md │ ├── schema.yaml │ ├── run.py │ └── examples.json ├── registry.yaml ├── tests/ └── README.md分組我建議按業(yè)務(wù)域分而不是按技術(shù)域分。為什么因?yàn)?Agent 的場(chǎng)景是用戶視角的用戶不會(huì)說“幫我調(diào)個(gè) API”用戶會(huì)說“幫我看看這個(gè)網(wǎng)頁講了什么”。按業(yè)務(wù)域組織模型在理解技能的時(shí)候語義距離更短按技術(shù)域組織比如把所有網(wǎng)絡(luò)相關(guān)的放一團(tuán)模型很難分清“抓網(wǎng)頁”和“查天氣”到底哪個(gè)該用。每個(gè)技能目錄下的文件職責(zé)也是固定的SKILL.md給模型看的說明書這是核心中的核心。schema.yaml給參數(shù)定義好結(jié)構(gòu)方便做校驗(yàn)。run.py真正干活的代碼不依賴任何 Agent 框架。examples.json幾組輸入輸出例子幫助模型理解“什么情況該用、怎么用”。2.2 SKILL.md模型能不能用對(duì)技能全看它很多剛上手的人不理解為什么一個(gè)技能要單獨(dú)寫一份說明書因?yàn)槟P筒皇侨怂x不懂你的代碼它只能看到你給它的描述。描述寫得爛再好的實(shí)現(xiàn)也白搭。我總結(jié)的 SKILL.md 模板如下每個(gè)技能都按這個(gè)模板寫缺一不可name: extract_content description: 從給定 URL 中提取網(wǎng)頁的標(biāo)題和正文內(nèi)容返回純凈文本。 when_to_use: 用戶給出一個(gè)網(wǎng)址要求了解頁面內(nèi)容、總結(jié)文章、提取重點(diǎn)時(shí)使用。 when_not_to_use: - 用戶沒有給出具體 URL只想做普通搜索 - 網(wǎng)頁需要登錄才能訪問 input: url: 網(wǎng)頁鏈接必須是完整的 https 或 http 地址 max_length: 最大返回字符數(shù)默認(rèn) 5000 output: title: 頁面標(biāo)題 text: 清洗后的正文純文本 examples: - input: {url: https://example.com/blog/hello} output: {title: Hello World, text: 這篇文章介紹了……}這里面的關(guān)鍵不是name和input而是description、when_to_use和when_not_to_use。模型選擇技能靠的就是這些描述。when_not_to_use特別容易被忽略但它非常有價(jià)值。比如“網(wǎng)頁需要登錄才能訪問”這個(gè)邊界條件寫清楚模型在遇到某些網(wǎng)站時(shí)就不會(huì)白白調(diào)用這個(gè)技能然后失敗而是會(huì)直接告訴用戶“這個(gè)頁面需要登錄我拿不到內(nèi)容”。這比調(diào)用失敗后報(bào)錯(cuò)要體面得多。寫 description 的時(shí)候還有兩個(gè)原則第一動(dòng)詞開頭說動(dòng)作。不要寫“網(wǎng)頁內(nèi)容提取工具”這種名詞短語要寫“從給定的 URL 中提取網(wǎng)頁正文并返回純文本”。第二明確邊界。用不用、什么時(shí)候用、什么時(shí)候不用都要寫清楚。描述里的每句話都會(huì)影響模型的判斷。2.3 輸入輸出用 JSON Schema 管住邊界大模型的輸出天然帶有不確定性我們不能寄希望于“它這次會(huì)好好傳參數(shù)”。所以每個(gè)技能都要定義嚴(yán)格的輸入輸出協(xié)議并且用 JSON Schema 做校驗(yàn)。schema.yaml大概長(zhǎng)這樣input_schema: type: object properties: url: type: string format: uri description: 網(wǎng)頁完整鏈接 max_length: type: integer minimum: 100 maximum: 20000 default: 5000 required: - url output_schema: type: object properties: title: type: string text: type: string required: - title - text有了 schema 之后技能內(nèi)部第一件事就是校驗(yàn)輸入不符合直接報(bào)錯(cuò)并返回給模型一個(gè)清晰的提示比如“url 格式不正確請(qǐng)?zhí)峁?https:// 開頭的完整網(wǎng)址”。這個(gè)“報(bào)錯(cuò)再喂回給模型”的機(jī)制是整個(gè)穩(wěn)定性設(shè)計(jì)的核心。模型第一次參數(shù)傳錯(cuò)了Agent 編排層拿到校驗(yàn)錯(cuò)誤把錯(cuò)誤信息連同原始任務(wù)一起再發(fā)給模型讓它重新調(diào)用。這一步看似簡(jiǎn)單實(shí)測(cè)能讓技能調(diào)用的最終成功率從七成左右拉到九成五以上。輸出側(cè)也一樣。技能跑完先做 schema 校驗(yàn)再交給上層。寧可在這里多花一點(diǎn)校驗(yàn)時(shí)間也不要讓臟數(shù)據(jù)流到下游。這跟傳統(tǒng)后端接口做參數(shù)校驗(yàn)是同一個(gè)道理只是很多人做 Agent 的時(shí)候把這個(gè)常識(shí)丟了。3. 從零實(shí)現(xiàn)一個(gè)可復(fù)用的 Agent 技能3.1 先選框架還是先自己擼聊到實(shí)現(xiàn)很多人第一反應(yīng)是上框架。LangChain、LlamaIndex、Coze 都行但我的建議是技能層不要綁定任何框架先用純 Python 把技能寫成普通函數(shù)再在需要的時(shí)候做一層薄適配。理由很簡(jiǎn)單技能是資產(chǎn)框架是工具。你今天用 LangChain 寫了技能明天換了技術(shù)棧技能如果和框架強(qiáng)耦合全都得重寫。而把技能寫成純函數(shù)任何框架都能調(diào)用——LangChain 可以把它包成一個(gè) ToolOpenAI Function Calling 可以把它映射成一個(gè) function自己寫的編排代碼也可以直接調(diào)。技能內(nèi)部可以用依賴庫比如提取網(wǎng)頁正文用trafilatura解析 PDF 用pypdf這些都屬于技能自己的實(shí)現(xiàn)細(xì)節(jié)不影響外部接口。3.2 手寫一個(gè)“網(wǎng)頁正文提取”技能我拿最常寫的“網(wǎng)頁正文提取”技能來講。這個(gè)技能的應(yīng)用場(chǎng)景極廣讓 Agent 總結(jié)一篇文章、分析競(jìng)品頁面、抓取新聞都離不開它。run.py的核心實(shí)現(xiàn)extract_content 技能實(shí)現(xiàn) import json import sys import trafilatura import requests from jsonschema import validate, ValidationError INPUT_SCHEMA { type: object, properties: { url: {type: string, format: uri}, max_length: {type: integer, minimum: 100, maximum: 20000}, }, required: [url], } OUTPUT_SCHEMA { type: object, properties: { title: {type: string}, text: {type: string}, }, required: [title, text], } def run(config: dict) - dict: try: validate(instanceconfig, schemaINPUT_SCHEMA) except ValidationError as e: return {ok: False, error: f輸入?yún)?shù)不合法: {e.message}} url config[url] max_length config.get(max_length, 5000) try: resp requests.get(url, timeout20, headers{ User-Agent: Mozilla/5.0 (compatible; agent-skills/1.0) }) resp.raise_for_status() except Exception as e: return {ok: False, error: f頁面請(qǐng)求失敗: {str(e)}} content trafilatura.extract( resp.text, include_commentsFalse, include_tablesFalse ) if not content: return {ok: False, error: 未能從頁面中提取到正文可能頁面是動(dòng)態(tài)渲染的} title trafilatura.extract(resp.text, output_formattxt) text content[:max_length] result {title: title, text: text} try: validate(instanceresult, schemaOUTPUT_SCHEMA) return {ok: True, result: json.dumps(result, ensure_asciiFalse)} except ValidationError: return {ok: False, error: 技能內(nèi)部輸出異常}注意幾個(gè)細(xì)節(jié)第一技能入口統(tǒng)一接收一個(gè) dict返回一個(gè) dict。返回結(jié)構(gòu)里帶ok標(biāo)志成功時(shí)result放結(jié)果失敗時(shí)error放原因。這樣上層編排只要判斷ok處理邏輯會(huì)非常統(tǒng)一。第二請(qǐng)求失敗、內(nèi)容提取失敗、輸出校驗(yàn)失敗全部有明確的錯(cuò)誤信息。這些信息最終會(huì)被回傳給模型模型才能“意識(shí)到”失敗并調(diào)整策略。第三設(shè)置 User-Agent 和超時(shí)是生產(chǎn)環(huán)境的基本素養(yǎng)。不做這兩件事技能上線后會(huì)因?yàn)楦鞣N反爬策略和慢響應(yīng)把 Agent 卡死。3.3 效果評(píng)測(cè)別只看“能跑”要看“穩(wěn)定跑”技能寫完測(cè)一兩次能跑不算完。你需要一個(gè)評(píng)測(cè)集反復(fù)跑、批量跑、看統(tǒng)計(jì)結(jié)果。我的做法是每個(gè)技能都建一個(gè)eval_cases.json放 20 到 50 個(gè)真實(shí)場(chǎng)景的輸入覆蓋正常情況、邊界情況、異常情況。比如網(wǎng)頁正文提取技能的評(píng)測(cè)集[ {url: https://example.com/blog/1, expected_contains: , note: 普通博客文章}, {url: https://example.com/empty, expected_contains: , note: 頁面無正文}, {url: not-a-url, expected_contains: , note: 非法URL} ]評(píng)測(cè)跑完之后我重點(diǎn)看四個(gè)指標(biāo)調(diào)用成功率整個(gè)技能運(yùn)行下來沒有拋異常的比例。輸出合規(guī)率返回結(jié)果通過 output_schema 校驗(yàn)的比例。任務(wù)完成率人肉抽查輸出內(nèi)容確實(shí)符合用戶需求的比例。平均耗時(shí)和消耗一次調(diào)用花多長(zhǎng)時(shí)間、消耗多少 token。這四個(gè)指標(biāo)里最難提升的是任務(wù)完成率因?yàn)樗简?yàn)的是技能的“內(nèi)功”——比如網(wǎng)頁正文提取正文提取得干不干凈標(biāo)題拿沒拿到直接決定了下游總結(jié)的質(zhì)量。我會(huì)把評(píng)測(cè)結(jié)果記錄在技能目錄的EVAL.md里每次改動(dòng)技能都重跑一遍評(píng)測(cè)防止改出回歸問題。4. 質(zhì)量、安全與維護(hù)4.1 技能常見的失敗模式技能寫得多了失敗模式其實(shí)很集中我把最常見的幾種列出來你對(duì)照著查自己的技能庫第一種描述模糊。技能說“獲取網(wǎng)頁內(nèi)容”但沒說清楚是否需要登錄、是否支持 JS 渲染。模型拿到一個(gè)需要?jiǎng)討B(tài)渲染的頁面就會(huì)瞎調(diào)用然后失敗。解法就是在 SKILL.md 里把邊界寫死。第二種技能職責(zé)太寬。一個(gè)技能既想抓網(wǎng)頁又想總結(jié)又想翻譯??雌饋砣f能實(shí)際上模型根本不知道它能帶來什么結(jié)果。技能要像函數(shù)一樣一個(gè)技能只做一件事把這一件事做到極致。第三種隱藏的系統(tǒng)依賴。技能代碼里用了某個(gè)系統(tǒng)命令或者依賴了某個(gè)沒在 requirements 里聲明的庫。換個(gè)環(huán)境直接跑不起來。技能一定要做環(huán)境隔離用虛擬環(huán)境、鎖定依賴版本、聲明所有外部依賴。第四種解析邏輯太脆。比如用正則去提取 HTML頁面結(jié)構(gòu)一改技能就掛了。盡量用成熟的解析庫少用脆弱的字符串操作。4.2 權(quán)限與安全邊界技能是會(huì)跑代碼的跑代碼就意味著有安全風(fēng)險(xiǎn)。這方面絕對(duì)不能偷懶。我給自己定了幾條鐵律第一技能運(yùn)行在最小權(quán)限環(huán)境。能用只讀權(quán)限絕不給寫權(quán)限。涉及文件系統(tǒng)操作的技能一律放到沙箱目錄里。第二外部請(qǐng)求做白名單控制。不是讓 Agent 隨便請(qǐng)求任何 URL尤其要防止 SSRF 攻擊——如果 Agent 部署在內(nèi)網(wǎng)被誘導(dǎo)請(qǐng)求內(nèi)網(wǎng)地址是很大的泄露風(fēng)險(xiǎn)。我通常會(huì)上一個(gè)域名白名單和 IP 黑名單。第三密鑰集中管理。技能里需要調(diào)用第三方 API 的密鑰統(tǒng)一從環(huán)境變量或密鑰管理服務(wù)里讀絕不寫死在代碼里。代碼倉庫泄漏的時(shí)候至少不會(huì)連密鑰一起泄漏。第四容器隔離跑高危技能。涉及代碼執(zhí)行、批量下載這種有副作用的技能我會(huì)用容器跑并且設(shè)置資源限制比如 CPU、內(nèi)存、網(wǎng)絡(luò)出口。安全這件事不容易出彩但一旦出事就是事故。寧可多花點(diǎn)時(shí)間做隔離也不要覺得“自己內(nèi)部用沒關(guān)系”。4.3 維護(hù)技能也要版本管理和測(cè)試技能庫是一個(gè)長(zhǎng)期演進(jìn)的資產(chǎn)不是寫完就完了。我用了一套常規(guī)但很有效的維護(hù)流程每個(gè)技能在 SKILL.md 里記錄version字段改動(dòng)就升版本號(hào)。協(xié)議有破壞性變更就升大版本。registry.yaml統(tǒng)一登記所有技能記錄每個(gè)技能的版本、負(fù)責(zé)人、部署狀態(tài)。每個(gè)技能配一組單元測(cè)試和一組集成測(cè)試。集成測(cè)試用錄制好的真實(shí)響應(yīng)來跑避免測(cè)試時(shí)頻繁請(qǐng)求外部服務(wù)。進(jìn) CI改動(dòng)技能必須過測(cè)試才能合并。這個(gè)流程一開始會(huì)覺得繁瑣但技能多了之后沒有自動(dòng)測(cè)試你根本不敢改代碼。例如tests/test_extract_content.py長(zhǎng)這樣def test_extract_content_success(): result extract_content.run({ url: https://example.com/blog/hello, max_length: 2000, }) assert result[ok] is True assert title in json.loads(result[result])這里的核心思想是技能不是腳本是產(chǎn)品。凡是沒有測(cè)試的技能本質(zhì)上都是未知狀態(tài)。5. 常見問題與排查實(shí)錄5.1 模型就是不調(diào)用技能這是最讓人頭禿的問題技能寫得清清楚楚模型就是不用自己去編一個(gè)答案。我排查下來的原因通常有三個(gè)一是技能列表中技能太多模型被淹沒。解決辦法是引入“技能門控”根據(jù)用戶意圖先粗篩一批技能只把最相關(guān)的幾個(gè)暴露出給模型而不是一股腦全塞進(jìn)去。二是描述寫得像文檔不像決策依據(jù)。模型讀到的描述如果都是“該工具用于……”它沒法判斷“現(xiàn)在該不該用”。把描述改成決策導(dǎo)向的比如“當(dāng)用戶給出具體網(wǎng)址并要求總結(jié)內(nèi)容時(shí)使用此技能”效果立竿見影。三是缺少示例。有些模型的少量示例學(xué)習(xí)能力很強(qiáng)在 SKILL.md 里放兩個(gè)例子說清楚“用戶這么問的時(shí)候你怎么調(diào)用”模型就更愿意走這條路。5.2 調(diào)用了但參數(shù)傳錯(cuò)參數(shù)傳錯(cuò)也很常見。類型不對(duì)、缺字段、URL 沒轉(zhuǎn)義五花八門。我的處理方式是在編排層加一個(gè)“修復(fù)循環(huán)”校驗(yàn)失敗 → 把錯(cuò)誤信息喂回給模型 → 讓模型重新生成調(diào)用。這個(gè)循環(huán)最多跑三次超過三次就放棄避免死循環(huán)消耗 token。還需要注意錯(cuò)誤信息一定要具體。不要返回“參數(shù)錯(cuò)誤”要返回“url 字段必須是合法的 http/https 鏈接當(dāng)前值 xxx 不符合要求”。模型看到了具體錯(cuò)誤才能修對(duì)。5.3 常見問題排查速查表我最后整理了一個(gè)速查表基本覆蓋了我在實(shí)踐中遇到的大部分問題可以直接抄癥狀可能原因先查什么解決辦法模型不調(diào)用技能描述不夠決策導(dǎo)向 / 技能太多SKILL.md 的描述重寫描述加“當(dāng)……時(shí)使用”句式加技能門控調(diào)用后參數(shù)格式亂schema 太寬松 / 無示例schema.yaml 的必填字段收緊 schema加 example加修復(fù)循環(huán)技能執(zhí)行報(bào)錯(cuò)依賴缺 / 網(wǎng)絡(luò)被墻 / 頁面結(jié)構(gòu)變了技能日志和異常信息鎖定依賴版本配代理如有需要改用成熟解析庫返回結(jié)果結(jié)構(gòu)不穩(wěn)輸出側(cè)沒做校驗(yàn)output_schema 校驗(yàn)結(jié)果補(bǔ)輸出校驗(yàn)解析失敗時(shí)返回明確錯(cuò)誤技能響應(yīng)超時(shí)外部服務(wù)慢 / 沒設(shè)超時(shí)requests 超時(shí)和調(diào)用日志設(shè)置合理超時(shí)加緩存考慮異步化技能庫里技能越多效果越差模型選擇困難技能列表長(zhǎng)度動(dòng)態(tài)門控 按用戶意圖粗篩排查這類問題最重要的一件事是——日志。技能的每次調(diào)用入?yún)?、出參、耗時(shí)、報(bào)錯(cuò)全部落日志。沒有日志排查就跟大海撈針一樣。我甚至建議給每個(gè)技能的調(diào)用鏈路加一個(gè) trace_id從用戶請(qǐng)求到技能執(zhí)行完畢整條鏈路可以串起來看。遇到線上問題先撈日志再談優(yōu)化。我個(gè)人的體會(huì)是agent-skills 這套東西真正的門檻不在寫代碼而在“克制”。克制自己想要加新技能的手克制把技能范圍擴(kuò)大的沖動(dòng)克制跳過測(cè)試和評(píng)測(cè)的僥幸心理。技能庫的每一個(gè)技能都應(yīng)該像正式產(chǎn)品一樣對(duì)待有說明、有協(xié)議、有測(cè)試、有版本。你覺得這是小題大做等你哪天在凌晨?jī)牲c(diǎn)被線上 Agent 的奇怪輸出叫醒就知道這些工作有多值錢了。