實戰(zhàn):從工具調用到可維護的工程化框架)
上個月我把手頭一個Agent項目重新整理了一遍最大的變化就是多了一個叫agent-skills的模塊。剛開始我覺得這名字太普通但用了幾周之后發(fā)現(xiàn)它才是決定Agent能不能從演示品變成生產(chǎn)力工具的那道分水嶺。后來不少朋友來問這個模塊是怎么設計的我干脆把整個思路、踩過的坑、以及一套可以直接復制的最小實現(xiàn)整理出來。先說清楚agent-skills是什么。它不是某個框架的專屬功能而是一種組織方式把Agent能夠調用的所有外部能力無論是查天氣、查訂單、發(fā)消息還是跑一段計算腳本都統(tǒng)一描述成帶名字、帶參數(shù)、帶說明的“技能”放進一個注冊表里再由一套調度器根據(jù)大模型的選擇去執(zhí)行。可以理解為模型是大腦技能系統(tǒng)是手腳。大腦負責決定要做哪件事手腳負責把事真正辦成。這套內容適合誰看如果你正在寫一個會調用工具的大模型應用或者你想把自己手頭零散寫的幾百個工具函數(shù)整理成一個可維護的技能庫又或者只是好奇Agent內部是怎么做到“讓它點擊按鈕它就點擊按鈕”的都可以接著往下看。我不打算用太高的門檻核心思路和實操細節(jié)都會展開。1. 為什么需要一套“技能系統(tǒng)”從裸模型到能干活的Agent1.1 Agent能干的活取決于技能邊界先聊一個很現(xiàn)實的問題大模型本身只會生成文本。你問它“北京今天適合跑步嗎”它能憑訓練數(shù)據(jù)給你編一段天氣但真讓它去查當前實時溫度、風力、空氣質量它做不到。這時你給它一個工具函數(shù)比如get_weather(city)模型就可以先決策調用這個函數(shù)拿到返回值之后再組織語言回答。沒有技能系統(tǒng)的時候我自己是這么干的寫一堆if 天氣 in user_input: result get_weather()之類的關鍵詞判斷然后人工把結果塞回提示詞里讓模型繼續(xù)生成。項目規(guī)模小還好一旦工具超過二十個關鍵詞匹配就徹底亂套。用戶說“幫我看看明天去杭州穿什么”你的關鍵詞里可能既配到“天氣”又配到“杭州”又配到“出行建議”代碼邏輯很快就變成一坨沒人敢動的面條。技能系統(tǒng)的核心價值是把“讓模型決定用哪個工具、參數(shù)是什么”這件事標準化。模型服務現(xiàn)在基本都開放了結構化調用接口前端聲明一批技能簽名模型在回答時直接輸出一個結構化的調用請求不再需要你自己寫關鍵詞匹配。也就是說Agent能干的活完全取決于你暴露給它的技能邊界。技能越豐富、定義越清晰Agent的上限越高反過來技能描述寫得稀爛模型再聰明也會像沒看說明書的新員工一樣亂操作。1.2 技能系統(tǒng)要解決的四個核心問題我整理技能系統(tǒng)時強迫自己用四個維度去檢驗每加一個技能都問一遍它夠不夠“可見、可用、可控、可復盤”??梢娔P湍懿荒茉诤蜻x技能列表里準確地“看見”這個技能。技能數(shù)量越多可見性問題越嚴重??捎媚P洼敵龅膮?shù)能不能被正確校驗、轉換然后真正落到函數(shù)調用上來。可控技能執(zhí)行有沒有權限邊界、超時控制、限流和審批機制。這點在接真實業(yè)務系統(tǒng)時特別要命??蓮捅P用戶那次請求到底用了哪些技能、傳了什么參數(shù)、執(zhí)行了多久、成功還是失敗能不能全程追溯。這四個維度里大多數(shù)人一開始只盯著“可用”代碼跑通就以為成了。結果上線之后模型頻繁選錯技能、參數(shù)校驗失敗、一個技能被并發(fā)調用幾百次導致下游服務被打掛又或者出了事故根本查不出是哪一步出的問題。所以我在一開始設計agent-skills時就把這四件事全部納入框架而不是等出了問題再補。1.3 為什么叫“技能”而不是“工具”或“插件”這個名字不只是為了好聽。我見過有團隊把每個工具函數(shù)定義成一個“tool”直接暴露給模型也有團隊把各種外部系統(tǒng)接入叫“plugin”插件本身帶了復雜的配置和UI能力。而“技能”這個詞強調的是一層更完整的行為包裝一個技能不光包含“執(zhí)行函數(shù)”還包含“這個技能在什么場景下用、參數(shù)怎么填、有哪些限制、失敗之后怎么處理”。換句話說工具偏向描述“我能做什么動作”技能偏向描述“我能在什么情況下、怎么把一件事做成”。以查天氣為例工具視角get_weather(city: str) - dict只暴露函數(shù)簽名。技能視角一段完整的技能說明書告訴模型“當用戶詢問某城市當前天氣、穿衣建議、出行裝備時使用參數(shù)city必須傳中文城市名沒有找到城市時返回錯誤碼”。這層包裝看起來多寫了不少字但它解決的是Agent應用最常見的問題模型想用工具卻不知道什么時候用最合適。后面我會專門講技能描述怎么寫那時候你就知道多寫的這些字有多值錢。2. 技能的定義與注冊把能力變成模型看得懂的說明書2.1 一份技能文件里該放什么我建議每個技能用一個獨立目錄去存放里面至少包含一份描述文件和一個執(zhí)行腳本。描述文件的最簡結構可以長這樣name: weather_query description: 查詢指定城市的實時天氣。適合在用戶詢問當前天氣、穿衣建議、出行準備時使用。如果用戶沒有給出城市名先把城市列表提供給用戶確認后再調用。 category: weather version: 1.0.0 timeout: 3000 parameters: - name: city type: string required: true description: 城市名使用中文全稱例如北京、杭州。不接受簡稱和拼音。 - name: unit type: string required: false enum: [celsius, fahrenheit] default: celsius description: 溫度單位默認攝氏度你可能會問這些字段是不是每項都必要我的經(jīng)驗是name、description、parameters是金三角缺一個都會出大問題。timeout是給調度器兜底用的后面你會看到模型傻等超時的痛苦。category在技能數(shù)量變多之后非常有用可以作為分組過濾的維度。關于name我強烈建議全項目唯一并且采用“領域_動作”的命名方式比如weather_query、order_cancel、doc_search。因為模型在同時看到幾十個技能時名字本身就是一種記憶錨點取一個一眼能看懂的名字比讓它讀完整段描述再判斷快得多。2.2 描述文本寫得越像“人話”模型選得越準這是我前后調了很多版最深的體會技能描述是寫給誰看的不是寫給人看的是寫給大模型看的。所以你寫的描述必須包含決策信息什么場景用、什么場景不要用、參數(shù)的坑在哪里。舉個反例。我早期給搜索技能寫的描述是description: 提供搜索服務然后模型在用戶問“今天新聞有哪些”的時候真的調搜索了在用戶問“2加2等于多少”的時候也調搜索了。它根本分不清什么時候該搜什么時候不該搜。后來我改成description: 搜索互聯(lián)網(wǎng)資源適合用戶需要獲取實時信息、外部資訊、專業(yè)知識或不確定事實時使用。 不適合簡單的數(shù)學計算、常識性問題、用戶明確不要聯(lián)網(wǎng)的內容。加了“不適合”之后誤調用率明顯下降。這個技巧非常推薦每個技能描述都寫清楚“什么時候別用我”作用等于給模型畫了一條減速帶。2.3 注冊中心命名空間、分類與版本當技能數(shù)量超過五十個你就不能只靠一個數(shù)組硬扛了。我的做法是引入注冊中心啟動時掃描技能目錄建立三層索引按命名空間索引例如weather.*、order.*、doc.*方便做權限控制按標簽索引例如read_only、needs_approval、high_frequency方便調度前做篩選按版本索引同一個技能可以存在v1、v2路由時按配置灰度切換。技能多了以后還會遇到命名沖突。比如兩個團隊各寫了一個send_message我要求所有技能必須帶命名空間前綴內部注冊名類似im.send_message、sms.send_message避免互相覆蓋。否則后加載的那個會把先加載的頂?shù)艟€上模型可能突然調用到一個錯誤實現(xiàn)。2.4 參數(shù)定義決定調用成功率模型選擇技能之后下一步是把自然語言里的關鍵信息映射成參數(shù)。這一步特別容易翻車。我有一次測試技能order_query的參數(shù)order_id被模型填成用戶說的“我上個月買的那個黑色耳機”而不是訂單編號結果查詢接口直接報錯。后來我把參數(shù)定義改得更細每個字段都寫清格式、枚舉、例句并在校驗層做了強校驗。理想的參數(shù)定義應該包括類型和格式比如“字符串長度8到32位”枚舉值寫死給定范圍依賴關系比如“query_type 為 phone 時phone 必填customer_id 可不填”是否需要用戶確認比如涉及下單、發(fā)送信息等敏感操作必須拿到用戶明確同意后再執(zhí)行。參數(shù)校驗失敗時不要直接把錯誤堆棧丟給模型而是返回一段友好、可供模型二次修正的錯誤信息。比如“order_id 格式不正確需要是10位純數(shù)字請向用戶確認訂單號。”模型看到之后往往會重新向用戶追問正確信息而不是直接放棄任務。3. 技能的執(zhí)行與調度連接模型和真實世界的關鍵一跳3.1 一次完整調用的五步流程技能系統(tǒng)跑通的核心流程我總結為五步解析、查找、校驗、執(zhí)行、回填。每一步都不能省。解析把模型輸出的結構化調用請求解析成具體的技能名和參數(shù)字典。注意這里要處理單輪多個技能同時調用的情況有些模型服務會一次返回多個工具調用。查找拿著技能名去注冊表里定位對應的技能實現(xiàn)。找不到時立即返回“技能不存在”的錯誤不要讓模型繼續(xù)猜。校驗按技能定義里的參數(shù)規(guī)則做類型、必填、枚舉檢查。檢查失敗就返回可讀的錯誤信息建議帶著錯誤信息再調一次模型讓它修正。執(zhí)行確認技能存在、參數(shù)合規(guī)之后進入真正的副作用操作比如查詢數(shù)據(jù)庫、發(fā)HTTP請求、落文件?;靥畎褕?zhí)行結果格式化成一段簡短的結構化文本塞回對話上下文讓模型基于真實結果繼續(xù)生成回答。下面這段偽代碼展示了最小調度器的核心結構def dispatch(model_output, registry): skills model_output.get(tool_calls, []) results [] for call in skills: skill_id call[name] args call[arguments] skill registry.lookup(skill_id) if skill is None: results.append({name: skill_id, ok: False, error: skill_not_found}) continue validated, error skill.validate(paramsargs) if error: results.append({name: skill_id, ok: False, error: error}) continue output skill.run(**validated) results.append({name: skill_id, ok: True, output: summarize(output)}) return results我特別想強調“回填”這一步的summarize把執(zhí)行結果壓縮之后再送回模型。直接往上下文里塞原始執(zhí)行結果是上下文爆炸和費用失控的最大來源。3.2 超時、重試和失敗降級真實世界里外部服務不可能永遠穩(wěn)定。我在做技能系統(tǒng)的時候給每個技能都配了獨立的超時時間并且區(qū)分“連接超時”和“讀取超時”。比如查詢類技能連接超時給2秒讀取超時給5秒發(fā)送類技能連接超時給3秒整鏈超時給10秒。超時之后不要靜默失敗要把超時原因寫成簡短提示回填給模型讓模型決定是重試、換方案還是如實告訴用戶。這里有個非常關鍵的坑帶副作用的技能不能盲目重試。查天氣重試三次問題不大但“發(fā)送訂單確認短信”這種技能如果第一次實際已經(jīng)發(fā)出去了只是網(wǎng)絡延遲導致超時你再重試一次用戶會收到兩條一模一樣的短信。我的處理方案是給這類技能增加冪等參數(shù)比如讓前端生成一個request_id后端通過這個ID去重。沒有冪等能力的技能超時后寧可告訴用戶“操作結果未知請稍后查詢”也不要無條件重發(fā)。3.3 技能編排讓技能自己調用技能單個技能解決單點問題但很多任務天然是復合的。比如用戶說“幫我把昨天的銷售數(shù)據(jù)整理成周報發(fā)給總監(jiān)”這里需要拆成“查數(shù)據(jù)”“生成周報文本”“發(fā)郵件”三個動作。如果讓模型一次性調三個技能參數(shù)很難一次給全而且中間一步失敗很難處理。我的做法是支持技能編排技能的執(zhí)行函數(shù)內部可以繼續(xù)調用其他技能。比如定義一個復合技能weekly_report_generate它的代碼邏輯是先調用sales_data_query拿到數(shù)據(jù)后調用content_maker生成文本最后把文本傳到email_send。這樣模型從一開始只需要決定調用一個技能內部流程全由代碼控制比讓模型自由編排穩(wěn)定得多。編排需要注意上下文傳遞。子技能的執(zhí)行結果不要直接丟給大模型的對話上下文而是在內部代碼里做處理和格式化最后只在對話里回填一個精簡結論。否則模型會被一大堆中間數(shù)據(jù)干擾很容易答非所問。3.4 安全邊界白名單、審批、沙箱與配額技能是Agent接觸真實世界的通道安全設計不能省。我把技能按風險分成三個等級L1只讀且低成本比如查天氣、查文檔、算算數(shù)可以直接執(zhí)行L2可寫但對影響面有限比如創(chuàng)建草稿、發(fā)起工單需要用戶在界面上確認一次L3高風險操作比如批量發(fā)送、刪除數(shù)據(jù)、發(fā)起支付必須打到人工審批或者走強校驗。除此之外調度器還需要做配額控制。我就遇到過幾次這樣的線上問題模型在單輪對話里被激發(fā)出十幾個技能調用連環(huán)執(zhí)行把下游系統(tǒng)打掛了。后來每個會話都加了配額限制單輪最多執(zhí)行5個技能、每分鐘最多執(zhí)行20個技能超過之后明確告訴模型“當前操作過于頻繁請先匯總給用戶確認”。配額限制看起來粗暴但在生產(chǎn)環(huán)境里是保命符。4. 從零搭一套最小可用的技能系統(tǒng)實操記錄4.1 第一步設計技能目錄結構我自己用的是下面這種目錄結構簡單清晰一個技能一個文件夾skills/ weather_query/ skill.yaml handler.py order_query/ skill.yaml handler.py weekly_report/ skill.yaml handler.pyskill.yaml放技能定義也就是前面那張表handler.py放執(zhí)行邏輯對外暴露一個run(params)函數(shù)。一個文件夾里多出的其他文件都算這個技能的私有資源比如order_query里放的數(shù)據(jù)庫連接配置、SQL模板只屬于它自己。這樣每個技能的邊界都清晰團隊多人協(xié)作時互不干擾。4.2 第二步寫一個通用的技能加載器加載器的任務是啟動時掃描目錄、讀取定義、動態(tài)導入處理函數(shù)然后注冊到中心注冊表。Python實現(xiàn)大概是這樣import importlib.util import yaml from pathlib import Path class SkillLoader: def __init__(self, root: Path): self.root root def load_all(self): registry {} for skill_dir in self.root.iterdir(): yaml_path skill_dir / skill.yaml handler_path skill_dir / handler.py if not (yaml_path.exists() and handler_path.exists()): continue spec yaml.safe_load(yaml_path.read_text()) handler self._load_handler(handler_path, spec[name]) registry[spec[name]] {spec: spec, handler: handler} return registry def _load_handler(self, path, skill_name): module_name fskills_{skill_name} module_spec importlib.util.spec_from_file_location(module_name, path) module importlib.util.module_from_spec(module_spec) module_spec.loader.exec_module(module) return module.run注意兩個細節(jié)第一動態(tài)導入的模塊緩存要小心熱更新技能時最好在獨立進程里做重載不要直接importlib.reload容易把舊狀態(tài)的全局變量帶進去第二加載前一定要做一次技能名合法性檢查避免路徑拼接問題。4.3 第三步讓模型學會選技能的提示詞設計技能注冊好了還得讓大模型知道怎么用。每次請求模型前需要把當前會話可用的技能列表拼進系統(tǒng)提示詞。我常用的一種寫法是你是智能助手負責根據(jù)用戶請求選擇合適的技能完成任務。 可用技能如下 {skills_json} 選擇規(guī)則 1. 僅在確實需要外部信息或操作時才調用技能簡單常識問題不要調用。 2. 一次可以調用多個技能但必須保證每個技能都有明確用途。 3. 調用時嚴格遵循技能參數(shù)要求缺少必要信息時先向用戶提問。 4. 技能執(zhí)行結果以觀測內容為準不要猜測。 請同時輸出要執(zhí)行的動作和回復文本動作格式為JSON數(shù)組。這里最大的坑是技能太多當技能列表超過30條模型的選擇準確率會明顯下降還會消耗大量token。我的解決方案是“兩級召回”先用分類粗篩比如根據(jù)對話意圖匹配若干候選技能只把候選技能列表拼進提示詞如果候選里沒有匹配項再退回全量技能列表。這樣既省token又提升準確率。4.4 第四步執(zhí)行結果回填與上下文管理技能執(zhí)行完返回的數(shù)據(jù)怎么進入下一輪模型對話是決定對話質量的關鍵。這里我特別不建議把原始返回一整個塞回去。舉例來說order_query可能返回一個幾十行的大訂單列表直接塞回去模型會迷失在數(shù)據(jù)里還會產(chǎn)生巨額token消耗。我通常先做一次結構化摘要比如把“查詢到3筆訂單合計金額1280元最近一筆是昨天購買的運動鞋”這種輕量結論回填。如果用戶后續(xù)明確要求查看明細再觸發(fā)一次帶明確條件的查詢把需要的那部分明細展示出來。這背后是一個“先粗后細”的思路技能系統(tǒng)不負責一次性輸出所有信息而是根據(jù)對話需要按需拉取。4.5 第五步日志、追蹤與效果度量沒有觀測的技能系統(tǒng)是沒法迭代的。我要求系統(tǒng)里所有技能調用統(tǒng)一打印結構化日志內容包括會話ID、技能名稱、入?yún)?、出參摘要、耗時、錯誤信息、模型生成的原始調用串。每個會話再分配一個trace_id把模型的推理輪次和技能調用串成一個完整鏈路。日志不只用來排查線上問題更是優(yōu)化技能描述的依據(jù)。我會定期統(tǒng)計幾個指標技能命中率候選列表里被實際調用的比例參數(shù)校驗失敗率模型生成參數(shù)一次通過校驗的比例執(zhí)行成功率技能真正跑通并返回有效結果的比例平均耗時單個技能從被選中到返回結果的時間。這幾個數(shù)字一出來哪些技能描述不清晰、哪些技能簽名太復雜立刻一目了然。比如參數(shù)校驗失敗率高的技能優(yōu)先去優(yōu)化parameters.description命中率低的技能大概率是因為描述里缺少典型觸發(fā)場景。5. 常見問題與排查經(jīng)驗實錄5.1 模型為什么老選錯技能這幾乎是每個人都會遇到的第一個瓶頸。癥狀是用戶問A話題模型卻去調了B技能。常見原因有三個技能描述太寬泛、技能命名近似、缺少反例約束。有一次我上線兩個技能doc_search和doc_preview。前者搜文檔列表后者預覽某個具體文檔內容。模型經(jīng)常在用戶想看某篇文檔具體內容的時候去調doc_search把返回的文檔列表原樣念給用戶。后來我把doc_preview的描述里加了“僅當用戶已經(jīng)確定了具體文檔ID或文檔標題時調用”并把doc_search的描述改成“僅返回文檔標題列表不包含正文內容需要預覽詳情請使用 doc_preview”。兩條技能描述一互補選錯率立刻降了下來。如果你發(fā)現(xiàn)怎么改描述都不見效建議直接加一個反面示例到系統(tǒng)提示詞里把用戶可能會說出的近義表達列出來并指明不該匹配的技能。給模型看例子比講一百遍規(guī)則都管用。5.2 模型把參數(shù)填成了幻覺值參數(shù)幻覺有兩種一種是填了不存在的值比如把“上個月”解釋成一個具體日期但算錯了另一種是填了用戶根本沒提到的值比如用戶只說了“查最近的訂單”模型自己補了一個order_id12345。我的處理套路是三層防線。第一層參數(shù)描述里寫清楚來源比如“order_id必須來自用戶提供的訂單號如果用戶未提供該參數(shù)置空并詢問用戶”。第二層校驗層對枚舉值和格式做強校驗格式不對直接攔截。第三層如果模型連續(xù)兩次校驗失敗就不再讓它猜測了改成向用戶發(fā)起一次結構化澄清列出當前已獲得的參數(shù)和缺失參數(shù)請用戶確認。5.3 技能執(zhí)行超時模型開始編造結果這是個很隱蔽的問題。有一次技能因為慢查詢超時了我在超時后沒有回填錯誤信息給模型結果模型為了完成回答自己“腦補”了一個查詢結果用戶還在那兒等。后來我特意驗證了一下發(fā)現(xiàn)確實有部分模型服務在拿不到工具結果時會選擇自行編造而不是承認失敗。這提醒我兩件事第一技能執(zhí)行結果不管是成功還是失敗一定要有明確狀態(tài)返回第二超時后必須把這個事實原樣告訴模型禁止它猜測。正確的回填格式類似工具狀態(tài)timeout 錯誤信息order_query 在5秒內未返回結果 處理建議告知用戶查詢超時建議稍后重試不要提供任何推測數(shù)據(jù)對于真正耗時的任務比如生成一份幾十頁報表就不要用同步等待的方案了。改為技能先返回“任務已受理ID為xx”系統(tǒng)后臺跑完后把結果回填進上下文。這種異步模式體驗會好很多。5.4 多輪對話里上下文被技能結果撐爆技能調用次數(shù)一多上下文里會積累大量工具結果、中間思考片段。我見過一個會話才聊了七八輪就觸發(fā)了模型上下文上限前端直接報錯。原因基本都出在把完整的技能返回原文塞回上下文。對策是“每層都要壓縮”技能返回的原始數(shù)據(jù)不進上下文只進執(zhí)行層處理邏輯進入上下文的必須是摘要摘要里只保留跟當前用戶問題直接相關的內容其余用一句“還有X條其他記錄”帶過。如果確實需要長文本比如調了一個文檔預覽技能返回了一篇長文就單獨把長文掛到會話的臨時存儲里上下文里只放一個文章ID和開頭摘要等模型決定要給用戶展示時再讀全文。5.5 技能上線前的驗收清單整理了一套我每次上技能前都會過的檢查清單分享出來給你抄作業(yè)。檢查項通過標準沒通過會怎樣描述清晰度讓一個不了解業(yè)務的同事讀一遍描述能說清什么場景用這個技能模型也會同樣困惑選錯技能參數(shù)完整性所有參數(shù)都有類型、必填標記、格式描述和示例參數(shù)幻覺、校驗失敗超時與重試策略明確超時時間副作用操作有冪等方案超時后模型編造結果或重復操作失敗提示文案執(zhí)行失敗時能返回一句可讀的處理建議模型無法自行修正任務中斷日志埋點包含技能名、參數(shù)、耗時、狀態(tài)、trace_id出問題無法定位配額與權限技能級別已分級敏感操作已加確認安全問題、系統(tǒng)被拖垮容量評估高頻技能已做緩存或限流下游服務被打掛每次上線新技能之前過一遍這張表能省掉至少一半線上事故。特別是“描述清晰度”這一項我強烈建議你在真實模型上多試幾個說法別只盯著代碼邏輯跑通就發(fā)版。這套agent-skills我前后迭代了三版最深的體會是代碼的復雜度沒有想象中高真正耗時間的是把每個技能說明書寫到讓模型一眼就能看懂。你去看市面上很多Agent能力不及預期的案例八成不是模型不行而是技能邊界描述得模模糊糊模型像個在公司沒待兩天、對流程一知半解的新人上哪兒找工具都不知道又怎么指望它把事辦漂亮。如果你正在搭自己的技能系統(tǒng)我建議從最小的兩三個技能開始先把加載、校驗、回填的閉環(huán)跑通再慢慢往上加。技能描述不要靠拍腦袋寫上線之后盯著命中率和校驗失敗率去調你會很明顯地看到Agent的表現(xiàn)一點點變好。這套方法不需要什么高深理論但它真的能讓你的Agent從“會聊天”升級成“會干活”。