設(shè)計實(shí)戰(zhàn):從概念到代碼實(shí)現(xiàn)與調(diào)試指南)
這兩天一直在折騰 Agent 相關(guān)的項(xiàng)目正好把技能這塊的內(nèi)容重新梳理了一遍。所謂 agent-skills說人話就是你給大模型裝上一雙手讓它不只是會聊天還能真正去操作點(diǎn)什么。今天我把自己的實(shí)踐經(jīng)驗(yàn)和踩坑記錄整理出來從技能的設(shè)計思路、實(shí)現(xiàn)方式到調(diào)試技巧一次性聊透。什么是 Agent 技能簡單講就是一組能被大模型自主調(diào)用的能力模塊。你可以把大模型想象成一個剛?cè)肼毜膶?shí)習(xí)生腦子好使、理解力強(qiáng)但什么工具都不會用。技能體系就是給這個實(shí)習(xí)生配齊辦公軟件、數(shù)據(jù)庫權(quán)限、API 接口并告訴他遇到什么情況該用哪個工具。沒有技能的 Agent 只能紙上談兵有了技能它才能真的干活。下面我會以一個真實(shí)項(xiàng)目為例拆解技能的完整落地過程包括框架選型、技能定義、代碼實(shí)現(xiàn)、測試調(diào)優(yōu)以及幾個我踩過的大坑。內(nèi)容偏實(shí)操想直接上手的朋友可以照著敲。1. 技能化改造為什么說這是 Agent 落地繞不開的一步1.1 大模型只是大腦技能才是手腳早期做 AI 應(yīng)用大家喜歡把業(yè)務(wù)邏輯全部塞進(jìn) Prompt 里讓模型自己發(fā)揮。這種方式在聊天場景還湊合但到了真實(shí)業(yè)務(wù)里就崩了模型不知道當(dāng)前庫存數(shù)據(jù)、沒法調(diào)外部接口、算不對復(fù)雜賬目、甚至?xí)褑挝桓慊?。問題不在模型笨而是它沒有工具。Agent 技能本質(zhì)上是把模型的能力邊界向外擴(kuò)展。我在項(xiàng)目里最開始用的方案是直接在 System Prompt 里寫幾十條規(guī)則告訴模型你能做這個、能做那個。結(jié)果 Prompt 越長模型越容易懵經(jīng)常答非所問還總幻覺出不存在的數(shù)據(jù)。后來換成了技能化方案把每個能力封裝成獨(dú)立的工具函數(shù)用 JSON Schema 描述參數(shù)讓模型按需調(diào)用。效果立竿見影模型不再瞎猜而是明確知道哦這里我需要調(diào) get_stock_price 這個技能。所以我的第一個建議是如果你的 Agent 還停留在純 Prompt 對話的階段那它距離可用還有很長的路。技能化改造是必經(jīng)之路越早做越好。1.2 技能化拆解帶來的三個實(shí)際好處我在實(shí)際項(xiàng)目中體會到的技能化收益總結(jié)下來就三點(diǎn)。第一可測試性大幅提升。每個技能函數(shù)都是獨(dú)立單元可以單獨(dú)寫測試用例出問題能快速定位是技能本身 bug 還是模型調(diào)度錯了。以前那種全塞 Prompt的方式出問題你根本不知道是哪里壞了。第二代碼復(fù)用變得自然。多個場景共用的能力比如查天氣、算運(yùn)費(fèi)、發(fā)通知抽成技能后任何業(yè)務(wù)場景的 Agent 都能直接掛載不用重復(fù)開發(fā)。我們團(tuán)隊(duì)后來新起一個項(xiàng)目復(fù)用已有技能模塊節(jié)省了大約三分之一的工作量。第三升級迭代的路徑清晰了。每次給 Agent 加新能力本質(zhì)上就是寫好一個新技能函數(shù) 定義好描述 注冊進(jìn)技能列表。這套流程標(biāo)準(zhǔn)化之后團(tuán)隊(duì)里的新同學(xué)也能快速上手。2. 技能體系設(shè)計從需求分析到模塊劃分的完整思路2.1 先梳理業(yè)務(wù)場景而不是先寫代碼我很早之前犯過一個錯一上來就開始寫技能函數(shù)寫完之后才發(fā)現(xiàn)技能列表東一個西一個有的技能之間功能重疊有的技能又沒人用。后來我老老實(shí)實(shí)先做了業(yè)務(wù)場景梳理效果完全不一樣。我用的方法是畫場景矩陣。把用戶可能會提的需求全部列出來然后按頻率和復(fù)雜度兩個維度分類高頻且低復(fù)雜度比如查訂單狀態(tài)、算價格這類技能優(yōu)先做ROI 最高。低頻但高復(fù)雜度比如生成季度報表這類可以做成復(fù)合技能內(nèi)部編排多個基礎(chǔ)技能完成。低頻且低復(fù)雜度暫時不做讓模型直接回答或引導(dǎo)用戶走人工流程。高頻但高復(fù)雜度這類是難點(diǎn)需要拆成多個細(xì)粒度技能再加一層調(diào)度邏輯。我自己實(shí)際項(xiàng)目里要處理的場景是企業(yè)內(nèi)部的智能助理用戶會問庫存、銷售額、物流狀態(tài)、售后政策等等。按上面的矩陣一梳理最終確定首批做 8 個基礎(chǔ)技能、2 個復(fù)合技能。先跑通核心閉環(huán)再逐步補(bǔ)充。2.2 技能粒度怎么定粗了不管用細(xì)了管不過來技能粒度的把控是設(shè)計階段最考究的一個點(diǎn)。我剛開始做的時候把技能切得非常碎比如獲取用戶姓名獲取用戶手機(jī)號獲取用戶地址分開做成了三個技能。結(jié)果模型調(diào)度的時候頻繁出錯經(jīng)常需要連續(xù)調(diào)用三次才能湊齊一個用戶信息。后來又走另一個極端把獲取全部用戶數(shù)據(jù)做成一整個大技能參數(shù)列表長得嚇人。這種設(shè)計的問題也不小一方面模型容易傳錯參數(shù)另一方面某些場景只需要一個字段也把全部數(shù)據(jù)拉回來白白浪費(fèi) token。最終我找到比較合理的平衡點(diǎn)是按業(yè)務(wù)操作而非數(shù)據(jù)字段來劃分技能。比如對用戶信息這個場景做一個 get_user_profile 技能內(nèi)部返回結(jié)構(gòu)化完整對象但允許模型只取其中部分字段來用。這樣既不會太碎也沒有過度聚合。粒度判斷的統(tǒng)一標(biāo)準(zhǔn)我總結(jié)了三句話一次技能調(diào)用對應(yīng)一個完整的業(yè)務(wù)動作而不是一個數(shù)據(jù)查詢技能參數(shù)控制在 3~5 個以內(nèi)太多說明粒度偏大或邊界不清如果多個技能內(nèi)部調(diào)用了同一段核心邏輯那這段核心邏輯應(yīng)該抽成公共組件而不是做進(jìn)多個并列技能。2.3 技能間的依賴與沖突避免一個技能調(diào)另一個技能實(shí)際操作中我強(qiáng)烈建議技能之間不要互相調(diào)用。先別急著反對我解釋下原因。技能互相調(diào)用會讓系統(tǒng)的調(diào)試難度直線上升。假設(shè)技能 A 內(nèi)部調(diào)了技能 BB 內(nèi)部又調(diào)了 C一旦 C 出問題你排查的時候根本不知道是哪一層出了問題。更重要的是大模型調(diào)度技能本身就存在不確定性技能嵌套調(diào)用等于在這個不確定的鏈條上又加了一層不確定。正確的做法是抽象出一層公共邏輯層。如果兩個技能都需要用某個公共能力那就把這個能力寫成一個普通函數(shù)放在 utils 里兩個技能直接調(diào)用它而不是技能之間互相對話。這個設(shè)計原則讓我在生產(chǎn)環(huán)境省了太多心強(qiáng)烈推薦。2.4 技能描述的寫法模型的使用說明書技能函數(shù)名、參數(shù) Schema 之外還有一個容易被低估的部分技能描述description。模型靠什么判斷該不該調(diào)用這個技能主要就看描述。我見過太多人草草寫一句獲取訂單信息就完事了結(jié)果模型該調(diào)用的不調(diào)用不該調(diào)用的一通亂調(diào)。好的技能描述應(yīng)該包含這個技能是干什么的、適用場景是什么、有哪些邊界或限制、調(diào)用后返回什么。我舉個例子get_order_info這個技能我最終定下來的描述是根據(jù)訂單ID查詢訂單詳情包括商品列表、金額、支付狀態(tài)、物流單號。用于用戶詢問我的訂單怎么樣了發(fā)貨了沒等場景。如果用戶沒提供訂單號先調(diào)用 get_order_list 獲取訂單列表。注意本技能只支持已支付的訂單預(yù)售訂單查不到物流信息。 寫完之后模型決策的準(zhǔn)確率肉眼可見地提高了。3. 實(shí)操過程從零實(shí)現(xiàn)一套 Agent 技能系統(tǒng)3.1 框架選型為什么我選擇了開源工具鏈而不是自研技能系統(tǒng)說白了有兩件事要做一是把技能函數(shù)注冊成模型能理解的格式二是把模型輸出的調(diào)用意圖解析成真實(shí)代碼執(zhí)行。這兩件事沒必要重復(fù)造輪子目前開源生態(tài)里已有比較成熟的方案。我做選型的時候?qū)Ρ冗^幾套方案基于 LangChain 的 Tool 機(jī)制生態(tài)最豐富網(wǎng)上資料多團(tuán)隊(duì)上手快。基于 OpenAI Function Calling 原生的函數(shù)定義方式最輕量適合沒有復(fù)雜鏈路、直接調(diào)用模型接口的場景?;?Semantic Kernel 的 Plugins 方案如果你在 .NET 技術(shù)棧這個可以看。自己寫一套描述 函數(shù)映射的輕量框架如果你們的技能數(shù)很少少于 5 個這種方式也可以夠用就行。我實(shí)際項(xiàng)目選的是 LangChain 原生 Function Calling 混用的方案。為什么這么選因?yàn)?LangChain 的抽象層級比較完善把模型的對話管理、上下文記憶、工具回調(diào)都封裝好了而原生 Function Calling 在函數(shù)定義格式上更標(biāo)準(zhǔn)兩者結(jié)合既能快速開發(fā)又不至于被框架綁死。這里提醒一句框架只是工具真正核心的其實(shí)是你的技能設(shè)計做得怎么樣。工具選得再好技能本身定義得稀爛模型照樣給你亂調(diào)。3.2 技能注冊與實(shí)現(xiàn)一份可落地的代碼示例下面這段代碼我從項(xiàng)目里簡化出來的展示了一個技能的全過程。技術(shù)棧是 Python LangChain OpenAI 兼容接口。首先要定義一個技能函數(shù)函數(shù)本體執(zhí)行真實(shí)的業(yè)務(wù)邏輯。比如查詢庫存# skills/inventory.py import json from typing import Optional def get_stock_level(sku: str, warehouse_id: Optional[str] None) - dict: 查詢商品庫存量。 Args: sku: 商品編碼必填。 warehouse_id: 倉庫ID可不填默認(rèn)查全部倉庫。 Returns: dict: 庫存信息包含 sku、總庫存、各倉庫明細(xì)。 # 這里替換成你的真實(shí)數(shù)據(jù)源 fake_db { SKU-1001: {total: 328, warehouses: {WH-01: 180, WH-02: 148}}, SKU-1002: {total: 56, warehouses: {WH-01: 30, WH-02: 26}}, } if sku not in fake_db: return {error: fsku {sku} 不存在} data fake_db[sku] if warehouse_id: if warehouse_id not in data[warehouses]: return {error: f倉庫 {warehouse_id} 無此商品} return {sku: sku, warehouse_id: warehouse_id, stock: data[warehouses][warehouse_id]} return {sku: sku, total: data[total], warehouse_detail: data[warehouses]}然后你需要把這個函數(shù)翻譯成大模型能讀懂的格式。以 OpenAI Function Calling 的格式為例# skills/registry.py get_stock_level_schema { type: function, function: { name: get_stock_level, description: 根據(jù) SKU 查詢商品庫存。當(dāng)用戶詢問還有貨嗎庫存多少時使用。 支持按倉庫查詢不傳倉庫ID則返回全倉匯總。, parameters: { type: object, properties: { sku: { type: string, description: 商品編碼格式如 SKU-1001 }, warehouse_id: { type: string, description: 倉庫ID可選格式如 WH-01 } }, required: [sku] } } } SKILL_REGISTRY { get_stock_level: get_stock_level, # 其他技能... }第三步是搭一個執(zhí)行循環(huán)讓模型在對話中自主決定是否調(diào)用技能以及調(diào)用哪個。核心邏輯用偽代碼表達(dá)# agent_loop.py from skills.registry import SKILL_REGISTRY, get_stock_level_schema import json def run_agent(user_input: str, messages: list): llm get_llm() # 你的模型實(shí)例 # 把技能格式傳給模型 tools [get_stock_level_schema] messages.append({role: user, content: user_input}) response llm.chat.completions.create( modelyour-model, messagesmessages, toolstools, tool_choiceauto, ) # 如果模型決定調(diào)用技能 while response.choices[0].message.tool_calls: msg response.choices[0].message messages.append(msg) for call in msg.tool_calls: func_name call.function.name arguments json.loads(call.function.arguments) result SKILL_REGISTRY[func_name](**arguments) messages.append({ role: tool, tool_call_id: call.id, content: json.dumps(result, ensure_asciiFalse) }) response llm.chat.completions.create( modelyour-model, messagesmessages, toolstools, tool_choiceauto, ) # 最終模型根據(jù)技能返回結(jié)果生成給用戶的自然語言回答 return response.choices[0].message.content這段邏輯你一定要親手寫一遍不要只停留在看過。它把 Agent 技能的完整閉環(huán)串了起來模型識別意圖 - 選擇技能 - 填入?yún)?shù) - 執(zhí)行函數(shù) - 把結(jié)果反饋給模型 - 模型組織自然語言回答。整個鏈路理解了Agent 技能你已經(jīng)掌握大部分了。3.3 一個復(fù)合技能的設(shè)計內(nèi)部編排多步操作前面提到復(fù)合技能這類技能本身也是一個函數(shù)但它內(nèi)部會依次調(diào)用多個基礎(chǔ)技能或外部服務(wù)。舉例我在項(xiàng)目里做過一個訂單全鏈路追蹤的復(fù)合技能用戶只需要說我的訂單到哪了這個技能會自動完成調(diào)訂單接口拿訂單詳情 - 判定訂單狀態(tài) - 如果已發(fā)貨則調(diào)物流接口 - 匯總狀態(tài)輸出。模型只需要做一次調(diào)用決策復(fù)雜的編排邏輯隱藏在技能內(nèi)部。為什么要這么設(shè)計核心原因是減少模型的決策負(fù)擔(dān)和調(diào)用次數(shù)。每次模型決策都伴隨不確定性調(diào)用次數(shù)越少出錯概率越小token 開銷也越低。如果你的復(fù)合技能內(nèi)部邏輯穩(wěn)定不變完全可以在代碼層面寫死沒必要讓模型分步執(zhí)行。# skills/order_tracker.py def track_order_full(order_id: str) - dict: 復(fù)合技能追蹤訂單全鏈路狀態(tài)。 # 第一步查訂單詳情 order_info get_order_detail(order_id) if not order_info or order_info[status] pending: return {order_id: order_id, status: 未發(fā)貨, timeline: []} # 第二步已發(fā)貨則查物流 shipment_id order_info[shipment_id] logistics get_logistics_track(shipment_id) latest_status logistics[tracks][-1][desc] if logistics[tracks] else 暫無軌跡 return { order_id: order_id, status: order_info[status], latest_logistics: latest_status, logistics_list: logistics[tracks] }復(fù)合技能的下層基礎(chǔ)技能同樣需要注冊到技能列表嗎不需要。下層技能是普通函數(shù)即可只暴露復(fù)合技能給模型調(diào)用。這樣技能列表保持精簡模型的決策空間也更干凈。3.4 參數(shù)校驗(yàn)與自動糾錯模型傳參不可靠必須有兜底作為一個踩坑無數(shù)的人我必須強(qiáng)調(diào)模型傳參真的不可靠。哪怕你 Schema 定義得再嚴(yán)格模型有時候就是會給你傳一個不存在的時間格式、漏傳必填字段、甚至把字符串和數(shù)字類型搞混。所以技能函數(shù)內(nèi)部一定要做防御式編程。我項(xiàng)目里類似 get_stock_level 這樣的函數(shù)開頭必然有一段參數(shù)校驗(yàn)def get_stock_level(sku: str, warehouse_id: Optional[str] None): sku (sku or ).strip().upper() if not sku.startswith(SKU-): # 嘗試自動補(bǔ)全 sku SKU- sku if warehouse_id: warehouse_id warehouse_id.strip().upper() # 后續(xù)邏輯...這個自動糾偏的思路很重要。你與其讓模型報錯重來不如在技能內(nèi)部做一層容錯。因?yàn)橐淮五e誤調(diào)用不僅浪費(fèi)一輪對話還可能讓用戶覺得 Agent 很蠢。3.5 技能測試單測、集成測試、回歸測試三層保障技能代碼寫完之后直接上線我勸你冷靜。技能系統(tǒng)的測試要分三層跑第一層是單元測試針對技能函數(shù)本身。用 pytest 之類的工具把正常輸入、異常輸入、邊界輸入全跑一遍。第二層是模型調(diào)度測試模擬用戶指令看模型選技能選得對不對、參數(shù)填得對不對。這一層是技能系統(tǒng)最特殊的地方因?yàn)槟P陀须S機(jī)性同一句話多跑幾次結(jié)果可能不同。我做的時候會固定隨機(jī)種子同時每條用例跑 5 遍要求至少 4 遍通過算合格。第三層是端到端回歸測試把真實(shí)用戶對話歷史回放看改造技能后有沒有影響其他現(xiàn)有功能。這個我建議接入 CI/CD每次改技能描述或參數(shù)都自動跑一遍。下表是我在日常測試的時候常用的用例示例大家可以參考一下用例類型用戶輸入示例期望觸發(fā)技能期望參數(shù)正常場景B1001 還剩多少庫存get_stock_level{sku: SKU-1001}參數(shù)缺失幫我看看庫存get_stock_level引導(dǎo)補(bǔ)充{}模型應(yīng)主動追問邊界場景庫存調(diào)一下無對應(yīng)技能拒絕調(diào)用-多技能場景這個訂單發(fā)貨沒順便看下庫存track_order_full{order_id: ...}4. 常見問題與排查技巧實(shí)錄4.1 模型死活不調(diào)用技能怎么辦這是群里被問得最多的問題。癥狀是用戶問了跟技能強(qiáng)相關(guān)的問題模型卻直接憑記憶編了一個答案。排查路徑我建議按順序來檢查技能是否真的傳給模型了。很多人改完代碼tools 列表沒更新模型自然不知道新技能。這個低級錯誤我犯過不止一次。檢查技能描述是否和用戶的表達(dá)方式匹配。模型判斷是否調(diào)用技能靠的核心就是描述文本。你寫查詢庫存量用戶說的是還有貨嗎模型可能就反應(yīng)不過來??梢园衙枋龈某刹樵儙齑媪坑糜诨卮鹩脩絷P(guān)于商品是否有貨、剩余多少的問題。檢查模型的 system prompt 里有沒有暗示它直接回答的語句。4.2 技能調(diào)用了但參數(shù)填得亂七八糟怎么治我遇到過一個具體案例get_stock_level 技能明明定義好了參數(shù)格式模型卻喜歡把 SKU-1001 傳成 sku1001導(dǎo)致每次查詢都報錯。后來我在技能描述里明確了SKU 格式為 SKU-四位數(shù)字用戶只提供數(shù)字時請自動補(bǔ)全 SKU- 前綴問題立刻緩解。另外參數(shù)描述里可以給出常見的錯誤示例模型會更清楚邊界在哪里。比如在描述里寫上如果用戶給出的是商品名稱而非 SKU不要調(diào)用本技能應(yīng)調(diào)用 search_product_by_name。這個技巧很實(shí)用。4.3 一次對話里模型連續(xù)調(diào)用同一技能多次浪費(fèi) token 還慢這也是我遇到過的問題。用戶問這雙鞋和那雙鞋的庫存分別是多少模型分成兩次調(diào)用 get_stock_level而不是一次數(shù)組參數(shù)搞掂。這種情況如果要根治需要把技能設(shè)計成支持批量查詢。如果不方便改接口另一個做法是在技能描述里寫明如需查詢多個商品庫存請分別調(diào)用本技能多次。這是目前我找到的最省事方案。4.4 我在技能系統(tǒng)迭代過程中沉淀的幾條心得最后分享幾點(diǎn)我在踩了無數(shù)坑之后才想明白的事。技能描述不要寫得像 API 文檔要寫得像寫給人類同事的便簽。你平時怎么告訴同事這個接口怎么用、注意什么就怎么寫描述。模型對自然語言的接受能力遠(yuǎn)超結(jié)構(gòu)化字段。技能數(shù)量和調(diào)用成功率不是線性關(guān)系。技能越多模型的選擇困難癥越明顯。我實(shí)測下來單一模型一次對話中暴露的技能數(shù)最好控制在 8~12 個以內(nèi)。超過這個量模型開始頻繁選錯或猶豫不決。解決方案是把技能分組根據(jù)用戶意圖先匹配合適的技能組再在組內(nèi)做具體選擇。技能的執(zhí)行結(jié)果一定要加錯誤信息。這樣即使用戶提供的參數(shù)有問題模型拿到工具報錯的結(jié)果后也能自動調(diào)整策略重新獲取正確參數(shù)。我就靠著這個機(jī)制讓很多原本需要人工介入的錯誤被模型自己糾正了。另外還要提一句在技能代碼里做好運(yùn)行日志是投資回報率極高的習(xí)慣。每次技能調(diào)用記錄一下輸入輸出用簡單的 JSON 格式打在本地日志里線上出問題能快速復(fù)盤。我見過太多人上線了技能卻不打日志出了問題只能拍腦袋猜。這套技能體系搭好之后之后再接新的業(yè)務(wù)場景其實(shí)就變成填空題了梳理需求、寫技能函數(shù)、定義描述、注冊、測試。流程順手之后一個中等復(fù)雜度的技能大概半天就能交付。這篇內(nèi)容算是我做 Agent 技能系統(tǒng)迄今比較完整的一次復(fù)盤希望里面提到的思路和坑能幫你少走一段彎路。