建確定性LLM管道:從采樣隨機(jī)性到工程化穩(wěn)定輸出)
用 LLM 搭過處理管道的人基本都遇到過同一類問題同一個(gè) prompt 跑兩次摘要細(xì)節(jié)不一樣批量任務(wù)跑到中間突然開始報(bào)錯(cuò)重試一次又好了重新啟動(dòng)進(jìn)程再跑上次輸出的字段數(shù)量都變了。這類問題表面上是模型“抽風(fēng)”本質(zhì)上是把 LLM 當(dāng)成了一個(gè)純函數(shù)卻忽略了它默認(rèn)就不是純函數(shù)。開發(fā)更確定性的 LLM pipelines核心不是追求一個(gè)字都不差而是讓管道里的每一段都有可復(fù)現(xiàn)的輸入、可觀測的中間狀態(tài)、可校驗(yàn)的輸出。下面這篇內(nèi)容就是為了解決這個(gè)問題寫的適合做批量文本處理、知識庫入庫、RAG 抽取、Agent 任務(wù)編排的開發(fā)者。1. 先定位問題LLM 管道里的“不確定性”到底來自哪里1.1 模型輸出隨機(jī)性的第一來源是采樣大多數(shù) LLM 在生成下一個(gè) token 時(shí)不是只選概率最高的那一個(gè)。它會從概率分布里抽樣。這個(gè)抽樣過程引入了隨機(jī)性所以即使輸入完全一樣輸出也可能不一樣。相關(guān)參數(shù)常見的有幾個(gè)參數(shù)作用對確定性的影響temperature控制概率分布平滑程度越小越接近貪心設(shè)為 0 時(shí)通常走 argmaxtop_p截?cái)喔怕世鄯e區(qū)間越小搜索空間越小波動(dòng)會降低top_k只保留概率最高的 k 個(gè) token能限制候選范圍但不能保證完全一致seed給隨機(jī)數(shù)生成器一個(gè)初始值可以在部分框架和 API 中復(fù)現(xiàn)結(jié)果max_tokens限制輸出長度影響截?cái)噙M(jìn)而影響結(jié)構(gòu)化解析結(jié)果溫度調(diào)低是多數(shù)團(tuán)隊(duì)做的第一件事。把 temperature 設(shè)置為 0代表盡量走最高概率路徑會顯著降低隨機(jī)性。但也要明白temperature0 不等于零風(fēng)險(xiǎn)。遇到 GPU 上的批量推理某些算子在并行歸約時(shí)存在浮點(diǎn)計(jì)算順序問題遇到云 API模型權(quán)重可能在你不知道的時(shí)候更新遇到帶隨機(jī)采樣但不允許傳入 seed 的接口你仍然無法從外部完全控制。1.2 “確定性管道”不等于“每次輸出都一樣”這是很容易被誤解的一個(gè)點(diǎn)。如果目標(biāo)是讓機(jī)器產(chǎn)生穩(wěn)定可靠的數(shù)據(jù)那么更合理的定義是在相同的輸入、相同的代碼版本、相同的模型版本、相同的參數(shù)條件下管道輸出可復(fù)現(xiàn)在輸入有微小差異時(shí)輸出能保持結(jié)構(gòu)一致在模型某一步抽風(fēng)時(shí)管道不會把錯(cuò)誤結(jié)果當(dāng)成功結(jié)果直接寫進(jìn)數(shù)據(jù)庫。這比“逐字一致”更貼近工程需要。比如我用一段文本抽取標(biāo)題、作者和發(fā)布時(shí)間。第一次跑出來author: 張三第二次跑出author: 張 三。逐字對比會認(rèn)為兩次結(jié)果不同但業(yè)務(wù)上可能都算正確。更有用的做法是先約定字段的 JSON schema再用校驗(yàn)器檢查字段類型和格式最后對可歸一化字段做后處理。所以先不要把目標(biāo)定成“每次一模一樣”而是定成“每次跑完都滿足統(tǒng)一 schema且同一輸入的可復(fù)現(xiàn)率足夠高”。1.3 并不是所有場景都值得追求確定性處理一批發(fā)票、合同、病例文本時(shí)字段抽取錯(cuò)誤會導(dǎo)致后續(xù)流程混亂這時(shí)候確定性很重要。做聊天助手、寫廣告文案、頭腦風(fēng)暴或者要求模型生成多個(gè)候選版本時(shí)過度固定參數(shù)反而會損失效用。你要的是多樣性而不是可復(fù)現(xiàn)性。判斷標(biāo)準(zhǔn)很簡單如果 LLM 的輸出會作為結(jié)構(gòu)化數(shù)據(jù)進(jìn)入下一個(gè)系統(tǒng)那就必須做控制如果只是給人看可以放寬。2. 從單次模型調(diào)用開始鎖定隨機(jī)源2.1 請求參數(shù)要完整保存而不是只保存輸出很多管道不穩(wěn)定不是模型本身的問題而是調(diào)用方根本沒記錄自己到底傳了什么參數(shù)。同一個(gè) prompt上一次走的是gpt-4o-mini下一次代碼里改成了gpt-4o上一次傳了 system prompt下一次 system prompt 丟了上一次 temperature 是 0下一次因?yàn)榕渲弥行母伦兂闪?0.7。這些變動(dòng)都會讓結(jié)果看起來“隨機(jī)”。我建議對每一次模型請求都記錄如下信息模型名稱和版本標(biāo)識system prompt 模板 IDuser prompt 模板 ID溫度、top_p、seed、max_tokens輸入內(nèi)容 hash輸出內(nèi)容返回的狀態(tài)碼、耗時(shí)、重試次數(shù)請求發(fā)起時(shí)間這些日志不一定都要進(jìn)業(yè)務(wù)表可以單獨(dú)放在請求日志里。等到排查時(shí)它們能幫你快速判斷問題出在模型還是出在調(diào)用方。2.2 給每個(gè) prompt 模板做版本號同一個(gè) prompt 模板沒有被修改但代碼里拼接變量的方式變了也可能導(dǎo)致輸出不穩(wěn)定。最穩(wěn)的做法是給模板命名并打版本。例如document-extract-v3、summary-qa-v2。模板內(nèi)容一旦修改就生成新版本不要原地覆蓋。這樣做的原因是當(dāng)管道批量跑完 5000 條數(shù)據(jù)后如果你的模板悄悄改動(dòng)了一個(gè)字那么從修改點(diǎn)之后的所有輸出都無法和之前的數(shù)據(jù)放在一起對比。模板版本號最好直接進(jìn)入日志字段并且作為緩存 key 的一部分。2.3 結(jié)構(gòu)化輸出在前自由文本在后如果你想盡可能穩(wěn)定就別讓模型自由發(fā)揮格式。最實(shí)用的手段是要求模型返回 JSON最好使用接口提供的 JSON Mode 或結(jié)構(gòu)化輸出能力。一個(gè)典型的穩(wěn)定請求配置可以長這樣{ model: your-model-name, messages: [ { role: system, content: 你是文檔信息抽取助手。只輸出 JSON不要輸出解釋。 }, { role: user, content: 從下面的文檔中抽取字段。\n\n文檔\n{{input_text}} } ], response_format: { type: json_object }, temperature: 0, seed: 42 }這里要強(qiáng)調(diào)即使模型支持 JSON Mode也不能完全信任輸出。校驗(yàn)器仍然要寫。JSON Mode 只能保證格式可解析不保證字段值正確更不保證模型沒有漏掉字段。3. 工程層面的確定性控制緩存、冪等、重試與校驗(yàn)3.1 用緩存消除重復(fù)計(jì)算也消除重復(fù)波動(dòng)當(dāng)輸入內(nèi)容幾乎不變時(shí)很多管道沒必要反復(fù)調(diào)用模型。比如一個(gè)文檔清洗任務(wù)每天跑一次但其中 80% 的文檔根本沒變過。如果每次都全量調(diào)用模型不僅浪費(fèi)成本還會因?yàn)槟P桶姹尽⒉蓸訁?shù)的變化把原本穩(wěn)定的結(jié)果改亂。更合理的做法是給每個(gè)任務(wù)算緩存 keyprompt 模板版本號模型名采樣參數(shù)輸入文本 hash附帶數(shù)據(jù) hash當(dāng)緩存 key 完全一致時(shí)直接返回上次結(jié)果。這個(gè)策略能大幅提升管道穩(wěn)定性。但要注意一點(diǎn)緩存 key 不能只放文件路徑。同一份文檔改過內(nèi)容但路徑?jīng)]變路徑就不適合做 key。用內(nèi)容 hash 更可靠。3.2 讓每次請求具備冪等性冪等的意思是同一個(gè)任務(wù)執(zhí)行一次和執(zhí)行多次最終結(jié)果一致。在 LLM 管道里冪等不是天然成立的。因?yàn)槟P驼{(diào)用可能有網(wǎng)絡(luò)超時(shí)、服務(wù)端 5xx、限流導(dǎo)致你不得不重發(fā)請求。重發(fā)時(shí)如果上下文里夾了時(shí)間戳或者隨機(jī) ID輸出就可能變化。為了讓管道更可重試需要把業(yè)務(wù)唯一 ID 和模型請求解耦。請求內(nèi)部不要依賴“當(dāng)前時(shí)間”這類不穩(wěn)定變量。若必須記錄時(shí)間請把它放在日志層而不是塞進(jìn) prompt。重試時(shí)要做的第一件事不是機(jī)械地重發(fā)原請求而是記錄上一次失敗的狀態(tài)。比如錯(cuò)誤類型是超時(shí)、限流還是 schema 校驗(yàn)失敗。不同錯(cuò)誤類型對應(yīng)的處理策略不同。超時(shí)和限流可以稍等重試schema 校驗(yàn)失敗則需要把錯(cuò)誤信息反饋給模型或者切換為更保守的 prompt。3.3 三層校驗(yàn)器格式校驗(yàn)、字段校驗(yàn)、業(yè)務(wù)規(guī)則校驗(yàn)我見過的穩(wěn)定管道幾乎都有一個(gè)三層校驗(yàn)機(jī)制。第一層是格式校驗(yàn)。檢查模型輸出的 JSON 能否被解析字段類型是否符合預(yù)期。第二層是內(nèi)容校驗(yàn)。檢查關(guān)鍵字段是否為空、枚舉值是否合法、日期格式是否正確。第三層是業(yè)務(wù)規(guī)則校驗(yàn)。例如金額不能為負(fù)數(shù)、文檔編號必須匹配正則、摘要不能超過指定字?jǐn)?shù)。如果模型輸出沒有通過校驗(yàn)不建議直接丟棄也不建議直接手動(dòng)糾錯(cuò)。更穩(wěn)的做法是保存原始模型輸出作為樣本記錄校驗(yàn)錯(cuò)誤原因基于錯(cuò)誤信息構(gòu)造一次修復(fù)請求修復(fù)失敗則進(jìn)入人工隊(duì)列這樣即使模型在個(gè)別樣本上出問題管道整體仍然可控。4. 批量場景里最容易破壞確定性的三個(gè)隱患4.1 輸入規(guī)范化不足順序和格式互相干擾批量處理的第一個(gè)問題不是模型而是輸入文件。一批文檔可能是 UTF-8也可能是 GBK有的行尾是 LF有的是 CRLF有的文件開頭有 BOM有的沒有。這些差異都會讓同一段文本在進(jìn)入模型前產(chǎn)生變化。為了穩(wěn)定建議先做輸入規(guī)范化統(tǒng)一轉(zhuǎn)碼為 UTF-8統(tǒng)一換行符移除或記錄 BOM去除不可見控制字符保留原始排版時(shí)至少保證分塊邏輯確定分塊也值得注意。如果按固定 token 數(shù)切分模型的 tokenizer 更新后切分位置會變。如果按段落切分又要先約定段落邊界。最好的辦法是既保存分塊結(jié)果也保存分塊 id。這樣即使重跑也能對比是哪一塊發(fā)生了變化。4.2 不要依賴“返回順序”來表示數(shù)據(jù)位置批量調(diào)用模型時(shí)很多管道會使用并發(fā)。并發(fā)返回順序經(jīng)常是亂的。第一份文檔可能 1 秒返回第二份文檔可能 5 秒返回。如果你直接把結(jié)果按返回順序?qū)懭虢Y(jié)果表那數(shù)據(jù)順序就和原始文檔順序?qū)Σ簧狭恕=鉀Q辦法是使用任務(wù) ID 關(guān)聯(lián)結(jié)果。每條輸入在進(jìn)入管道時(shí)拿到一個(gè)唯一 ID模型返回后把結(jié)果寫回task_id對應(yīng)的記錄里。最后再按task_id聚合而不是按時(shí)間順序聚合。這一步看起來多余但能避免非常多奇怪的問題。尤其當(dāng)你做長文檔摘要或知識庫切片入庫時(shí)只有順序可控后續(xù)檢索才能穩(wěn)定復(fù)現(xiàn)。4.3 Agent 或多工具鏈路里還要控制工具調(diào)用歷史當(dāng)管道升級為 Agent也就是讓模型決定下一步調(diào)用哪個(gè)工具時(shí)不確定性會明顯增加。原因不完全是模型推理隨機(jī)而是工具本身會改變上下文。第一次調(diào)用工具返回了 10 條檢索結(jié)果第二次調(diào)用可能因?yàn)闄z索接口排序變化返回了另外 10 條。模型后續(xù)決策自然就不同了。要讓 Agent 管道更確定至少要做到工具調(diào)用的入?yún)⒑头祷亟Y(jié)果都必須記錄檢索結(jié)果進(jìn)入上下文前要排序并裁剪設(shè)定允許的最大調(diào)用輪數(shù)每一步都校驗(yàn)工具調(diào)用格式而不是讓模型無限嘗試不要把所有工具都塞給模型只暴露當(dāng)前子任務(wù)需要的工具如果你還在早期學(xué)習(xí)階段直接讀 LLM 官方文檔或觀察框架源碼先跑通單工具調(diào)用再增加第二個(gè)工具。不要一上來就讓模型自己決定復(fù)雜流程。5. 一個(gè)可落地的執(zhí)行順序從最小樣例到批量回歸5.1 先用 5 條樣本跑通全鏈路真正做起確定性控制時(shí)最忌諱直接開全量。我一般先用 5 到 10 條高代表性樣本跑一遍。樣本要覆蓋不同長度的文本、不同格式、特殊字符、空字段等邊界情況。然后檢查三個(gè)東西模型輸出是否都能被解析為預(yù)期結(jié)構(gòu)校驗(yàn)器有沒有誤報(bào)日志里能不能看到每條請求對應(yīng)的模型版本、參數(shù)、輸入 hash如果這些都沒問題再擴(kuò)大到 50 條。50 條能幫你發(fā)現(xiàn)并發(fā)、超時(shí)、限流這類單條跑時(shí)看不見的問題。5.2 固化執(zhí)行計(jì)劃并填寫記錄搭建管道時(shí)可以用一個(gè)簡單的執(zhí)行計(jì)劃來表示完整流程。例如1. 規(guī)范化輸入統(tǒng)一編碼、讀取文本、計(jì)算內(nèi)容 hash 2. 查緩存如果內(nèi)容 hash 和參數(shù)組合命中直接返回 3. 構(gòu)造 prompt使用固定模板注入規(guī)范化文本 4. 調(diào)用模型溫度 0記錄完整請求日志 5. 解析輸出嘗試 JSON 解析 6. 校驗(yàn)結(jié)果格式、字段、業(yè)務(wù)規(guī)則 7. 校驗(yàn)失敗則進(jìn)入修復(fù)子流程 8. 成功則寫入結(jié)果表并記錄輸出 content hash這套流程不依賴具體框架。你可以在 LangChain、自研代碼或任何 LLM 框架上實(shí)現(xiàn)但每一步都要獨(dú)立可觀測方便重跑時(shí)對比。5.3 把失敗樣本收集成回歸集穩(wěn)定管道不是一次寫出來的而是靠回歸集長期維護(hù)出來的。準(zhǔn)備一個(gè)失敗樣本目錄里面放典型的解析失敗樣本字段缺失樣本校驗(yàn)失敗樣本超時(shí)或限流樣本每次你修改 prompt 模板、調(diào)整切分邏輯、升級模型版本后都用這批樣本重新跑一遍對比修復(fù)率。如果原來失敗的樣本變得能通過說明改動(dòng)方向正確。這個(gè)回歸集不要太大。50 到 100 條就足夠。太大了反而會拖慢開發(fā)迭代。5.4 模型升級前先做對比實(shí)驗(yàn)無論你用的是本地模型還是云 API換版本之前都必須先跑對比實(shí)驗(yàn)。先把舊版本的輸出保存下來再切到新版本在回歸集上跑一遍。逐條對比字段一致性。遇到差異較大的情況不要急著改代碼先把 prompt 模板和模型版本日志調(diào)出來確認(rèn)差異源。真正跑生產(chǎn)時(shí)我更建議把模型名稱寫成環(huán)境變量或配置項(xiàng)而不是硬寫在代碼里。這樣切版本時(shí)可以隨時(shí)做 A/B 對比也方便回滾。6. 輸出不一致時(shí)按這個(gè)順序排查遇到管道結(jié)果不穩(wěn)定時(shí)很多人第一反應(yīng)是調(diào)整 temperature或者懷疑模型能力。但實(shí)際踩坑后會發(fā)現(xiàn)很多問題出在管道本身。排查順序建議如下先看同一個(gè)輸入是不是真的“同一個(gè)輸入”。比較字節(jié)級 hash。再看 prompt 模板版本是否被改動(dòng)或者變量注入順序是否發(fā)生變化。再看請求參數(shù)確認(rèn)模型名、溫度、seed、max_tokens 沒有在運(yùn)行期間被覆蓋。再看模型服務(wù)端側(cè)本地模型確認(rèn)權(quán)重文件和推理框架版本云 API 確認(rèn)模型是否被配置成自動(dòng)升級。最后看重試邏輯確認(rèn)上一次超時(shí)或失敗后重試是否帶上了額外上下文或新參數(shù)。這里可以配合一張快速判斷表現(xiàn)象優(yōu)先排查可能原因同一條輸入偶爾結(jié)果不同請求參數(shù)、服務(wù)端版本溫度非 0、seed 未生效、模型版本漂移批量跑到后期逐漸變慢或報(bào)錯(cuò)上下文長度、并發(fā)數(shù)、限流輸入沒截?cái)?、請求堆積JSON 解析偶發(fā)失敗輸出格式約束、校驗(yàn)器模型返回了額外解釋文字換環(huán)境后結(jié)果大面積變化依賴版本、Python 或 Node 版本、GPU 驅(qū)動(dòng)推理框架不一致結(jié)果文件里順序錯(cuò)亂任務(wù) ID 關(guān)聯(lián)、寫入邏輯并發(fā)返回順序被當(dāng)作業(yè)務(wù)順序新增字段后舊數(shù)據(jù)和新數(shù)據(jù)對不上prompt 模板版本、schema 版本沒有做版本遷移如果你看到錯(cuò)誤消息里有類似request timed out或schema相關(guān)的報(bào)錯(cuò)先不要急著認(rèn)為是模型能力不足。第一個(gè)動(dòng)作是查請求日志看超時(shí)發(fā)生在哪個(gè)階段schema 校驗(yàn)到底因?yàn)槟膫€(gè)字段失敗。很多時(shí)候這類報(bào)錯(cuò)是輸入格式不符合預(yù)期或者重試時(shí)沒有帶上原始校驗(yàn)錯(cuò)誤。把錯(cuò)誤信息喂回給模型是一種常見補(bǔ)救方法但喂回去之前一定要先做長度控制否則可能把模型上下文撐爆。7. 不必強(qiáng)求完全一致把確定性留給真正需要的地方7.1 需要高確定性的典型場景如果你正在做下面這些事確定性控制應(yīng)該寫進(jìn)需求而不是事后補(bǔ)救批量文檔字段抽取合同、票據(jù)、日志的結(jié)構(gòu)化落庫自動(dòng)化測試中的預(yù)期結(jié)果比對合規(guī)審計(jì)場景中的處理留痕知識庫入庫前的切分和元數(shù)據(jù)提取需要重復(fù)實(shí)驗(yàn)的算法評測這些場景的共同點(diǎn)是模型輸出不是終點(diǎn)而是要進(jìn)入數(shù)據(jù)庫、報(bào)表或下游系統(tǒng)。輸出不穩(wěn)定會造成臟數(shù)據(jù)而臟數(shù)據(jù)比慢數(shù)據(jù)更難處理。7.2 更適合保留多樣性的場景對應(yīng)地這些場景不要過度固定聊天機(jī)器人回復(fù)風(fēng)格營銷文案生成頭腦風(fēng)暴和探索性問答推薦標(biāo)題或摘要的候選生成教學(xué)場景中的多角度解釋如果做聊天助手時(shí)也把溫度強(qiáng)行調(diào)成 0用戶會發(fā)現(xiàn)每次問同一句話得到近乎相同的回答體驗(yàn)反而生硬。這類系統(tǒng)應(yīng)該把人放在校驗(yàn)環(huán)節(jié)用產(chǎn)品邏輯規(guī)避質(zhì)量波動(dòng)而不是靠壓制隨機(jī)性。7.3 我的最終建議開發(fā)更確定性的 LLM pipelines不是一個(gè)開關(guān)能解決的問題。它是一套工程習(xí)慣輸入要規(guī)范化參數(shù)要記錄模板要版本化輸出要校驗(yàn)失敗要可重試模型升級要對比。我更建議先把單條任務(wù)跑穩(wěn)再改造成批量任務(wù)先保證 100 條數(shù)據(jù)可復(fù)現(xiàn)再加入 Agent 或工具調(diào)用先做輸出 schema 約束再去追求緩存和調(diào)度優(yōu)化。踩過幾次之后會發(fā)現(xiàn)很多問題不是模型能力不夠而是管道設(shè)計(jì)沒有把“不確定性”當(dāng)作頭等變量來對待。一旦你把模型當(dāng)作管道里一個(gè)可能犯錯(cuò)、需要約束的外部組件穩(wěn)定性就會慢慢長出來。