化測(cè)試實(shí)踐:從用例設(shè)計(jì)到CI流水線集成)
把LLM技能當(dāng)成普通函數(shù)來(lái)做自動(dòng)化測(cè)試這個(gè)想法我第一次聽(tīng)的時(shí)候覺(jué)得有點(diǎn)“想多了”——模型輸出本來(lái)就是概率性的同一句話問(wèn)兩次答案都能不一樣怎么測(cè)但后來(lái)真正開(kāi)始在Agent項(xiàng)目里維護(hù)二十多個(gè)技能模塊每次改一句提示詞都要把十幾個(gè)場(chǎng)景手工點(diǎn)一遍我才意識(shí)到不把這件事自動(dòng)化后面只會(huì)越來(lái)越痛苦。這篇就是用skillprobe這套工具把LLM技能測(cè)試落地的全記錄從測(cè)試用例設(shè)計(jì)到指標(biāo)判定再到接進(jìn)CI流水線踩過(guò)的坑和驗(yàn)證過(guò)的思路都會(huì)寫(xiě)到。如果你正在做Agent類(lèi)應(yīng)用或者給LLM封裝了各種“技能”RAG檢索、工具調(diào)用、結(jié)構(gòu)化抽取、內(nèi)容生成你大概率也會(huì)遇到同一個(gè)問(wèn)題技能看著能用但改一版提示詞之后到底有沒(méi)有改壞別的地方基本靠運(yùn)氣。skillprobe就是拿來(lái)解決這個(gè)問(wèn)題的它的定位很像pytest之于Python——只不過(guò)被測(cè)對(duì)象是LLM技能而不是普通函數(shù)。下面直接進(jìn)入正題。1. 為什么必須給LLM技能上自動(dòng)化測(cè)試1.1 LLM技能不是代碼但它也需要“單元測(cè)試”先對(duì)齊一下概念。這里說(shuō)的“LLM技能”指的是你在Agent或應(yīng)用里給大模型封裝好的能力模塊。舉個(gè)例子一個(gè)給客服機(jī)器人用的“訂單查詢(xún)技能”它的輸入可能是用戶(hù)原話和用戶(hù)ID內(nèi)部邏輯是把用戶(hù)原話做意圖識(shí)別、抽取出訂單號(hào)再去調(diào)用訂單系統(tǒng)API最后把結(jié)果整理成自然語(yǔ)言回答。整個(gè)鏈路是“模型提示詞外部工具調(diào)用”的組合而不是一段普通的函數(shù)代碼。普通函數(shù)測(cè)試很簡(jiǎn)單輸入A斷言輸出是B。但LLM技能不一樣它的輸出是概率性的同一個(gè)輸入配上稍微不一樣的采樣參數(shù)結(jié)果就會(huì)有波動(dòng)。而且它的“正確”也不只是數(shù)值對(duì)錯(cuò)還有語(yǔ)義是否正確、格式是否規(guī)范、引用是否能對(duì)上原文。所以傳統(tǒng)的單元測(cè)試框架直接用不了需要一個(gè)能適配“非確定性輸出”的測(cè)試方案。skillprobe做的事就是把LLM技能測(cè)試拆成幾層技能注冊(cè)和加載是否正常、輸入輸出schema是否匹配、端到端行為是否正確、多次執(zhí)行是否穩(wěn)定。每一層都有對(duì)應(yīng)的判定邏輯而不是簡(jiǎn)單拿一個(gè)字符串去比對(duì)。我自己的體會(huì)是LLM技能的維護(hù)成本其實(shí)比普通代碼高得多。普通代碼出問(wèn)題報(bào)錯(cuò)堆棧一看就明白了。LLM技能出問(wèn)題往往是“回答得挺流暢但內(nèi)容里有小錯(cuò)”或者“這個(gè)場(chǎng)景回答得很好換一個(gè)說(shuō)法就不行了”。這些問(wèn)題單靠開(kāi)發(fā)時(shí)手動(dòng)測(cè)兩條路徑根本發(fā)現(xiàn)不了。自動(dòng)化測(cè)試不是錦上添花而是把質(zhì)量底線兜住。1.2 手工驗(yàn)證的三個(gè)致命問(wèn)題先說(shuō)說(shuō)我最早是怎么干的。每個(gè)技能寫(xiě)完之后打開(kāi)一個(gè)測(cè)試面板把樣例輸入粘貼進(jìn)去點(diǎn)一次看輸出是否合理。流程看起來(lái)沒(méi)毛病但實(shí)際用下來(lái)有三個(gè)繞不開(kāi)的問(wèn)題。第一個(gè)問(wèn)題是覆蓋度太低。手工測(cè)試天然偏向happy path你會(huì)下意識(shí)地測(cè)那些“自己知道應(yīng)該有結(jié)果”的輸入。但真實(shí)用戶(hù)不會(huì)按測(cè)試用例說(shuō)話他們可能一句話里帶口音、可能只給一半信息、可能上來(lái)就問(wèn)一個(gè)完全超出技能范圍的問(wèn)題。這些邊界情況手工測(cè)試基本覆蓋不到。第二個(gè)問(wèn)題是回歸很難發(fā)現(xiàn)。LLM技能里面的提示詞是脆弱的你為了修A問(wèn)題改了一句話很可能B問(wèn)題就跑偏了。手工測(cè)試的記錄都在腦子里時(shí)間一長(zhǎng)你根本想不起來(lái)上一版在某個(gè)輸入上的輸出長(zhǎng)什么樣。等你發(fā)現(xiàn)B問(wèn)題掛了往往是在線上被用戶(hù)投訴之后。第三個(gè)問(wèn)題是對(duì)“狀態(tài)”幾乎無(wú)法驗(yàn)證。很多技能是有上下文的比如多輪對(duì)話場(chǎng)景里的“追問(wèn)訂單號(hào)”技能需要在多輪里累積信息。手工測(cè)試時(shí)狀態(tài)在你腦子里你記得上一輪說(shuō)過(guò)什么。但自動(dòng)化測(cè)試要驗(yàn)證的是“技能自己在多輪之間對(duì)不對(duì)”這需要把對(duì)話歷史當(dāng)輸入?yún)?shù)顯式管理起來(lái)手工方式很難穩(wěn)定復(fù)現(xiàn)。這三個(gè)問(wèn)題疊加起來(lái)結(jié)論很清楚LLM技能的測(cè)試必須自動(dòng)化而且必須是可重復(fù)執(zhí)行的自動(dòng)化否則你沒(méi)法在版本迭代里建立信心。2. skillprobe能測(cè)什么從技能注冊(cè)到端到端行為2.1 技能核心要素拆解在用skillprobe之前我先把一個(gè)LLM技能拆成了四個(gè)要素這樣才知道該測(cè)什么。第一個(gè)要素是技能聲明。每個(gè)技能都有一個(gè)描述文件里面寫(xiě)清楚技能名稱(chēng)、一句話描述、輸入?yún)?shù)的定義、輸出結(jié)果的定義。這個(gè)文件的作用是讓Agent的主模型知道“什么情況下該調(diào)用這個(gè)技能”同時(shí)讓外部系統(tǒng)知道“該傳什么進(jìn)來(lái)、能拿什么回去”。技能聲明要是寫(xiě)錯(cuò)了模型根本不會(huì)在合適的時(shí)機(jī)調(diào)用這個(gè)技能。第二個(gè)要素是實(shí)現(xiàn)邏輯。包括提示詞模板、內(nèi)部的工具調(diào)用鏈、后處理邏輯。這是整個(gè)技能的核心也是最容易改壞的部分。提示詞模板改了輸出風(fēng)格可能變工具調(diào)用順序變了結(jié)果可能延遲或者出錯(cuò)后處理邏輯改了格式可能不符合schema。第三個(gè)要素是運(yùn)行配置。模型名稱(chēng)、溫度、最大token數(shù)、超時(shí)時(shí)間、是否啟用流式輸出等。這類(lèi)配置直接影響輸出的穩(wěn)定性和性能。溫度太高同一輸入每次輸出都不同超時(shí)太短長(zhǎng)文本生成直接失敗。第四個(gè)要素是外部依賴(lài)。RAG技能依賴(lài)向量庫(kù)和檢索器工具調(diào)用技能依賴(lài)下游API。這些依賴(lài)的狀態(tài)變化也會(huì)影響技能行為比如向量庫(kù)里新增了一批文檔技能的引用來(lái)源就會(huì)變。測(cè)試時(shí)需要考慮這些依賴(lài)是否可用、是否可控。skillprobe的測(cè)試設(shè)計(jì)基本就圍繞這四個(gè)要素展開(kāi)每個(gè)要素對(duì)應(yīng)一類(lèi)測(cè)試項(xiàng)而不是只盯著“輸出內(nèi)容像不像”。2.2 skillprobe的四個(gè)核心能力具體來(lái)說(shuō)skillprobe做了下面四類(lèi)事情。第一類(lèi)是技能注冊(cè)健康檢查。它會(huì)驗(yàn)證技能的描述文件是否能被正常解析輸入輸出的schema定義是否合法技能能否被模擬的Agent運(yùn)行時(shí)加載。這一步很像傳統(tǒng)的冒煙測(cè)試——跑不通就不往下走了省得后面所有用例都因?yàn)榧寄芗虞d不了而失敗。第二類(lèi)是輸入輸出schema校驗(yàn)。執(zhí)行技能之后先不看內(nèi)容質(zhì)量先看返回的結(jié)果結(jié)構(gòu)和定義是否一致。比如你定義輸出是一個(gè)JSON對(duì)象包含“訂單號(hào)”和“狀態(tài)”兩個(gè)字段那么返回結(jié)果就必須是一個(gè)合法的JSON而且這兩個(gè)字段必須存在類(lèi)型必須正確。這層校驗(yàn)?zāi)茏コ鲆淮箢?lèi)“模型沒(méi)按格式輸出”的臟數(shù)據(jù)問(wèn)題。第三類(lèi)是端到端行為斷言。這是核心部分。給定一組輸入執(zhí)行整個(gè)技能鏈路然后對(duì)輸出做語(yǔ)義和結(jié)構(gòu)層面的判定。判定方式可以是結(jié)構(gòu)化斷言、語(yǔ)義相似度、LLM評(píng)分等后面會(huì)細(xì)講。第四類(lèi)是穩(wěn)定性與性能采集。同一個(gè)用例執(zhí)行多次收集輸出的波動(dòng)情況、耗時(shí)分布、token消耗。LLM技能上線前最好知道它在“大概率情況下”表現(xiàn)如何而不是只測(cè)一次看個(gè)結(jié)果。把這幾類(lèi)能力組合起來(lái)skillprobe對(duì)技能的覆蓋就比較完整了。實(shí)踐下來(lái)技能注冊(cè)和schema校驗(yàn)抓的問(wèn)題最多因?yàn)樘崾驹~一改輸出格式經(jīng)常悄悄變了端到端行為斷言的價(jià)值在回歸測(cè)試?yán)镒蠲黠@。3. 如何設(shè)計(jì)一份能抓Bug的測(cè)試用例3.1 測(cè)試用例的最小單元樣例輸入加參考輸出加容忍度測(cè)試用例設(shè)計(jì)是整個(gè)方案里最需要花心思的部分。skillprobe里一個(gè)用例的最小單元由三部分組成樣例輸入、參考輸出、容忍度設(shè)置。樣例輸入要好理解就是一個(gè)能被技能執(zhí)行的輸入。對(duì)于訂單查詢(xún)技能樣例輸入可能是“幫我查一下單號(hào)SD20240513的物流進(jìn)度”。對(duì)于內(nèi)容生成技能樣例輸入可能是一段產(chǎn)品需求和目標(biāo)用戶(hù)描述。樣例輸入的覆蓋策略要兼顧正常場(chǎng)景、邊界場(chǎng)景和異常場(chǎng)景。參考輸出是“這個(gè)輸入在理想情況下應(yīng)該返回什么”。這里有個(gè)關(guān)鍵點(diǎn)LLM技能的參考輸出不應(yīng)該是一個(gè)字都不能差的固定文本而應(yīng)該是一個(gè)帶語(yǔ)義空間的結(jié)果。你可以定義參考輸出的核心要素比如“必須包含訂單狀態(tài)字段”“必須提及最新的物流節(jié)點(diǎn)”“語(yǔ)氣必須禮貌”而不是規(guī)定整個(gè)句子長(zhǎng)什么樣。容忍度設(shè)置就是告訴skillprobe“偏差多少算可以接受”。語(yǔ)義相似度閾值設(shè)0.8還是0.9結(jié)構(gòu)化字段允許哪些字段缺失調(diào)用外部API的次數(shù)允許幾次都在這里配置。一個(gè)完整的用例設(shè)計(jì)好了之后相當(dāng)于把“這個(gè)技能在這個(gè)輸入上應(yīng)該表現(xiàn)成什么樣”這個(gè)模糊的判斷變成了可以被機(jī)器執(zhí)行和復(fù)現(xiàn)的規(guī)則。3.2 參考輸出怎么生成人工種子加LLM輔助構(gòu)造參考輸出是新手最容易卡住的地方。一開(kāi)始我天真地想讓我一個(gè)一個(gè)手工寫(xiě)參考輸出寫(xiě)出來(lái)應(yīng)該很精準(zhǔn)吧。結(jié)果寫(xiě)了十幾個(gè)用例之后發(fā)現(xiàn)速度太慢而且我寫(xiě)參考輸出的語(yǔ)氣和技能真實(shí)輸出的語(yǔ)氣總有偏差導(dǎo)致語(yǔ)義相似度算出來(lái)很低誤報(bào)率很高。后來(lái)我換了個(gè)策略三步走。第一步人工標(biāo)注種子集。每個(gè)技能先挑5到10個(gè)典型輸入手工寫(xiě)出參考輸出。不需要長(zhǎng)關(guān)鍵是包含該有的信息要素和正確的格式。第二步用LLM輔助擴(kuò)充參考輸出。把種子集喂給一個(gè)輔助模型讓它按照樣例的風(fēng)格和結(jié)構(gòu)給一批新的輸入生成參考輸出。這一步能快速擴(kuò)充用例庫(kù)但生成出來(lái)的東西只能當(dāng)候選不能直接用。第三步人工審閱。所有輔助生成的參考輸出都要過(guò)一遍眼確認(rèn)信息要素正確、格式符合預(yù)期、沒(méi)有幻覺(jué)。這一步很重要因?yàn)閰⒖驾敵霰旧砣绻绣e(cuò)后面所有判定就失去了基準(zhǔn)。這里有個(gè)容易忽視的坑生成參考輸出時(shí)用的模型和技能運(yùn)行時(shí)用的模型最好不要是同一次調(diào)用、同一個(gè)溫度配置。否則“測(cè)試參考輸出”和“被測(cè)輸出”之間可能會(huì)存在隱性的同源偏見(jiàn)導(dǎo)致在生成模型上測(cè)試結(jié)果虛高換一個(gè)模型就崩了。3.3 負(fù)面用例和對(duì)抗性樣本必須單獨(dú)建一批很多人做LLM技能測(cè)試時(shí)因?yàn)閰⒖驾敵霾恢涝趺炊ň椭苯臃艞壛素?fù)面用例。這是不對(duì)的。負(fù)面用例反而不需要復(fù)雜的參考輸出只需要一個(gè)預(yù)期行為要么拒絕要么觸發(fā)澄清要么明確返回“無(wú)法處理”。我維護(hù)的用例集里負(fù)面樣本大概占三成。常見(jiàn)的有這么幾類(lèi)空輸入、超長(zhǎng)輸入、格式錯(cuò)誤的輸入、明顯超出技能范圍的問(wèn)題、含有誘導(dǎo)內(nèi)容的輸入比如讓模型忽略系統(tǒng)指令、輸出Prompt原文等。負(fù)面用例最典型的驗(yàn)證方式是執(zhí)行技能后斷言輸出里是否包含“無(wú)法回答”之類(lèi)的拒絕語(yǔ)義或者斷言技能是否正確返回了某個(gè)錯(cuò)誤標(biāo)識(shí)。你不需要它答得漂亮只需要它不要“硬答”。很多線上事故就是負(fù)面輸入觸發(fā)了模型的胡編亂造而手工測(cè)試又從來(lái)不會(huì)拿這些輸入去點(diǎn)一遍。對(duì)抗性樣本這東西一開(kāi)始沒(méi)那么復(fù)雜。先把項(xiàng)目里用戶(hù)反饋過(guò)的問(wèn)題話術(shù)沉淀成用例再周期性跑一遍比專(zhuān)門(mén)去構(gòu)造隨機(jī)攻擊句子實(shí)用得多。3.4 測(cè)試用例也要做版本管理測(cè)試用例不是一次性寫(xiě)完了就完事。技能版本迭代用例也要跟著更新。skillprobe的用例集建議直接用Git管理和技能代碼放在同一個(gè)倉(cāng)庫(kù)里。這樣每次改動(dòng)技能代碼的時(shí)候能直接看到用例集有沒(méi)有同步更新。用例文件本身建議用可讀性強(qiáng)的格式組織每個(gè)用例有個(gè)唯一ID有標(biāo)簽標(biāo)明場(chǎng)景類(lèi)型有創(chuàng)建時(shí)間有最后更新人。我在項(xiàng)目里遇到過(guò)一次很尷尬的情況一個(gè)用例從創(chuàng)建起就沒(méi)人維護(hù)技能行為已經(jīng)合法變更了兩次用例的參考輸出卻還在描述最原始的版本每次跑測(cè)試必掛。最后排查了很久才發(fā)現(xiàn)不是技能的問(wèn)題是用例過(guò)期了。所以用例也要有一個(gè)生命周期管理定期Review、清理和更新。我會(huì)在每次技能版本發(fā)布前跑一遍全量用例同時(shí)看一眼有沒(méi)有用例被跳過(guò)或者被標(biāo)記為“已知過(guò)時(shí)”。4. 指標(biāo)與判定邏輯別讓測(cè)試自己變成玄學(xué)4.1 先做結(jié)構(gòu)化斷言再做語(yǔ)義判斷判定邏輯是skillprobe的核心設(shè)計(jì)。我把它分成三層優(yōu)先做結(jié)構(gòu)化斷言再做語(yǔ)義判斷最后才用LLM評(píng)分。結(jié)構(gòu)化斷言是最硬的一層。它校驗(yàn)輸出是否滿(mǎn)足明確的規(guī)則比如輸出是否是合法JSON關(guān)鍵字段是否存在、類(lèi)型是否正確、數(shù)值是否在一定范圍內(nèi)、枚舉值是否合法。這層判斷不需要模型參與純代碼就可以做速度極快準(zhǔn)確率百分百。我在項(xiàng)目里見(jiàn)過(guò)太多“模型一本正經(jīng)地輸出了一坨格式錯(cuò)誤的內(nèi)容”的案例。尤其是讓模型輸出JSON的場(chǎng)景經(jīng)常出現(xiàn)末尾多一個(gè)逗號(hào)、字段名被引號(hào)包錯(cuò)、嵌套結(jié)構(gòu)錯(cuò)亂這類(lèi)問(wèn)題。結(jié)構(gòu)化斷言能把這些全部攔下來(lái)。結(jié)構(gòu)化斷言通過(guò)之后再看語(yǔ)義。語(yǔ)義判斷通常用向量相似度把模型輸出和參考輸出分別轉(zhuǎn)換成向量計(jì)算余弦相似度。但這里要有心理準(zhǔn)備語(yǔ)義相似度是一個(gè)很粗糙的指標(biāo)它適合“生成型任務(wù)”的粗篩不適合“抽取型任務(wù)”的細(xì)判。抽取型任務(wù)建議直接用結(jié)構(gòu)化斷言加信息要素比對(duì)比如檢查輸出里是否包含指定實(shí)體。給不同任務(wù)設(shè)定合適的閾值很重要。生成型任務(wù)比如郵件撰寫(xiě)、內(nèi)容總結(jié)語(yǔ)義相似度閾值設(shè)在0.75到0.85之間比較合理因?yàn)橥痪湓捒梢杂卸喾N合法表達(dá)方式。抽取型任務(wù)比如訂單信息抽取、實(shí)體識(shí)別閾值要往高了設(shè)或者干脆不用語(yǔ)義相似度改用要素級(jí)別的匹配。提示如果發(fā)現(xiàn)測(cè)試結(jié)果頻繁在“通過(guò)”和“失敗”之間橫跳先別急著懷疑模型先看一下閾值是不是設(shè)得太緊。閾值不是越大越好要結(jié)合技能的真實(shí)輸出分布來(lái)定。4.2 LLM-as-Judge的引入與偏見(jiàn)規(guī)避有些技能沒(méi)有標(biāo)準(zhǔn)答案比如“幫用戶(hù)把一段投訴內(nèi)容改寫(xiě)得更委婉”。這種任務(wù)用語(yǔ)義相似度和結(jié)構(gòu)化斷言都很難覆蓋全這時(shí)候就需要引入LLM-as-Judge——用另外一個(gè)模型來(lái)當(dāng)評(píng)審給輸出打分。用LLM當(dāng)評(píng)審是有用的但它有自己的偏見(jiàn)這個(gè)必須得防。常見(jiàn)偏見(jiàn)有三個(gè)一是立場(chǎng)偏好評(píng)分模型容易給“和自己生成風(fēng)格相似的輸出”打高分二是長(zhǎng)度偏好越長(zhǎng)越容易得高分哪怕內(nèi)容冗余三是位置偏好在多條候選輸出對(duì)比時(shí)排在前面或后面的結(jié)果會(huì)影響評(píng)分。規(guī)避這些偏見(jiàn)skillprobe的做法是評(píng)分Prompt里明確給出評(píng)分維度比如“信息完整性”“格式規(guī)范性”“語(yǔ)氣恰當(dāng)性”每個(gè)維度單獨(dú)打分最后再看總分。同時(shí)把參考輸出也作為提示的一部分給評(píng)分模型讓它做的是“對(duì)照參考標(biāo)準(zhǔn)來(lái)打分”而不是“憑感覺(jué)給個(gè)分”。另外一個(gè)細(xì)節(jié)是評(píng)分模型最好和被測(cè)技能用的模型不是一個(gè)。雖然成本會(huì)高一點(diǎn)但至少能避免“自己夸自己”的嫌疑。我自己常用一個(gè)相對(duì)便宜但指令跟隨能力強(qiáng)的模型做評(píng)分效果是夠用的。4.3 可復(fù)現(xiàn)性才是自動(dòng)化的命根子LLM技能測(cè)試最讓人頭大的就是今天通過(guò)了明天掛了而且找不到原因。要根治這個(gè)問(wèn)題必須把可復(fù)現(xiàn)性當(dāng)成一等公民??蓮?fù)現(xiàn)性主要靠?jī)杉?。第一件是固定采樣配置。skillprobe執(zhí)行用例時(shí)會(huì)把模型名稱(chēng)、溫度、top_p、seed等參數(shù)全部記錄在測(cè)試報(bào)告里。如果你的模型接口支持固定seed就一定要固定下來(lái)如果不支持那就把溫度設(shè)成0降低隨機(jī)性。我在實(shí)踐里發(fā)現(xiàn)溫度設(shè)0也不意味著完全確定因?yàn)橛行┠P头?wù)端還有其他隨機(jī)因素這時(shí)候就要靠多次執(zhí)行取結(jié)果分布。第二件是固定依賴(lài)環(huán)境。RAG類(lèi)的技能要固定檢索器和知識(shí)庫(kù)版本工具調(diào)用類(lèi)的技能要mock掉外部API或者記錄下真實(shí)API在測(cè)試時(shí)刻的返回結(jié)果。如果不固定這些外部依賴(lài)那你的測(cè)試結(jié)果根本沒(méi)法定位問(wèn)題——到底是技能本身壞了還是檢索器數(shù)據(jù)變了還是外部API抽風(fēng)了完全說(shuō)不清。skillprobe支持給用例配置一個(gè)“依賴(lài)環(huán)境指紋”跑測(cè)試的時(shí)候自動(dòng)記錄當(dāng)前檢索器版本、API mock版本、模型版本。這樣出了問(wèn)題先看報(bào)告里環(huán)境指紋有沒(méi)有變化能節(jié)省大量排查時(shí)間。5. 把skillprobe接進(jìn)CI流水線5.1 流水線的最小閉環(huán)設(shè)計(jì)測(cè)試用例設(shè)計(jì)好了指標(biāo)判定邏輯也清晰了下一步就是讓它跑起來(lái)。我建議的最小閉環(huán)是觸發(fā)事件、執(zhí)行測(cè)試、生成報(bào)告、結(jié)果判定。觸發(fā)事件可以根據(jù)團(tuán)隊(duì)情況選。最常見(jiàn)的是PR合并前跑也就是代碼變更時(shí)觸發(fā)另外還可以加一個(gè)定時(shí)觸發(fā)比如每天夜里跑一次全量用例防止模型側(cè)或者依賴(lài)側(cè)悄悄變化導(dǎo)致技能行為漂移。執(zhí)行測(cè)試這一步skillprobe會(huì)按照配置去加載技能代碼、讀取測(cè)試用例集、逐個(gè)執(zhí)行。整個(gè)執(zhí)行過(guò)程必須是無(wú)狀態(tài)的每次跑之前從Git拉取指定版本跑完之后清理臨時(shí)文件。這樣才能保證測(cè)試結(jié)果可復(fù)現(xiàn)。生成報(bào)告是很多人容易忽略的環(huán)節(jié)。報(bào)告里除了“通過(guò)率”還必須有失敗用例的完整上下文輸入是什么、輸出是什么、參考輸出是什么、哪一項(xiàng)斷言沒(méi)過(guò)、耗時(shí)多少、當(dāng)時(shí)的環(huán)境指紋是什么。沒(méi)有這些信息測(cè)試掛了你也只能干瞪眼。結(jié)果判定要嚴(yán)格但也要留出人工確認(rèn)的通道。我的做法是通過(guò)率達(dá)到設(shè)定閾值就放行比如95%但如果低于閾值CI就直接失敗不允許合并代碼。同時(shí)配置了一個(gè)白名單機(jī)制允許維護(hù)者手動(dòng)標(biāo)記某些用例為“已知問(wèn)題”避免一個(gè)長(zhǎng)期存在的小問(wèn)題卡住整個(gè)團(tuán)隊(duì)的發(fā)布流程。5.2 一個(gè)可以直接抄的配置文件示例skillprobe的配置用YAML格式組織整體結(jié)構(gòu)比較直觀。下面這個(gè)是我在一個(gè)類(lèi)似訂單查詢(xún)技能的項(xiàng)目里實(shí)際用的配置骨架project: name: order-query-skill version: 1.4.2 model: base: gpt-4-turbo # 技能運(yùn)行時(shí)使用的模型 temperature: 0 # 低溫度優(yōu)先保證可復(fù)現(xiàn) max_tokens: 1024 timeout_seconds: 30 tests: dir: ./tests/cases pass_threshold: 0.95 report: output_dir: ./reports format: [json, html] judge: mode: hybrid # 混合模式結(jié)構(gòu)化斷言 語(yǔ)義相似度 LLM評(píng)分 semantic_similarity: 0.80 llm_judge: model: gpt-4o-mini dimensions: - information_completeness - format_compliance - tone_appropriateness dependency_environment: retriever_version: 2024.06.01 mock_api: true幾個(gè)關(guān)鍵參數(shù)說(shuō)一下。pass_threshold: 0.95的意思是全量用例集里至少九成五的用例要通過(guò)否則CI失敗。這個(gè)值一開(kāi)始不要定太高建議先跑一周看基線再逐步收緊。semantic_similarity: 0.80是語(yǔ)義相似度的全局默認(rèn)閾值個(gè)別用例可以在用例文件里覆蓋。5.3 CI流水線腳本的關(guān)鍵步驟流水線腳本本身不復(fù)雜核心邏輯就四步裝環(huán)境、拉代碼、跑測(cè)試、上傳報(bào)告。# 裝依賴(lài) pip install skillprobe # 拉技能代碼和用例集 git checkout commit_id git pull --recurse-submodules # 執(zhí)行測(cè)試指定配置文件和輸出目錄 skillprobe run --config ./config.yaml \ --report ./reports/ci_${BUILD_ID}.json # 檢查退出碼0表示通過(guò)非0表示未達(dá)閾值 exit_code$? # 把報(bào)告上傳到團(tuán)隊(duì)的可視化平臺(tái) skillprobe upload --report ./reports/ci_${BUILD_ID}.json \ --project order-query-skill exit $exit_code用skillprobe CLI的時(shí)候有一點(diǎn)要特別提醒它支持兩種執(zhí)行模式。一種是快速模式只跑結(jié)構(gòu)化斷言和schema校驗(yàn)速度快適合PR階段快速反饋另一種是全量模式會(huì)帶上語(yǔ)義判定和LLM評(píng)分耗時(shí)較長(zhǎng)適合夜間定時(shí)任務(wù)跑。我在實(shí)際項(xiàng)目里就是兩檔并行PR階段跑快速模式保證格式類(lèi)問(wèn)題不會(huì)流進(jìn)去每天晚上跑全量模式把語(yǔ)義和穩(wěn)定性問(wèn)題撈一遍。這樣既能快速迭代又不會(huì)把質(zhì)量底線放掉。6. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄6.1 測(cè)試總是飄紅先查三個(gè)東西如果你剛開(kāi)始用skillprobe跑出來(lái)的測(cè)試結(jié)果今天紅一片、明天綠一片別急著去調(diào)模型提示詞先按順序查三件事。第一查用例集的參考輸出是不是過(guò)期了。這是最常見(jiàn)的原因。技能行為已經(jīng)迭代了用例參考輸出還停留在老版本那今天通過(guò)了反而奇怪。把參考輸出同步一下問(wèn)題大概率就解決了。第二查模型配置是不是變了。可能是技能代碼里默認(rèn)溫度改了可能是模型服務(wù)端把版本升級(jí)了也可能是你從GPT-4換到了GPT-4-Turbo但沒(méi)有同步更新配置。這些都算環(huán)境漂移報(bào)告里的環(huán)境指紋一看便知。第三查外部依賴(lài)的狀態(tài)。RAG技能要去看檢索器是否加了新數(shù)據(jù)工具類(lèi)技能要看下游API是否改了返回格式。我遇到過(guò)一個(gè)問(wèn)題下游API在某個(gè)字段里多返回了一個(gè)“customer_note”結(jié)果技能原本的字段補(bǔ)全邏輯沒(méi)跟上輸出里把備注拼到訂單號(hào)里去了。這種問(wèn)題如果不固定依賴(lài)環(huán)境排查起來(lái)會(huì)非常痛苦。6.2 提示詞改一句二十個(gè)用例掛了十八個(gè)這種大面積回歸是最嚇人的但也是自動(dòng)化測(cè)試價(jià)值最大的時(shí)刻。我碰到過(guò)一次不過(guò)不是技能提示詞的問(wèn)題而是基礎(chǔ)模型的版本被服務(wù)商悄悄升級(jí)了輸出風(fēng)格大變導(dǎo)致所有依賴(lài)“固定措辭”的用例全部掛掉。遇到這種大面積失敗第一件事是冷靜看報(bào)告不要直接改提示詞。先把失敗用例集中起來(lái)看它們共同的特征是什么。如果全部和“語(yǔ)氣”相關(guān)那大概率是模型風(fēng)格漂移如果全部和“字段缺失”相關(guān)那可能是輸出格式變了如果零散的掛才需要去懷疑具體用例。大面積回歸往往意味著要調(diào)整的是“默認(rèn)閾值”或者“參考輸出”這層而不是逐條修用例。skillprobe支持批量更新參考輸出但一定要走人工審閱尤其是生成型任務(wù)不要讓測(cè)試數(shù)據(jù)自己修改自己。注意提示詞里加任何內(nèi)容之前先想一想這個(gè)改動(dòng)會(huì)不會(huì)影響輸出格式或者風(fēng)格。LLM技能的脆弱就在這里加一句“你是一個(gè)親切的客服”可能就會(huì)讓JSON輸出多一句客套話直接破壞結(jié)構(gòu)化斷言。6.3 并發(fā)執(zhí)行時(shí)API配額被瞬間打爆用例上百個(gè)之后串行執(zhí)行肯定等不起于是你開(kāi)并發(fā)。然后問(wèn)題來(lái)了LLM服務(wù)的API配額被瞬間打爆測(cè)試跑一半開(kāi)始大量報(bào)超時(shí)整個(gè)流水線紅掉。這是我自己踩過(guò)的坑。解決方案是加限流而不是無(wú)腦提高并發(fā)數(shù)。skillprobe里可以配置最大并發(fā)數(shù)和每秒請(qǐng)求數(shù)。建議先把并發(fā)數(shù)設(shè)成4~6每秒請(qǐng)求數(shù)設(shè)成一個(gè)保守值跑完一輪看總耗時(shí)再逐步往上調(diào)。還有一點(diǎn)PR階段的快速模式尤其適合開(kāi)高并發(fā)因?yàn)榻Y(jié)構(gòu)化斷言不需要調(diào)用大模型幾乎不占配額全量模式里的語(yǔ)義相似度和LLM評(píng)分是重頭戲并發(fā)要控制得更嚴(yán)格。如果你用的是按token計(jì)費(fèi)的服務(wù)建議給評(píng)分模型單獨(dú)開(kāi)一個(gè)便宜一點(diǎn)的模型成本能省不少。6.4 歷史用例逐漸失真測(cè)試集需要體檢用例集不是資產(chǎn)用得不好就是負(fù)債。跑了一段時(shí)間后你會(huì)發(fā)現(xiàn)一些舊用例的輸入已經(jīng)和當(dāng)前業(yè)務(wù)不太匹配了或者參考輸出描述的場(chǎng)景已經(jīng)下線了。這類(lèi)用例就會(huì)變成“永久紅色”第一周你還會(huì)點(diǎn)開(kāi)看看第二周開(kāi)始就變成“已知問(wèn)題”了。我的建議是每個(gè)迭代都做一次用例體檢??催@么幾類(lèi)長(zhǎng)時(shí)間重復(fù)失敗的用例、執(zhí)行耗時(shí)異常的用例、輸入場(chǎng)景已經(jīng)被業(yè)務(wù)廢棄的用例。該更新的更新該標(biāo)記的標(biāo)記該刪的刪。寧可用例數(shù)量少而精也不要讓一坨僵尸用例在測(cè)試集里發(fā)臭。7. 實(shí)操心得與后續(xù)擴(kuò)展最后聊一點(diǎn)我個(gè)人在項(xiàng)目里摸爬滾打總結(jié)的東西。第一個(gè)建議是別一開(kāi)始就想把全部技能都納入自動(dòng)化測(cè)試。挑一個(gè)你正在頻繁迭代的技能先用skillprobe把基線跑起來(lái)。所謂基線就是你有把握它當(dāng)前行為是對(duì)的然后把當(dāng)前測(cè)試結(jié)果保存為基線版本。以后任何改動(dòng)拿新結(jié)果和基線對(duì)比多出來(lái)的紅點(diǎn)就是改進(jìn)方向。第二個(gè)建議是LLM技能的自動(dòng)化測(cè)試本質(zhì)上是在建立一種“行為契約”。你給技能設(shè)定了輸入和輸出的預(yù)期契約越清晰技能就越像一個(gè)可以被維護(hù)的系統(tǒng)。我見(jiàn)過(guò)很多同事在調(diào)提示詞的時(shí)候完全靠感覺(jué)跑一遍發(fā)現(xiàn)輸出還不錯(cuò)就提交了。但感覺(jué)不能成為質(zhì)量保障的手段尤其是在Agent應(yīng)用開(kāi)始承載真實(shí)業(yè)務(wù)數(shù)據(jù)之后。第三個(gè)建議是關(guān)于擴(kuò)展的。skillprobe目前已經(jīng)能支撐單技能的端到端測(cè)試和CI集成但它還能往幾個(gè)方向延伸。一個(gè)是多模型對(duì)比測(cè)試同一個(gè)技能用例集在多個(gè)模型上跑看哪個(gè)模型表現(xiàn)得最穩(wěn)定另一個(gè)是技能版本A/B測(cè)試新老兩版技能在同一個(gè)用例集上對(duì)比量化評(píng)估改動(dòng)帶來(lái)的收益或者損失。這兩個(gè)方向?qū)ψ鯝gent框架選型和技能優(yōu)化都特別有價(jià)值。如果你正在維護(hù)任何形式的LLM技能我真的建議花一個(gè)下午把skillprobe的閉環(huán)搭起來(lái)。它能解決的問(wèn)題非常明確讓技能迭代從“靠感覺(jué)”變成“看數(shù)據(jù)”。這個(gè)轉(zhuǎn)變?cè)皆绨l(fā)生越好。