的設(shè)計與落地復盤)
如果你正在做一個Agent項目大概率會遇到那個經(jīng)典瓶頸功能越堆越多工具函數(shù)越加越復雜但大模型選擇工具的準確率反而越來越差。我們一度被“工具函數(shù)越加越多、調(diào)用準確率越來越差”這個問題卡了很久直到把思路從“堆工具”換成“搭技能體系”——也就是給Agent配置一套可注冊、可檢索、可組合的skills庫讓模型不再面對龐大的工具海而是按需選擇當前任務真正需要的能力單元。這篇文章是我在項目里把agent-skills落到生產(chǎn)環(huán)境后的一些完整復盤包括為什么需要它、技能清單怎么設(shè)計、注冊與檢索機制怎么做、描述怎么寫得讓模型更準確以及我們踩過的幾個坑。如果你也在做Agent應用、AI工作流或者正準備給助手類產(chǎn)品加“技能擴展”能力這篇應該能給你一些實際參考。1. 從“工具堆”到“技能庫”這一步為什么繞不開1.1 二十個工具函數(shù)塞進System Prompt之后發(fā)生了什么先說說我最開始的做法。早前某Agent項目只有幾個固定功能比如查天氣、設(shè)提醒、算個算術(shù)模型調(diào)用得挺準。后來業(yè)務一擴張功能漲到二三十個我把所有工具定義全部序列化后塞進System Prompt里讓大模型自己挑。結(jié)果很快就出問題了。首先是Prompt膨脹。每個工具定義都帶名稱、描述、參數(shù)Schema和示例二十個工具加起來接近上萬token模型在長上下文里做“選擇”的難度成倍上升token成本和響應延遲也跟著漲。其次是選擇準確率下降。明明用戶問的是“明天出門要不要帶傘”Agent先調(diào)了“查詢待辦事項”又調(diào)了“查詢?nèi)諝v”最后才調(diào)用天氣類工具。原因不難理解工具之間描述相似、邊界模糊模型在幾十個選項里做決策天然容易受干擾。后來我做過一次統(tǒng)計在有近三十個工具定義同時注入的情況下工具選擇的正確率只有六成多部分低頻工具幾乎永遠不被選中哪怕用戶的需求和它完全匹配。說到底傳統(tǒng)工具函數(shù)是“被動等待調(diào)用”的它不知道當前對話上下文也不知道自己的能力邊界只是一堆平鋪的函數(shù)簽名。功能少的時候沒問題功能一多這個模型必然崩。1.2 技能系統(tǒng)和工具系統(tǒng)到底差在哪工具和技能的差別我用生活里的話說工具就像一把螺絲刀技能不是螺絲刀是怎么換好一顆螺絲釘?shù)耐暾ば颉x什么刀頭、用多大力度、怎么防滑、怎么知道擰緊了。技能系統(tǒng)本質(zhì)上做了一次“能力抽象”工具系統(tǒng)只提供一個函數(shù)入口技能系統(tǒng)則提供完整的“能力單元”包含觸發(fā)條件、前置檢查、執(zhí)行步驟、失敗處理方式、依賴關(guān)系和輸出約定。工具系統(tǒng)把所有函數(shù)一次性暴露給模型技能系統(tǒng)則通過注冊中心和檢索器每次只暴露當前場景需要的一小部分能力。工具往往是原子操作技能可以組合和嵌套。例如“生成周報”這個技能內(nèi)部可以依賴“拉取項目進度”“匯總會議記錄”“格式化Markdown”三個子技能。工具系統(tǒng)的可測試性一般聚焦在單函數(shù)邏輯技能系統(tǒng)則在“模型是否會正確選擇該技能”這個層面也需要測試。下面這張對比表可以直觀說明維度普通工具函數(shù)技能系統(tǒng)暴露方式全部注入Prompt按需檢索后注入上下文占用隨數(shù)量線性增長相對可控能力邊界不清晰顯式聲明觸發(fā)條件和禁用條件復用與組合單函數(shù)復用支持技能間依賴與編排失敗處理由外層代碼處理技能內(nèi)部自帶降級邏輯可測試性單測函數(shù)調(diào)用決策執(zhí)行結(jié)果雙層測試從工具到技能的轉(zhuǎn)變其實是一次分層設(shè)計。最底層是具體執(zhí)行代碼中間層是技能注冊中心上層是決策層也就是讓大模型面對“能力地圖”而不是“源代碼”。這個分層非常關(guān)鍵它直接決定了Agent能不能做“大而全”的功能擴展。2. 技能的最小可用形態(tài)清單、運行器與邊界約定2.1 一份技能清單里需要寫哪些字段我們把每個技能都放在一個獨立目錄里目錄下必有一個清單文件skill.yaml用來告訴注冊中心“我是什么、什么時候該用我、怎么調(diào)用我”。這是我們項目里實際用的一個最小清單id: meeting-minutes name: 生成會議紀要 summary: 根據(jù)會議轉(zhuǎn)寫文本生成結(jié)構(gòu)化會議紀要 tags: - meeting - minutes - summarize description: | # 何時使用 當用戶提供會議錄音轉(zhuǎn)寫、會議筆記或聊天記錄要求輸出會議紀要、行動項、決策記錄時使用。 # 何時不要使用 如果用戶只是提到會議但并沒有要求整理紀要不要調(diào)用本技能。 如果用戶要求的內(nèi)容和會議無關(guān)屬于普通文本摘要請考慮使用通用摘要技能。 inputs: - name: transcript type: string required: true description: 會議轉(zhuǎn)寫文本或會議筆記原始內(nèi)容 - name: language type: string required: false default: zh description: 輸出語言zh/en outputs: - name: meeting_minutes type: string description: Markdown格式的會議紀要 dependencies: - calendar-reader # 可選依賴用于自動讀取關(guān)聯(lián)日程 run: mode: prompt template: templates/meeting_minutes.jinja2 fallback: | 如果無法生成紀要先嘗試精簡輸入仍然失敗則返回錯誤碼MM_001。這個清單里有兩個特別容易忽略的字段一個是summary一個是description里的“何時不要使用”。前者用來做快速檢索后者用來約束模型的行為邊界。我們后來發(fā)現(xiàn)很多“亂調(diào)用”問題根因就是技能描述里只寫了“我擅長什么”沒有寫“我不該什么時候出現(xiàn)”。2.2 技能不只有Prompt代碼、流程與混合實現(xiàn)很多人以為一個技能就是一段提示詞模板實際遠遠不夠。在項目里我們把運行模式分成了四種prompt模式純模板生成能力適合“改寫、總結(jié)、抽取”等任務。code模式直接執(zhí)行Python函數(shù)或外部API調(diào)用適合強邏輯操作比如發(fā)請求、算數(shù)據(jù)。flow模式編排多個子技能每個子技能完成后進入下一步適合業(yè)務流程。hybrid模式先用代碼獲取數(shù)據(jù)再用Prompt整理結(jié)果例如先拉取報表數(shù)據(jù)再生成解讀結(jié)論。不同模式對應不同的執(zhí)行器。代碼模式需要聲明入口函數(shù)和參數(shù)映射Prompt模式需要聲明模板路徑Flow模式則要聲明子技能列表和流轉(zhuǎn)條件。這樣設(shè)計的好處是技能的元數(shù)據(jù)足夠完整注冊中心可以提前做靜態(tài)校驗而不是等運行時才發(fā)現(xiàn)配置錯誤。2.3 一個可直接抄的會議紀要技能樣例上面那個YAML已經(jīng)比較完整了。對應的目錄結(jié)構(gòu)通常是skills/ meeting-minutes/ skill.yaml templates/ meeting_minutes.jinja2 assets/ output_schema.json calendar-reader/ skill.yaml run.py weekly-report/ skill.yaml flow.yaml run.py schema.json實際編寫時建議給每個技能配一個assets/output_schema.json輸出Schema會被嵌入到模型調(diào)用約束里保證技能返回格式穩(wěn)定后續(xù)解析不容易出錯。這個“輸出Schema”是小成本大收益的設(shè)計尤其是當技能會被其他技能依賴的時候穩(wěn)定的輸出格式就是技能之間協(xié)作的基礎(chǔ)。3. 注冊、檢索、按需加載把技能系統(tǒng)真正跑起來3.1 基于目錄約定的注冊中心掃描與校驗技能系統(tǒng)第一步是注冊中心。我們選擇目錄約定大于配置也就是固定規(guī)則掃描skills/下的每個子目錄解析skill.yaml做靜態(tài)校驗后建立索引。注冊中心的加載邏輯大概是這樣的import yaml from pathlib import Path def load_skill_from_dir(skill_dir: Path): manifest yaml.safe_load((skill_dir / skill.yaml).read_text(encodingutf-8)) required_fields {id, name, summary, description, run} missing required_fields - set(manifest.keys()) if missing: raise ValueError(f技能清單缺少字段: {missing}) run_mode manifest[run][mode] if run_mode prompt: template_path skill_dir / manifest[run][template] if not template_path.exists(): raise FileNotFoundError(fPrompt模板不存在: {template_path}) elif run_mode code: entry_file skill_dir / manifest[run][entry] if not entry_file.exists(): raise FileNotFoundError(f技能入口文件不存在: {entry_file}) manifest[_dir] skill_dir return manifest def load_all_skills(skills_root: Path) - dict: registry {} for skill_dir in skills_root.iterdir(): if not skill_dir.is_dir(): continue manifest_file skill_dir / skill.yaml if not manifest_file.exists(): continue skill load_skill_from_dir(skill_dir) if skill[id] in registry: raise ValueError(f技能ID沖突: {skill[id]}) registry[skill[id]] skill return registry注意兩點第一校驗依賴閉環(huán)比如A技能依賴BB又依賴A需要報警第二校驗字段沖突不同技能的ID和名稱都不能重復。這些靜態(tài)檢查雖然簡單但能避免后面發(fā)布階段很多詭異問題。3.2 兩層召回策略關(guān)鍵詞粗篩與語義精排注冊中心構(gòu)建好之后最核心的問題是怎么把技能按需加載給模型。我們沒有用單一檢索方式而是用兩層召回關(guān)鍵詞粗篩對用戶當前消息和摘要做關(guān)鍵詞匹配匹配技能名稱、summary、tags、aliases召回一批候選保證“確定性”。語義精排把用戶當前意圖和歷史對話摘要拼成一個查詢向量對所有候選技能的描述做embedding相似度計算把最相似的若干技能排到前面。為什么需要兩層純向量檢索對代碼類技能名稱不敏感比如“待辦”和“todo”這類語義向量不一定拉得近純關(guān)鍵詞匹配又覆蓋不了“幫我記一下明天下午三點開會”這種自然語言表達。兩層一結(jié)合穩(wěn)定性和泛化性都有了。def retrieve_skills(query: str, history_summary: str, top_k: int 5): combined f{history_summary} [SEP] {query} keyword_candidates keyword_recall(combined, k20) vector_candidates semantic_recall(combined, k20) merged merge_candidates(keyword_candidates, vector_candidates) ranked rerank(combined, merged) return ranked[:top_k], ranked合并候選的時候關(guān)鍵詞命中的技能可以加權(quán)0.2語義相似的分數(shù)保持原值最終按綜合分數(shù)排序。我在項目里實測過四十多個技能全部暴露給模型時選擇正確率不到七成改成按需加載到五到八個技能后正確率能明顯提升。這個提升不是模型變強了而是決策面變小了。3.3 上下文組裝給模型看的不是全部技能檢索完成之后還有一個容易被忽略的步驟如何把技能信息組裝成模型能讀懂的輸入。我們不是把整個skill.yaml全量塞給模型而是裁剪成類似工具的JSON Schema保持輕量[ { name: meeting-minutes, description: 根據(jù)會議轉(zhuǎn)寫生成結(jié)構(gòu)化會議紀要。適用于用戶要求整理會議內(nèi)容、產(chǎn)出行動項時。, parameters: { type: object, properties: { transcript: {type: string, description: 會議轉(zhuǎn)寫內(nèi)容}, language: {type: string, enum: [zh, en], default: zh} }, required: [transcript] } } ]至于技能內(nèi)部更詳細的Prompt模板、執(zhí)行步驟、依賴關(guān)系、失敗處理策略都不進入模型上下文而是保存在技能執(zhí)行器里等模型決定調(diào)用之后再由執(zhí)行器加載。這個設(shè)計保持了Prompt的輕量也讓技能內(nèi)部細節(jié)可以隨時修改而不影響決策層。4. 描述質(zhì)量才是調(diào)用準確率的隱藏杠桿4.1 同樣的技能兩種描述兩種效果技能描述是模型決定“要不要調(diào)用它”的唯一依據(jù)很多時候準確率上不去的瓶頸不在模型而在描述寫得太敷衍。同樣是“創(chuàng)建待辦”這個技能差描述是這樣用于創(chuàng)建待辦事項參數(shù)內(nèi)容、時間。模型看到這種描述很難判斷“明天下午提醒我交報告”是不是應該調(diào)它。好描述是這樣當用戶提出要在任務管理、日歷、待辦清單中記錄一件事或者要求設(shè)置提醒、安排后續(xù)動作時使用。 適用場景記錄任務、創(chuàng)建提醒、預約安排。 不適用場景用戶只是口頭討論計劃但沒有明確表示要記錄時不要調(diào)用。 參數(shù)content為待辦內(nèi)容due_time為ISO8601格式截止時間priority可選low/medium/high。 輸出創(chuàng)建成功后的待辦ID。后者包含了三層信息觸發(fā)場景、邊界條件、參數(shù)約束。這就是“給模型看的描述”和“給開發(fā)者看的注釋”之間的核心區(qū)別——描述不是在解釋函數(shù)邏輯而是在幫模型做決策。4.2 讓Alias替描述分擔口語化的表達我們接入了很多口語化表達的用戶。用戶可能說“記一下”“安排一下”“提醒我”這些都不帶“待辦”二字。為了解決這個問題我們在技能清單里增加了aliases字段aliases: - 記錄 - 安排 - 提醒 - 待辦 - to-do - remind me這個字段不只是給關(guān)鍵詞召回用的在給模型的描述里也會出現(xiàn)類似“別名記錄/安排/提醒”的說法幫助模型把口語化表達映射到技能。aliases怎么來我們沒有完全靠人工維護而是定期從線上日志里抽取“模型未調(diào)用任何技能但用戶明顯有需求”的對話總結(jié)高頻表達再回流到這個字段里。4.3 描述顆粒度怎么對齊模型的決策能力描述寫得太粗或太細都會出問題。太粗的例子是“提供全面信息處理功能”模型根本不知道什么時候該選它太細的例子是“先用JSON解析器將字符串轉(zhuǎn)為對象再遍歷數(shù)組調(diào)用API”把執(zhí)行邏輯寫在描述里不僅占token還容易讓模型產(chǎn)生誤解。我們內(nèi)部的控制原則是描述里只保留“什么時候用、傳什么參數(shù)、得到什么結(jié)果”三層信息執(zhí)行細節(jié)全部放到技能內(nèi)部。另外所有描述盡量控制在100到300字太長的描述在檢索排序和模型讀取時都會產(chǎn)生干擾。寫完描述后我們還會做一個自查如果一個新來的工程師只讀description不看代碼能不能準確判斷調(diào)用場景如果不能說明描述還需要改。5. 實測踩坑五個問題對應五套解法5.1 技能在庫里卻找不到的“沉默失敗”最讓人頭疼的問題不是模型亂調(diào)用而是該調(diào)用的技能它不調(diào)用而且不帶任何報錯靜默失敗。我做排查時發(fā)現(xiàn)這類問題基本都出在檢索召回環(huán)節(jié)要么關(guān)鍵詞沒匹配上要么語義相似度排序太低候選里壓根沒有這個技能。后來我養(yǎng)成了一個調(diào)試習慣每次請求都把“用戶問題、檢索返回的前十候選、最終注入給模型的技能列表”完整打進日志。查看時先看候選列表里有沒有目標技能再去看模型到底選了哪個最后看執(zhí)行結(jié)果。這樣一步步定位而不是上來就懷疑模型能力。排查鏈路固定為檢索結(jié)果 - 模型決策 - 執(zhí)行狀態(tài)三個環(huán)節(jié)逐層看。5.2 相似技能互相打架不如合并接口項目里有個場景特別典型郵件類功能拆成了“發(fā)送郵件”“回復郵件”“創(chuàng)建郵件草稿”三個技能。這三個技能描述高度重疊模型經(jīng)常選錯比如用戶說“把草稿發(fā)給小張”模型先去調(diào)“創(chuàng)建郵件草稿”再調(diào)“發(fā)送郵件”白白多跑幾輪。我們的解法不是把描述改得更細而是把三個技能合并成一個“郵件操作”技能把具體動作移到參數(shù)里id: email-operation name: 郵件操作 description: 處理郵件創(chuàng)建、發(fā)送、回復、草稿保存等操作。通過action參數(shù)區(qū)分具體行為。 inputs: - name: action type: string enum: [draft, send, reply, save] - name: email_data type: object合并不是萬能的但接口語義相近、無法清晰切分邊界的技能合并成“一個技能一個動作參數(shù)”往往比硬拆三個更好。反之如果兩個技能的輸入輸出差異很大、適用場景判若鴻溝那還是要拆開。5.3 技能內(nèi)部子步驟失敗不能拖垮主線技能內(nèi)部的子步驟很容易失敗比如“生成會議紀要”依賴“讀取日歷”這個子技能如果日歷接口超時整個紀要技能就崩了外層Agent直接報錯。這暴露了一個問題技能執(zhí)行器缺少局部降級能力。我們的改進方式是給執(zhí)行器加了兩層容錯第一層是局部重試對可重試的子步驟最多重試兩次第二層是降級邏輯在技能清單的run.fallback里聲明備用方案。比如日歷讀取失敗時可以利用會議轉(zhuǎn)寫文本里的時間信息生成紀要并標記“日歷部分未驗證”。這樣主流程還能繼續(xù)走只是質(zhì)量打了折扣而不是整個鏈路斷掉。5.4 長對話下檢索漂移需要意圖快照對話超過十幾輪之后我們用歷史摘要拼出來的檢索Query會變得越來越“飄”包含了太多無關(guān)信息經(jīng)常召回一些和當前意圖完全無關(guān)的技能比如用戶正在聊天氣檢索結(jié)果里卻出現(xiàn)“排班表”技能。后來我們改成了維護一個“意圖快照”而不是每輪都用整段歷史做檢索。具體做法是每一輪結(jié)束后用一個小模型對“當前正在執(zhí)行的任務棧”做增量更新下一輪檢索時用任務棧 最新一條用戶消息組合成Query。這個改動之后長對話的召回準確率穩(wěn)定了很多也不容易被歷史噪聲帶偏。5.5 熱更新不生效版本快照與緩存失效技能系統(tǒng)的迭代頻率很高但我們最初遇到過“配置改了線上完全沒生效”的情況排查了一圈發(fā)現(xiàn)是技能元數(shù)據(jù)緩存沒有及時失效。后來我們給注冊中心增加了版本管理每個技能清單里加version字段執(zhí)行器按版本號綁定同時支持兩種失效策略一種是TTL定期刷新另一種是文件系統(tǒng)監(jiān)聽變更后主動通知。更保險的是“發(fā)布快照”機制新版本通過審批后才發(fā)布到生產(chǎn)當前請求固定引用某個版本快照避免請求處理過程中技能配置被中途替換導致行為不一致。6. 下一步組合編排、自動生成與回歸評測6.1 用Workflow把技能編排成完整工序當技能數(shù)量多了之后單個技能完成一個任務的情況越來越少更多時候是幾個技能串成一條流程。我們的做法是在技能清單里增加flow字段id: weekly-report name: 生成周報 run: mode: flow steps: - skill: event-collector input_map: start_time: 上周一 00:00 end_time: 本周日 23:59 - skill: progress-summarizer input_map: events: {{ steps.event-collector.output }} - skill: markdown-formatter input_map: content: {{ steps.progress-summarizer.output }}這種編排的價值在于每個步驟獨立、可復用、可測試中間任何一步失敗了都能定位到具體的子技能。相比把整個流程寫死在一個代碼文件里編排方式更靈活也更貼近業(yè)務邏輯。6.2 從調(diào)用技能到生成技能漸進式玩法我們還在嘗試讓Agent自己生成新技能。做法是定期分析線上日志里“模型檢索了但候選為空且用戶問題反復出現(xiàn)”的失敗樣本把這些樣本聚類嘗試讓大模型根據(jù)歷史對話生成候選的skill.yaml和run.py骨架再由工程師審核上線。目前這個流程還不能完全無人化但已經(jīng)能明顯縮短新技能的開發(fā)時間。之前拉一個技能可能要半天現(xiàn)在只要人工確認描述、補兩個邊界分支就能發(fā)布。這個方向讓我覺得“技能系統(tǒng)”最終會從“手寫技能庫”走向“技能生態(tài)”由Agent自己擴展能力邊界。6.3 技能也有回歸測試建立調(diào)用預期樣本集技能系統(tǒng)上線久了最怕的就是“改了一個技能描述另一個場景的調(diào)用全亂了”。我們?yōu)榇私⒘艘粋€技能級回歸測試集每條樣本包含三部分用戶輸入、期望調(diào)用的技能ID、期望的關(guān)鍵參數(shù)。每次技能配置變更或模型升級都會批量跑一遍這個樣本集統(tǒng)計調(diào)用準確率和參數(shù)正確率。這個做法本質(zhì)上有點像接口自動化測試只是被測對象換成了“模型的調(diào)用決策”。它能很好地兜住技能系統(tǒng)的底讓我們每次改描述的時候都有一個量化標準而不是靠感覺說“應該更準了”?;氐介_頭那個項目現(xiàn)在這套技能系統(tǒng)已經(jīng)穩(wěn)定跑了一段時間最直觀的感受是新功能接入成本降下來了模型選錯工具的case也少了而且每個技能都可以被獨立追蹤和優(yōu)化。如果你也正在經(jīng)歷“工具越加越多、準確率越來越差”的困局建議先從盤點已有工具開始把它們拆成帶邊界、帶依賴、帶降級方案的技能單元再搭一個簡單的注冊和檢索層你會明顯感覺到Agent的行為從“碰運氣”變成了“有章法”。