實戰(zhàn):從空白項目名到記錄-評估-行動的閉環(huán)設(shè)計)
我在某協(xié)作平臺上收到一個交接單項目代號寫著rea正文空白關(guān)鍵詞空白一句話摘要也沒有。點開附件只有三個以rea_開頭的文件一疊日志樣本、一份規(guī)則草稿、一個幾乎沒寫的 README。這種“只有一個代號”的項目現(xiàn)實中不算罕見常見做法是馬上找人來問或者立刻去搜系統(tǒng)里有沒有同名老代碼。但這次我選了另一種方式先把rea當(dāng)成一個動詞組合來拆拆完再決定做什么。后面整套方案的骨架——記錄、評估、行動——就是從這個連注釋都沒寫滿的空白代號里長出來的。1. 一個只有“rea”的交接單我是怎么把它拆成三個動詞面對只有三個字母的項目名最容易出的問題是一上來就查它的“標(biāo)準(zhǔn)含義”。rea可以對應(yīng) read-eval-apply也可以對應(yīng) resource-event-agent甚至可以被理解成某個內(nèi)部產(chǎn)品的縮寫??扇绻笇?dǎo)意見、用戶背景、使用場景都沒有任何一個標(biāo)準(zhǔn)含義都只是猜測。我的習(xí)慣是反過來先問一個問題這個系統(tǒng)最終要“動”什么是動界面、動數(shù)據(jù)、動流程還是動消息通知1.1 先別猜技術(shù)名先確定這東西最終會“動”什么把這個動詞定下來再回看項目名就清晰很多。我當(dāng)時列了一張表把可能的解釋按交付形態(tài)排開候選含義常見領(lǐng)域適合的交付形態(tài)對組織的要求read-eval-apply運行時/語言工具命令行工具、在線求值服務(wù)使用者得懂代碼request-evaluate-act風(fēng)控/爬蟲/審批API服務(wù)、規(guī)則平臺需要明確的請求來源resource-event-agent會計/進銷存業(yè)務(wù)領(lǐng)域模型需要熟悉財務(wù)的同事record-evaluate-act監(jiān)控/數(shù)據(jù)/閉環(huán)任務(wù)事件管道、自動化通知能接受“事件驅(qū)動”思維對比下來只有 record-evaluate-act 和“動數(shù)據(jù)、動通知、回寫業(yè)務(wù)”最吻合。這不是因為它更高端而是因為它照顧了現(xiàn)狀我手上正好有一批日志樣本和規(guī)則草稿這些東西天然就是這個框架的“輸入”。1.2 三條業(yè)務(wù)路線收斂在我選的方向上選定方向之前我個人喜歡再追問三個定位問題用來排除不靠譜的路線。第一誰會看這個系統(tǒng)的輸出如果輸出是給值班工程師的那動作大概率是通知如果是給管理層的那動作就是報表。第二系統(tǒng)的輸出會不會反向影響輸入比如補貨單產(chǎn)生后會生成新的入庫單這就是一個閉環(huán)。第三動作要不要經(jīng)過人審批如果要那“一觸發(fā)就執(zhí)行”的設(shè)計就必須被改成“一觸發(fā)就建議”。順著這三個問題逼自己想我從“做通用后臺”跑偏到“做一條閉環(huán)事件管道”。最后我得到的具體故事大概是日志樣本表明有一次外部系統(tǒng)上報了貨品入庫卻未登記數(shù)量規(guī)則草稿里有一條“某倉庫物料低于安全水位就提示補貨”README 里只寫了rea三個字母似乎暗示這個系統(tǒng)本身就是動作鏈。當(dāng)這些材料建立聯(lián)系后rea在我這邊就不再是一個詞而是一個最小閉環(huán)記錄每一筆業(yè)務(wù)事件評估它該觸發(fā)什么規(guī)則最終交給能執(zhí)行的模塊去行動。于是整個項目骨架變成記錄、評估、行動。這也是后續(xù)所有設(shè)計和評審的統(tǒng)一語言。2. 記錄、評估、行動為什么“rea”最終落在這一組語義上一個項目只有三個字母的名字好處是約束范圍壞處是容易把范圍定得過大。把rea讀成 record-evaluate-act 之后我做的第一件事不是建倉庫而是把每個詞都圈成一塊能獨立復(fù)用的能力。2.1 三個模塊各自的邊界記錄模塊負責(zé)一切“進來”的數(shù)據(jù)。它不判斷數(shù)據(jù)有沒有業(yè)務(wù)價值只做四件事接收原始事件、做基本校驗、保留一份原始副本、給事件分配唯一編號。很多人會在這里忍不住做業(yè)務(wù)清洗比如順手把錯誤的訂單狀態(tài)改正或者把字段格式統(tǒng)一。我不建議這么做因為一旦記錄模塊承擔(dān)了業(yè)務(wù)改寫后面評估階段看到的就不是真實事件出了問題會很難定位。評估模塊是整個rea的核心負責(zé)把“記錄”階段拿到的原始事件轉(zhuǎn)化為決策。它有幾種典型形態(tài)閾值規(guī)則、統(tǒng)計模型、由人操作的復(fù)核隊列。在我的實現(xiàn)里評估模塊被設(shè)計成純函數(shù)式同一個事件進來無論評估多少次結(jié)果都應(yīng)該一樣。如果做不到這一點說明評估的輸入不夠干凈需要往前回到記錄階段找問題。行動模塊最直接也最容易被人忽略。它的職責(zé)是把評估產(chǎn)生的決策變成具體業(yè)務(wù)動作比如發(fā)送通知、創(chuàng)建工單、調(diào)用下游接口。這個模塊不能自己決定做什么只能執(zhí)行已經(jīng)形成的決策同時必須給每次執(zhí)行留痕。下面這張表就體現(xiàn)了當(dāng)時我要求團隊關(guān)注的邊界模塊輸入輸出最容易踩的坑record原始事件規(guī)范化事件 event_id過度清洗導(dǎo)致原始信息丟失evaluate規(guī)范化事件decision 記錄依賴外部實時數(shù)據(jù)導(dǎo)致結(jié)果抖動actdecision 記錄動作日志 回執(zhí)重復(fù)執(zhí)行下游操作2.2 從單次執(zhí)行到可持續(xù)閉環(huán)三個模塊單獨拎出來都很簡單真正讓它值錢的是閉環(huán)關(guān)系。行動并不是終點行動發(fā)生后系統(tǒng)會產(chǎn)生新的狀態(tài)變化比如工單被點擊、補貨被確認(rèn)、通知被閱讀這些新事件又會回到 record 模塊變成下一次評估的輸入。換句話說rea運轉(zhuǎn)起來之后就像一條環(huán)形的傳送帶而不是三段直連的管道。我在測試環(huán)境里搭過一個“庫存到位提醒”的驗證倉庫收到一次入庫但數(shù)量低于某個水位record 模塊記錄了該事件評估模塊判斷“數(shù)量低于安全水位”給出一個補貨建議行動模塊生成一條補貨通知。隨后運營人員點了“確定補貨”這個點擊動作本身又是一條新的事件會再次進入記錄列表。于是在日志里你能看到一條完整的鏈入庫事件 → 低庫存決策 → 補貨動作 → 確認(rèn)事件 → 新一輪決策。這套循環(huán)跑通后它就不再是一個局部工具而是一個可持續(xù)擴張的平臺。我當(dāng)時的判斷是如果后面的需求只是想“看數(shù)據(jù)”rea的三個模塊都能單獨接出報表如果想做自動化三個模塊必須同時上。正是這種“既能拆開用也能合起來跑”的特性讓它變成很穩(wěn)的項目底盤。3. 最小閉環(huán)落地從事件申報到自動動作的三步實現(xiàn)理論拆得再好最后也得落在可運行代碼上。這里分享我當(dāng)時做的最小閉環(huán)實現(xiàn)它不依賴重量級框架任何細節(jié)都只是為了說明 REA 的執(zhí)行路徑。3.1 先約定事件結(jié)構(gòu)再談代碼必須先確定協(xié)議再寫代碼。我用了一種很樸素的事件結(jié)構(gòu)每個事件必須有四樣?xùn)|西event_id用于全局唯一標(biāo)識event_type用于描述業(yè)務(wù)類型occurred_at用于保留發(fā)生時間payload用于攜帶業(yè)務(wù)字段。可選字段包括原始大小寫raw、來源系統(tǒng)source和調(diào)用方標(biāo)識trace_id。校驗規(guī)則也定得很克制必填字段缺一不可類型不對就打回死信但業(yè)務(wù)規(guī)則盡量不看。比如“庫存數(shù)量不能為負”肯定要校驗這是常識但“低于安全水位就補貨”這類業(yè)務(wù)判斷留給評估階段。這樣做的原因是評估規(guī)則會頻繁修改把規(guī)則綁死在入口校驗層會讓每次改規(guī)則都牽連輸入?yún)f(xié)議。事件結(jié)構(gòu)界定如下字段是否必填說明event_type是區(qū)分 stock.in、order.finish、link.click 等occurred_at是統(tǒng)一為 UTC ISO8601source否哪個上游系統(tǒng)上報payload是業(yè)務(wù)字段建議最外層固定raw否原始內(nèi)容排查時使用3.2 用 Python 實現(xiàn)一個不依賴框架的 30 行 REA 樣例我盡量讓三個環(huán)節(jié)保持各自獨立于是把邏輯壓縮成一個很簡單的 Python Runtimefrom datetime import datetime, timezone from typing import Any, Dict class ReaRuntime: def __init__(self): self.events: Dict[str, dict] {} self.decisions: Dict[str, dict] {} def record(self, event: Dict[str, Any]) - str: event_id evt_ str(len(self.events) 1) normalized { event_type: event[event_type], occurred_at: event.get( occurred_at, datetime.now(timezone.utc).isoformat() ), source: event.get(source, unknown), payload: event.get(payload, {}), raw: event, status: recorded } self.events[event_id] normalized return event_id def evaluate(self, event_id: str): ev self.events[event_id] if ev[event_type] ! stock.in: ev[status] passed return None quantity int(ev[payload].get(quantity, 0)) if quantity 10: decision_id dec_ str(len(self.decisions) 1) self.decisions[decision_id] { event_id: event_id, level: low_stock, suggestion: 檢查補貨渠道, status: suggested } ev[status] evaluated return decision_id ev[status] passed return None def act(self, decision_id: str): decision self.decisions[decision_id] if decision[status] ! suggested: raise ValueError(decision already acted) decision[status] acted decision[acted_at] datetime.now(timezone.utc).isoformat() # 這里連接通知、工單或下游接口 return faction_{decision_id}這一段直接表達了 REA 三元組。record負責(zé)入?yún)valuate根據(jù)數(shù)量和事件類型產(chǎn)出一個決策act給決策狀態(tài)做過渡并留下動作號。在這個最小例子里沒有連接真實下游但只要把act里的注釋改成 HTTP 調(diào)用或隊列寫入閉環(huán)就能跑起來。調(diào)用方式也簡單直接runtime ReaRuntime() eid runtime.record({ event_type: stock.in, occurred_at: 2024-06-01T08:00:0000:00, payload: {item: A001, quantity: 8} }) did runtime.evaluate(eid) if did: action_no runtime.act(did) print(action_no)真實項目里不建議把這些模塊揉在一個對象里至少應(yīng)該拆成 record 服務(wù)、evaluate 服務(wù)、act 服務(wù)三個獨立部署單元。但第一版可以揉目的是讓參與者看清楚 REA 的骨架而不是一上來就面對微服務(wù)復(fù)雜度。我習(xí)慣用“先跑通閉環(huán)、再拆服務(wù)”的推進順序收益遠大于直接設(shè)計一個分布式系統(tǒng)。4. 只給模塊命名還不夠這些邊界條件決定了REA能不能用模塊名字確認(rèn)了、最小閉環(huán)也跑通了剩下的都是真正上線時會暴露的硬問題。這里把我遇到的和事后復(fù)盤的內(nèi)容集中寫出來基本都是常規(guī)文檔里少見但實際價值很高的經(jīng)驗。4.1 事件進得來但干凈的日志才是真正的入口REA 系統(tǒng)的第一道關(guān)不是代碼而是日志。有一回測試數(shù)據(jù)里混入了一個occurred_at缺失的舊事件record 階段按當(dāng)時時間補了時間戳結(jié)果評估階段把它當(dāng)成“剛剛發(fā)生”的低庫存事件誤發(fā)了一輪補貨建議。查了一下午才發(fā)現(xiàn)是記錄階段的“好心補全”惹的禍。從那之后我規(guī)定任何缺失字段都打成missing標(biāo)記不在入口偷偷補值。時間字段是重災(zāi)區(qū)。不同上游系統(tǒng)上報的時間可能有時區(qū)差異有的給 08:00有的給 UTC還有的直接傳本地時間。我的處理方式是occurred_at必須包含時區(qū)如果上游做不到就在 source 系統(tǒng)名里標(biāo)注“默認(rèn) UTC”再由記錄模塊統(tǒng)一轉(zhuǎn)換。轉(zhuǎn)換規(guī)則寫死在模塊里而不是寫在評估代碼里。4.2 評估結(jié)果不一致帶來最隱蔽的系統(tǒng)抖動REA 閉環(huán)上線后最隱蔽的問題是“同一個事件被重新評估時結(jié)論變了”。比如一條 low_stock 建議第一次評估是“建議補貨”幾小時后重跑卻變成“無需補貨”。導(dǎo)致這種結(jié)果的常見原因有幾種評估函數(shù)里直接查了外部數(shù)據(jù)庫、接口響應(yīng)會變化、用了當(dāng)前時間而不是事件時間。我在評估階段調(diào)整了一個原則評估只讀“記錄階段已經(jīng)沉淀下來的數(shù)據(jù)”外部需要的信息必須在事件進入時同步成快照。這個改動成本不高收益很直接。以后再看決策記錄就可以明確區(qū)分“當(dāng)時的事實”和“后來的修正”。做規(guī)則引擎和自動化任務(wù)最忌諱讓規(guī)則結(jié)果隨時間漂移寧可先做大事件間的快照也不要讓業(yè)務(wù)人員開始懷疑系統(tǒng)為什么“一票兩判”。4.3 動作要做成冪等這是REA閉環(huán)最后的尊嚴(yán)行動模塊接到?jīng)Q策后調(diào)用下游可能超時、失敗、重復(fù)投遞。如果沒做冪等一個補貨建議可能變成兩個補貨工單。我在 act 階段給決策記錄增加了一個執(zhí)行狀態(tài)suggested → acted并在調(diào)用外部系統(tǒng)之前先以決策 ID 為冪等鍵寫入“執(zhí)行中”標(biāo)記。下游允許重復(fù)請求時還會帶上decision_id作為業(yè)務(wù)參數(shù)讓下游自行去重。執(zhí)行狀態(tài)含義重試策略suggested決策生成未執(zhí)行正常進入 actacting已下發(fā)未確認(rèn)超時后最多重試一次acted下游確認(rèn)成功不重試只記錄failed執(zhí)行失敗進人工列表cancelled人工取消不自動重試另一個容易忽略的點是“動作成功不等于下游成功”。我曾遇到通知模塊返回 200但下游工單系統(tǒng)實際創(chuàng)建失敗因為沒有檢查業(yè)務(wù)回執(zhí)。后來 act 模塊統(tǒng)一要求只有收到下游業(yè)務(wù)狀態(tài)碼才把 acted 狀態(tài)寫回。簡單說動作是 REA 面向外界的手指手指按下去是否真的生效必須自己確認(rèn)否則閉環(huán)就不成立。5. 關(guān)于“rea”的復(fù)盤命名如何幫一個空項目落地最后聊點偏軟的、但對實際推進很有幫助的東西。很多項目有非常完整的說明文檔卻因為團隊語言不統(tǒng)一做出來的東西總是各說各話。rea這個項目反過來信息少得可憐反而是因為名字太短逼出了共識。5.1 用名字倒逼細節(jié)每次討論都從“它記錄什么、評估什么、行動什么”開始當(dāng)項目只有三個字母時每次評審我都被迫使用同一套問法這個東西記錄了什么事件評估用了什么數(shù)據(jù)行動會觸達哪里只要問題能完整回答細節(jié)基本就齊了。而 REA 的名字恰好是一個天然的提問框架它不會讓你跑偏到“要不要做一個可視化大屏”因為大屏既不是記錄、也不是評估、也不是行動。內(nèi)部做模塊劃分時我也建議用 REA 把需求歸檔。某次需求方提出“希望把歷史異常也納入信號”按模塊對應(yīng)關(guān)系一目了然這是評估模塊的特征來源問題不是記錄模塊的問題。這樣處理問題時就不會出現(xiàn)“一個需求改遍三端”的情況。5.2 從“建議模式”切成“執(zhí)行模式”的灰度經(jīng)驗最后補一條控制策略的實踐。REA 閉環(huán)真正上線時不建議第一天就讓 act 直接執(zhí)行所有下游操作。更穩(wěn)妥的做法是先切“建議模式”評估結(jié)果只寫成建議消息放到預(yù)覽區(qū)域等人點確認(rèn)。確認(rèn)動作觸發(fā) act未確認(rèn)則定期匯總。等運維或業(yè)務(wù)人員對規(guī)則有信任度后再把某些低風(fēng)險動作切成“執(zhí)行模式”。我實際用到的狀態(tài)鏈?zhǔn)莝uggested → previewed → approved → executed。人工確認(rèn)期間如果發(fā)現(xiàn)規(guī)則誤判可以直接把 decision 改成 cancelled并攔住后續(xù)重試如果正常approved 之后的動作才會繼續(xù)。這套狀態(tài)機在后面幾乎沒變過因為它用最小代價守住了信任底線。行動自動化的時間點取決于錯誤容忍度。低風(fēng)險通知可以一周后自動化涉及資金或庫存的動作我會保持人工確認(rèn)更久寧慢勿快。這個判斷沒有統(tǒng)一標(biāo)準(zhǔn)但原則很簡單出錯代價低放權(quán)給系統(tǒng)出錯代價高放權(quán)給人直到人對規(guī)則不再挑刺。我對rea的最大心得不是它最終做成一個多聰明的系統(tǒng)而是看到了“一個空白代號能被三字架構(gòu)撐起來”的完整路徑。只要每個閉環(huán)還回答得了“記錄了什么、評估了什么、行動了什么”這個項目就不算沒方向。