盤流程從49分鐘到43秒:并行批量分層優(yōu)化實(shí)戰(zhàn))
1. 從49分鐘到43秒一次AI復(fù)盤流程的極限提速實(shí)錄49分鐘這是某次AI復(fù)盤任務(wù)跑完一輪的耗時(shí)。43秒這是優(yōu)化之后同樣的任務(wù)跑完一輪的耗時(shí)。中間差了68倍。這不是什么實(shí)驗(yàn)室里的理想數(shù)據(jù)是我在一個(gè)實(shí)際項(xiàng)目里被逼出來(lái)的結(jié)果。當(dāng)時(shí)的情況很簡(jiǎn)單團(tuán)隊(duì)每天要用AI做一次復(fù)盤分析輸入是當(dāng)天積累的對(duì)話日志、操作記錄和反饋數(shù)據(jù)輸出是一份結(jié)構(gòu)化的復(fù)盤報(bào)告。聽起來(lái)不復(fù)雜但第一次跑的時(shí)候我看著進(jìn)度條從下午兩點(diǎn)一直轉(zhuǎn)到了兩點(diǎn)四十九分中間還差點(diǎn)以為程序卡死了。這個(gè)復(fù)盤流程的核心邏輯其實(shí)不復(fù)雜把原始數(shù)據(jù)清洗一遍按維度切分逐段送給模型做摘要和歸因分析最后匯總成報(bào)告。問(wèn)題出在“逐段”這兩個(gè)字上。原始數(shù)據(jù)有幾千條記錄每條都要單獨(dú)調(diào)用一次模型接口串行執(zhí)行每次調(diào)用平均耗時(shí)幾百毫秒到一兩秒不等累積起來(lái)就是幾十分鐘。更麻煩的是有些記錄特別長(zhǎng)模型處理起來(lái)更慢偶爾還會(huì)超時(shí)重試時(shí)間就這么被一點(diǎn)點(diǎn)吃掉了。我后來(lái)做的事情本質(zhì)上就是把“串行”改成“并行”把“逐條”改成“批量”把“全量”改成“分層”。聽起來(lái)像是老生常談的優(yōu)化套路但真正落地的時(shí)候每一步都有坑。比如并行度開太高會(huì)被接口限流批量太大模型會(huì)丟細(xì)節(jié)分層策略設(shè)計(jì)不好會(huì)導(dǎo)致復(fù)盤質(zhì)量下降。這些都不是拍腦袋能解決的得靠實(shí)測(cè)數(shù)據(jù)一點(diǎn)點(diǎn)調(diào)。這篇文章適合兩類人看一類是正在做類似AI批處理任務(wù)、被耗時(shí)問(wèn)題折磨的開發(fā)者另一類是對(duì)性能優(yōu)化感興趣、想看看真實(shí)項(xiàng)目里怎么一步步把時(shí)間壓下來(lái)的技術(shù)愛(ài)好者。我會(huì)把整個(gè)優(yōu)化過(guò)程拆開講包括每一步的思考邏輯、具體參數(shù)怎么定、踩過(guò)哪些坑、最后怎么穩(wěn)定在43秒。代碼層面會(huì)給關(guān)鍵片段但更重要的是背后的決策依據(jù)——畢竟參數(shù)是死的場(chǎng)景是活的。2. 復(fù)盤流程的原始架構(gòu)與瓶頸定位2.1 最初的串行方案是怎么跑的原始方案用Python寫的核心就是一個(gè)for循環(huán)。偽代碼大概長(zhǎng)這樣results [] for record in all_records: cleaned clean_data(record) summary call_llm(cleaned) # 單條調(diào)用模型 results.append(summary) report aggregate(results)這個(gè)方案的好處是邏輯簡(jiǎn)單、調(diào)試方便每條記錄的處理結(jié)果都能單獨(dú)看到出了問(wèn)題容易定位。但壞處也很明顯假設(shè)有3000條記錄每條平均處理時(shí)間0.8秒加上網(wǎng)絡(luò)抖動(dòng)和偶爾的重試總耗時(shí)輕松突破40分鐘。實(shí)測(cè)下來(lái)49分鐘里有大約35分鐘花在模型調(diào)用上10分鐘花在數(shù)據(jù)清洗和格式轉(zhuǎn)換上剩下4分鐘是匯總和報(bào)告生成。數(shù)據(jù)清洗這塊也有問(wèn)題。原始日志格式不統(tǒng)一有的是JSON有的是純文本還有的是帶特殊分隔符的混合格式。清洗函數(shù)寫得比較粗暴用了大量的正則匹配和字符串替換單條處理雖然只要幾毫秒但3000條累積起來(lái)也有十幾秒。更關(guān)鍵的是清洗和模型調(diào)用是串行的清洗完一條才能調(diào)一條CPU和網(wǎng)絡(luò)資源完全沒(méi)有重疊利用。2.2 瓶頸到底在哪里一次 profiling 的發(fā)現(xiàn)我沒(méi)有憑感覺(jué)猜瓶頸而是用cProfile跑了一遍完整流程把每個(gè)函數(shù)的耗時(shí)都打出來(lái)。結(jié)果很清晰環(huán)節(jié)耗時(shí)占比單條平均耗時(shí)備注數(shù)據(jù)清洗20%約3ms正則匹配為主模型調(diào)用72%約800ms含網(wǎng)絡(luò)往返結(jié)果匯總6%約1ms純內(nèi)存操作報(bào)告生成2%一次性約1分鐘模型調(diào)用是大頭占了七成以上。而且這800ms里真正花在模型推理上的時(shí)間可能只有200-300ms剩下的是網(wǎng)絡(luò)傳輸、排隊(duì)等待和接口開銷。這意味著單純優(yōu)化模型本身意義不大得從調(diào)用方式上動(dòng)手。另一個(gè)發(fā)現(xiàn)是數(shù)據(jù)清洗雖然單條快但它是純CPU操作而模型調(diào)用是IO密集型操作。兩者串行執(zhí)行等于CPU等網(wǎng)絡(luò)、網(wǎng)絡(luò)等CPU資源利用率很低。如果能把清洗和調(diào)用重疊起來(lái)理論上能省下不少時(shí)間。2.3 為什么不能簡(jiǎn)單粗暴地加線程第一反應(yīng)肯定是上多線程。Python的GIL雖然限制CPU密集型任務(wù)的并行但模型調(diào)用是IO密集型GIL在IO等待時(shí)會(huì)釋放所以多線程理論上有效。我試了一版開了10個(gè)線程結(jié)果耗時(shí)從49分鐘降到了12分鐘左右有提升但遠(yuǎn)不夠。問(wèn)題出在兩個(gè)地方一是接口有速率限制10個(gè)線程同時(shí)打過(guò)去很快就被限流觸發(fā)退避重試反而拖慢了整體進(jìn)度二是有些記錄特別長(zhǎng)單條處理時(shí)間可能是普通記錄的5-10倍線程池里如果恰好幾個(gè)長(zhǎng)任務(wù)擠在一起短任務(wù)就得等著負(fù)載不均衡。這說(shuō)明光加并發(fā)不夠還得控制并發(fā)節(jié)奏、做任務(wù)分片、處理長(zhǎng)尾任務(wù)。后面我會(huì)詳細(xì)講怎么解決這些問(wèn)題。3. 核心優(yōu)化策略從串行到并行再到分層3.1 第一步把清洗和調(diào)用拆成流水線最開始的改動(dòng)是把數(shù)據(jù)清洗和模型調(diào)用解耦。具體做法是先用一個(gè)線程池把所有記錄清洗完放到一個(gè)隊(duì)列里然后用另一個(gè)線程池從隊(duì)列里取數(shù)據(jù)去調(diào)模型。這樣清洗和調(diào)用可以重疊執(zhí)行CPU和網(wǎng)絡(luò)資源都能跑滿。from concurrent.futures import ThreadPoolExecutor from queue import Queue cleaned_queue Queue(maxsize500) def clean_worker(records): for r in records: cleaned_queue.put(clean_data(r)) def call_worker(): while True: item cleaned_queue.get() if item is None: break result call_llm(item) results.append(result) cleaned_queue.task_done()這個(gè)改動(dòng)本身只省了大概3-4分鐘因?yàn)榍逑凑急炔桓?。但它為后面的并行調(diào)用打下了基礎(chǔ)——清洗和調(diào)用解耦之后調(diào)用端的線程數(shù)可以獨(dú)立調(diào)整不受清洗速度制約。注意隊(duì)列的maxsize要設(shè)一個(gè)合理值。設(shè)太小會(huì)導(dǎo)致清洗線程頻繁阻塞設(shè)太大會(huì)占用大量?jī)?nèi)存。我實(shí)測(cè)下來(lái)500左右比較平衡具體取決于單條數(shù)據(jù)的大小。3.2 第二步批量調(diào)用替代逐條調(diào)用逐條調(diào)用模型的最大浪費(fèi)在于每次都要建立連接、傳輸請(qǐng)求頭、等待響應(yīng)。如果能把多條記錄合并成一個(gè)請(qǐng)求均攤下來(lái)單條成本會(huì)大幅下降。我試了三種批量策略第一種是固定批量比如每50條打一個(gè)包。實(shí)現(xiàn)簡(jiǎn)單但遇到長(zhǎng)記錄時(shí)容易超時(shí)。第二種是動(dòng)態(tài)批量根據(jù)記錄的總token數(shù)來(lái)打包保證每個(gè)包不超過(guò)模型的最大輸入限制。第三種是分層批量先按記錄長(zhǎng)度分桶短記錄大批量、長(zhǎng)記錄小批量。最后我選了動(dòng)態(tài)批量核心邏輯是維護(hù)一個(gè)token計(jì)數(shù)器累加到閾值就發(fā)車。閾值怎么定我實(shí)測(cè)了從1000到8000的不同值結(jié)果如下批量token閾值平均單包耗時(shí)總耗時(shí)失敗率10001.2s18min0.1%20001.8s12min0.3%40003.1s8min0.8%60004.5s7min2.1%80006.2s6.5min5.3%閾值越大總耗時(shí)越低但失敗率也越高。失敗主要來(lái)自超時(shí)和模型輸出截?cái)?。綜合考慮我選了4000作為默認(rèn)閾值失敗率控制在1%以內(nèi)總耗時(shí)降到8分鐘左右。對(duì)于失敗的任務(wù)單獨(dú)走重試隊(duì)列不影響主流程。3.3 第三步分層處理把長(zhǎng)尾任務(wù)單獨(dú)拎出來(lái)批量調(diào)用之后大部分任務(wù)都很快但總有那么幾條“釘子戶”——特別長(zhǎng)的記錄或者格式特別復(fù)雜的日志處理時(shí)間是普通記錄的十幾倍。這些長(zhǎng)尾任務(wù)如果混在批量里會(huì)拖慢整個(gè)批次。我的做法是在清洗階段就給每條記錄打一個(gè)“預(yù)估耗時(shí)”標(biāo)簽依據(jù)是字符數(shù)、嵌套深度和特殊符號(hào)數(shù)量。預(yù)估耗時(shí)超過(guò)閾值的記錄不走批量通道而是進(jìn)入一個(gè)單獨(dú)的“慢速通道”用更小的批量、更長(zhǎng)的超時(shí)時(shí)間來(lái)處理。這樣快慢分離互不干擾。def estimate_cost(record): length_score len(record) / 1000 depth_score record.count({) record.count([) special_score len(re.findall(r[^\w\s], record)) / 100 return length_score depth_score * 0.5 special_score * 0.3 FAST_THRESHOLD 5.0 fast_records [r for r in records if estimate_cost(r) FAST_THRESHOLD] slow_records [r for r in records if estimate_cost(r) FAST_THRESHOLD]實(shí)測(cè)下來(lái)慢速通道的記錄占比不到5%但如果不分離它們會(huì)消耗掉30%以上的總時(shí)間。分離之后快速通道可以放心用大批量和高并發(fā)慢速通道則用保守參數(shù)整體效率提升明顯。3.4 第四步并發(fā)控制與自適應(yīng)限流并發(fā)數(shù)不是越高越好。我一開始設(shè)了20個(gè)并發(fā)結(jié)果接口頻繁返回限流錯(cuò)誤重試機(jī)制反而讓總耗時(shí)飆升。后來(lái)改成自適應(yīng)限流初始并發(fā)設(shè)為5每成功完成一批就加1遇到限流就減半并暫停一小段時(shí)間。class AdaptiveLimiter: def __init__(self, init5, max_concurrent20): self.current init self.max max_concurrent self.lock threading.Lock() def on_success(self): with self.lock: self.current min(self.current 1, self.max) def on_rate_limit(self): with self.lock: self.current max(self.current // 2, 1) time.sleep(1)這套機(jī)制跑下來(lái)并發(fā)數(shù)穩(wěn)定在12-15之間既沒(méi)有觸發(fā)限流也沒(méi)有浪費(fèi)空閑窗口。比固定并發(fā)數(shù)省了大概2-3分鐘更重要的是穩(wěn)定性大幅提升不會(huì)因?yàn)橥话l(fā)限流導(dǎo)致整個(gè)流程卡住。4. 實(shí)操落地43秒版本的完整實(shí)現(xiàn)4.1 整體架構(gòu)與數(shù)據(jù)流最終版本的架構(gòu)分四層采集層、清洗層、調(diào)度層、執(zhí)行層。采集層負(fù)責(zé)從各個(gè)來(lái)源拉取原始數(shù)據(jù)清洗層做格式統(tǒng)一和預(yù)處理調(diào)度層負(fù)責(zé)分片、打標(biāo)和排隊(duì)執(zhí)行層負(fù)責(zé)實(shí)際調(diào)用模型和收集結(jié)果。數(shù)據(jù)流是這樣的原始數(shù)據(jù)先進(jìn)入清洗隊(duì)列清洗完成后根據(jù)預(yù)估耗時(shí)打上fast或slow標(biāo)簽分別進(jìn)入兩個(gè)不同的調(diào)度隊(duì)列。調(diào)度器從隊(duì)列里取數(shù)據(jù)按動(dòng)態(tài)批量策略打包交給執(zhí)行層的線程池去調(diào)用。執(zhí)行層的結(jié)果寫回一個(gè)共享的結(jié)果緩沖區(qū)最后由匯總模塊統(tǒng)一生成報(bào)告。整個(gè)流程用了一個(gè)主控線程來(lái)協(xié)調(diào)各個(gè)子模塊通過(guò)隊(duì)列和事件通信沒(méi)有用復(fù)雜的框架就是Python標(biāo)準(zhǔn)庫(kù)的threading和queue。這樣做的好處是依賴少、啟動(dòng)快、調(diào)試方便。4.2 關(guān)鍵參數(shù)的計(jì)算與選擇參數(shù)選擇這塊我做了不少實(shí)驗(yàn)這里把核心參數(shù)的推導(dǎo)過(guò)程列出來(lái)。批量token閾值模型的最大輸入是8192個(gè)token但實(shí)際使用時(shí)不能頂滿因?yàn)檩敵鲆残枰臻g。我留了大約一半的余量所以輸入閾值定在4000。這個(gè)值也經(jīng)過(guò)了實(shí)測(cè)驗(yàn)證前面表格里4000對(duì)應(yīng)的失敗率是0.8%在可接受范圍內(nèi)。并發(fā)數(shù)上限接口的速率限制是每分鐘60次請(qǐng)求。如果每個(gè)請(qǐng)求平均耗時(shí)3秒那么理論上最多支持3個(gè)并發(fā)60/203因?yàn)槊糠昼娒總€(gè)并發(fā)能發(fā)20次請(qǐng)求。但實(shí)際請(qǐng)求耗時(shí)波動(dòng)很大所以我把上限設(shè)到20靠自適應(yīng)機(jī)制來(lái)動(dòng)態(tài)調(diào)整。實(shí)測(cè)穩(wěn)定在12-15之間沒(méi)有觸發(fā)限流。重試次數(shù)與退避策略失敗請(qǐng)求最多重試3次退避時(shí)間按指數(shù)增長(zhǎng)1秒、2秒、4秒。超過(guò)3次就標(biāo)記為失敗寫入錯(cuò)誤日志不阻塞主流程。實(shí)測(cè)下來(lái)99%的失敗請(qǐng)求在第一次重試就成功了。隊(duì)列大小清洗隊(duì)列設(shè)500快速通道隊(duì)列設(shè)1000慢速通道隊(duì)列設(shè)100。這些值是根據(jù)內(nèi)存占用和吞吐量平衡出來(lái)的。隊(duì)列太大占內(nèi)存太小會(huì)導(dǎo)致生產(chǎn)者阻塞。4.3 代碼實(shí)現(xiàn)核心調(diào)度器調(diào)度器是整個(gè)流程的心臟負(fù)責(zé)從隊(duì)列取數(shù)據(jù)、打包、分發(fā)。核心邏輯如下import threading import queue import time class Scheduler: def __init__(self, batch_token_limit4000, max_workers15): self.batch_limit batch_token_limit self.max_workers max_workers self.fast_queue queue.Queue(maxsize1000) self.slow_queue queue.Queue(maxsize100) self.results [] self.lock threading.Lock() self.limiter AdaptiveLimiter() def pack_batch(self, q): batch [] token_count 0 while token_count self.batch_limit: try: item q.get(timeout0.1) except queue.Empty: break batch.append(item) token_count item[token_estimate] return batch def worker(self, q): while True: batch self.pack_batch(q) if not batch: if q.empty(): break continue try: result call_llm_batch(batch) with self.lock: self.results.extend(result) self.limiter.on_success() except RateLimitError: self.limiter.on_rate_limit() for item in batch: q.put(item) # 重新入隊(duì) except Exception as e: log_error(batch, e) def run(self): threads [] for _ in range(self.max_workers): t threading.Thread(targetself.worker, args(self.fast_queue,)) t.start() threads.append(t) # 慢速通道用更少的線程 for _ in range(3): t threading.Thread(targetself.worker, args(self.slow_queue,)) t.start() threads.append(t) for t in threads: t.join()這段代碼的關(guān)鍵點(diǎn)在于批量打包時(shí)用了超時(shí)機(jī)制避免隊(duì)列空時(shí)死等限流時(shí)把整批數(shù)據(jù)重新入隊(duì)保證不丟數(shù)據(jù)快慢通道用不同的線程數(shù)快速通道15個(gè)線程慢速通道3個(gè)。4.4 實(shí)測(cè)數(shù)據(jù)與對(duì)比優(yōu)化前后的對(duì)比如下指標(biāo)優(yōu)化前優(yōu)化后變化總耗時(shí)49分鐘43秒下降98.5%模型調(diào)用次數(shù)3000次約80次下降97.3%平均單條處理時(shí)間980ms14ms下降98.6%失敗率0.5%0.8%略升內(nèi)存峰值約200MB約450MB上升內(nèi)存上升是因?yàn)榕看虬完?duì)列緩存占用了更多空間但450MB在現(xiàn)代機(jī)器上完全可以接受。失敗率略升是因?yàn)榕空{(diào)用時(shí)單條失敗會(huì)影響整批但通過(guò)重試機(jī)制基本都能恢復(fù)。43秒這個(gè)數(shù)字是跑了10次取的平均值最快的一次38秒最慢的一次51秒。波動(dòng)主要來(lái)自網(wǎng)絡(luò)狀況和模型服務(wù)的負(fù)載。相比之前的49分鐘這個(gè)穩(wěn)定性已經(jīng)好太多了。5. 踩過(guò)的坑與排查技巧5.1 批量調(diào)用導(dǎo)致輸出截?cái)嘧钤缬么笈康臅r(shí)候發(fā)現(xiàn)有些批次的輸出不完整模型只返回了前幾條的結(jié)果后面的丟了。排查后發(fā)現(xiàn)是輸出token超限了——輸入批量太大模型生成結(jié)果時(shí)空間不夠被截?cái)嗔?。解決辦法是在批量打包時(shí)同時(shí)估算輸入和輸出的token需求。輸出token的估算比較粗糙按輸入token的30%來(lái)算。如果輸入輸出超過(guò)模型上限就減小批量。這個(gè)調(diào)整之后截?cái)鄦?wèn)題基本消失了。實(shí)操心得不要只看輸入限制輸出限制同樣重要。很多模型文檔只寫了最大輸入長(zhǎng)度但實(shí)際使用時(shí)輸入輸出共享同一個(gè)上限。5.2 線程池死鎖有一次跑著跑著程序卡住了所有線程都在等待CPU占用為零。用py-spy dump了線程棧之后發(fā)現(xiàn)清洗線程在往隊(duì)列里put數(shù)據(jù)但隊(duì)列滿了在阻塞等待而調(diào)用線程在等待清洗線程產(chǎn)出數(shù)據(jù)但自己又因?yàn)槟硞€(gè)異常退出了沒(méi)有繼續(xù)消費(fèi)隊(duì)列。典型的死鎖。修復(fù)方法是給隊(duì)列的put操作加超時(shí)超時(shí)后記錄日志并跳過(guò)而不是無(wú)限等待。同時(shí)給調(diào)用線程加了異常捕獲確保即使出錯(cuò)也會(huì)繼續(xù)消費(fèi)隊(duì)列。try: cleaned_queue.put(item, timeout5) except queue.Full: log_warning(Queue full, dropping item)5.3 限流誤判與重試風(fēng)暴自適應(yīng)限流剛上線的時(shí)候遇到過(guò)一次“重試風(fēng)暴”某個(gè)批次因?yàn)榫W(wǎng)絡(luò)抖動(dòng)失敗了觸發(fā)重試重試的請(qǐng)求又因?yàn)椴l(fā)太高被限流限流后降低并發(fā)但已經(jīng)發(fā)出的請(qǐng)求還在重試導(dǎo)致大量重復(fù)請(qǐng)求堆積。解決辦法是給重試加一個(gè)全局令牌桶限制單位時(shí)間內(nèi)的重試次數(shù)。同時(shí)重試隊(duì)列和正常隊(duì)列分開重試請(qǐng)求優(yōu)先級(jí)更低避免擠占正常請(qǐng)求的資源。5.4 常見問(wèn)題速查表問(wèn)題現(xiàn)象可能原因排查方法解決方案總耗時(shí)突然變長(zhǎng)接口限流或網(wǎng)絡(luò)抖動(dòng)查看限流日志和網(wǎng)絡(luò)延遲降低并發(fā)等待恢復(fù)部分結(jié)果缺失批量輸出截?cái)鄼z查輸出token數(shù)減小批量或增加輸出預(yù)留程序卡死隊(duì)列死鎖py-spy dump線程棧加超時(shí)和異常捕獲失敗率升高批量太大或超時(shí)太短查看錯(cuò)誤日志調(diào)整批量閾值和超時(shí)時(shí)間內(nèi)存暴漲隊(duì)列積壓監(jiān)控隊(duì)列長(zhǎng)度減小隊(duì)列maxsize5.5 獨(dú)家避坑技巧第一個(gè)技巧是“灰度上線”。不要一次性把所有流量切到新方案先拿10%的數(shù)據(jù)跑對(duì)比新舊方案的輸出質(zhì)量確認(rèn)沒(méi)問(wèn)題再逐步放大。我一開始直接全量切結(jié)果因?yàn)榕拷財(cái)鄦?wèn)題導(dǎo)致一批復(fù)盤報(bào)告質(zhì)量下降返工花了不少時(shí)間。第二個(gè)技巧是“保留原始調(diào)用路徑”。優(yōu)化后的批量方案雖然快但調(diào)試起來(lái)不如單條調(diào)用直觀。我在代碼里保留了一個(gè)開關(guān)可以隨時(shí)切回單條模式方便對(duì)比和排查。第三個(gè)技巧是“監(jiān)控隊(duì)列水位”。隊(duì)列長(zhǎng)度是最直觀的健康指標(biāo)。如果快速通道隊(duì)列持續(xù)增長(zhǎng)說(shuō)明消費(fèi)速度跟不上生產(chǎn)速度可能需要加線程或減小批量。如果隊(duì)列經(jīng)常為空說(shuō)明生產(chǎn)速度不夠可以適當(dāng)加大清洗端的并發(fā)。6. 復(fù)盤質(zhì)量如何不降反升6.1 批量調(diào)用對(duì)輸出質(zhì)量的影響很多人擔(dān)心批量調(diào)用會(huì)降低輸出質(zhì)量我一開始也有這個(gè)顧慮。實(shí)測(cè)下來(lái)只要批量打包時(shí)做好上下文隔離質(zhì)量不僅不降反而更穩(wěn)定。原因是單條調(diào)用時(shí)模型每次都是“冷啟動(dòng)”沒(méi)有上下文參考而批量調(diào)用時(shí)同一批數(shù)據(jù)在同一個(gè)請(qǐng)求里處理模型可以看到批次內(nèi)的其他記錄做歸因分析時(shí)能參考更多信息。當(dāng)然前提是批次內(nèi)的記錄要有一定的相關(guān)性。我是按時(shí)間順序打包的同一時(shí)間段的記錄放在一起這樣模型做趨勢(shì)分析時(shí)更準(zhǔn)確。如果隨機(jī)打包效果會(huì)差一些。6.2 分層策略對(duì)長(zhǎng)尾任務(wù)的處理長(zhǎng)尾任務(wù)單獨(dú)處理之后我給了它們更長(zhǎng)的超時(shí)時(shí)間和更詳細(xì)的提示詞。慢速通道的提示詞里額外加了“請(qǐng)仔細(xì)分析以下復(fù)雜記錄”的引導(dǎo)模型處理起來(lái)更認(rèn)真。實(shí)測(cè)下來(lái)慢速通道的輸出質(zhì)量比之前混在批量里的時(shí)候更好因?yàn)橹芭空{(diào)用時(shí)長(zhǎng)記錄往往被截?cái)嗷蚝雎浴?.3 質(zhì)量校驗(yàn)機(jī)制為了保證優(yōu)化后的輸出質(zhì)量我加了一個(gè)自動(dòng)校驗(yàn)環(huán)節(jié)對(duì)每條復(fù)盤結(jié)果檢查是否包含關(guān)鍵字段如時(shí)間、事件、原因、建議字段缺失的標(biāo)記為低質(zhì)量進(jìn)入人工復(fù)核隊(duì)列。實(shí)測(cè)下來(lái)低質(zhì)量比例從優(yōu)化前的3%降到了1.2%說(shuō)明批量分層的方案確實(shí)提升了質(zhì)量。校驗(yàn)邏輯很簡(jiǎn)單REQUIRED_FIELDS [timestamp, event, cause, suggestion] def validate(result): missing [f for f in REQUIRED_FIELDS if f not in result] if missing: return False, missing return True, []這個(gè)校驗(yàn)不消耗模型資源純規(guī)則匹配跑一遍只要幾秒鐘但能攔住大部分明顯有問(wèn)題的結(jié)果。7. 后續(xù)可擴(kuò)展的方向這套方案目前跑在單機(jī)上43秒是單機(jī)成績(jī)。如果數(shù)據(jù)量再漲10倍單機(jī)可能扛不住到時(shí)候可以考慮把調(diào)度層拆出來(lái)做成分布式的用消息隊(duì)列做任務(wù)分發(fā)多個(gè)工作節(jié)點(diǎn)并行消費(fèi)。不過(guò)那是另一個(gè)量級(jí)的工程了現(xiàn)階段單機(jī)完全夠用。另一個(gè)方向是把批量策略做得更智能比如根據(jù)歷史數(shù)據(jù)學(xué)習(xí)每條記錄的預(yù)估耗時(shí)而不是用簡(jiǎn)單的規(guī)則估算。這樣分桶更準(zhǔn)確長(zhǎng)尾任務(wù)能更早被識(shí)別出來(lái)。不過(guò)這需要積累一定量的運(yùn)行數(shù)據(jù)目前還在觀察階段。還有一個(gè)想法是把復(fù)盤流程做成增量的只處理新增數(shù)據(jù)而不是每次全量跑。這樣日常運(yùn)行的耗時(shí)可以進(jìn)一步壓縮到幾秒級(jí)別。但增量處理需要維護(hù)狀態(tài)復(fù)雜度會(huì)上升得權(quán)衡收益和成本。我個(gè)人在實(shí)際操作中的體會(huì)是性能優(yōu)化這件事最怕的就是憑感覺(jué)猜瓶頸。一定要先測(cè)量、再優(yōu)化、再測(cè)量。我一開始以為瓶頸在數(shù)據(jù)清洗差點(diǎn)花大力氣去優(yōu)化正則表達(dá)式結(jié)果profiling一跑才發(fā)現(xiàn)清洗只占20%。把時(shí)間花在真正的瓶頸上才能事半功倍。另外優(yōu)化過(guò)程中一定要保留回退路徑新方案出問(wèn)題時(shí)能快速切回舊方案這樣才敢大膽嘗試。