計與實現(xiàn):從Prompt塞邏輯到標準化Skills調(diào)用的工程實踐)
做 Agent 開發(fā)的朋友應(yīng)該都有這種體驗?zāi)P捅旧砟芰υ購姴唤庸ぞ呔褪恰翱沼邢敕]手”一旦接了工具又開始頭疼“什么時候調(diào)、調(diào)哪個、參數(shù)怎么填”。我最早做智能體的時候也是踩了一堆坑——把所有邏輯全塞進 Prompt 里模型倒是能“理解”但一旦任務(wù)復(fù)雜起來它就亂選、漏選、甚至自己編參數(shù)。后來我把思路從“讓模型自己發(fā)揮”改成“給模型一張標準化的技能清單”也就是把能力抽成 agent-skills 的形式模型按清單去匹配和調(diào)用。這個轉(zhuǎn)變之后調(diào)用準確率明顯上了一個臺階而且整個系統(tǒng)變得特別好維護。這篇文章我把這套思路、定義方法、完整實現(xiàn)鏈路和踩坑記錄都整理出來正在做 Agent、RAG 或自動化工作流的朋友可以直接參考。1. 為什么需要“技能庫”而不是寫死邏輯很多人一開始的做法是把工具描述一股腦寫在 System Prompt 里告訴模型“你可以使用以下工具”然后靠模型自己去理解。前期工具少的時候還能跑通等到工具超過五六個或者任務(wù)開始分步依賴、條件判斷的時候馬上露餡模型要么漏調(diào)用要么把互斥的工具一起調(diào)用要么根本不知道該先調(diào)哪個。技能庫的核心思路是把“能不能調(diào)”變成“該不該調(diào)”——每一個技能都是一個獨立的模塊模型的任務(wù)不是理解工具本身而是理解“當前場景匹配哪個技能”。1.1 直調(diào)模型和技能調(diào)用的本質(zhì)差異直接讓模型調(diào)用工具模型面對的是“一堆函數(shù)簽名”它需要自己判斷參數(shù)類型、邊界、調(diào)用時機這其實是把工程上的判斷壓力全甩給了模型。而技能調(diào)用是給每個能力配上“使用說明書”說明書里寫清楚觸發(fā)條件、前置依賴、輸出格式。兩者最大的區(qū)別在于直調(diào)模型是基于“概率”在猜技能調(diào)用是基于“匹配”在做選擇。拿一個最簡單的場景來說用戶問“北京今天多少度”直調(diào)方案是告訴模型有個get_weather(city)函數(shù)模型自己推斷出你要傳city北京。聽著沒問題但如果同時還有g(shù)et_air_quality(city)、get_forecast(city)模型就開始糾結(jié)了——到底哪個才是用戶真正想要的技能庫的方案會在get_weather的描述里寫清楚“僅當用戶詢問當前溫度或天氣狀況時使用不用于空氣質(zhì)量或未來預(yù)測”模型一看這個描述匹配起來就不費勁了。1.2 agent-skills 的完整調(diào)用鏈路長什么樣一套標準的技能調(diào)用鏈路應(yīng)該有五個環(huán)節(jié)技能注冊、技能發(fā)現(xiàn)、技能選擇、技能執(zhí)行、結(jié)果回填。技能注冊是把所有可用的能力登記到一張清單里這個清單就是模型唯一的“菜單”技能發(fā)現(xiàn)是根據(jù)用戶當前的問題先從清單里篩出候選技能這一步通???embedding 召回或關(guān)鍵詞匹配技能選擇是模型在候選中做出最終決定并且輸出結(jié)構(gòu)化調(diào)用參數(shù)技能執(zhí)行是后端真正跑這段邏輯結(jié)果回填是把執(zhí)行結(jié)果交還給模型讓它繼續(xù)推理。這個鏈路里面最容易被忽略的是“技能發(fā)現(xiàn)”這一步。很多人直接讓模型在全量技能里選技能少還行一旦技能上百個模型在選擇時就會產(chǎn)生注意力分散選錯率明顯上升。我個人一開始也是直接全量塞給模型后來技能多了才發(fā)現(xiàn)前置一個召回步驟能過濾掉 80% 完全不相關(guān)的技能模型的選擇壓力小很多準確率自然就上來了。1.3 哪些場景收益最大不是說所有項目都要上技能庫我實踐下來下面這幾類場景收益最大。工具數(shù)量超過五個五個以上工具同時暴露給模型時選擇準確率會顯著下降技能庫的“先召回再選擇”能有效緩解。任務(wù)存在先后依賴比如“先查訂單狀態(tài)再決定是否發(fā)起退款”這種流程不能靠模型一次調(diào)用搞定必須拆成多個技能分步執(zhí)行。多輪對話中需要保持上下文技能執(zhí)行結(jié)果要能“記住”并參與后續(xù)輪次的推理這時候技能庫的標準化輸出就很重要。團隊協(xié)作場景同一個技能庫可以被多個 Agent 復(fù)用寫好一次到處調(diào)用。反過來如果項目只有一個工具或者流程是完全固定的直接調(diào)用函數(shù)比上技能庫劃算得多。技能庫不是銀彈它的核心價值是“在動態(tài)場景下提供結(jié)構(gòu)化的選擇能力”。2. 技能定義與描述成敗的隱藏關(guān)鍵技能庫的整個地基就是“技能定義”。定義寫得好模型選得準定義寫得爛后面全白搭。一個技能定義至少需要包含五部分名稱、描述、參數(shù)、返回、權(quán)限。這五部分各有各的講究尤其是描述這一塊大部分人都沒寫到位。2.1 一個技能清單該有的字段技能清單有時候叫 skill manifest是整體技能的注冊表我習(xí)慣用一個 YAML 或 JSON 文件來維護。每個技能的骨架大概長這樣技能 ID 用于程序內(nèi)部識別名稱用人類可讀的短句描述是給模型看的觸發(fā)條件說明參數(shù)是 JSON Schema 格式的結(jié)構(gòu)化定義返回值定義執(zhí)行結(jié)果的格式約束。設(shè)計的時候有一個重要原則技能 ID 和名稱要“見名知意”但描述要“見文知用”。什么意思ID 可以直接叫g(shù)et_weather沒問題但描述里一定要寫清“什么時候用、什么時候不用、參數(shù)怎么從對話里提取”。我自己維護技能清單時還會加一個enabled開關(guān)。這個字段特別實用——線上突然發(fā)現(xiàn)某個技能有 bug或者要灰度測試新技能直接把開關(guān)關(guān)掉就行不必改代碼、不必新發(fā)版。等測試好了再把開關(guān)打開對生產(chǎn)環(huán)境非常友好。2.2 技能描述是寫給模型看的“說明書”描述寫得好不好直接決定模型能不能正確選擇技能。很多人寫描述會寫成“獲取天氣信息的工具”這其實是一種無效描述因為它只說了“是什么”沒說“什么時候用”。有效的描述應(yīng)該包含三部分內(nèi)容觸發(fā)條件、不觸發(fā)條件、參數(shù)來源。舉一個負面例子和正面例子的對比。負面寫法是“該工具可以查詢指定城市的天氣信息參數(shù)為城市名稱?!闭鎸懛ㄊ恰爱斢脩粼儐柈斍盎蛭磥淼奶鞖鉅顩r、氣溫、降雨概率時使用。從對話中提取城市名稱作為參數(shù)。當用戶詢問空氣質(zhì)量、歷史天氣時不要調(diào)用此工具?!边@兩種描述在模型面前效果差別非常明顯。原因是模型不是靠“理解”工具而是靠“匹配”場景描述里把場景寫全匹配的準確度就高。還有一種進階寫法就是在描述里加入“示例對話片段”。比如寫清楚“用戶說‘北京熱不熱’也算天氣查詢可以調(diào)用?!边@種方式特別適合那些觸發(fā)邊界模糊的技能能顯著降低模型誤判率。2.3 技能粒度的選擇太細和太粗都有問題技能粒度是我覺得整個設(shè)計里最需要拿捏的部分。粒度太細比如把“查天氣”拆成“查溫度”“查濕度”“查風(fēng)力”三個技能模型反而會困惑用戶說“今天冷嗎”到底調(diào)哪個粒度太粗比如把所有信息查詢類能力揉成一個大工具那模型就退化成了在讀一本巨大的說明書和直接塞工具給模型沒有區(qū)別。我的經(jīng)驗是按“用戶意圖邊界”來切分技能而不是按“功能邊界”。用戶說“幫我安排明天的日程”這是一個完整意圖哪怕內(nèi)部需要調(diào)日歷、設(shè)提醒、查時間沖突三個后端能力對外也應(yīng)該是一個“日程安排”技能。這個技能內(nèi)部可以編排多個函數(shù)調(diào)用但模型不需要知道這些細節(jié)它只需要知道“這個技能能安排日程”。另外技能之間盡量不要有功能重疊。我踩過的一個坑是兩個技能都能查訂單狀態(tài)一個查普通訂單一個查售后訂單結(jié)果模型經(jīng)常選錯。后來把兩個技能的描述徹底區(qū)分開在觸發(fā)條件上加了明確邊界“僅當……”問題才解決。重疊的邊界必須要在描述里“劃清領(lǐng)地”。3. 實戰(zhàn)從零搭一個可用的 agent-skills 引擎概念講再多不如直接動手。我這邊用一個實際的例子帶大家完整走一遍讓 Agent 具備兩個技能一個是“查天氣”一個是“生成日程提醒”然后讓模型根據(jù)用戶的一句話自動選擇技能并調(diào)用。3.1 準備階段技能清單與工具定義首先定義技能清單。我會把它寫成一個 JSON 文件因為 JSON 的結(jié)構(gòu)化程度高模型讀取時不容易誤解。下面這份清單定義了weather_query和schedule_reminder兩個技能注意看清我描述的寫法——每個技能都寫清楚了觸發(fā)條件、不觸發(fā)條件、參數(shù)來源、返回格式。{ skills: [ { id: weather_query, name: 查詢天氣, description: 當用戶詢問當前或未來某天的天氣、溫度、降雨概率時使用。需要從對話中提取城市名稱可選日期默認為今天。當用戶詢問空氣質(zhì)量、歷史天氣、穿衣建議時不要調(diào)用。, parameters: { type: object, properties: { city: { type: string, description: 城市名稱如北京、上海 }, date: { type: string, description: 日期格式Y(jié)YYY-MM-DD默認當天 } }, required: [city] } }, { id: schedule_reminder, name: 創(chuàng)建日程提醒, description: 當用戶要求創(chuàng)建提醒、設(shè)置鬧鐘、安排日程時使用。需要提取時間和提醒內(nèi)容。當用戶只是詢問日程列表不涉及新增提醒時不要調(diào)用。, parameters: { type: object, properties: { time: { type: string, description: 提醒時間格式Y(jié)YYY-MM-DD HH:mm }, event: { type: string, description: 提醒內(nèi)容 } }, required: [time, event] } } ] }這份清單會作為 System Prompt 的一部分傳給模型。但注意實際傳給模型的內(nèi)容我會做一次精簡只保留技能的id、name、description、parameters去掉工程上的冗余字段避免模型讀太長的內(nèi)容產(chǎn)生注意力偏移。3.2 核心鏈路讓模型在“思考”和“調(diào)用”之間切換接下來是核心的運行時鏈路我用 Python 寫一個簡化版。這個流程分成三步第一步讓模型判斷當前用戶的輸入是否需要技能并輸出一個結(jié)構(gòu)化指令第二步解析指令執(zhí)行對應(yīng)技能第三步把執(zhí)行結(jié)果回填給模型讓它基于結(jié)果做最終回復(fù)。這里的關(guān)鍵設(shè)計是不要讓模型“直接說話調(diào)工具”混在一起而是強制模型輸出一個 JSON 動作指令。這樣可以避免模型在回復(fù)文本里夾雜工具調(diào)用解析起來特別痛苦。下面這個函數(shù)展示了動作解析的過程import json import openai client openai.OpenAI() def parse_action(user_input, skills_prompt): response client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: skills_prompt \n你需要輸出JSON動作指令格式為{\action\: \技能ID或none\, \parameters\: {參數(shù)對象}}。如果不需調(diào)用任何技能action為none。}, {role: user, content: user_input} ], response_format{type: json_object} ) return json.loads(response.choices[0].message.content) def execute_skill(action, params): if action weather_query: # 實際工程中這里調(diào)用天氣API return f{params[city]}今天晴25°C降水概率10% elif action schedule_reminder: return f已設(shè)置提醒{params[time]} {params[event]} return None注意兩點第一我用了response_format強制模型輸出 JSON 對象這比讓模型“自由說話”再解析穩(wěn)定得多第二動作指令里的parameters是模型根據(jù)技能描述里的 JSON Schema 生成的不是我們代碼里定義好的所以我們執(zhí)行前一定要做校驗。實際執(zhí)行的時候我會把這兩個函數(shù)串起來再讓模型基于工具結(jié)果生成用戶能看懂的回復(fù)。整個鏈路就是“用戶輸入 - 動作解析 - 技能執(zhí)行 - 結(jié)果回填 - 最終回復(fù)”。多輪對話時每一輪都重復(fù)這個鏈路并把前一輪的技能執(zhí)行結(jié)果作為歷史上下文傳給模型。3.3 參數(shù)校驗與失敗回退機制模型生成參數(shù)這件事永遠不能百分百信任。用戶說“提醒我明天早上八點開會”模型可能會把時間格式寫成明天早上8點而不是2025-01-15 08:00。如果后端直接拿這個字符串去存數(shù)據(jù)庫肯定要出問題。我的做法是在技能執(zhí)行前加一層參數(shù)清洗和校驗。第一步檢查必填參數(shù)是否齊全缺了就直接返回“參數(shù)缺失”的錯誤信息而不是硬著頭皮執(zhí)行第二步對時間、日期這類格式敏感的參數(shù)做解析能轉(zhuǎn)標準格式就轉(zhuǎn)轉(zhuǎn)不了就返回給模型去追問用戶第三步參數(shù)范圍也要校驗比如查天氣的城市參數(shù)如果模型輸出了“地球”那后端 API 肯定會報錯這時候返回一個通俗的錯誤提示比堆棧信息友好得多。from datetime import datetime def clean_params(action, params): if action schedule_reminder: if time not in params: return None, 缺少提醒時間 try: # 嘗試把各種常見說法歸一為標準時間 params[time] normalize_datetime(params[time]) except ValueError: return None, 無法解析提醒時間請讓用戶補充具體時間 return params, None養(yǎng)成一個習(xí)慣把技能執(zhí)行的結(jié)果盡量設(shè)計成“可以直接回填給模型”的字符串而且要簡潔。像查天氣接口原始返回可能是一大段 JSON里面有幾十個字段模型看到那么長的內(nèi)容反而容易迷失重點。我會在技能內(nèi)部就把返回結(jié)果提煉成“北京今天晴25°C降水概率10%”這種一句話模型拿到的信息干凈明確后續(xù)推理質(zhì)量也會提升。4. 常見問題與排查技巧實錄技能庫搭建起來不難真正難的是跑起來之后的各種“幺蛾子”。我把自己在生產(chǎn)和實驗環(huán)境中踩過的坑整理成了一份速查表這些問題的表現(xiàn)形態(tài)各不相同但根因往往都出在技能定義或參數(shù)處理上。4.1 模型不調(diào)用技能或亂調(diào)用技能這是最常見的問題表現(xiàn)形式有兩種該調(diào)的時候不調(diào)或者不該調(diào)的時候瞎調(diào)。遇到這種情況我的排查順序是先看技能描述里有沒有寫清楚“觸發(fā)條件”和“不觸發(fā)條件”再看是不是兩個技能描述存在模糊的邊界最后看是不是技能太多了模型注意力分散。如果你用的是 GPT 這類能力較強的模型并且技能數(shù)在十個以下不調(diào)用多半是描述問題。描述不要寫“查詢天氣的工具”要寫“當用戶詢問天氣……時”。如果你用的是開源的小參數(shù)模型不調(diào)用還有一個常見原因模型輸出格式不穩(wěn)定沒有嚴格遵循“輸出 JSON 動作指令”的要求。這時候考慮換一個更大的模型或者在解析時做容錯例如支持解析“帶代碼塊包裹的 JSON”和“純文本里的 JSON 片段”。還有一個容易忽略的點System Prompt 里的技能列表排位。模型對靠前的內(nèi)容注意力更強所以高頻技能要往前放。我實測過同一個技能放在第一位和第五位被選中率有明顯差距。4.2 參數(shù)幻覺模型自己編造參數(shù)值模型在用戶沒有提供某個參數(shù)時經(jīng)常會“腦補”一個值。比如用戶說“幫我查天氣”沒提城市模型可能自己填一個city北京導(dǎo)致結(jié)果完全偏離用戶預(yù)期。這個問題單靠描述很難根治因為模型有很強的“補全”傾向。我的方案有兩層。第一層是在技能描述的參數(shù)說明里明確標記“該參數(shù)必須從用戶對話中提取未明確提及時設(shè)為空不得自行猜測”。這句話能起到一定約束作用但不是完全可靠。第二層是在代碼里做“信息缺失檢測”如果必填參數(shù)沒有被用戶提供直接讓模型反問用戶而不是拿猜測值去執(zhí)行。具體做法是在動作解析時同時要求模型輸出“參數(shù)來源置信度”對于置信度低的參數(shù)就走追問流程。4.3 工具返回體過大或過于結(jié)構(gòu)化拖垮推理質(zhì)量天氣 API 原文可能是這樣的{city: {name: 北京, id: 101010100}, now: {temp: 25, feels_like: 26, humidity: 30}, daily: [{date: 2025-01-15, temp_max: 27, temp_min: 18}, ...]}如果直接把這么一大坨 JSON 丟給模型它雖然能看懂但會把注意力浪費在無關(guān)字段上而且模型回復(fù)時會忍不住引用那些原始字段導(dǎo)致解釋冗長且不貼近用戶。技能層一定要做“信息提煉”把模型需要的核心信息抽出來變成一句話或一個小表。這個操作我給一個很樸素的比喻技能庫提供給模型的應(yīng)該是“菜單”而不是“后廚”。模型不是數(shù)據(jù)管道它不需要看到所有原始數(shù)據(jù)。我建議設(shè)定一個硬性規(guī)范任何技能返回給模型的內(nèi)容都要經(jīng)過一個“提煉函數(shù)”確保模型拿到的是一段不超過 200 字、且直接面向用戶問題的結(jié)論。如果技能邏輯復(fù)雜需要模型基于多步驟推理那可以把中間結(jié)果放在內(nèi)部存儲里模型每步只看到當前需要的信息。4.4 速查表最常見的六個問題與直接對策問題現(xiàn)象可能原因直接對策該調(diào)用的技能不調(diào)用描述中未寫清觸發(fā)條件在描述里增加“當用戶……時使用”句式和負向觸發(fā)條件兩個相似技能選錯技能邊界重疊給每個技能劃分“領(lǐng)地”描述中明確指出各自的排除場景參數(shù)被模型編造模型補全傾向描述中標記“參數(shù)必須來自用戶”代碼層做缺失檢測并追問調(diào)用順序不穩(wěn)定缺乏流程編排把有依賴的調(diào)用拆成“技能鏈”用前一個技能結(jié)果驅(qū)動后一個技能工具返回體過大未做信息提煉增加提煉函數(shù)只把核心結(jié)論回填給模型模型輸出的 JSON 解析失敗格式不穩(wěn)定使用強制 JSON 輸出的接口或做帶容錯的解析器從實際經(jīng)驗來看日常 Agent 開發(fā)中遇到的絕大多數(shù)“模型不聽話”的問題本質(zhì)上都不是模型的問題而是我們提供的信息不夠結(jié)構(gòu)化。把技能庫做好模型的表現(xiàn)通常會比你反復(fù)調(diào) Prompt 要穩(wěn)定得多。做技能庫這件事值得在前期的定義上多花時間——我自己的體會是定義技能比寫調(diào)用代碼多花了三倍時間但后期的調(diào)試成本降了十倍。多花點時間把技能邊界描述清楚絕對不虧。