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