建智能體的專業(yè)技能樹:Agent Skills生態(tài)全析(中篇)——從零搭建可復(fù)用的技能注冊與調(diào)度層)
1. 為什么你的 Agent 技能總是“一次性”的很多人搭智能體時都遇到過這個場景寫了一個能查天氣、能讀文件、能調(diào)接口的 Agent跑通一次挺開心但換個任務(wù)就得重寫一遍。技能和業(yè)務(wù)邏輯攪在一起參數(shù)校驗散落在各個函數(shù)里調(diào)度全靠 if-else 硬編碼。結(jié)果就是——技能不可復(fù)用Agent 越寫越臃腫。這篇要解決的就是這個問題給智能體搭一棵可插拔的技能樹。核心思路是把技能從 Agent 主邏輯里抽出來做成獨立的注冊表讓技能發(fā)現(xiàn)、參數(shù)校驗、調(diào)度執(zhí)行三層解耦。你可以把它理解成給 Agent 裝了一個“應(yīng)用商店”技能按統(tǒng)一格式注冊進來Agent 運行時按需查找、校驗、調(diào)用用完即走。適合誰看如果你正在做多 Agent 協(xié)作、想讓技能跨項目復(fù)用或者單純覺得現(xiàn)在的 Agent 代碼太亂想重構(gòu)這篇的配置和腳本可以直接抄。我會用兩個示例技能一個查匯率、一個算文本統(tǒng)計走完整流程注冊 → 發(fā)現(xiàn) → 校驗 → 調(diào)度 → 驗證返回。全程本地可跑不依賴任何外部服務(wù)最后再講怎么把調(diào)用憑證統(tǒng)一管起來。先明確一個概念邊界。Skills 和 Tools 不是一回事Tools 是原子能力讀文件、發(fā)請求Skills 是編排好的工作流先校驗參數(shù)、再調(diào)工具、最后格式化輸出。我們要搭的注冊與調(diào)度層管的是 Skills 這一層。MCP 負責(zé)數(shù)據(jù)接入Subagents 負責(zé)并行隔離這些是上下游本篇聚焦中間那層“技能怎么管”。2. TaoToken 前置把調(diào)用憑證從技能里剝出來技能樹要可復(fù)用有個前提容易被忽略技能本身不能綁死某一家模型的 Key。如果每個技能里都硬編碼一個 API Key那技能就沒法跨環(huán)境遷移也沒法集中輪換憑證。所以第一步是把模型調(diào)用通道統(tǒng)一出去。我用的方案是 TaoToken它提供一個統(tǒng)一的 Key/API 通道兼容 OpenAI 風(fēng)格的接口格式。好處是技能注冊表里只存“模型標識”不存憑證真正調(diào)用時由調(diào)度層統(tǒng)一注入 Base URL 和 Key。這樣技能文件可以進 Git憑證留在環(huán)境變量里。你需要準備三樣?xùn)|西我列成表格方便對照項目值說明Base URLhttps://taotoken.net/api統(tǒng)一入口不加任何多余路徑API Key在控制臺生成形如sk-開頭只存環(huán)境變量Model ID按需選擇調(diào)度層配置里引用技能文件不寫死獲取 Key 的入口在控制臺的 API Keys 頁面生成后復(fù)制一次即可頁面刷新后不再完整顯示。如果你還沒賬號從官網(wǎng)進https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注冊后在控制臺左側(cè)找到 API Keys。這里有個工程習(xí)慣值得養(yǎng)成把憑證讀取封裝成一個函數(shù)所有技能調(diào)度都走它。這樣以后換通道、加限流、做審計只改一處。下面是我用的最小封裝放在config/llm_client.pyimport os from openai import OpenAI def get_client(): base_url os.environ.get(TAOTOKEN_BASE_URL, https://taotoken.net/api) api_key os.environ.get(TAOTOKEN_API_KEY) if not api_key: raise RuntimeError(TAOTOKEN_API_KEY 未設(shè)置請先導(dǎo)出環(huán)境變量) return OpenAI(base_urlbase_url, api_keyapi_key) DEFAULT_MODEL os.environ.get(TAOTOKEN_MODEL, gpt-4o-mini)環(huán)境變量這樣導(dǎo)出Linux/macOSexport TAOTOKEN_BASE_URLhttps://taotoken.net/api export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_MODELgpt-4o-miniWindows PowerShell 用$env:TAOTOKEN_API_KEYsk-...。注意 Base URL 結(jié)尾不要帶/v1SDK 會自己拼多寫一層會 404。這一步做完技能樹就有了統(tǒng)一的“電源接口”后面所有技能都從這里取電。3. 可復(fù)制的技能注冊表與調(diào)度腳本現(xiàn)在進入核心部分。技能樹要落地得先定義技能的“身份證”格式。我用 JSON 存注冊表每個技能一條記錄包含名稱、描述、參數(shù) schema、執(zhí)行入口。這樣調(diào)度層可以只讀注冊表就知道有哪些技能、怎么校驗參數(shù)不用 import 具體實現(xiàn)。先建目錄結(jié)構(gòu)agent_skills/ ├── registry/ │ └── skills.json ├── skills/ │ ├── currency_convert.py │ └── text_stats.py ├── config/ │ └── llm_client.py └── dispatcher.py注冊表registry/skills.json內(nèi)容如下兩個示例技能都注冊進去{ version: 1.0, skills: [ { name: currency_convert, description: 把一種貨幣金額換算成另一種貨幣使用固定匯率表, entry: skills.currency_convert:run, parameters: { type: object, properties: { amount: { type: number, minimum: 0 }, from_currency: { type: string, enum: [CNY, USD, EUR] }, to_currency: { type: string, enum: [CNY, USD, EUR] } }, required: [amount, from_currency, to_currency] } }, { name: text_stats, description: 統(tǒng)計一段文本的字符數(shù)、詞數(shù)和行數(shù), entry: skills.text_stats:run, parameters: { type: object, properties: { text: { type: string, minLength: 1 } }, required: [text] } } ] }注意entry字段用的是模塊路徑:函數(shù)名格式調(diào)度層用importlib動態(tài)加載。這樣加新技能只需改 JSON不用動調(diào)度代碼——這就是“可插拔”的關(guān)鍵。兩個技能實現(xiàn)skills/currency_convert.pyRATES { (CNY, USD): 0.14, (USD, CNY): 7.15, (CNY, EUR): 0.13, (EUR, CNY): 7.70, (USD, EUR): 0.92, (EUR, USD): 1.09, } def run(amount, from_currency, to_currency): if from_currency to_currency: return {result: amount, rate: 1.0} rate RATES.get((from_currency, to_currency)) if rate is None: raise ValueError(f不支持的貨幣對: {from_currency}-{to_currency}) return {result: round(amount * rate, 2), rate: rate}skills/text_stats.pydef run(text): lines text.splitlines() words text.split() return { chars: len(text), words: len(words), lines: len(lines), }調(diào)度層dispatcher.py負責(zé)三件事加載注冊表、用 JSON Schema 校驗參數(shù)、動態(tài)調(diào)用。校驗用jsonschema庫沒裝的話pip install jsonschemaimport json import importlib from pathlib import Path from jsonschema import validate, ValidationError REGISTRY_PATH Path(__file__).parent / registry / skills.json class SkillDispatcher: def __init__(self): self.registry {} self._load() def _load(self): data json.loads(REGISTRY_PATH.read_text(encodingutf-8)) for item in data[skills]: self.registry[item[name]] item def list_skills(self): return [ {name: k, description: v[description]} for k, v in self.registry.items() ] def call(self, name, params): if name not in self.registry: raise KeyError(f技能未注冊: {name}) meta self.registry[name] try: validate(instanceparams, schemameta[parameters]) except ValidationError as e: raise ValueError(f參數(shù)校驗失敗: {e.message}) module_path, func_name meta[entry].split(:) module importlib.import_module(module_path) func getattr(module, func_name) return func(**params)這段代碼里list_skills就是“技能發(fā)現(xiàn)”接口call就是“校驗 調(diào)度”。技能發(fā)現(xiàn)和調(diào)度徹底解耦A(yù)gent 主邏輯只需要拿到技能列表決定調(diào)哪個剩下的交給 dispatcher。4. 驗證請求注冊兩個技能后觸發(fā)調(diào)用配置寫完得驗證路由和返回是否符合預(yù)期。我寫了個驗證腳本verify.py依次做四件事列出技能、正常調(diào)用、故意傳錯參數(shù)、調(diào)用不存在的技能。from dispatcher import SkillDispatcher d SkillDispatcher() print( 1. 技能發(fā)現(xiàn) ) for s in d.list_skills(): print(f- {s[name]}: {s[description]}) print(\n 2. 正常調(diào)用 currency_convert ) print(d.call(currency_convert, { amount: 100, from_currency: CNY, to_currency: USD })) print(\n 3. 正常調(diào)用 text_stats ) print(d.call(text_stats, {text: hello agent skills\nsecond line})) print(\n 4. 參數(shù)校驗金額為負) try: d.call(currency_convert, { amount: -5, from_currency: CNY, to_currency: USD }) except ValueError as e: print(已攔截:, e) print(\n 5. 未注冊技能 ) try: d.call(not_exist, {}) except KeyError as e: print(已攔截:, e)跑python verify.py預(yù)期輸出 1. 技能發(fā)現(xiàn) - currency_convert: 把一種貨幣金額換算成另一種貨幣使用固定匯率表 - text_stats: 統(tǒng)計一段文本的字符數(shù)、詞數(shù)和行數(shù) 2. 正常調(diào)用 currency_convert {result: 14.0, rate: 0.14} 3. 正常調(diào)用 text_stats {chars: 30, words: 5, lines: 2} 4. 參數(shù)校驗金額為負 已攔截: 參數(shù)校驗失敗: -5 is less than the minimum of 0 5. 未注冊技能 已攔截: 技能未注冊: not_exist看到這個輸出說明路由正確、校驗生效、異??煽?。第 2 步返回14.0是 100 CNY 按 0.14 匯率換算的結(jié)果第 3 步chars是 30含換行符words是 5都對得上。如果你想讓 Agent 自己決定調(diào)哪個技能可以把list_skills()的結(jié)果塞進模型上下文讓模型輸出技能名和參數(shù)再交給 dispatcher。這一步的模型調(diào)用就走第 2 章封裝的 clientfrom config.llm_client import get_client, DEFAULT_MODEL client get_client() resp client.chat.completions.create( modelDEFAULT_MODEL, messages[ {role: system, content: 你是技能路由器根據(jù)用戶請求輸出技能名和JSON參數(shù)。}, {role: user, content: 幫我把 200 美元換成人民幣} ] ) print(resp.choices[0].message.content)這一步能跑通說明“模型決策 本地調(diào)度”的鏈路是通的。模型只負責(zé)選技能和填參數(shù)真正的執(zhí)行和校驗在本地安全邊界清晰。5. 本篇常見錯排查401、校驗失敗與路由異常技能樹搭起來后報錯基本集中在四類。我把真實遇到的錯誤和定位方法列出來你對照著查。第一類401 Unauthorized / invalid api key。這個幾乎都是憑證問題。先確認TAOTOKEN_API_KEY真的導(dǎo)出了用echo $TAOTOKEN_API_KEY看有沒有值。如果值對但還報 401檢查 Base URL 是不是寫成了https://taotoken.net/api/v1——多一層/v1會導(dǎo)致路徑拼接錯誤。正確寫法就是https://taotoken.net/api。還有一種情況是 Key 復(fù)制時帶了空格或換行重新生成一次最省事。第二類參數(shù)校驗失敗報is not of type number或is not one of。這是 JSON Schema 在起作用不是 bug。常見原因是模型返回的參數(shù)類型不對比如把amount輸出成字符串100。解決辦法是在調(diào)度層加一層輕量轉(zhuǎn)換或者在 system prompt 里明確要求“數(shù)值字段輸出數(shù)字類型”。enum報錯則是貨幣代碼不在允許列表里檢查注冊表的enum是否覆蓋了實際用到的值。第三類ModuleNotFoundError: No module named skills。動態(tài)加載時模塊路徑找不到。確認dispatcher.py和skills/目錄在同一級且skills/下有__init__.py空文件即可。如果是從其他目錄運行腳本importlib的搜索路徑可能不對在dispatcher.py頂部加一句把項目根目錄塞進sys.pathimport sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent))第四類reading choices相關(guān)報錯比如KeyError: choices或返回體里沒有 choices。這通常說明請求根本沒到模型或者返回的是錯誤結(jié)構(gòu)。先打印完整響應(yīng)體看error字段。如果是local proxy failed這類提示說明網(wǎng)絡(luò)層有問題檢查 Base URL 是否可達。如果響應(yīng)正常但結(jié)構(gòu)不對確認 SDK 版本和接口格式匹配——TaoToken 兼容 OpenAI 格式用官方openaiSDK 即可。排查順序建議固定成先看憑證401→ 再看參數(shù)校驗→ 再看模塊路徑import→ 最后看響應(yīng)結(jié)構(gòu)choices。按這個順序走九成問題五分鐘內(nèi)能定位。6. 把技能樹接上統(tǒng)一通道下一步做并行調(diào)度到這里一棵最小可用的技能樹就跑起來了注冊表管技能元數(shù)據(jù)dispatcher 管發(fā)現(xiàn)和校驗技能實現(xiàn)只管業(yè)務(wù)邏輯憑證統(tǒng)一走 TaoToken 通道。這套結(jié)構(gòu)的好處是加第三個、第十個技能時你只需要往skills.json里追加一條記錄再寫一個純函數(shù)調(diào)度層一行都不用改。如果你要把這套東西用到實際項目里有兩個方向可以繼續(xù)。一是把list_skills()的輸出做成工具描述喂給模型讓模型自主路由這就是“模型決策 本地執(zhí)行”的 Agent 形態(tài)。二是把 dispatcher 的call改成異步配合 Subagents 做并行調(diào)用——多個互不依賴的技能同時跑主線程只收結(jié)果上下文不被污染。憑證這塊建議你盡早把 Key 從代碼里徹底剝離。我現(xiàn)在的做法是本地用環(huán)境變量CI 里用 secrets所有模型調(diào)用都走config/llm_client.py一個出口。這樣以后要換通道、加限流、做用量統(tǒng)計改一個文件就夠了。需要生成新 Key 或者查看用量從控制臺的 API Keys 進想先試試模型對話效果可以從模型對話頁面直接測如果打算長期跑編碼類 AgentCoding Plan 的額度更劃算。接入細節(jié)看文檔里面有各語言的示例。下一篇會講技能樹的第二層怎么讓多個 Agent 共享同一棵技能樹以及技能版本管理和灰度發(fā)布。那部分會涉及注冊表的版本字段和調(diào)度層的路由策略感興趣可以先把手頭這版跑通把兩個示例技能換成你自己的業(yè)務(wù)邏輯試試。