戰(zhàn):從零散函數(shù)調(diào)用到可復(fù)用技能庫(kù)設(shè)計(jì))
直接上手做 Agent 技能體系之后我最大的感受是大部分項(xiàng)目不是死在模型能力上而是死在“技能”太散、太隨意。今天想認(rèn)真聊聊 agent-skills 這件事——它指的是把 Agent 能執(zhí)行的動(dòng)作、工具調(diào)用、業(yè)務(wù)邏輯沉淀成一套可復(fù)用、可組合、可維護(hù)的技能單元。說(shuō)得直白點(diǎn)就是把零散的 function calling 變成有設(shè)計(jì)感的“技能庫(kù)”。這篇文章適合正在搭 Agent 應(yīng)用、做工具編排、或者想把手頭一堆 API 整合成智能體能力層的開(kāi)發(fā)者尤其適合那些已經(jīng)跑通 demo、但一上生產(chǎn)就發(fā)現(xiàn)“改一個(gè)工具牽一發(fā)動(dòng)全身”的朋友。我見(jiàn)過(guò)太多人把技能直接寫(xiě)成 if-else 分支或者在 prompt 里堆幾十個(gè) function schema。早期 demo 看著很靈活代碼量一上去就崩要么模型選錯(cuò)工具要么工具參數(shù)傳錯(cuò)要么技能之間互相覆蓋。我自己踩過(guò)一輪之后把 agent-skills 拆成四層來(lái)看技能定義、技能注冊(cè)、技能編排、技能觀測(cè)。這四層各管一段合起來(lái)才是一個(gè)能扛住真實(shí)業(yè)務(wù)的 Agent 技能體系。1. 整體設(shè)計(jì)思路為什么 Agent 需要“技能”而不是“函數(shù)”1.1 函數(shù)調(diào)用和技能體系的本質(zhì)區(qū)別很多人覺(jué)得“技能”就是換了個(gè)馬甲的函數(shù)調(diào)用。我用一個(gè)貼近生活的例子說(shuō)明差距你讓一個(gè)新來(lái)的實(shí)習(xí)生去“處理客戶退款”他需要知道查訂單、算金額、走審批、發(fā)通知這是技能你直接甩給他四個(gè)按鈕叫他挨個(gè)點(diǎn)這是函數(shù)調(diào)用。Agent 也一樣模型需要通過(guò)“技能”來(lái)理解“這個(gè)場(chǎng)景下該做什么”而不是在幾十個(gè)函數(shù)簽名里反復(fù)猜測(cè)。函數(shù)調(diào)用時(shí)代的典型問(wèn)題是你把所有能力平鋪給模型參數(shù)多、邊界模糊、命名不統(tǒng)一。技能體系的做法是給每個(gè)能力一個(gè)明確的“意圖外殼”——技能名稱、描述、適用場(chǎng)景、輸入輸出約定、前置條件全部封裝成一個(gè)獨(dú)立單元。模型不是從海量函數(shù)里挑而是先從技能列表里選“誰(shuí)最匹配當(dāng)前意圖”再進(jìn)入技能內(nèi)部執(zhí)行具體動(dòng)作。這個(gè)差異我在實(shí)際項(xiàng)目里感受特別明顯。有一次同時(shí)接入了天氣查詢和航班查詢兩個(gè)工具參數(shù)都是“城市日期”函數(shù)調(diào)用模式下模型經(jīng)常把航班查詢的城市參數(shù)直接塞給天氣接口。后來(lái)我把它們封裝成兩個(gè)獨(dú)立技能各自寫(xiě)明“適用場(chǎng)景查詢目的地未來(lái)天氣”“適用場(chǎng)景查詢某城市某日航班”誤調(diào)用率立刻降下來(lái)了。核心原因在于技能的描述不是給開(kāi)發(fā)者看的是給模型看的它必須描述“什么時(shí)候用”而不是“怎么實(shí)現(xiàn)”。1.2 技能命名的可視化與心智負(fù)擔(dān)技能命名這事看著小實(shí)際影響巨大。你給模型看的技能名應(yīng)該像 App 圖標(biāo)一樣直觀。我見(jiàn)過(guò)有團(tuán)隊(duì)把技能叫execute_order_ops_v2模型根本分不清它和order_execution_v1有什么區(qū)別。更好的做法是用動(dòng)詞開(kāi)頭的短句結(jié)構(gòu)QueryOrderStatus、ApplyRefund、CheckFlightInfo。命名本質(zhì)上是在降低模型的“選擇成本”。除了名稱每個(gè)技能的 description 也要按固定模板來(lái)寫(xiě)我這里有一個(gè)經(jīng)過(guò)多輪測(cè)試相對(duì)穩(wěn)定的模板技能用途一句話說(shuō)明這個(gè)技能在什么業(yè)務(wù)場(chǎng)景下使用 觸發(fā)條件哪些用戶意圖或上下文狀態(tài)下應(yīng)該調(diào)用本技能 輸入?yún)?shù)參數(shù)名、類型、取值范圍、含義解釋、示例值 輸出格式返回?cái)?shù)據(jù)的結(jié)構(gòu)和關(guān)鍵字段說(shuō)明 失敗場(chǎng)景什么情況下會(huì)報(bào)錯(cuò)、超時(shí)、或返回空數(shù)據(jù)這份描述不是給人類看的文檔而是模型做“技能路由”時(shí)的決策依據(jù)。寫(xiě)得好不好直接決定模型能不能在正確時(shí)機(jī)點(diǎn)選正確技能。我建議至少寫(xiě)滿 50~100 字不要偷懶只寫(xiě)一句“查詢訂單”。1.3 技能的最小完備原則技能設(shè)計(jì)的另一個(gè)重要原則是“最小完備”一個(gè)技能只負(fù)責(zé)一個(gè)完整動(dòng)作不做半吊子拆分也不把一堆無(wú)關(guān)動(dòng)作塞在一起。比如“查詢訂單狀態(tài)”和“更新訂單狀態(tài)”是兩個(gè)技能絕不能合并成“訂單操作”?!安樵冇唵螤顟B(tài)”內(nèi)部先取訂單號(hào)再查庫(kù)再組裝返回這是一條完整鏈路雖然內(nèi)部有多步但在語(yǔ)義上它是原子的。這個(gè)原則直接決定技能的可組合性。技能設(shè)計(jì)得像樂(lè)高積木每個(gè)都單拎出來(lái)有意義組合起來(lái)能覆蓋復(fù)雜流程。如果技能粒度太粗組合時(shí)必然互相糾纏太細(xì)則模型選擇負(fù)擔(dān)爆炸而且每個(gè)技能都要維護(hù) schema成本飆升。我自己的經(jīng)驗(yàn)是一個(gè)技能內(nèi)部能寫(xiě)成 5~15 步操作對(duì)外只暴露 1 個(gè)明確的業(yè)務(wù)意圖粒度和復(fù)雜度的平衡點(diǎn)基本在這里。2. 技能封裝與核心實(shí)現(xiàn)要點(diǎn)2.1 標(biāo)準(zhǔn)技能接口設(shè)計(jì)封裝技能第一步是定義統(tǒng)一的接口規(guī)范。不管內(nèi)部邏輯多復(fù)雜對(duì)外暴露的接口必須長(zhǎng)一個(gè)樣。我這里直接用一套類似抽象基類的定義方式來(lái)約束所有技能from abc import ABC, abstractmethod from typing import Dict, Any class BaseSkill(ABC): # 技能唯一標(biāo)識(shí)建議用 snake_case 命名 skill_name: str # 給模型看的描述必須包含觸發(fā)條件和適用場(chǎng)景 skill_description: str # 輸入?yún)?shù) JSON Schema嚴(yán)格定義類型、必填項(xiàng)、取值范圍 input_schema: Dict[str, Any] # 輸出結(jié)構(gòu)描述讓模型知道返回結(jié)果里有什么字段 output_schema: Dict[str, Any] abstractmethod async def execute(self, params: Dict[str, Any]) - Dict[str, Any]: 執(zhí)行技能的核心邏輯 pass async def validate(self, params: Dict[str, Any]) - None: 參數(shù)合法性校驗(yàn)?zāi)J(rèn)實(shí)現(xiàn)可被子類覆蓋 for field, meta in self.input_schema.get(properties, {}).items(): if field not in params and meta.get(required): raise ValueError(fMissing required param: {field})這套接口的核心價(jià)值在于“約束大于約定”。所有技能必須實(shí)現(xiàn)execute所有參數(shù)必須走validate所有描述必須有結(jié)構(gòu)化格式。團(tuán)隊(duì)里新來(lái)的同學(xué)照著這個(gè)模板加新技能基本不會(huì)跑偏。比單純寫(xiě)注釋好用太多了。2.2 技能注冊(cè)中心的實(shí)現(xiàn)有了技能接口下一步要解決注冊(cè)和發(fā)現(xiàn)的問(wèn)題。技能注冊(cè)中心本質(zhì)上一個(gè)字典結(jié)構(gòu)但實(shí)際項(xiàng)目里要考慮三個(gè)層面靜態(tài)路由全量技能列表、動(dòng)態(tài)過(guò)濾根據(jù)上下文裁剪候選技能、優(yōu)先級(jí)排序重疊場(chǎng)景下誰(shuí)先被選中。我這里提供一個(gè)剪掉業(yè)務(wù)細(xì)節(jié)后的注冊(cè)中心核心邏輯class SkillRegistry: def __init__(self): self._skills: Dict[str, BaseSkill] {} def register(self, skill: BaseSkill) - None: if skill.skill_name in self._skills: raise ValueError(fDuplicate skill: {skill.skill_name}) self._skills[skill.skill_name] skill # 注冊(cè)時(shí)立即編譯一次 schema避免運(yùn)行時(shí)才暴露錯(cuò)誤 self._compile_schema(skill) def list_skills(self, context: Dict[str, Any]) - list[BaseSkill]: # 根據(jù)上下文過(guò)濾技能減少模型的候選范圍 candidates [] for skill in self._skills.values(): if self._match_context(skill, context): candidates.append(skill) return candidates def get(self, skill_name: str) - BaseSkill: return self._skills.get(skill_name)_match_context可以做成基于關(guān)鍵詞、實(shí)體、業(yè)務(wù)場(chǎng)景標(biāo)簽的匹配規(guī)則。比如說(shuō)上下文里出現(xiàn)“退款”相關(guān)實(shí)體時(shí)優(yōu)先保留退款流程相關(guān)的技能上下文里完全沒(méi)有“訂單”實(shí)體時(shí)訂單類技能直接過(guò)濾掉。別小看這一步候選技能從 30 個(gè)降到 5 個(gè)模型選錯(cuò)的概率降一個(gè)數(shù)量級(jí)不止。2.3 技能描述生成與模型感知優(yōu)化技能描述不能只靠人肉寫(xiě)尤其是技能數(shù)量一多描述質(zhì)量很難穩(wěn)定。我在項(xiàng)目里實(shí)踐過(guò)一個(gè)做法每個(gè)技能掛一個(gè)“調(diào)用樣例”字段這些樣例不是給開(kāi)發(fā)者看的是給模型 few-shot 用的。模型在做技能選擇時(shí)如果有樣例參考準(zhǔn)確率比純看描述高很多。{ skill_name: QueryOrderStatus, description: 查詢訂單當(dāng)前狀態(tài)。當(dāng)用戶詢問(wèn)訂單進(jìn)展、物流信息、是否發(fā)貨時(shí)使用。, examples: [ {user: 我的訂單到哪了, skill: QueryOrderStatus, params: {order_id: 20250101}}, {user: 上周買(mǎi)的鍵盤(pán)發(fā)貨沒(méi), skill: QueryOrderStatus, params: {order_id: 20250102}} ] }另一個(gè)重要的優(yōu)化是“格式化輸出約定”。模型執(zhí)行完技能后返回結(jié)構(gòu)必須是干凈的結(jié)構(gòu)化數(shù)據(jù)不要讓它自己發(fā)揮。你必須在 description 里寫(xiě)清楚“輸出必須包含 status / message / data 三個(gè)字段status 取值為 success 或 error”。模型對(duì)格式的理解高度依賴說(shuō)明你說(shuō)得越具體它執(zhí)行得越精準(zhǔn)。2.4 技能的錯(cuò)誤處理與容錯(cuò)機(jī)制生產(chǎn)環(huán)境里技能調(diào)用一定會(huì)出錯(cuò)關(guān)鍵是怎么讓錯(cuò)誤不炸穿整個(gè)流程。我總結(jié)了三層容錯(cuò)第一層是參數(shù)校驗(yàn)在技能內(nèi)部入口就攔住非法參數(shù)返回結(jié)構(gòu)化錯(cuò)誤碼絕不讓臟數(shù)據(jù)往下游傳。第二層是超時(shí)控制每個(gè)技能執(zhí)行必須帶超時(shí)時(shí)間我用的是 30 秒硬限制超過(guò)就返回timeout狀態(tài)并觸發(fā)降級(jí)策略。第三層是降級(jí)策略例如主技能失敗時(shí)是否可以用備選技能頂上。我自己常用的一種降級(jí)寫(xiě)法是給技能標(biāo)注 fallbackclass QueryLogisticsInfo(BaseSkill): # 如果查不到物流詳情可以降級(jí)為查詢訂單主狀態(tài) fallback_skills [QueryOrderStatus]模型拿到錯(cuò)誤結(jié)果后會(huì)知道“這個(gè)技能不行我換那個(gè)技能試試”。這一套下來(lái)整個(gè) Agent 的魯棒性明顯提升。建議大家一定要把“技能會(huì)失敗”當(dāng)成默認(rèn)預(yù)期來(lái)設(shè)計(jì)而不是僥幸覺(jué)得它永遠(yuǎn)能跑通。3. 技能組合與工作流編排3.1 技能鏈與并行技能調(diào)用單一技能只能解決單步問(wèn)題真實(shí)業(yè)務(wù)幾乎都是多技能的協(xié)作。我這里把技能組合分成兩種模式串聯(lián)和并聯(lián)。串聯(lián)適合有嚴(yán)格先后邏輯的場(chǎng)景比如“下單”技能完成后才能執(zhí)行“支付”技能并聯(lián)適合互相獨(dú)立的技能比如同時(shí)查天氣和查航班可以一次并行發(fā)起節(jié)省延遲。Agent 的技能編排里我通常不寫(xiě)死流程圖而是讓模型自己根據(jù)用戶意圖生成執(zhí)行序列。但完全放手也不行需要在系統(tǒng)層面加一層約束# 簡(jiǎn)化的編排約束配置 skill_constraints { CreateOrder: { requires: [], # 前置技能空表示無(wú)依賴 conflicts: [], # 互斥技能存在沖突時(shí)不能同時(shí)調(diào)用 next_allowed: [MakePayment, CancelOrder] # 當(dāng)前技能完成后的合法后繼技能 }, MakePayment: { requires: [CreateOrder], conflicts: [CancelOrder], next_allowed: [] } }這層約束本質(zhì)上是給模型套了一個(gè)安全網(wǎng)。模型可以自由探索技能組合路徑但必須遵守依賴關(guān)系不合法路徑直接在系統(tǒng)層攔掉不用等模型自己反應(yīng)過(guò)來(lái)。這個(gè)設(shè)計(jì)我覺(jué)得是 Agent 上生產(chǎn)的重要分水嶺沒(méi)有約束的純自由編排線上一定會(huì)出幺蛾子。3.2 工具選擇的上下文裁剪每次把全量技能都塞給模型效果好但成本高、延遲高。我測(cè)過(guò)同樣一個(gè)需求候選技能 10 個(gè)和 35 個(gè)模型選對(duì)率差了將近 12 個(gè)百分點(diǎn)token 消耗也差了一大截。所以上下文裁剪是技能編排里的剛需。我的做法是分兩步先基于規(guī)則粗篩再基于語(yǔ)義精排。粗篩用實(shí)體和關(guān)鍵詞匹配把明顯無(wú)關(guān)的技能去掉精排用 embedding 把用戶 query 和技能描述做相似度排序取 top N。兩步合起來(lái)候選技能能壓到 5 個(gè)以內(nèi)模型拿到的是一個(gè)小而精的技能列表。# 偽代碼兩步裁剪 def select_skills(query, context, all_skills): # 第一步規(guī)則過(guò)濾 rule_filtered [s for s in all_skills if _rule_match(s, context)] # 第二步embedding 排序取 topK ranked _embedding_rank(query, rule_filtered) return ranked[:5]這里的 embedding 排序不需要額外模型服務(wù)直接用現(xiàn)成的 text-embedding 接口就行。關(guān)鍵在于把“技能描述”作為排序索引而不是技能名稱。描述里包含的場(chǎng)景詞匯越豐富排序就越準(zhǔn)。3.3 多技能協(xié)作的記憶與狀態(tài)管理多技能配合跑一個(gè)長(zhǎng)任務(wù)的時(shí)候狀態(tài)管理是繞不開(kāi)的坑。技能 A 生成了訂單號(hào)技能 B 要用這個(gè)訂單號(hào)之間怎么傳我的方案是引入一個(gè)“會(huì)話狀態(tài)容器”所有技能共享讀寫(xiě)但只在執(zhí)行上下文中可見(jiàn)。class SessionContext: def __init__(self): self._state {} self._history [] def set(self, key, value, source_skillNone): self._state[key] value self._history.append({key: key, value: value, source: source_skill}) def get(self, key): return self._state.get(key)技能之間不直接互調(diào)而是通過(guò) SessionContext 交換數(shù)據(jù)。這樣做的好處是鏈路可追溯每一步誰(shuí)寫(xiě)的、誰(shuí)讀的全在歷史記錄里復(fù)盤(pán)和 Debug 都很方便。我還建議給關(guān)鍵狀態(tài)字段加來(lái)源標(biāo)記出現(xiàn)臟數(shù)據(jù)時(shí)能快速定位是哪一步技能寫(xiě)壞的。4. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄4.1 模型把技能調(diào)用參數(shù)傳錯(cuò)了怎么辦這是我被問(wèn)最多的問(wèn)題。比如技能要求參數(shù)是訂單號(hào)模型傳了用戶手機(jī)號(hào)。排查后我發(fā)現(xiàn)根因基本是技能描述里沒(méi)有限制參數(shù)來(lái)源。解決辦法是在 schema 里增加“參數(shù)來(lái)源規(guī)則”描述字段明確告訴模型這個(gè)參數(shù)應(yīng)該從用戶對(duì)話里哪個(gè)部分提取還是從上下文里拿。{ order_id: { type: string, description: 訂單號(hào)來(lái)源于用戶提供或上下文中的賬號(hào)訂單記錄, source: user_query_or_session_state } }加了這行后模型傳錯(cuò)參數(shù)的頻率大幅下降。還有一個(gè)技巧是在少數(shù)關(guān)鍵參數(shù)上使用“枚舉約束”把常見(jiàn)取值列出來(lái)模型會(huì)傾向于從枚舉里選而不是自己發(fā)揮。4.2 技能描述過(guò)長(zhǎng)導(dǎo)致模型截?cái)嗷蚧靵y技能數(shù)量多了之后描述總和動(dòng)不動(dòng)就上萬(wàn) token。我一開(kāi)始把所有技能描述全部拼進(jìn) system prompt結(jié)果模型行為反而變差甚至出現(xiàn)把后一個(gè)技能的字段值寫(xiě)到前一個(gè)技能里的情況。排查到頭發(fā)現(xiàn)是 prompt 太長(zhǎng)、注意力分散了。為此我把技能描述拆成兩層總覽層只放每個(gè)技能的一句話摘要細(xì)節(jié)層按需拉取模型選定技能后才把完整描述和 schema 補(bǔ)充進(jìn)去。這就像點(diǎn)菜時(shí)先看菜單概覽選中某道菜再看它的詳細(xì)配料。實(shí)測(cè)相同場(chǎng)景下 token 消耗降了 60%工具選擇準(zhǔn)確率反而提升了。4.3 技能互相沖突或邊界不清晰兩個(gè)技能描述之間有重疊模型就會(huì)犯選擇困難。比如“查詢訂單狀態(tài)”和“查詢訂單物流信息”在用戶說(shuō)“我的訂單怎么樣了”時(shí)模型經(jīng)常不知道該調(diào)用哪個(gè)。解決辦法不是讓描述更詳細(xì)而是明確“邊界優(yōu)先級(jí)”。我會(huì)在這個(gè)場(chǎng)景里約定模糊表達(dá)時(shí)優(yōu)先調(diào)用更通用的技能更具體的技能要求用戶表達(dá)中出現(xiàn)對(duì)應(yīng)關(guān)鍵詞才觸發(fā)。QueryOrderStatus通用查詢?nèi)魏斡唵我蓡?wèn)的首選 QueryLogisticsInfo僅當(dāng)用戶明確提到物流、快遞、發(fā)貨、運(yùn)輸?shù)汝P(guān)鍵詞時(shí)使用把這條規(guī)則直接寫(xiě)進(jìn)系統(tǒng)提示詞里模型就不再猶豫了。這類邊界問(wèn)題一定要靠顯式規(guī)則來(lái)解決不要指望模型自己悟。4.4 技能執(zhí)行超時(shí)與重試策略技能內(nèi)部調(diào)用外部 API 時(shí)超時(shí)是家常便飯。我的建議是做好“三級(jí)超時(shí)”技能內(nèi)部單次 API 請(qǐng)求 5 秒超時(shí)技能整體執(zhí)行 30 秒超時(shí)Agent 整體交互 60 秒超時(shí)。每一級(jí)超時(shí)后都有對(duì)應(yīng)的重試或降級(jí)策略。尤其注意重試不能無(wú)腦重發(fā)。比如支付類技能重試可能導(dǎo)致重復(fù)扣款這種場(chǎng)景必須設(shè)計(jì)冪等鍵。技能每次執(zhí)行前生成一個(gè) request_id下游接口用這個(gè) id 去重重試時(shí)帶上同樣的 request_id 就不會(huì)重復(fù)扣款。4.5 技能調(diào)試的日志與復(fù)盤(pán)方法論最后聊聊調(diào)技能時(shí)的工程習(xí)慣。我在本地和線上都開(kāi)了完整的技能調(diào)用日志每條日志至少包含技能名、輸入?yún)?shù)、執(zhí)行耗時(shí)、錯(cuò)誤信息、會(huì)話上下文摘要、模型決策路徑。集成到一個(gè)表里方便復(fù)盤(pán)日志字段說(shuō)明常見(jiàn)問(wèn)題skill_name被調(diào)用的技能名命名不規(guī)范導(dǎo)致歸類困難params傳入?yún)?shù)的原始 JSON參數(shù)缺失或類型錯(cuò)誤duration_ms技能執(zhí)行耗時(shí)耗時(shí)突增說(shuō)明下游 API 異常error_code錯(cuò)誤碼錯(cuò)誤碼不統(tǒng)一難以聚合統(tǒng)計(jì)model_decision模型選擇該技能的理由片段驗(yàn)證模型是否按預(yù)期做選擇有了這些日志線上出問(wèn)題基本十分鐘能定位。我強(qiáng)烈建議每個(gè)技能上線前先跑一遍全量的“模擬用戶意圖”測(cè)試把常見(jiàn)的用戶說(shuō)法、變體和臟輸入都跑一遍一次性把邊界情況收干凈。上線后每周對(duì)日志做一遍聚類分析你會(huì)發(fā)現(xiàn)很多技能描述可以優(yōu)化很多參數(shù)約束可以收緊。按這套思路把 agent-skills 體系搭起來(lái)后我最明顯的感覺(jué)是Agent 的行為可預(yù)測(cè)了。之前靠 prompt 硬頂模型偶爾靈光偶爾抽風(fēng)現(xiàn)在技能定義清晰、路由規(guī)則明確、容錯(cuò)兜底完整生產(chǎn)環(huán)境里跑起來(lái)格外省心。最后一個(gè)建議是別把技能體系想成一次性工程它更像一個(gè)不斷生長(zhǎng)的能力庫(kù)每遇到一次新場(chǎng)景就沉淀一個(gè)技能越用越順手。你要做的就是建好底座、定好規(guī)范剩下交給時(shí)間。