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