:從混亂工具調(diào)用到可復(fù)用技能庫)
1. “agent-skills”到底在解決什么問題我在做 Agent 項目的第三個月決定把所有的能力模塊全部重寫成一套統(tǒng)一的 agent-skills 體系。起因很直接同一個智能體換了個業(yè)務(wù)場景之后原來的提示詞和工具調(diào)用邏輯全亂套了。當(dāng)時最大的感受是模型本身并不弱真正拖后腿的是我給它的“手和腳”——那些能力邊界模糊、互相耦合、難以單獨驗證的函數(shù)與提示詞片段。這里說的 agent-skills不是某個框架的專有名詞而是我習(xí)慣用的一套工程化組織方式把 Agent 能做的事拆成一個個可聲明、可測試、可復(fù)用的技能模塊每個技能都有清晰的描述、輸入輸出協(xié)議、實現(xiàn)邏輯和驗證方式。如果你正在開發(fā) AI 助手、自動化流程、智能客服這類產(chǎn)品或者你發(fā)現(xiàn)自己的 Agent 到了“demo 能跑、上線就崩”的階段那么這篇文章里的思路和踩坑記錄應(yīng)該能幫上忙。1.1 一次事故讓我決定重寫技能層事情發(fā)生在給一個客戶做智能客服機器人時。當(dāng)時為了快速上線我把所有工具調(diào)用邏輯都堆在一個大文件里意圖判斷靠大段 if-else業(yè)務(wù)規(guī)則散落在各處。最初功能很少跑起來還算順暢。后來客戶要求增加節(jié)假日話術(shù)我在某個工具函數(shù)里改了一行返回值格式結(jié)果導(dǎo)致另一個不相關(guān)的意圖分支開始錯誤觸發(fā)線上對話連續(xù)出現(xiàn)答非所問。排查了很久才發(fā)現(xiàn)問題根源不是模型而是我把“技能”和“業(yè)務(wù)規(guī)則”焊死在了同一段代碼里。節(jié)假日話術(shù)本質(zhì)上是一個獨立的領(lǐng)域能力它應(yīng)該有自己獨立的入口、參數(shù)和返回結(jié)構(gòu)可以被單獨替換和測試。但在當(dāng)時的架構(gòu)里它和周邊十幾個功能共用同一個狀態(tài)機牽一發(fā)而動全身。這次事故之后我把思路調(diào)整為“技能優(yōu)先”凡是 Agent 需要對外執(zhí)行的動作先抽象成技能再通過技能模塊去組合業(yè)務(wù)規(guī)則。這樣每個能力像抽屜一樣獨立存在模型按需抽取工程側(cè)也能針對單一技能做回歸測試。聽起來像常識但真正動手做之前我確實低估了這件事的價值。1.2 技能模塊解決的三個真實痛點第一個痛點是模型的可理解性。如果你把一堆業(yè)務(wù)邏輯直接寫進(jìn)系統(tǒng)提示詞模型需要從長文本里自己找調(diào)用條件稍微復(fù)雜一點就容易漏。技能模塊則把每個能力壓縮成一段“能力說明書”模型只需要在候選列表里做匹配理解和選擇的成本都低很多。第二個痛點是工程的可測試性。傳統(tǒng)工具函數(shù)可以單測但 Agent 的工具調(diào)用鏈路很難單測因為你不知道模型會在什么上下文里觸發(fā)它。技能模塊通過標(biāo)準(zhǔn)化輸入輸出和獨立執(zhí)行邏輯讓測試變成一個純粹的函數(shù)驗證過程。先測技能本身能不能跑再測模型能不能選對技能問題邊界變得非常清楚。第三個痛點是能力的可復(fù)用性。同一個“查詢訂單”技能可以用在客服機器人、工單助手、企業(yè)微信機器人里只要描述和協(xié)議不變換場景就是換 Agent 殼。之前我所有的能力都長在某個項目里換個項目就要復(fù)制粘貼改一堆東西現(xiàn)在沉淀成 agent-skills 庫之后新項目的冷啟動速度明顯快了很多。這三件事聽起來不大卻直接影響 Agent 從“能用”到“好用”的關(guān)鍵一步。1.3 什么內(nèi)容才配叫 skill不是所有函數(shù)都值得做成技能。我現(xiàn)在的判斷標(biāo)準(zhǔn)有三個一是輸入輸出邊界是否清晰二是是否有可預(yù)期的副作用三是能否獨立驗證。比如“查詢訂單狀態(tài)”“計算運費”“發(fā)送提醒消息”這類操作邊界明確、結(jié)果可檢驗天然適合做成技能。相反有些任務(wù)過于開放比如“寫一篇爆款文章”就不適合直接塞成一個技能。這類任務(wù)需要進(jìn)一步拆解為“生成標(biāo)題候選”“搭建文章大綱”“生成正文段落”等更小的子技能否則描述寫不清楚模型也不知道從哪下手。過早把大而泛的能力固化成技能只會讓注冊表變得臃腫還會增加模型選錯技能的幾率。判斷一個能力能不能沉淀為技能我的經(jīng)驗是先在對話里手工測試三次如果你每次都需要補充新規(guī)則才能讓它穩(wěn)那就說明這個技能還沒有收斂繼續(xù)拆。2. 重新拆解 agent-skills一個技能單元長什么樣我落地的 agent-skills 體系里一個完整的技能單元由四部分組成技能描述、輸入輸出協(xié)議、實現(xiàn)邏輯、自檢測試。很多人只重視實現(xiàn)邏輯把技能當(dāng)成普通函數(shù)來寫結(jié)果模型根本不調(diào)用或者調(diào)用了卻傳錯參數(shù)。其實在大模型應(yīng)用里最關(guān)鍵的往往是那幾行“給模型看的描述”。2.1 技能描述給模型看的產(chǎn)品說明書技能描述的目標(biāo)是讓模型在候選列表里一眼認(rèn)出“這個能力該不該由我來觸發(fā)”。我寫描述的格式基本固定第一句說明能力邊界第二句說明執(zhí)行前提第三句給出常見參數(shù)示例。最后還會加一句“什么時候不要用”用來減少誤觸發(fā)。舉個例子一個查詢天氣的技能我會寫成這樣name: get_weather description: | 查詢指定城市當(dāng)前天氣和未來三天預(yù)報。 當(dāng)用戶明確提到天氣、氣溫、降水、風(fēng)力等意圖時使用。 參數(shù) city 需要是中文城市名盡量從用戶原句中提取。 如果用戶只是在閑聊天氣感受不要調(diào)用本技能。這樣的描述看起來很短但信息密度很高。模型在做技能選擇時本質(zhì)上是在做語義匹配它不需要看到你的 Python 類型注解它需要的是“觸發(fā)條件”和“參數(shù)來源”。我見過很多團隊把函數(shù)文檔直接復(fù)制成技能描述里面全是技術(shù)術(shù)語模型當(dāng)然容易選錯。2.2 輸入輸出協(xié)議一切可以序列化技能和普通函數(shù)的另一個區(qū)別是它面向的調(diào)用方不是程序員而是模型。模型生成的是結(jié)構(gòu)化文本所以技能輸入輸出必須是可序列化的最好用 JSON Schema 明確約束。我在定義協(xié)議時會為每個技能建立一個輸入輸出結(jié)構(gòu)例如from typing import TypedDict, Optional class GetWeatherInput(TypedDict): city: str date: Optional[str] # 缺省則為今天 class GetWeatherOutput(TypedDict): status: str # ok 或 error data: Optional[dict] # 天氣詳情 message: str # 給模型看的簡短說明輸出里一定要帶上message。因為模型需要根據(jù)返回值決定下一步動作如果技能只返回一個裸 dict模型很容易不知道發(fā)生了什么。加上一句“查詢成功北京今天晴最高溫度 30 度”這樣自然的描述模型就能直接理解并轉(zhuǎn)述給用戶。協(xié)議設(shè)計要盡量扁平避免深層嵌套。模型擅長生成平面結(jié)構(gòu)復(fù)雜的嵌套對象容易出現(xiàn)字段缺失或類型錯誤。寧可多幾個頂層字段也不要搞三層以上的對象。2.3 注冊與發(fā)現(xiàn)把代碼變?yōu)閿?shù)據(jù)最初的版本里我是用一個巨大的 if-else 去分發(fā)工具調(diào)用后來換成注冊表機制。注冊表的核心思路是把技能名、技能描述、輸入結(jié)構(gòu)和實現(xiàn)函數(shù)登記到一個全局字典里模型只需要看這個字典的“目錄頁”就能了解整個 Agent 的能力范圍。我通常用 Python 裝飾器來做這件事代碼會清清爽爽SKILL_REGISTRY: dict[str, Skill] {} def skill(func): name func.__name__ SKILL_REGISTRY[name] Skill( namename, descriptionfunc.__doc__.strip(), fnfunc, ) return func skill def get_weather(city: str, date: str ): 查詢指定城市當(dāng)前天氣和未來三天預(yù)報。 ...當(dāng)模型需要調(diào)用時Agent 會把SKILL_REGISTRY里所有技能名和描述拼成一個“技能菜單”讓模型從中選擇。這個過程中代碼變成了數(shù)據(jù)新增技能不再需要改分發(fā)邏輯只需要寫一個函數(shù)并加上裝飾器。這里有個容易被忽略的點注冊順序會影響模型的選擇概率。如果兩個技能描述相似通常排在前面的更容易被選中。所以我會把高頻技能放在前面低頻技能放在后面而不是按字母序排列。3. 實操從零搭建一套可落地的 skill 庫理論說了一堆接下來展示一套我實際在用的技能庫結(jié)構(gòu)和完整示例。沿著這個模板你可以很快把現(xiàn)有代碼整理成屬于自己的 agent-skills 庫。3.1 目錄結(jié)構(gòu)與命名規(guī)范項目根目錄下我會專門建一個skills文件夾每個技能獨占一個子目錄子目錄里放描述文件、實現(xiàn)文件和測試文件。skills/ ├── get_weather/ │ ├── skill.yaml │ ├── impl.py │ └── test_impl.py ├── get_exchange_rate/ │ ├── skill.yaml │ ├── impl.py │ └── test_impl.py └── send_email/ ├── skill.yaml ├── impl.py └── test_impl.py技能命名的規(guī)范我用的是“動詞 目標(biāo)對象”比如get_weather、send_email、calculate_delivery_fee。盡量避免使用process_data、handle_request這類模糊名字因為模型在技能匹配時對名稱很敏感動詞越具體召喚成功率越高。skill.yaml存放模型的可見元信息包括技能名、描述、參數(shù)示例和授權(quán)級別等。impl.py是純實現(xiàn)邏輯不摻雜任何 Agent 上下文。test_impl.py是單元測試直接調(diào)用函數(shù)驗證結(jié)果。3.2 一個最小案例匯率查詢技能我們做一個最簡單的匯率查詢技能。先寫skill.yamlname: get_exchange_rate description: | 查詢實時匯率支持常見貨幣之間換算。 當(dāng)用戶提到匯率、換匯、外匯、某貨幣兌某貨幣時使用。 參數(shù) base 表示基礎(chǔ)貨幣代碼quote 表示目標(biāo)貨幣代碼。 如果用戶未指定目標(biāo)貨幣默認(rèn)使用 CNY。 返回?fù)Q算比例和參考金額。 version: 1.0.0然后寫impl.pyfrom typing import TypedDict, Optional class ExchangeRateInput(TypedDict): base: str # 基礎(chǔ)貨幣如 USD quote: str # 目標(biāo)貨幣如 CNY amount: Optional[float] # 金額缺省則返回匯率 class ExchangeRateOutput(TypedDict): status: str rate: Optional[float] converted: Optional[float] message: str def get_exchange_rate(input_data: ExchangeRateInput) - ExchangeRateOutput: base input_data[base].upper() quote input_data.get(quote, CNY).upper() try: rate fetch_rate_from_db(base, quote) except Exception as e: return { status: error, rate: None, converted: None, message: f查詢失敗{e}請檢查貨幣代碼是否輸入正確, } amount input_data.get(amount) converted amount * rate if amount is not None else None if amount is not None: message f{amount} {base} 約等于 {converted:.2f} {quote}當(dāng)前匯率為 {rate} else: message f當(dāng)前 {base}/{quote} 匯率為 {rate} return { status: ok, rate: rate, converted: converted, message: message, }接入 Agent 時只需要把get_exchange_rate注冊進(jìn)SKILL_REGISTRY然后把技能菜單交給模型。整個過程不需要改業(yè)務(wù)代碼。我第一次重構(gòu)時最驚訝的就是原來加一個新能力可以這么快。3.3 技能自檢與 dry_run實際運行中模型經(jīng)常傳錯參數(shù)比如把“人民幣”直接當(dāng)貨幣代碼傳進(jìn)來或者把日期傳成“明天”。為了減少這類問題我在每個技能里加了一個輕量自檢邏輯當(dāng)參數(shù)缺省或明顯異常時返回一個“提示型錯誤”告訴模型應(yīng)該怎么補參數(shù)。我還會給關(guān)鍵技能增加dry_run模式。這個模式只做校驗和演練不真正產(chǎn)生副作用。比如發(fā)送郵件技能在dry_run下只會打印“將向某某發(fā)送主題為某某的郵件”不會真的發(fā)出去。這樣能讓模型在正式生成動作前先自檢一遍大幅減少誤操作。dry_run的實現(xiàn)也簡單就是給輸入增加一個test_mode字段處理函數(shù)在開頭判斷一下。別小看這個字段它是我做技能灰度時最依賴的安全閥。3.4 版本管理與灰度技能不是寫一次就不動了。業(yè)務(wù)規(guī)則一改技能實現(xiàn)就要跟著改。但模型的行為需要保持一致所以我給每個技能加了version字段注冊表里記錄當(dāng)前請求使用的版本號。升級時先記錄舊版本行為方便回滾。灰度策略我做得比較樸素注冊表里同時保留新舊兩個版本通過一個開關(guān)分配流量。比如get_exchange_rate從 v1 升到 v2先讓 10% 的請求走到 v2觀察調(diào)用成功率和用戶反饋再逐步放量。這個方法不花哨但確實能幫我避免“一次性全量上線然后被模型的新錯誤行為淹沒”的情況。版本管理最需要注意的坑是不要只改描述不改協(xié)議。如果 v2 改了參數(shù)結(jié)構(gòu)一定要在skill.yaml里同步更新否則模型按舊描述生成新參數(shù)技能直接報錯。4. 真正讓 skills 好用的幾個關(guān)鍵細(xì)節(jié)如果說前面是骨架這一節(jié)就是血肉。我自己在把 agent-skills 打磨到能上生產(chǎn)環(huán)境的過程中積累了幾個非常實際的經(jīng)驗。4.1 描述里要明確“什么時候不要用”給技能寫描述時大家很容易只寫“什么時候用”卻忘了寫“什么時候不要用”。在真實對話里模型經(jīng)常過度調(diào)用技能。比如用戶只是抱怨“今天天氣太糟了”并不想查天氣但如果你的天氣技能描述里全是“天氣”“氣溫”等詞模型可能就觸發(fā)查詢。我現(xiàn)在會在描述末尾固定加一句如果用戶只是在表達(dá)主觀感受不要調(diào)用本技能。這句“負(fù)向提示”對降低誤觸發(fā)非常有效。同樣地如果一個技能只能由管理員使用就在描述里寫清楚“普通用戶詢問權(quán)限相關(guān)問題時不要調(diào)用轉(zhuǎn)交由權(quán)限判斷邏輯處理”。負(fù)向描述不用太長一兩句話點中常見混淆場景就夠了。寫得太多反而會讓模型困惑。4.2 錯誤信息是給模型看的糾錯信號技能里拋異常很容易但模型拿到的只是一個異常字符串時往往不知道下一步該做什么。我把錯誤信息改成了結(jié)構(gòu)化格式包含狀態(tài)、原因和建議{ status: error, reason: invalid_currency_code, suggestion: 請將貨幣參數(shù)改為國際標(biāo)準(zhǔn)代碼例如 USD、CNY再重試 }這樣模型看到suggestion后會自然地對用戶說“請?zhí)峁?biāo)準(zhǔn)貨幣代碼”甚至主動修正參數(shù)后重試。我實測過結(jié)構(gòu)化錯誤讓技能調(diào)用失敗后的恢復(fù)成功率提高了不少。記住技能返回的錯誤也是模型的一次“輸入”你的錯誤信息寫得越像給同事看的消息模型就越容易接著干活。4.3 控制返回體量與敏感信息Agent 的上下文窗口是有限的。如果技能返回一大段完整訂單明細(xì)、幾十條搜索結(jié)果模型還沒開始推理上下文就已經(jīng)被撐爆了。我的原則是技能返回給模型的內(nèi)容只保留“決策所需的最小信息量”詳細(xì)信息寫入外部存儲需要時再按消息 ID 拉取。舉個例子查詢訂單列表時不要在返回值里塞完整的商品詳情和物流軌跡只返回訂單號、狀態(tài)、金額、時間這幾個字段就夠了。如果用戶追問詳情再通過另一個“查詢訂單詳情”技能去取。這種拆分不僅省 token還讓每個技能的鏈路更短、更容易排查。敏感信息方面技能輸出里絕不能帶明文密碼、完整身份證號、銀行卡號等。我在輸出層做了一層脫敏比如只返回尾號四位。防的不只是模型還有日志系統(tǒng)和下游服務(wù)。任何時候?qū)徲嬋罩纠锒疾辉摮霈F(xiàn)用戶敏感字段。4.4 冪等性與并發(fā)安全多個技能被并行調(diào)用時很容易出現(xiàn)重復(fù)副作用。最典型的是“支付”或“發(fā)消息”這類操作模型判斷失誤重試兩次用戶就收到兩條消息。所以我在技能設(shè)計里強制要求凡是有副作用的技能必須支持冪等。冪等的做法很簡單給每次調(diào)用生成一個request_id服務(wù)端記錄這個 id 是否已經(jīng)處理過。如果重復(fù)提交同一個request_id直接返回上一次的結(jié)果不再次執(zhí)行副作用。同時技能內(nèi)部盡量保持無狀態(tài)不要依賴全局變量避免并發(fā)時數(shù)據(jù)互相污染。這個設(shè)計在單機 demo 里看不出來一旦技能被多個 Agent 實例共享或者被用戶手動觸發(fā)和模型觸發(fā)同時調(diào)用冪等就是保命符。5. 常見問題與踩坑實錄再正確的理論落到實戰(zhàn)里都會有一堆意想不到的問題。這里記錄幾個我反復(fù)遇到的坑以及對應(yīng)的排查方法。5.1 模型不調(diào)用技能時先別急著調(diào) prompt模型完全無視技能菜單是最常見的問題。很多人第一反應(yīng)是加長系統(tǒng)提示詞結(jié)果越加越亂。我現(xiàn)在的排查順序是先看技能描述是否出現(xiàn)在模型上下文中再看描述里的關(guān)鍵詞是否和用戶表達(dá)有明顯匹配最后才考慮調(diào)整 prompt。如果用戶說“幫我查下美元兌人民幣”技能描述里卻沒有“美元”“人民幣”這些具體詞模型就很難觸發(fā)。解決方法是把常見說法作為示例寫進(jìn)描述里比如支持 USD/CNY、EUR/CNY 等常見貨幣對。這不是讓模型死記硬背而是給它更容易匹配的錨點。另一個容易被忽略的原因是技能菜單太長。當(dāng)候選技能超過十幾個時模型可能遺漏靠后的技能。我會把高頻技能排在前面并且為同一類能力做一個“分組描述”減少候選數(shù)量。5.2 技能明明存在卻選錯了技能選錯技能比不調(diào)用更隱蔽。我遇到過兩個技能描述高度相似一個是“查詢訂單”另一個是“查詢售后單”模型總是把售后單查詢請求派給訂單查詢。后來我在兩個描述里分別加入了“如果不確定是哪個先問用戶是否有售后糾紛”效果立竿見影。還有一個技巧是給每個技能寫一個“反例”字段比如not_to_use: 當(dāng)用戶提到退貨、換貨、維修時請選擇 query_after_sale。這種顯式的互斥指引比單純加形容詞有用得多。要徹底排查我會維護(hù)一個技能評測集每個評測樣本包含“用戶話術(shù)”和“期望技能名”。每次改動描述后跑一遍看準(zhǔn)確率和召回率變化。沒有評測集你根本不知道哪次描述改動是變好還是變壞。5.3 技能返回內(nèi)容撐爆上下文早期我做一個搜索類技能直接把前幾十條搜索結(jié)果全部返回模型還沒來得及總結(jié)上下文就已經(jīng)爆了。后來我改用“分頁摘要”策略技能只返回前 5 條結(jié)果的核心標(biāo)題和摘要如果用戶要更多再通過參數(shù)page翻頁。這樣既控制了 token又讓模型每一步只聚焦一小批信息。還要注意返回值里的“冗余信息”。有些技能實現(xiàn)者圖省事把整個數(shù)據(jù)庫行原樣返回里面全是創(chuàng)建時間、更新時間、內(nèi)部 ID。這些字段對模型決策沒有幫助只會稀釋注意力。我在輸出層做白名單字段明確哪些可以出站。5.4 問題排查速查表癥狀可能原因處理方式模型從不調(diào)用某技能描述關(guān)鍵詞不匹配、技能排序太后補充用戶常見說法調(diào)整注冊順序調(diào)用技能但參數(shù)頻繁錯誤輸入?yún)f(xié)議定義過寬缺少示例在描述中增加參數(shù)格式示例多個技能經(jīng)?;煜枋鱿嗨贫雀呷鄙倩コ庹f明加反例字段明確觸發(fā)邊界技能返回內(nèi)容太長未做摘要和字段白名單只返回決策所需最小字段升級后行為變化大描述或協(xié)議未同步更新版本號分離灰度放量重復(fù)執(zhí)行副作用操作技能非冪等增加 request_id 去重這張表是我每次上線前都會過一遍的基礎(chǔ)檢查清單能幫我快速定位八層以上的問題。6. 落地 agent-skills 一年后的個人體會6.1 技能庫需要“新陳代謝”技能庫不能只增不減。我在維護(hù)了大半年后發(fā)現(xiàn)很多早期技能已經(jīng)沒人調(diào)用但還在注冊表里占著位置每次模型選擇時都會浪費注意力。后來我加了一個“調(diào)用日志”統(tǒng)計每個月清理一次近 30 天調(diào)用次數(shù)為零的技能。不是直接刪而是先標(biāo)記為deprecated下線再刪代碼。這個過程讓我意識到agent-skills 不是一個靜態(tài)的目錄它更像代碼庫本身需要持續(xù)的 review 和重構(gòu)。給技能寫描述時我心里會有個標(biāo)準(zhǔn)如果一個新同事不看實現(xiàn)代碼只看skill.yaml能完全理解這個技能的能力邊界那才算合格。達(dá)不到標(biāo)準(zhǔn)的描述一律重寫。6.2 下一步可以從評測與觀測入手如果你已經(jīng)搭好了一套技能庫我建議下一步把重心放在“可觀測性”上。每次技能調(diào)用都記錄下選技結(jié)果、參數(shù)、耗時、返回狀態(tài)然后定期統(tǒng)計調(diào)準(zhǔn)率。我后來用這些數(shù)據(jù)做過一次很有效的優(yōu)化發(fā)現(xiàn)某個技能雖然經(jīng)常被選中但執(zhí)行成功率只有六成原因是它的輸入?yún)f(xié)議和描述之間存在兩張皮模型按描述生成參數(shù)函數(shù)卻按更嚴(yán)格的協(xié)議校驗。改掉這個不一致后整體成功率立刻回升。最后再分享一個小技巧新增技能之前先問自己三個問題——這個能力可以被一句話說明嗎它的輸入輸出可以被結(jié)構(gòu)化嗎它值得被獨立測試和復(fù)用嗎三個問題都回答“是”再做。少建一個模糊技能比多寫一個完美技能更重要。這套思路陪我走過幾個項目也希望幫你少踩一些坑。