據(jù)清洗到RAG:AI工程從零實(shí)戰(zhàn)學(xué)習(xí)路徑)
別人問我在做什么我說(shuō)在做AI工程他們會(huì)先入為主地覺得我天天在訓(xùn)練模型、調(diào)參數(shù)。做了一整年之后我想說(shuō)真正的AI工程大多數(shù)時(shí)間其實(shí)都在跟數(shù)據(jù)、接口、異常處理較勁模型反而是最好搞定的一環(huán)。這篇內(nèi)容就是從我自己的經(jīng)歷出發(fā)聊聊一條“from scratch”的成長(zhǎng)路徑以及一個(gè)完全沒有AI基礎(chǔ)的人到底該怎么一步步把AI工程這件事跑通。這里的“from scratch”不是說(shuō)非要自己從零手寫神經(jīng)網(wǎng)絡(luò)的每一層而是指不依賴現(xiàn)成的“保姆級(jí)平臺(tái)”——真正把數(shù)據(jù)、模型、訓(xùn)練、部署、調(diào)優(yōu)全鏈路理解一遍親手打通一套最小可用的系統(tǒng)。很多新人上來(lái)就想挑戰(zhàn)大規(guī)模語(yǔ)言模型微調(diào)或者多模態(tài)系統(tǒng)結(jié)果被環(huán)境配置、數(shù)據(jù)清洗、顯存溢出三座大山直接勸退。我的建議是換一條路走先把小模型的小任務(wù)做熟再一步步擴(kuò)展到LLM應(yīng)用。這篇文章適合幾類人剛?cè)腴T想做AI應(yīng)用開發(fā)的學(xué)生工作中需要把算法落地成服務(wù)的工程師以及想轉(zhuǎn)型做AI產(chǎn)品但不知道從哪下手的朋友。我會(huì)盡量把每一步的“為什么”也講清楚而不只是給一段可以抄的代碼。1. 從“調(diào)包俠”到AI工程一條反常識(shí)的學(xué)習(xí)路徑1.1 為什么說(shuō)“從零開始”不等于“從算法開始”多數(shù)人學(xué)習(xí)AI的第一反應(yīng)是找一本深度學(xué)習(xí)教材從頭啃反向傳播、注意力機(jī)制啃完再去看PyTorch文檔。這條路不是不對(duì)而是它對(duì)一個(gè)目標(biāo)是“做工程”的人來(lái)說(shuō)效率實(shí)在太低了。工程視角的“從零開始”應(yīng)該是你想讓一個(gè)系統(tǒng)完成什么任務(wù)把這個(gè)任務(wù)拆成數(shù)據(jù)、模型、服務(wù)、評(píng)估四塊然后逐塊打通。算法細(xì)節(jié)可以在用到的時(shí)候再補(bǔ)而不是在最開始就把自己淹沒在數(shù)學(xué)公式里。我見過太多人卡在“我覺得我還沒學(xué)好理論不敢動(dòng)手寫代碼”這個(gè)心態(tài)上。實(shí)際上AI工程是典型的“先跑通再優(yōu)化”的領(lǐng)域你第一次寫的訓(xùn)練循環(huán)哪怕很丑、哪怕只在玩具數(shù)據(jù)集上能跑它給你的正反饋也比啃三章教材強(qiáng)得多。先建立完整的工程閉環(huán)感再回去補(bǔ)理論你會(huì)突然發(fā)現(xiàn)很多公式都有了落地的意義。1.2 以終為始先看清一條生產(chǎn)級(jí)流水線長(zhǎng)什么樣如果不清楚終點(diǎn)長(zhǎng)什么樣很容易在中間走彎路。一條哪怕是初級(jí)的AI工程流水線通常都包含下面這些環(huán)節(jié)數(shù)據(jù)獲取與清洗原始數(shù)據(jù)永遠(yuǎn)臟到你難以想象格式不統(tǒng)一、缺字段、標(biāo)簽錯(cuò)誤、編碼亂這些才是日常。特征工程與預(yù)處理文本要分詞、過濾停用詞圖片要縮放、歸一化表格數(shù)據(jù)要處理缺失值。這塊往往比模型本身更影響最終效果。模型選擇與基線先有一個(gè)最簡(jiǎn)單的模型跑通流程拿到一個(gè)不漂亮的基線分?jǐn)?shù)再去換更復(fù)雜的模型。訓(xùn)練與驗(yàn)證訓(xùn)練循環(huán)、驗(yàn)證集、早停、超參調(diào)整保證模型不僅能跑還能穩(wěn)定收斂。部署與服務(wù)化把模型封裝成接口處理并發(fā)、限流、日志、異常。監(jiān)控與迭代上線之后觀察真實(shí)數(shù)據(jù)分布漂移定期評(píng)估持續(xù)迭代。一個(gè)人做全流程可能聽起來(lái)很嚇人但關(guān)鍵是先把這個(gè)閉環(huán)跑起來(lái)哪怕每個(gè)環(huán)節(jié)都簡(jiǎn)陋一點(diǎn)。我自己的第一個(gè)項(xiàng)目只用了一千條數(shù)據(jù)、一個(gè)非常小的文本分類模型但完整經(jīng)歷了清洗、訓(xùn)練到部署的過程之后再看任何AI系統(tǒng)腦子里都會(huì)自動(dòng)浮現(xiàn)出它處在流水線的哪一環(huán)。1.3 我現(xiàn)在推薦的學(xué)習(xí)順序如果讓我重新走一遍我會(huì)這樣安排學(xué)習(xí)路徑保證每個(gè)階段都有看得見的產(chǎn)出Python基礎(chǔ)與數(shù)據(jù)處理不用等到精通能熟練用pandas、numpy處理表格和文本就夠了一周時(shí)間足夠。機(jī)器學(xué)習(xí)最小閉環(huán)用scikit-learn跑一個(gè)分類/回歸任務(wù)理解訓(xùn)練集、驗(yàn)證集、準(zhǔn)確率這些核心概念。深度學(xué)習(xí)框架入門選PyTorch也可以選JAX但生態(tài)還是PyTorch最順跑通一個(gè)圖像或文本的小模型訓(xùn)練循環(huán)。部署最小實(shí)踐用FastAPI封裝模型在本地跑通接口調(diào)用理解前后端是怎么連起來(lái)的。再回頭補(bǔ)理論這時(shí)候再讀關(guān)于損失函數(shù)、反向傳播、Transformer架構(gòu)的資料你會(huì)覺得每一段都似曾相識(shí)且恍然大悟。最后再上LLM與RAG有前面完整的工程直覺再接觸Prompt設(shè)計(jì)、向量檢索、大模型API就不會(huì)覺得它們是玄學(xué)。這套順序的最大特點(diǎn)是每?jī)芍芏寄苡谐晒皇菍W(xué)了三個(gè)月還在“打基礎(chǔ)”。信心這個(gè)東西在工程入門階段比智商重要多了。2. 最小可行項(xiàng)目用文本分類跑通第一條訓(xùn)練循環(huán)2.1 項(xiàng)目設(shè)定為什么選文本分類當(dāng)?shù)谝粋€(gè)項(xiàng)目文本分類幾乎是AI工程入門的最佳項(xiàng)目形態(tài)。它不需要處理圖像那樣復(fù)雜的數(shù)據(jù)增強(qiáng)也不需要像生成模型那樣擔(dān)心輸出的多樣性任務(wù)定義清晰評(píng)估指標(biāo)直觀而且數(shù)據(jù)集很容易通過公開渠道獲取。我第一次完成的項(xiàng)目是“客服工單自動(dòng)分類”把客服收到的用戶反饋分成“賬單問題”“網(wǎng)絡(luò)故障”“設(shè)備維修”“其他”四類。數(shù)據(jù)集不過幾百條Excel表格都是人工標(biāo)注過的歷史工單。這個(gè)項(xiàng)目看似簡(jiǎn)單但它強(qiáng)行讓我把整條流水線的每個(gè)環(huán)節(jié)都親手做了一遍從CSV里各種奇怪的換行符到模型上線后被人投訴分類不準(zhǔn)每一步都是真實(shí)世界的毒打。為了讀者復(fù)現(xiàn)方便我建議你找一個(gè)公開的數(shù)據(jù)集比如新聞分類、情感分析都可以。重要的是數(shù)據(jù)量不要太大幾千條就夠因?yàn)槟愕哪繕?biāo)不是刷精度是打通流程。2.2 數(shù)據(jù)準(zhǔn)備先別碰模型至少花一半時(shí)間在數(shù)據(jù)上多數(shù)新手最容易犯的錯(cuò)誤是急著把數(shù)據(jù)丟給模型結(jié)果發(fā)現(xiàn)訓(xùn)練出來(lái)的東西根本不能用然后又不知道為什么。我可以負(fù)責(zé)任地說(shuō)AI工程里70%的坑都出在數(shù)據(jù)處理階段而不是模型階段。以我當(dāng)時(shí)的工單數(shù)據(jù)為例里面的真實(shí)情況包括同一類問題有七八種不同的描述方式、有的字段里混入了HTML標(biāo)簽、有些標(biāo)簽明顯標(biāo)錯(cuò)了、還有大量空值和重復(fù)記錄。我想在真實(shí)工單數(shù)據(jù)集上訓(xùn)練出一個(gè)能用的模型第一步就是做數(shù)據(jù)探索。import pandas as pd df pd.read_csv(tickets.csv) print(df.head()) print(df.info()) print(df[category].value_counts()) # 清洗基本問題去空白、去重、統(tǒng)一文本大小寫 df[text] df[text].astype(str).str.strip() df[text] df[text].replace(r\s, , regexTrue) df df.dropna(subset[category]) df df.drop_duplicates(subset[text])清洗完之后我還干了件很關(guān)鍵的事把類別不均衡的情況列出來(lái)看看?!熬W(wǎng)絡(luò)故障”的樣本可能是“設(shè)備維修”的五倍如果不處理模型學(xué)到的就是“永遠(yuǎn)猜網(wǎng)絡(luò)故障”。最簡(jiǎn)單的處理方式是過采樣少數(shù)類或者給損失函數(shù)加類別權(quán)重后者的實(shí)現(xiàn)成本更低。我在處理時(shí)用了class_weightbalanced這個(gè)參數(shù)讓少數(shù)類樣本在計(jì)算損失時(shí)獲得更大的權(quán)重這個(gè)操作直接讓“設(shè)備維修”這一類的F1分?jǐn)?shù)提高了十幾個(gè)百分點(diǎn)。建議你也在自己的項(xiàng)目里試一下你會(huì)很明顯地看到分類報(bào)告里各類指標(biāo)的變化。2.3 訓(xùn)練循環(huán)的骨架與其復(fù)制粘貼不如手打一遍在第一次跑通訓(xùn)練循環(huán)時(shí)我強(qiáng)烈建議你不要直接復(fù)制現(xiàn)成的完整代碼而是自己動(dòng)手敲一遍邏輯。原因很簡(jiǎn)單訓(xùn)練循環(huán)里的每一步都對(duì)應(yīng)著概念手敲一遍之后你才能從“背代碼”進(jìn)步到“理解它在干什么”。一個(gè)標(biāo)準(zhǔn)的PyTorch訓(xùn)練循環(huán)骨架是這樣的import torch import torch.nn as nn from torch.utils.data import DataLoader, Dataset class TicketDataset(Dataset): def __init__(self, texts, labels): self.texts texts self.labels labels def __len__(self): return len(self.texts) def __getitem__(self, idx): return self.texts[idx], self.labels[idx] # 模型不復(fù)雜Embedding LSTM 全連接 class SimpleClassifier(nn.Module): def __init__(self, vocab_size, emb_dim, num_classes): super().__init__() self.embedding nn.Embedding(vocab_size, emb_dim) self.lstm nn.LSTM(emb_dim, hidden_size64, batch_firstTrue) self.fc nn.Linear(64, num_classes) def forward(self, x): embedded self.embedding(x) output, (hidden, cell) self.lstm(embedded) last_hidden hidden[-1] return self.fc(last_hidden) # 訓(xùn)練 model SimpleClassifier(vocab_sizevocab_size, emb_dim64, num_classes4) optimizer torch.optim.Adam(model.parameters(), lr1e-3) loss_fn nn.CrossEntropyLoss() for epoch in range(10): total_loss 0 for batch_texts, batch_labels in DataLoader(train_dataset, batch_size32, shuffleTrue): optimizer.zero_grad() logits model(batch_texts) loss loss_fn(logits, batch_labels) loss.backward() optimizer.step() total_loss loss.item() print(fEpoch {epoch1}, loss: {total_loss})這個(gè)循環(huán)有四個(gè)關(guān)鍵動(dòng)作缺一環(huán)都不行zero_grad()清掉上一輪的梯度f(wàn)orward前向傳播拿到輸出backward()計(jì)算梯度step()用梯度更新參數(shù)。新手最容易忘掉zero_grad()后果就是梯度在批次之間不斷累加loss曲線飄忽不定。在這個(gè)項(xiàng)目里我用的詞表是自己基于訓(xùn)練數(shù)據(jù)構(gòu)建的。簡(jiǎn)單做法是給每個(gè)出現(xiàn)過的詞編一個(gè)索引加一個(gè)UNK給沒見過的詞。值得注意的一點(diǎn)是驗(yàn)證集里如果出現(xiàn)詞表外的詞就映射到UNK不要跳過樣本否則你的dataloader會(huì)在評(píng)估時(shí)報(bào)錯(cuò)。2.4 驗(yàn)證與過擬合準(zhǔn)確率不是唯一的答案訓(xùn)練跑通之后我的第一個(gè)模型在訓(xùn)練集上準(zhǔn)確率高達(dá)98%當(dāng)時(shí)還挺興奮結(jié)果放到驗(yàn)證集上一看只有72%。這就是典型的過擬合模型把訓(xùn)練樣本背下來(lái)了而不是學(xué)到了模式。應(yīng)對(duì)過擬合的思路按見效速度排增加訓(xùn)練數(shù)據(jù)哪怕做一個(gè)簡(jiǎn)單的數(shù)據(jù)增強(qiáng)。減小模型容量比如把LSTM的hidden_size從64降到32。加Dropout或L2正則化。用早停監(jiān)控驗(yàn)證集loss連續(xù)若干輪不下降就停。我當(dāng)時(shí)加了nn.Dropout(0.3)并采用早停驗(yàn)證集準(zhǔn)確率穩(wěn)定在了85%左右。這個(gè)數(shù)字并不驚艷但對(duì)一個(gè)小模型來(lái)說(shuō)已經(jīng)是能上線服務(wù)的水平了。還有一個(gè)新手容易忽略的點(diǎn)只盯著準(zhǔn)確率會(huì)被坑。因?yàn)闃颖静痪鈺r(shí)模型全部預(yù)測(cè)多數(shù)類也能拿到很高的準(zhǔn)確率。我當(dāng)時(shí)就是看了分類報(bào)告才發(fā)現(xiàn)“設(shè)備維修”的召回率是0。正確的做法是看每個(gè)類別的精確率、召回率和F1分?jǐn)?shù)對(duì)自己模型的效果有一個(gè)更精確的把握。3. RAG工作流從玩具到可用系統(tǒng)的關(guān)鍵轉(zhuǎn)折3.1 什么時(shí)候該從“訓(xùn)練小模型”升級(jí)到“LLM應(yīng)用”當(dāng)你已經(jīng)能在自己的小模型上完成訓(xùn)練、評(píng)估和部署的閉環(huán)再面對(duì)“我要做一個(gè)客服問答機(jī)器人”這類需求你很快會(huì)發(fā)現(xiàn)傳統(tǒng)小模型的天花板——它能分類但沒法生成內(nèi)容、沒法理解沒見過的問法更沒法回答需要外部知識(shí)的問題。這時(shí)候就該接觸到LLM。很多人以為用LLM就是從OpenAI或開源模型調(diào)用API然后把Prompt一寫就結(jié)束了。實(shí)際上把一個(gè)LLM從“聊天玩具”變成“可用的業(yè)務(wù)系統(tǒng)”關(guān)鍵要看兩點(diǎn)私有知識(shí)怎么接入以及輸出質(zhì)量怎么保持穩(wěn)定。這就很自然地引出了RAG檢索增強(qiáng)生成。3.2 RAG的基本流程檢索拼接生成RAG的思路一句話講清楚不要指望大模型記住所有知識(shí)而是讓它在回答之前先從知識(shí)庫(kù)里檢索到相關(guān)文本然后把這些文本連同用戶問題一起喂給模型生成答案。它的好處是知識(shí)庫(kù)可以隨時(shí)更新不需要重新訓(xùn)練模型模型的回答有依據(jù)不容易隨意編造而且你可以追溯它用的哪一篇資料便于處理“胡說(shuō)八道”的爭(zhēng)議。我在實(shí)踐中搭RAG用的流程非常簡(jiǎn)單把文檔比如產(chǎn)品的FAQ、操作手冊(cè)切成片段一般按段落或固定長(zhǎng)度切片。用Embedding模型把每個(gè)片段轉(zhuǎn)成一個(gè)向量存進(jìn)向量數(shù)據(jù)庫(kù)。用戶提問時(shí)把問題也轉(zhuǎn)成向量在庫(kù)里做相似度檢索取Top 5相關(guān)片段。把這些片段和用戶問題拼成一個(gè)Prompt交給LLM生成答案。用代碼實(shí)現(xiàn)的話核心部分大概長(zhǎng)這樣from sentence_transformers import SentenceTransformer import numpy as np # 加載Embedding模型這里選的是小巧易用的開源模型 encoder SentenceTransformer(BAAI/bge-small-zh-v1.5) # 文檔切片與向量化 chunks split_documents(documents, chunk_size300) chunk_vectors encoder.encode(chunks) # 檢索把問題和所有片段向量的余弦相似度算一遍取最高分的幾個(gè) def retrieve(query, top_k5): query_vec encoder.encode([query])[0] scores cosine_similarity(query_vec, chunk_vectors) top_indices np.argsort(scores)[::-1][:top_k] return [chunks[i] for i in top_indices]如果是當(dāng)作演示直接用numpy算相似度就夠用。但真的要上到幾十萬(wàn)條文檔、還要做并發(fā)檢索的級(jí)別就該換用專門的向量數(shù)據(jù)庫(kù)在工程上更靠譜不然檢索性能會(huì)成為瓶頸。3.3 檢索質(zhì)量比模型大小更重要一個(gè)常被忽視的真相在RAG系統(tǒng)里很多人把注意力全放在“用哪個(gè)生成模型”上結(jié)果忽略了一個(gè)更關(guān)鍵的問題檢索結(jié)果到底準(zhǔn)不準(zhǔn)。如果檢索回來(lái)的5個(gè)片段里有3個(gè)是無(wú)關(guān)內(nèi)容再?gòu)?qiáng)的生成模型也只能被帶到溝里去。我自己驗(yàn)證過一個(gè)很典型的案例同一個(gè)客服知識(shí)庫(kù)換掉檢索策略之后回答的正確率提升了將近30%。提升點(diǎn)主要來(lái)自三處切片策略把固定長(zhǎng)度切片改成按語(yǔ)義段落切片避免一句話被切成兩半。檢索數(shù)量原來(lái)只取Top 3改為Top 5并讓模型在Prompt里注明“如果下文沒有相關(guān)信息請(qǐng)直接說(shuō)不清楚”。查詢改寫把一次檢索改成針對(duì)用戶問題關(guān)鍵詞的兩次檢索合并結(jié)果去重。這里的關(guān)鍵認(rèn)知是RAG系統(tǒng)的質(zhì)量天花板主要由檢索環(huán)節(jié)決定而不是由生成模型決定。模型再聰明檢索的內(nèi)容不對(duì)它也只能巧婦難為無(wú)米之炊。在這個(gè)階段你還需要建立“評(píng)估”的閉環(huán)。我當(dāng)時(shí)的做法是維護(hù)一個(gè)固化的問答測(cè)試集每次改完檢索策略都跑一遍這個(gè)測(cè)試集統(tǒng)計(jì)答案中“包含正確知識(shí)片段”的比例寧可慢一點(diǎn)也要保證每一次改動(dòng)帶來(lái)的都是正向效果。4. 部署與推理優(yōu)化模型能用和好用是兩碼事4.1 模型包裝成服務(wù)FastAPI是最順手的解法模型訓(xùn)練好只是萬(wàn)里長(zhǎng)征第一步在生產(chǎn)環(huán)境里別人只能通過接口來(lái)使用它。無(wú)論前面做得多么出色沒有穩(wěn)定的服務(wù)一切都等于零。所以要把“模型能用”變成“模型好用”部署服務(wù)這一環(huán)我強(qiáng)烈推薦用FastAPI異步、輕量又省事而且自帶接口文檔。from fastapi import FastAPI from pydantic import BaseModel app FastAPI() model load_model() class Query(BaseModel): text: str class Response(BaseModel): label: str score: float app.post(/predict) def predict(query: Query): label, score model.predict(query.text) return Response(labellabel, scorescore)如果只是在本地跑一跑你可能體會(huì)不到服務(wù)化的意義。但一旦要對(duì)接前端、給別的同事調(diào)用、讓腳本批量處理數(shù)據(jù)這個(gè)接口就是整個(gè)系統(tǒng)的門面。我當(dāng)時(shí)第一次部署完用curl試了一下接口能返回正確結(jié)果那種感覺和“模型在Jupyter Notebook里跑出準(zhǔn)確率”是完全不同的——前者意味著真的被使用了。部署環(huán)節(jié)有四個(gè)最容易出問題的點(diǎn)提前處理能省下大量時(shí)間模型預(yù)熱服務(wù)啟動(dòng)后先跑一次推理避免第一個(gè)真實(shí)請(qǐng)求因?yàn)槟P蛻屑虞d而超時(shí)。輸入校驗(yàn)用Pydantic做好類型與字段校驗(yàn)別讓臟數(shù)據(jù)跑到模型內(nèi)部才報(bào)錯(cuò)。異常隔離把模型推理包在try/except里返回友好的錯(cuò)誤信息而不是直接把堆棧拋給調(diào)用方。版本管理模型文件和數(shù)據(jù)預(yù)處理方式一起記錄版本否則有一天改了數(shù)據(jù)預(yù)處理邏輯線上模型效果突然變了你都不知道為什么。4.2 量化、批處理與并發(fā)把成本打下來(lái)的實(shí)踐調(diào)好接口之后你可能很快發(fā)現(xiàn)模型推理有點(diǎn)慢尤其是當(dāng)模型體積稍大、請(qǐng)求量一上來(lái)一次推理幾百毫秒就會(huì)成為瓶頸。優(yōu)化推理性能的常見手段主要包括模型量化把浮點(diǎn)權(quán)重從FP32壓到FP16甚至INT8速度能提升兩三倍效果損失很多時(shí)候在可接受范圍內(nèi)。批處理把多個(gè)請(qǐng)求合并成一個(gè)批次推理能用滿GPU并行能力。這在文本分類這類小模型上尤其明顯批量推理吞吐量能提升一個(gè)量級(jí)。緩存對(duì)同樣的輸入直接返回緩存結(jié)果適合重復(fù)提問比例高的場(chǎng)景。我在文本分類那個(gè)項(xiàng)目里試用過最簡(jiǎn)單的PyTorch量化代碼很短import torch model torch.quantization.quantize_dynamic( model, {torch.nn.Linear}, dtypetorch.qint8 )模型體積縮小近四倍推理延遲從13ms下降到5ms準(zhǔn)確率幾乎沒有變化。這個(gè)優(yōu)化對(duì)一套小服務(wù)來(lái)說(shuō)收益非常明顯。有一點(diǎn)要提醒量化不是、也不應(yīng)該是無(wú)腦開啟的。如果是復(fù)雜的生成模型量化后可能出現(xiàn)輸出質(zhì)量下降尤其是代碼生成、數(shù)學(xué)推理這類任務(wù)非常明顯。所以量化前后一定要保留評(píng)估集用數(shù)字對(duì)比說(shuō)話而不是感覺“差不多就行”。4.3 上線后的監(jiān)控與回歸測(cè)試沒有評(píng)估就沒有迭代模型一旦上線事情并不會(huì)就此結(jié)束。真實(shí)用戶的數(shù)據(jù)分布和你的訓(xùn)練數(shù)據(jù)很可能不一樣用戶提問的方式、術(shù)語(yǔ)、長(zhǎng)度都可能漂移。所以從上線第一天起就要記錄兩樣?xùn)|西輸入日志和預(yù)測(cè)日志。我當(dāng)時(shí)在日志里發(fā)現(xiàn)了一個(gè)特別有意思的現(xiàn)象很多用戶會(huì)發(fā)超長(zhǎng)文本而模型在訓(xùn)練時(shí)基本沒見過超過200字的樣本。于是把所有超過最大長(zhǎng)度的輸入做了截?cái)嗵幚砟P烷_始逐步輸出合理結(jié)果。這種問題如果不在線上持續(xù)觀察靠離線評(píng)估是根本發(fā)現(xiàn)不了的。另外一個(gè)工程上的好習(xí)慣是準(zhǔn)備一套回歸測(cè)試集。每次更新模型或者調(diào)整預(yù)處理邏輯都先跑一遍回歸測(cè)試集確保老的正常功能沒被自己改壞。我第一次更新模型時(shí)就沒跑回歸測(cè)試改完亂整了熱詞表結(jié)果之前好好的中性評(píng)論被分到負(fù)面類別還被產(chǎn)品同事發(fā)現(xiàn)了。這碗毒打嘗過一次就夠了之后我再也沒跳過回歸測(cè)試。5. 半年踩坑實(shí)錄數(shù)據(jù)泄漏、顯存溢出與不穩(wěn)定的Prompt5.1 數(shù)據(jù)泄漏準(zhǔn)確率虛高還會(huì)被當(dāng)成榮耀數(shù)據(jù)泄漏是AI工程里最隱蔽也最致命的錯(cuò)誤之一。它的本質(zhì)是模型在訓(xùn)練時(shí)“偷看”了本該在測(cè)試時(shí)才能見到的信息導(dǎo)致訓(xùn)練時(shí)看起來(lái)很強(qiáng)真實(shí)場(chǎng)景里卻一碰就碎。我在做文本分類時(shí)遇到過兩個(gè)例子。第一個(gè)是把文本做TF-IDF向量化的時(shí)候?qū)φ麄€(gè)數(shù)據(jù)集先f(wàn)it再拆分訓(xùn)練集和測(cè)試集實(shí)際上測(cè)試集的信息已經(jīng)滲進(jìn)了訓(xùn)練過程模型表現(xiàn)虛高得離譜。正確的做法是先拆分?jǐn)?shù)據(jù)再單獨(dú)對(duì)訓(xùn)練集fit、對(duì)測(cè)試集transform。第二個(gè)例子是數(shù)據(jù)去重時(shí)把同一用戶的多條工單一起處理結(jié)果同屬一個(gè)用戶的樣本同時(shí)出現(xiàn)在訓(xùn)練和測(cè)試集合里模型記住的是用戶特征而不是內(nèi)容特征。如果你發(fā)現(xiàn)模型在訓(xùn)練集上表現(xiàn)極好、驗(yàn)證集上也不錯(cuò)但一上線就崩建議第一時(shí)間排查數(shù)據(jù)預(yù)處理過程里有沒有“全局統(tǒng)計(jì)量”被提前計(jì)算了。5.2 顯存溢出從無(wú)腦換大卡到優(yōu)雅地用盡每一字節(jié)早年在沒有顯存概念的時(shí)候我拿顯卡跑模型動(dòng)不動(dòng)就“CUDA out of memory”。我的第一反應(yīng)是提高顯卡配置但后來(lái)發(fā)現(xiàn)問題沒那么簡(jiǎn)單顯存溢出其實(shí)是有很多優(yōu)雅應(yīng)對(duì)方式的。主要方案有幾個(gè)減小batch size、序列截?cái)?、梯度累積、使用混合精度訓(xùn)練。很多人不知道“梯度累積”是怎么解決的思路其實(shí)原理是如果batch size32會(huì)爆顯存那就先用batch size8每4步才更新一次參數(shù)效果在數(shù)學(xué)上和一次看32個(gè)樣本基本等價(jià)。accumulation_steps 4 for i, batch in enumerate(DataLoader(ds, batch_size8)): loss model(batch) loss loss / accumulation_steps # 歸一化 loss.backward() if (i 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()混合精度訓(xùn)練則是用torch.cuda.amp把前向計(jì)算的一部分放到FP16顯存占用能降低不少同時(shí)保留FP32的精度用于梯度計(jì)算。這套組合技對(duì)付顯存不足的問題比直接換顯卡更務(wù)實(shí)。5.3 LLM輸出不穩(wěn)定Prompt工程要有工程化的紀(jì)律大模型輸出的隨機(jī)性是個(gè)繞不開的問題。同樣一個(gè)問題加熱度參數(shù)調(diào)成0.8它可能給出三種不同風(fēng)格的回答這讓“穩(wěn)定輸出”成了工程上必須解決的事情。我的基本做法是把生成參數(shù)固定下來(lái)包括temperature、top_p、max_tokens等不讓它們隨業(yè)務(wù)狀態(tài)隨意改動(dòng)。另一個(gè)更關(guān)鍵的問題是讓模型輸出結(jié)構(gòu)化內(nèi)容。需要模型返回JSON時(shí)我會(huì)在Prompt里要求它只輸出JSON并在代碼里加入解析失敗時(shí)重試一次或降級(jí)返回默認(rèn)值的邏輯。import json, re def safe_json_parse(raw_output): # 有些模型會(huì)輸出Markdown代碼塊包裹的JSON需要?jiǎng)兊?cleaned re.sub(rjson|, , raw_output).strip() try: return json.loads(cleaned) except json.JSONDecodeError: return {error: parse_failed, raw: raw_output}Prompt的調(diào)試也要有工程化的方式。我自己維護(hù)了一個(gè)測(cè)試Prompt的清單每次修改系統(tǒng)提示詞都會(huì)用同一組問題跑一遍對(duì)比輸出質(zhì)量而不是憑感覺看著“這次回答好像更好”。只有把Prompt當(dāng)成代碼一樣去管理、去測(cè)試大模型應(yīng)用才算真正進(jìn)入了“工程”的范疇。在整個(gè)學(xué)習(xí)路徑里最容易讓人放棄的時(shí)刻不是第一次見到復(fù)雜公式也不是環(huán)境配置反復(fù)報(bào)錯(cuò)而是同時(shí)面對(duì)數(shù)據(jù)臟、顯存爆、模型效果差、接口沒人用的多重打擊。我的經(jīng)驗(yàn)是每次只拆解一個(gè)小問題解決之后再往前推進(jìn)一小步半年之后回頭看你已經(jīng)走完了最艱難的那一截。把第一個(gè)小項(xiàng)目完整跑通比收藏一百份學(xué)習(xí)資料都有用。