進化:Sidecar架構(gòu)與LoRA后訓(xùn)練實戰(zhàn))
很多人把大模型跑在本地覺得“能離線推理、不傳數(shù)據(jù)出去”就算成功了。但我一直覺得這還不夠如果模型部署之后就不再更新那它只是一個會說話的靜態(tài)詞典。模型的價值在于它能不能跟上你的知識變化、術(shù)語變化和使用習(xí)慣。為了折騰這個問題我給自己維護的這套本地模型迭代流水線起了個代號叫 YoungAi核心思路就兩條用 Sidecar 把推理服務(wù)和訓(xùn)練流水線拆開用后訓(xùn)練讓模型能持續(xù)吸收新知識。這篇文章把我這段時間的完整實驗記錄整理出來包括架構(gòu)取舍、數(shù)據(jù)閉環(huán)、訓(xùn)練參數(shù)和踩坑修復(fù)給已經(jīng)在本地部署過模型、想讓模型越用越聰明的朋友一條可以直接照搬的路徑。1. 為什么本地模型也需要“持續(xù)進化”而不只是一次性微調(diào)1.1 本地部署的真正優(yōu)勢不是“跑得動”而是“留得住”聊本地部署的時候大家關(guān)心最多的永遠是顯存、速度、模型尺寸仿佛只要能把模型塞進顯卡就是勝利。但這些討論忽略了一個更本質(zhì)的問題本地和云端最大的區(qū)別到底是什么我的答案是本地部署讓數(shù)據(jù)不需要離開你的硬盤這比推理速度值錢得多。企業(yè)內(nèi)部的知識庫、個人的筆記和偏好、行業(yè)里的黑話和術(shù)語這些數(shù)據(jù)在合規(guī)和隱私的限制下根本不適合傳到云端 API。你想讓模型真正理解這些內(nèi)容就必須在本地給模型“補課”。并不是模型本身不夠聰明而是它還不知道你所在領(lǐng)域的上下文。比如一個模型可以寫出漂亮的財務(wù)分析但如果沒見過你們公司的報銷流程和項目代號它就很難回答員工的具體提問。問題是大多數(shù)人補課的方式是“一次性微調(diào)”整理一批語料訓(xùn)完部署然后忘掉。這種做法的問題在于知識是變化的。你上個月整理的內(nèi)部流程文檔這個月就改版了用戶反復(fù)問的問題可能集中在你完全沒預(yù)料到的方向。如果訓(xùn)練是一條只做一次的死流程模型就會在部署的那一刻開始變得過時。換句話說部署到本地這個動作本身只是起點。模型能不能持續(xù)跟上環(huán)境的變化才是本地 AI 真正要解決的難題。我給這個難題的答案是把“訓(xùn)練”從一次性行為變成一條自動運轉(zhuǎn)的基礎(chǔ)設(shè)施。這也是 YoungAi 這個項目存在的理由。1.2 “一次性微調(diào)”為什么撐不起持續(xù)變化的需求舉個實際例子你就明白了。假設(shè)你維護一個面向內(nèi)部員工的問答機器人模型剛上線時效果不錯但三個月后業(yè)務(wù)系統(tǒng)升級舊文檔全部作廢模型還在按舊邏輯回答。這時候你想更新它常規(guī)做法是重新整理語料、重新微調(diào)、重新部署。這個流程要幾個小時甚至幾天期間模型一直處于“錯誤狀態(tài)”。更麻煩的是如果你三個月才更新一次中間積累的大量真實交互反饋——用戶覺得哪些回答有用、哪些回答被打回重寫——就全部浪費了。這些反饋其實是訓(xùn)練數(shù)據(jù)里最值錢的部分因為它們反映的是真實需求而不是你腦補出來的需求。你辛辛苦苦手寫一百條標準問答可能都不如用戶真實問過的五十個問題更貼合業(yè)務(wù)。所以要支持“持續(xù)進化”第一步不是選什么模型而是改掉“一次訓(xùn)練定終身”的心態(tài)。你的目標應(yīng)該是讓每一次有價值的交互都能以較低的成本沉淀回模型里。這個沉淀過程如果靠人肉跑腳本肯定堅持不下來你很快就會被重復(fù)勞動耗盡耐心。于是就有了自動化架構(gòu)的需求。這就是后面要說的 Sidecar 和后訓(xùn)練流水線。2. 用 Sidecar 把“推理服務(wù)”和“訓(xùn)練流水線”拆開的設(shè)計邏輯2.1 Sidecar 模式從云原生搬到本地模型到底拆了什么Sidecar 這個詞來自云原生里的“邊車”模式主容器旁邊掛一個輔助容器幫主容器完成日志收集、監(jiān)控上報、網(wǎng)絡(luò)代理這些旁路任務(wù)主容器自己保持干凈。我把這個思路搬到本地 AI 場景基于一個很樸素的擔(dān)心推理進程和訓(xùn)練任務(wù)混在一起會互相傷害。推理服務(wù)要求穩(wěn)定、低延遲、隨時在線訓(xùn)練任務(wù)則相反它吃滿顯存、間歇性運行、運行過程中的失敗和抖動都很正常。如果這兩件事放在同一個進程里訓(xùn)練一跑推理就卡訓(xùn)練崩了推理也跟著死。這就像讓同一個廚師既站在前臺招呼客人又跑去后廚顛勺兩頭都做不好。所以我讓架構(gòu)上分成兩個角色一個主服務(wù)負責(zé)模型推理只處理 generate 和對話補全一個 Sidecar 進程負責(zé)訓(xùn)練流水線監(jiān)聽數(shù)據(jù)、清洗樣本、啟動后訓(xùn)練、評估模型、發(fā)布新權(quán)重。Sidecar 掛了不影響主服務(wù)繼續(xù)回答問題主服務(wù)升級也不影響 Sidecar 里正在跑的訓(xùn)練。這個拆分帶來的另一個好處是升級路徑清晰。推理主服務(wù)更新時不需要把整個訓(xùn)練流水線一起重跑訓(xùn)練邏輯調(diào)整時也不需要重啟常駐的推理服務(wù)。兩個進程各自迭代故障域完全隔離這對于個人維護的項目來說省掉了大量“改一處崩全局”的痛苦。2.2 一條數(shù)據(jù)從進來到變成權(quán)重的完整路徑Y(jié)oungAi 的數(shù)據(jù)流我可以用一張文字圖說清楚它不算復(fù)雜但每一步都有自己的角色推理主服務(wù)每次處理用戶請求會把交互日志寫到本地數(shù)據(jù)目錄Sidecar 進程定時或按事件監(jiān)聽這個目錄讀取新產(chǎn)生的交互日志Sidecar 對日志做清洗去重剔除壞樣本組裝成標準訓(xùn)練格式當(dāng)有效樣本累積到設(shè)定閾值Sidecar 啟動一輪后訓(xùn)練任務(wù)先用低秩適配訓(xùn)練產(chǎn)出增量權(quán)重任務(wù)完成后進入離線評估用預(yù)留的評測集打分通過門禁的版本被發(fā)布到模型倉庫未通過的自動丟棄Sidecar 通知推理主服務(wù)重新加載新權(quán)重新請求開始使用新版本。這條鏈路里最容易被忽略的是最后一步推理主服務(wù)要支持熱重載。很多本地推理引擎默認啟動時一次性加載權(quán)重不會自己去看磁盤上的新文件。所以我會在主服務(wù)里留一個 reload 端點Sidecar 在發(fā)布新版本后主動調(diào)用它。如果 reload 失敗主服務(wù)繼續(xù)用舊權(quán)重用戶無感知。部署上我建議把兩個服務(wù)分別跑成獨立進程用 systemd 或者容器編排管理。它們共享兩個目錄日志交換目錄和模型權(quán)重目錄。這樣設(shè)計之后Sidecar 即使整晚訓(xùn)練推理服務(wù)的延遲也一點不受影響。對于大多數(shù)自托管場景這個方案比在一個框架里硬塞所有功能要可靠得多。2.3 為什么本地場景不需要引入消息中間件一開始我習(xí)慣性地想在上游加一個消息隊列把交互日志推過去。后來發(fā)現(xiàn)在單人或者小團隊維護的本地場景這套東西純粹是給自己添負擔(dān)。本地數(shù)據(jù)量不大直接用文件目錄加輪詢就能滿足需求引入隊列還得多維護一個組件數(shù)據(jù)鏈路長了反而更難排查。設(shè)計原則也要跟著場景走單機本地場景的 Sidecar 要的是簡單和穩(wěn)定不是分布式擴展。只要日志目錄寫入和模型倉庫發(fā)布這兩個環(huán)節(jié)做好原子性保護整條流水線已經(jīng)足夠可靠。這也算是我在設(shè)計過程中做過的一個減法減掉了一堆不必要的外部依賴。3. 后訓(xùn)練不是一句口號LoRA、增量預(yù)訓(xùn)練與偏好對齊怎么落地3.1 為什么選 LoRA / QLoRA 而不是全參微調(diào)想讓模型持續(xù)吸收新知識第一個要解決的問題是本地顯存扛不住全參微調(diào)。一個 7B 量級的模型全參微調(diào)需要保存優(yōu)化器狀態(tài)和完整梯度通常要 8 張以上大顯存顯卡才舒服個人開發(fā)者手里通常只有一兩張顯卡這條路根本走不通。LoRA 的思路很直接不修改原來的大權(quán)重矩陣而是在權(quán)重旁邊掛兩個小矩陣用它們的乘積來模擬權(quán)重的變化。模型推理時原始權(quán)重 W 和低秩增量 BA 一起參與計算訓(xùn)練時只更新 B 和 A 這兩個小矩陣。由于矩陣的秩 r 壓得比隱藏維度小一個數(shù)量級可訓(xùn)練參數(shù)往往只有全模型的 0.1%~1%顯存和訓(xùn)練時間都能降一個量級。QLoRA 再把底座參數(shù)量化到 4bit進一步壓縮基座模型占用的顯存。這樣在 24GB 顯存的單卡上訓(xùn) 7B 模型是可行的。實測跑一輪一兩千條樣本的指令微調(diào)普通消費級顯卡也能在幾小時內(nèi)完成。全參微調(diào)每一步都要動整個模型本地進化這件事根本做不起來低秩適配才讓“隔幾天訓(xùn)一次”變得現(xiàn)實。為什么不把 adapter 合并回一個大模型文件合并之后推理時少一步低秩計算速度略快部署也更簡單不合并則可以在同一個基座上掛多個能力適配器按場景切換。我自己的選擇是發(fā)布到對外服務(wù)用的版本會合并成一個完整權(quán)重但訓(xùn)練產(chǎn)出的 adapter 文件也會存檔。這樣既有部署便利又保留了多個能力包。3.2 三條訓(xùn)練支路分別在什么時候用后訓(xùn)練不是一個筐什么都能裝。YoungAi 里我主要走三條支路按目標選用。第一條是監(jiān)督微調(diào) SFT。它的作用是讓模型學(xué)會“怎么回答”適合整理出高質(zhì)量的問答對把回答格式、語氣、內(nèi)部規(guī)范灌進模型。數(shù)據(jù)格式通常是 system / user / assistant 三段式對話樣本。比如你希望模型在回答前先說明“根據(jù)最新文檔”或者讓它在不確定時直接說“我不確定”這些規(guī)范都能通過 SFT 灌進去。第二條是增量預(yù)訓(xùn)練。它針對的是“模型不知道的新知識”——專業(yè)術(shù)語、最新文檔、內(nèi)部產(chǎn)品介紹。做法是把領(lǐng)域語料喂給模型讓它繼續(xù)學(xué)習(xí)文本里的知識分布。注意增量預(yù)訓(xùn)練不是為了學(xué)會對話格式而是為了擴充詞匯和知識所以語料要盡量貼近真實業(yè)務(wù)文本而不是問答對。第三條是偏好對齊比如用 DPO 這類不需要單獨訓(xùn)練獎勵模型的方法。它的輸入是“好回答/差回答”這樣的成對樣本。用戶對某條回答點了贊但沒有手動修改可以當(dāng)成 chosen被用戶明確改寫或吐槽的可以作為 rejected。DPO 的訓(xùn)練目標是提高模型輸出好答案的概率更適合利用交互反饋這類信號。三種手段的適用場景我用一個對比表歸納訓(xùn)練方式解決什么問題典型數(shù)據(jù)訓(xùn)練成本SFT對話風(fēng)格、回答結(jié)構(gòu)、指令遵循手工整理的問答對低增量預(yù)訓(xùn)練注入新的領(lǐng)域知識與術(shù)語最新文檔、知識庫文本中DPO對齊用戶偏好減少糟糕回答成對的好/差回答樣本低訓(xùn)練超參上我建議以小步快跑為主學(xué)習(xí)率 1e-4 到 5e-5訓(xùn)練 1 到 3 個 epoch上下文長度根據(jù)實際數(shù)據(jù)設(shè) 2048 或 4096。單輪樣本控制在 500 到 2000 條之間比攢一堆數(shù)據(jù)一口氣訓(xùn)練更可控。工具鏈用常見開源大模型訓(xùn)練棧就行不需要自己造輪子。3.3 一份可以被直接復(fù)用的訓(xùn)練數(shù)據(jù)樣例給一個實用的數(shù)據(jù)格式方便直接上手{instruction: 用一句話解釋什么是增量預(yù)訓(xùn)練。, output: 增量預(yù)訓(xùn)練是在已有模型基礎(chǔ)上繼續(xù)學(xué)習(xí)新領(lǐng)域文本以擴充知識和術(shù)語覆蓋。, category: concept}實際訓(xùn)練時很多訓(xùn)練框架更習(xí)慣 chat template 格式把 system、user、assistant 的輪次組織好。我的經(jīng)驗是多輪對話必須帶上下文不要只給最后一輪否則模型在真實使用中會忘記歷史會話。你可以在每條樣本里把前兩三輪對話作為前綴保留。4. “進化”的閉環(huán)從交互日志到新模型權(quán)重的完整流水線4.1 訓(xùn)練數(shù)據(jù)從哪里來把交互反饋當(dāng)成礦脈持續(xù)進化的前提是持續(xù)有新數(shù)據(jù)。YoungAi 采集數(shù)據(jù)主要有幾個來源用戶與模型的真實對話日志用戶對回答點贊、點踩、改寫后的最終答案知識庫和幫助文檔的更新內(nèi)容人工整理的高質(zhì)量問題。但原始日志不能直接拿來訓(xùn)練。一個最典型的坑是用戶對某條回答不滿意改寫了一版更好的答案這個改寫后的版本應(yīng)該作為訓(xùn)練樣本但如果采集邏輯沒做對齊把用戶抱怨的話也當(dāng)成了 assistant 回復(fù)模型就會學(xué)到非常奇怪的東西。所以清洗是必須的。我用的清洗流程包含四步格式過濾把過短、亂碼、純 URL 的樣本扔掉精確和模糊去重避免大量重復(fù)樣本讓模型反復(fù)背同一句話噪聲檢測剔除與業(yè)務(wù)無關(guān)的閑聊質(zhì)量打分用一些啟發(fā)式規(guī)則加模型打分給每條樣本一個質(zhì)量分低于閾值的不用。一句話總結(jié)處理原則訓(xùn)練數(shù)據(jù)的價值密度比數(shù)量重要得多100 條干凈的高質(zhì)量樣本勝過 1000 條噪聲數(shù)據(jù)。這條原則我是在踩了幾個坑之后才真正認同的后面會專門說那個“復(fù)讀機”事故。4.2 評估門禁和版本回滾不能讓模型越進化越退化持續(xù)進化最怕什么怕模型學(xué)完新數(shù)據(jù)舊能力崩了更怕訓(xùn)練集太偏練完之后模型看起來 loss 很低實際業(yè)務(wù)反而更差。所以我給流水線加了一道評估門禁。做法是每周從真實交互中留出約 300 條新樣本單獨保存不進訓(xùn)練集。每次訓(xùn)練完Sidecar 先在這套評估集上跑一遍和當(dāng)前線上版本對比。指標可以看正確率、回答長度、拒答率、和參考答案的語義相似度也可以混入幾個固定的人工檢查點。只有綜合分不低于當(dāng)前版本新版本才允許發(fā)布否則自動回滾保留舊權(quán)重。對應(yīng)的模型倉庫我按版本目錄管理models/ base_7b/ train_v010/ merged/ lora/ manifest.json train_v011/ merged/ lora/ manifest.json current - train_v011current 是一個符號鏈接指向當(dāng)前線上版本。Sidecar 發(fā)布新版本時不會直接覆蓋正在使用的文件目錄而是先寫一個新目錄全部寫完、校驗通過后再切換符號鏈接并觸發(fā)主服務(wù) reload。這樣即使 reload 前主服務(wù)還在讀取也不會有半份權(quán)重文件的問題。熱重載之后我還會讓主服務(wù)跑一次固定的健康檢查請求比如問幾個固定 prompt對比輸出簽名。輸出預(yù)期的版本號的才認為發(fā)布成功。這一步雖然簡單但能避免“訓(xùn)練成功但加載的其實還是舊文件”這種烏龍。5. 實測中踩過的坑災(zāi)難性遺忘、樣本污染、熱切換失靈5.1 災(zāi)難性遺忘比想象中來得快我第一次嘗試持續(xù)進化時喂了大約 2000 條內(nèi)部問答結(jié)果模型對新領(lǐng)域問題答得像模像樣但日常閑聊和通用知識明顯變笨了甚至偶爾回答格式都變形。復(fù)盤下來原因很清晰訓(xùn)練樣本太偏全是新領(lǐng)域數(shù)據(jù)學(xué)習(xí)率給得偏高訓(xùn)練輪數(shù)也偏多。解法是給訓(xùn)練集加入 10%~20% 的歷史通用樣本做“重放”學(xué)習(xí)率降到 5e-5 左右epoch 不超過 3。另外不要試圖一輪吃成胖子把一次大訓(xùn)練拆成多次小訓(xùn)練每次只針對最近累積的幾百條有效樣本反而更穩(wěn)。這個調(diào)整之后災(zāi)難性遺忘基本被控制住了。注意重放樣本不是隨便找一些通用 QA 就行最好是從過去幾輪訓(xùn)練數(shù)據(jù)里均勻抽樣這樣能覆蓋模型已經(jīng)學(xué)會的舊知識防止新的權(quán)重覆蓋舊能力。5.2 樣本污染模型學(xué)會“復(fù)讀機”的教訓(xùn)有段時間我的模型出現(xiàn)一種很迷惑的行為同一個問題問兩次第一次回答還行第二次開始復(fù)讀第一次的答案。查了很久才發(fā)現(xiàn)訓(xùn)練數(shù)據(jù)里有大量重復(fù)問題而且有些樣本把“用戶改寫后的最終答案”錯誤地拼進 assistant 輪次導(dǎo)致模型把“重復(fù)”當(dāng)成了規(guī)律。這個坑給我的教訓(xùn)是訓(xùn)練數(shù)據(jù)必須做會話輪次對齊和去重。去重不能只看完全相同的字符串還要用相似度計算把語義重復(fù)的樣本也去掉。清洗腳本里我會給每條樣本計算一個內(nèi)容哈希再用 embedding 距離做近重復(fù)檢測超過相似度閾值的直接丟棄。更重要的是一句提醒不要迷信“數(shù)據(jù)越多越好”。如果自動化流水線把用戶產(chǎn)生的壞回答也當(dāng)成正確答案采集進來模型會穩(wěn)定地學(xué)會那些壞回答。數(shù)據(jù)來源的質(zhì)量信號比清洗算法本身更能決定訓(xùn)練效果。我現(xiàn)在寧可把采集門檻調(diào)高一點只吸收有明確正向反饋的樣本。5.3 熱切換不生效與權(quán)重文件競爭有個問題我調(diào)試了一整個下午訓(xùn)練完成新版權(quán)重也發(fā)布了推理服務(wù) reload 成功但回答內(nèi)容沒有任何變化。后來發(fā)現(xiàn)是推理進程把模型權(quán)重映射到內(nèi)存后不會去重新讀取磁盤的符號鏈接目標我修改的是鏈接指向的目標進程卻還在用舊的緩存文件。解決辦法是發(fā)布新版本時使用全新的目錄名并且在 reload 接口里顯式指定新的模型路徑不依賴符號鏈接原地更新reload 完成后必須跑一輪健康檢查。另一種常見問題是訓(xùn)練還在往模型目錄里寫權(quán)重推理進程就已經(jīng)開始加載導(dǎo)致讀到一半的文件。所以寫入過程要先寫臨時目錄全部寫完再做原子切換這是必須養(yǎng)成的習(xí)慣。5.4 訓(xùn)練和推理爭搶 GPU本地場景往往只有一塊 GPU。訓(xùn)練任務(wù)一旦跑起來推理服務(wù)立刻變慢經(jīng)常 OOM。我的解決辦法是給訓(xùn)練任務(wù)加了一個簡單的調(diào)度策略只在推理服務(wù)低峰時段啟動訓(xùn)練比如深夜如果訓(xùn)練過程中推理服務(wù)檢測到顯存吃緊也可以主動暫停訓(xùn)練任務(wù)等空閑時再恢復(fù)。這個機制不需要很復(fù)雜一個鎖文件加一個定時檢查就夠用了。整條流水線跑順之后我最大的體會是不要把持續(xù)進化想成一次兩次微調(diào)它本質(zhì)上是把訓(xùn)練變成和推理一樣平常的基礎(chǔ)設(shè)施。YoungAi 到現(xiàn)在已經(jīng)自動迭代了好幾輪每次只進步一點點但累計起來的變化非??捎^。如果再給我一次機會我會更早地引入評估門禁——自動訓(xùn)練的每一步都值得被檢驗。最后再分享一個實用建議在你開始做全自動閉環(huán)之前先把數(shù)據(jù)采集、訓(xùn)練、發(fā)布、回滾這四個環(huán)節(jié)手工跑通一遍。確認每一步都有重啟能力和人工介入手段再把它們自動化串聯(lián)。任何自動進化系統(tǒng)都要留一個緊急停止開關(guān)這可能是整個設(shè)計里最重要的一句話。