機搭建AI Agent治理層)
如果你用過 Claude Code 做過稍微復(fù)雜一點的任務(wù)十有八九遇到過這種場景它跑得很嗨日志刷了一屏最后停下來告訴你“已完成”你一看測試沒跑文檔沒更新甚至改的文件跟需求都不相關(guān)。這不是個別模型的“態(tài)度問題”而是 Agent 類工具在沒有外部約束時的必然行為。我們內(nèi)部從第一次被坑到?jīng)Q定系統(tǒng)性解決大概花了兩三周最后產(chǎn)出一套被叫做 wow-harness 的治理層框架16 個 Hook 覆蓋任務(wù)從進來到交付的每一個關(guān)鍵動作外加 8 關(guān)狀態(tài)機把整個任務(wù)的推進過程固定在軌道上。這篇文章把完整思路和可復(fù)現(xiàn)的配置從零拆一遍適合正在用 Claude Code 做真實項目、又不想每次都被它“差不多得了”式交付折磨的團隊。1. 為什么我會在 Claude Code 外面再加一層治理框架先說結(jié)論治理層的目標不是限制 Claude Code 的能力而是把它從“憑感覺自由發(fā)揮”變成“按流程推進”。Agent 工具的價值在于它能把多步任務(wù)串起來但只要中間沒有硬性檢查點它就一定會走最短路徑哪怕這條路徑是錯的。1.1 連續(xù)被“看起來完成”坑了半個月在引入治理層之前我這邊幾乎每天都會收到類似的反饋“它怎么又沒寫測試”“它怎么把配置文件也改了”“我讓它先出方案它上來就寫代碼?!蔽耶敃r沒有急著批評模型而是把所有失敗案例收集到一起發(fā)現(xiàn)偷懶行為其實可以歸納成五類跳步。明明是“先設(shè)計方案、確認后再實施”的任務(wù)模型跳過方案評審直接改文件。假測試。測試文件確實建了測試命令也跑了但測試函數(shù)里沒有一條 assert或者斷言恒為 True退出碼照樣是 0。范圍蔓延。一個需求沒做完但它順手“重構(gòu)”了相鄰幾個模塊理由通常是“為后續(xù)打好基礎(chǔ)”。風(fēng)險掩藏。命令執(zhí)行遇到環(huán)境變量缺失、網(wǎng)絡(luò)超時、權(quán)限不足它選擇忽略錯誤繼續(xù)往下寫最后告訴你一切正常。幻覺閉環(huán)。它在最終回復(fù)里寫“已驗證通過”但翻遍日志都沒有找到一次真實的測試運行記錄。這五類問題沒有一個靠提醒能解決。我試過在系統(tǒng)提示里寫“你必須先寫測試”當天有效任務(wù)一復(fù)雜就失效也試過讓它每一步反問自己結(jié)果是日志變長了交付質(zhì)量沒有明顯變化。原因很簡單提醒是軟約束可以被上下文稀釋而 Agent 的行為終點只有一個——生成一段看起來合理的回復(fù)。1.2 AI Agent 偷懶的根因不是態(tài)度是目標函數(shù)很多人把“偷懶”理解成模型故意偷工減料其實不是。大模型生成代碼時優(yōu)化的目標是預(yù)測最合理的下一個 token而不是“讓測試全部通過”。你說“幫我加個緩存”模型天然傾向于輸出一段緩存代碼而不是去跑一遍回歸測試。輸出一段代碼是低成本的跑測試、分析失敗、修修復(fù)、再跑測試是高成本的多輪行動。在沒有任何外部反饋的情況下模型會選擇最短路徑來結(jié)束這一輪對話。工具調(diào)用還會放大這個問題。Claude Code 一旦接入終端、文件系統(tǒng)、代碼搜索等工具它就有了“動手”的能力但它的反饋回路里缺少一層“質(zhì)檢員”。命令退出碼是局部的模型不一定在下一輪決策中讀取測試報告再難看只要沒有人在中間打斷它就可能視而不見。所以我們需要的不是更長的提示詞而是一個能在關(guān)鍵動作發(fā)生前后判斷“現(xiàn)在能不能做、做完對不對”的外部層。1.3 為什么是 Hook 狀態(tài)機而不是繼續(xù)調(diào) prompt治理層到底怎么搭我評估過三條路線。第一條是把約束全部寫進 prompt包括禁止行為清單、流程要求、驗收標準。優(yōu)點是零成本缺點是約束會被新輸入的上下文沖淡任務(wù)越長效果越差。第二條是讓模型每一步自問自答比如“我現(xiàn)在完成的步驟是否滿足驗收標準”。優(yōu)點是能讓它慢下來缺點是自問自答仍然是 token 生成它完全可以自問完之后又自答一個“滿足”很難被證偽。第三條才是 wow-harness 走的路線在真實的事件鏈路上插樁。Claude Code 在執(zhí)行周期里會產(chǎn)生大量事件比如收到新指令、準備改文件、調(diào)用外部命令、拿到測試結(jié)果、準備輸出最終回答。治理層在事件前后掛 Hook用外部腳本判斷“這個動作該不該放行”同時用狀態(tài)機記錄任務(wù)的推進階段。狀態(tài)機解決的是橫跨多輪對話的“流程問題”你從哪個階段進來、現(xiàn)在該在哪個階段、有沒有跳過必經(jīng)關(guān)卡。Hook 解決的是單點動作的“合規(guī)問題”這一步改的文件是否越界、這條命令是否高危、這份測試證據(jù)是否真實。這兩者組合起來相當于給 Agent 裝了一整套安檢設(shè)備和一張流水線工單。它不再靠自覺而是靠結(jié)構(gòu)。2. 16個Hook的埋點拆解每個關(guān)鍵動作都有一道安檢16 這個數(shù)字不是湊出來的而是從任務(wù)生命周期里一條條數(shù)出來的。我按“入口、規(guī)劃、執(zhí)行、收尾”四段來分布每一段對應(yīng) Claude Code 行為鏈路上的一個關(guān)鍵拐點。下面先把全景表放出來再逐個說清楚每個 Hook 到底檢查什么、為什么需要它。2.1 先看全景16個Hook分布總覽編號Hook 名稱所屬階段核心職責(zé)E1intent_guard入口校驗任務(wù)描述是否包含目標、范圍、完成定義E2context_injector入口注入倉庫規(guī)范、技術(shù)棧邊界、文檔模板E3task_splitter入口強制把大任務(wù)拆成可驗證的子任務(wù)E4goal_locker入口將驗收標準寫入任務(wù)狀態(tài)文件并鎖定P5plan_validator規(guī)劃校驗方案是否包含步驟、風(fēng)險、驗收方式P6tech_choice_review規(guī)劃新引入技術(shù)棧必須有明確理由和對比P7risk_register規(guī)劃登記高危操作供后續(xù)命令守衛(wèi)使用P8queue_builder規(guī)劃生成有序子任務(wù)隊列X9edit_scope_guard執(zhí)行改文件前檢查是否越出允許目錄X10command_guard執(zhí)行命令執(zhí)行前匹配高危清單與白名單X11dependency_probe執(zhí)行依賴文件變更時要求兼容性說明X12test_trigger執(zhí)行每個子任務(wù)完成后強制觸發(fā)對應(yīng)測試X13self_review_pump執(zhí)行向上下文注入質(zhì)檢問題并要求逐條自答X14progress_heartbeat執(zhí)行記錄有效產(chǎn)出檢測原地空轉(zhuǎn)C15evidence_check收尾核驗測試斷言、失敗計數(shù)、運行記錄C16delivery_signoff收尾校驗 diff、文檔、回滾方案后簽收交付這四個階段和后面的 8 關(guān)狀態(tài)機是對應(yīng)的入口對應(yīng) S0 到 S1規(guī)劃對應(yīng) S1 到 S2執(zhí)行對應(yīng) S2 到 S4收尾對應(yīng) S4 到 S7。Hook 負責(zé)事件級檢查狀態(tài)機負責(zé)任務(wù)級推進兩者不沖突。2.2 入口四個Hook把任務(wù)說清楚、鎖清楚再開工入口階段的四個 Hook 解決的是同一個問題Claude Code 經(jīng)常在任務(wù)描述模糊的情況下就開始動手。你只說一句“優(yōu)化一下接口性能”它就敢直接改核心模塊至于“優(yōu)化到什么程度”“哪些接口算在里面”“怎么證明優(yōu)化有效”全是空白。intent_guard 會在任務(wù)進入時檢查描述里有沒有可執(zhí)行的目標、明確的范圍邊界和可驗證的完成定義。如果沒有它不會直接拒絕而是返回一組提問讓 Claude Code 先把缺失信息補齊。比如任務(wù)里只說“加緩存”intent_guard 會問緩存鍵是什么失效策略是什么命中率指標是什么。context_injector 負責(zé)把治理層配置好的上下文注入進去。它不等同于系統(tǒng)提示而是按項目動態(tài)加載倉庫的技術(shù)棧約定、禁止修改的目錄、代碼風(fēng)格要求、文檔模板。這樣做的原因是讓模型在開局就把邊界裝進上下文而不是中途自己去猜。task_splitter 是專門治“一口吞”的。任務(wù)描述過于龐大時它要求必須拆成子任務(wù)列表每個子任務(wù)都要有獨立的驗收標準。拆不出來就停在原地。很多偷懶都是從“任務(wù)太大、模型隨便做幾下就說完成”開始的。goal_locker 則是把前面確認好的驗收標準固化到任務(wù)狀態(tài)文件里比如目標、范圍、禁止項、完成定義全部寫入 harness/state/task_xxx.json。之后所有 Hook 都以這份文件為準防止中間對話太長模型把最初的目標忘了。2.3 規(guī)劃四個Hook方案不行代碼一行都不許動到了規(guī)劃期AI Agent 最容易犯的錯誤是不做規(guī)劃直接寫代碼。它天生傾向于“從第一行代碼開始”因為寫代碼才是它能獲得正反饋的路徑。plan_validator 就是在這里設(shè)卡Claude Code 必須先產(chǎn)出方案方案里必須有實施步驟、風(fēng)險點、驗證方式。沒有驗收方式的方案直接打回。tech_choice_review 看起來像是限制技術(shù)自由其實是防止“順手換輪子”。模型經(jīng)常會在中途說“這個庫不如換成另一個更現(xiàn)代”然后就改了依賴。這類變更不是絕對禁止但必須有對比和理由比如性能數(shù)據(jù)、維護狀態(tài)、遷移成本。沒有對比就攔截有對比就放行。risk_register 把方案里識別出的危險操作記錄下來例如刪除目錄、批量修改文件、操作生產(chǎn)環(huán)境。它不會單獨攔人而是把風(fēng)險清單傳給后面執(zhí)行期的 command_guard讓命令守衛(wèi)有依據(jù)地攔截高危動作。風(fēng)險識別放在規(guī)劃期比在執(zhí)行期臨時判斷要準得多。queue_builder 負責(zé)把子任務(wù)轉(zhuǎn)換成有序隊列寫入狀態(tài)機可識別的字段。這個隊列決定了執(zhí)行期和測試觸發(fā)器的節(jié)奏。隊列的意義在于任務(wù)一旦被拆解并鎖定順序Claude Code 就不能隨心所欲地先做后做它必須按隊列推進。2.4 執(zhí)行六個Hook把工具的每一次調(diào)用都管起來執(zhí)行期是自由度最高的階段也是治理層最容易誤傷的地方。六個 Hook 里edit_scope_guard 和 command_guard 是硬閘門另外四個是證據(jù)采集和節(jié)奏控制。edit_scope_guard 在文件寫入前觸發(fā)檢查目標路徑是否落在允許目錄清單內(nèi)。比如某次任務(wù)只允許改 src/order 和 tests/order如果 Claude Code 準備寫 config/production.yaml直接被攔。范圍蔓延這個毛病靠提示詞根本管不住只有路徑級攔截才可靠。command_guard 看的是終端命令。規(guī)則分兩層黑名單命令直接拒絕比如清庫、強制推送、刪除整個目錄風(fēng)險命令進入人工審批或要求附帶說明。執(zhí)行高危命令時治理層會把 risk_register 里的上下文一并帶出讓模型解釋這條命令與當前任務(wù)的關(guān)聯(lián)。dependency_probe 監(jiān)聽依賴文件比如 package.json、requirements.txt 等。只要這些文件發(fā)生變化它就會要求 Claude Code 在 diff 說明里寫明變更理由和兼容性驗證結(jié)果。沒有理由就回滾這次依賴變更。這樣能防止模型為了完成任務(wù)偷偷升級依賴。test_trigger 是“每個子任務(wù)完成”時的強制動作。它做的事情很簡單找到當前子任務(wù)關(guān)聯(lián)的測試文件執(zhí)行對應(yīng)測試命令。注意它不只跑一次而是每個子任務(wù)完成都跑一次。測試失敗時任務(wù)不能進入下一個子任務(wù)。self_review_pump 不是硬攔截而是往上下文中注入一組質(zhì)檢問題例如改動的邊界是否超出需求異常路徑是否處理測試是否覆蓋了失敗場景。它強制模型在繼續(xù)之前完成一次自檢回答。這看起來很像第二類方案里的“自問自答”但區(qū)別在于自答內(nèi)容會被記錄后面收尾的 evidence_check 會交叉校驗自答說有覆蓋而實際沒有覆蓋時會被抓住。progress_heartbeat 是防“原地空轉(zhuǎn)”的。模型有可能在某個問題上反復(fù)繞圈生成大量日志但沒有任何文件變化或命令執(zhí)行。它定期檢查最近有沒有“有效產(chǎn)出”——文件變更、命令完成、測試結(jié)果——如果長時間沒有就暫停當前任務(wù)并通知人工介入。2.5 收尾兩個Hook驗證據(jù)不驗收態(tài)度收尾是整個治理層最關(guān)鍵的一段。因為大多數(shù)偷懶都發(fā)生在“最后一個環(huán)節(jié)”模型知道快要交付了所以開始縮短流程、跳過驗證、直接寫“已完成”。evidence_check 做的第一件事是解析測試日志。它不只看退出碼還會統(tǒng)計通過數(shù)量、失敗數(shù)量、斷言數(shù)量、實際執(zhí)行時間。如果測試文件里沒有任何 assert哪怕退出碼是 0也會被判定為假測試。如果日志顯示有失敗用例但最終被模型忽略任務(wù)會被打回執(zhí)行期。delivery_signoff 是最后一關(guān)。它會檢查本次 diff 與任務(wù)目標是否匹配有沒有新增與需求無關(guān)的改動檢查相關(guān)文檔是否同步更新檢查變更說明里是否包含回滾方案。三項都通過后任務(wù)才允許進入人工評審狀態(tài)。說白了收尾 Hook 驗收的是證據(jù)鏈不是模型嘴上說的“我認為沒問題”。2.6 Hook 是內(nèi)置還是按場景掛載怎么取舍不是 16 個 Hook 都要為一個項目全部打開。我的經(jīng)驗是分成三檔。必須內(nèi)置的是 intent_guard、edit_scope_guard、command_guard、evidence_check。這四個涉及最基本的準入、越界、危險命令和假測試問題。按項目掛載的是 tech_choice_review、dependency_probe。純前端項目、無依賴變動的腳本項目可以不啟用減少誤傷。按成本掛載的是 self_review_pump、progress_heartbeat。前者會消耗額外 token后者需要有人處理暫停任務(wù)。小項目、短任務(wù)開這兩個反而增加復(fù)雜度只有長時間跑的大任務(wù)才值得。3. 8關(guān)狀態(tài)機任務(wù)像過檢品線一樣少一關(guān)就出不去Hook 管的是“當下這個動作對不對”狀態(tài)機管的是“整個任務(wù)走到哪一步了”。只有 Hook 沒有狀態(tài)機Claude Code 還是可以東一榔頭西一棒子最后繞一個大圈說完成了。狀態(tài)機的作用就是強制單向流轉(zhuǎn)想從第一關(guān)直接跳到最后一關(guān)門都沒有。3.1 八個關(guān)卡從進來到歸檔走一遍這套狀態(tài)機的主干有八個狀態(tài)我把它們稱為八關(guān)。S0 HOLDING 是任務(wù)的初始掛起狀態(tài)。Claude Code 收到任務(wù)后先不干活等 intent_guard 和 task_splitter 確認信息完整、子任務(wù)拆解成功才允許進入下一關(guān)。S1 INTENT_LOCKED 表示意圖已鎖定。此時 goal_locker 已經(jīng)把目標、范圍、驗收標準寫進狀態(tài)文件。這個狀態(tài)存在的意義是后面無論對話多長模型都必須圍繞這份鎖定目標工作不能擅自改需求。S2 PLAN_APPROVED 是方案通過狀態(tài)。plan_validator 通過、tech_choice_review 通過、risk_register 完成、queue_builder 生成隊列后任務(wù)才進入這里。從這一關(guān)開始Claude Code 才被允許動代碼。S3 EXECUTING 是執(zhí)行狀態(tài)。它不是一個靜止狀態(tài)而是一個持續(xù)過程每完成一個子任務(wù)test_trigger 必須跑一次測試progress_heartbeat 持續(xù)檢查有效產(chǎn)出。只有隊列里的子任務(wù)全部完成才允許嘗試進入下一關(guān)。S4 SELF_VERIFIED 是自驗證通過狀態(tài)。進入這個狀態(tài)的條件是 all tests passed并且 evidence_check 驗證了斷言數(shù)量與測試日志不存在假測試。注意這里是“自驗證”還沒經(jīng)過人。S5 HUMAN_REVIEW 是人工評審狀態(tài)。任務(wù)進入這里后Claude Code 的自動流程暫停等待人工查看 diff、測試日志、文檔更新和回滾方案。人工有“通過”和“打回”兩個動作打回則回到 S3 或 S2。S6 RELEASED 是發(fā)布狀態(tài)。人工通過后任務(wù)進入可發(fā)布序列。如果項目對接了 CI/CD這里可以觸發(fā)上線流水線。S7 ARCHIVED 是歸檔狀態(tài)。發(fā)布完成、復(fù)盤記錄寫完后任務(wù)關(guān)閉。歸檔時會保存任務(wù)的狀態(tài)記錄、Hook 執(zhí)行日志、測試證據(jù)摘要后續(xù)想追溯某次“偷懶是怎么被攔住的”直接查這份檔案。3.2 狀態(tài)流轉(zhuǎn)表與看門狗機制八關(guān)的流轉(zhuǎn)表如下當前狀態(tài)遷移觸發(fā)條件依賴的證據(jù)目標狀態(tài)S0 HOLDING意圖校驗與拆解通過intent_guard、task_splitter 輸出S1 INTENT_LOCKEDS1 INTENT_LOCKED方案評審?fù)ㄟ^plan_validator、queue_builder 輸出S2 PLAN_APPROVEDS2 PLAN_APPROVED子任務(wù)隊列開始執(zhí)行queue_builder 隊列已加載S3 EXECUTINGS3 EXECUTING隊列清空且測試全通過test_trigger、evidence_check 輸出S4 SELF_VERIFIEDS4 SELF_VERIFIED人工點擊通過人工評審意見S5 HUMAN_REVIEWS5 HUMAN_REVIEW發(fā)布動作完成CI/CD 或發(fā)布確認S6 RELEASEDS6 RELEASED復(fù)盤與歸檔完成歸檔記錄S7 ARCHIVED光有流轉(zhuǎn)規(guī)則還不夠狀態(tài)機要有自己的看門狗。每個狀態(tài)都帶一個 TTL比如 S3 EXECUTING 最長允許 30 分鐘。同一個狀態(tài)停留超過閾值看門狗就會觸發(fā)先提醒 Claude Code 提供進展再沒反應(yīng)就自動降級到 PAUSED。PAUSED 不是主干八關(guān)之一而是一個異常掛起狀態(tài)專門等人來處理。3.3 異常分支駁回、暫停、熔斷與恢復(fù)狀態(tài)機只有主干會顯得很死板所以還需要幾個不在主干計數(shù)里的分支REJECTED、PAUSED、FAILED、ROLLBACK。REJECTED 是任意階段被打回。比如 entry 階段信息不完整、plan 階段方案不合格、執(zhí)行階段被 command_guard 攔截了高危命令。REJECTED 會帶著原因跳回對應(yīng)狀態(tài)比如方案不合格回到 S0 或 S1執(zhí)行中被攔截回到 S3。PAUSED 是暫停。人工干預(yù)、長時間無有效產(chǎn)出、測試一直失敗但模型在瞎繞圈時狀態(tài)機會把任務(wù)掛起。掛起期間不允許繼續(xù)消耗工具調(diào)用只能等人工判斷“繼續(xù)”“打回”還是“終止”。FAILED 表示任務(wù)確實失敗。S3 階段測試失敗且三分鐘內(nèi)沒有任何修復(fù)進展FAILED 會把失敗原因歸檔任務(wù)回到 S3 重置執(zhí)行上下文而不是直接在舊上下文里繼續(xù)試錯。很多時候模型在失敗后反復(fù)繞圈是因為上下文已經(jīng)被錯誤日志污染了重置反而更快。ROLLBACK 是回滾分支。S6 RELEASED 后發(fā)現(xiàn)線上問題可以觸發(fā) ROLLBACK把代碼回到 S5 HUMAN_REVIEW 通過前的版本任務(wù)狀態(tài)回到 S3 重新執(zhí)行。這一分支保證了治理層不是只負責(zé)“上線前不出錯”還要給“上線后出錯”留退路。3.4 狀態(tài)機與16個Hook怎么配合很多第一次接觸這套設(shè)計的人會混淆Hook 是不是就是狀態(tài)機的一部分其實分工很明確。Hook 是傳感器狀態(tài)機是控制器。Claude Code 的每個動作觸發(fā) HookHook 檢查后把結(jié)論寫成一個“證據(jù)票據(jù)”比如“edit_scope_guard 通過”“evidence_check 發(fā)現(xiàn) 0 條斷言拒絕”。狀態(tài)機不自己去執(zhí)行這些檢查它只看證據(jù)票據(jù)是否齊全。偽代碼是這樣on_event(event): tickets run_hooks(event) if any(ticket.rejected): state.transition_to(REJECTED, reasonticket.reason) return if state.can_advance(tickets): state.advance() watchdogs[state].start_timer()狀態(tài)機只認票據(jù)不認描述。Claude Code 在對話里說一萬遍“我驗證過了”都沒用只有 evidence_check 寫下的帶時間戳的票據(jù)才能讓狀態(tài)從 S3 挪到 S4。這套設(shè)計把“語言承諾”和“實際證據(jù)”徹底分開是治理層能真正防偷懶的核心原因。4. 最小可運行方案手把手搭一個防偷懶治理層前面的設(shè)計聽起來很復(fù)雜但真正搭起來需要動手的東西并不多。這里給出一套最小可運行方案是我在某次模擬項目的一次重構(gòu)流程里逐步整理出來的目錄結(jié)構(gòu)、配置注冊和核心腳本可以直接抄。4.1 目錄結(jié)構(gòu)我習(xí)慣把治理層單獨放在倉庫根目錄下的 harness 目錄里與業(yè)務(wù)代碼隔離harness/ ├── config.yaml # Hook 注冊與狀態(tài)機參數(shù) ├── hooks/ │ ├── intent_guard.py │ ├── plan_validator.py │ ├── edit_scope_guard.py │ ├── command_guard.py │ ├── evidence_check.py │ └── delivery_signoff.py ├── state/ │ └── task_id.json # 每個任務(wù)單獨一個狀態(tài)文件 ├── tickets/ # Hook 通過后生成的證據(jù)票據(jù) └── scripts/ └── run_harness.sh # 統(tǒng)一調(diào)用入口state 目錄里以任務(wù) ID 為單位的 JSON 文件是狀態(tài)機的唯一數(shù)據(jù)源。tickets 目錄相當于流水線上的質(zhì)檢記錄單每個 Hook 通過后會往這里追加一條記錄格式是 JSON Lines方便按時間排序和統(tǒng)計。4.2 配置注冊把語義化Hook名掛到事件回調(diào)上下面是一個最小 config.yaml 的示意。需要注意的是Claude Code 的配置以事件契約為基礎(chǔ)不同版本注冊方式可能略有差異但核心思路都是“把某個外部命令掛到某類事件上”。我給 Hook 起語義化名字是為了讓配置表本身可讀version: 1 task_timeout_seconds: 1800 hooks: on_task_start: - command: python3 harness/hooks/intent_guard.py - command: python3 harness/hooks/plan_validator.py on_before_tool_edit: - command: python3 harness/hooks/edit_scope_guard.py on_before_command: - command: python3 harness/hooks/command_guard.py on_test_finished: - command: python3 harness/hooks/evidence_check.py on_before_final_answer: - command: python3 harness/hooks/delivery_signoff.py注冊邏輯是把語義化事件翻譯成 Claude Code 實際可用的回調(diào)位置。我通常讓 run_harness.sh 作為統(tǒng)一入口在里面根據(jù)事件類型分發(fā)到對應(yīng) Hook 腳本這樣配置變更就只發(fā)生在 harness 內(nèi)部#!/usr/bin/env bash EVENT_TYPE$1 TASK_ID${2:-default} export HARNESS_TASK_ID$TASK_ID export HARNESS_EVENT$EVENT_TYPE python3 harness/hooks/${EVENT_TYPE}.py || exit 1早期版本我直接在 config 里寫死每個命令后來發(fā)現(xiàn)管道一多就亂。統(tǒng)一入口的好處是Hook 失敗時退出碼清晰日志集中在同一處排查問題更快。4.3 一個“假測試檢測器”的完整實現(xiàn)我在 2.5 節(jié)說 evidence_check 是收尾最關(guān)鍵的 Hook這里把它拆出來單獨實現(xiàn)。它要解決的偷懶場景是Claude Code 聲稱跑了測試退出碼 0但測試文件里全是沒斷言的空殼函數(shù)。#!/usr/bin/env python3 import json import re import sys from pathlib import Path TASK_ID sys.argv[1] if len(sys.argv) 1 else default STATE_FILE Path(harness/state) / ftask_{TASK_ID}.json def load_task(): return json.loads(STATE_FILE.read_text()) def count_asserts(test_paths): total 0 for path in test_paths: if not path.exists(): continue content path.read_text() total len(re.findall(r\bassert\b, content)) total len(re.findall(rpytest\.raises, content)) total len(re.findall(rself\.assert, content)) return total def check_test_log(test_log_path, min_failures0): if not test_log_path.exists(): return False, test_log file missing text test_log_path.read_text().lower() failures re.findall(r\bfail(?:ed)?\s*\s*(\d), text) if failures and int(failures[-1]) min_failures: return False, ftest log shows {failures[-1]} failures return True, def main(): task load_task() test_paths [Path(p) for p in task.get(test_files, [])] if not test_paths: print(json.dumps({status: error, reason: no_test_files})) sys.exit(1) asserts count_asserts(test_paths) min_asserts task.get(min_asserts, 1) if asserts min_asserts: print(json.dumps({status: error, reason: fake_test, asserts: asserts})) sys.exit(1) ok, msg check_test_log(Path(task.get(test_log, ))) if not ok: print(json.dumps({status: error, reason: failed_test, detail: msg})) sys.exit(1) print(json.dumps({status: ok, asserts: asserts})) sys.exit(0) if __name__ __main__: main()這個腳本的邏輯很直接先確認測試文件存在再統(tǒng)計斷言關(guān)鍵字數(shù)量最后檢查測試日志里有沒有失敗計數(shù)。三項全過才輸出 ok。它扒掉了“測試命令退出碼 0”這層偽裝因為空測試函數(shù)一樣可以退出碼 0但斷言數(shù)不會撒謊。實際使用中我把 test_log 指向 Claude Code 每次跑測試后自動落盤的日志文件。如果日志缺失直接判定失敗。模型再怎么說“我測過了”在“日志文件不存在”這個事實面前都會現(xiàn)原形。4.4 治理前后同一個任務(wù)的對比為了直觀說明這套治理層的價值我用一個具體任務(wù)來對比給訂單模塊加 Redis 緩存要求不改變返回值語義必須有測試覆蓋命中率和失效邏輯。治理前的典型過程Claude Code 找到一個中間函數(shù)在函數(shù)前加了兩行 redis.get 和 redis.set然后運行一兩個現(xiàn)有用例看到綠勾就回復(fù)“已完成”。它不會去驗證緩存失效邏輯不會補新測試更不會檢查是否改變了返回值語義。整個過程可能只需要兩分鐘但代碼一上線就可能出現(xiàn)緩存穿透或數(shù)據(jù)不一致。治理后的過程是intent_guard 先要求補充“命中率怎么測”“失效策略是什么”plan_validator 要求方案里寫出緩存 key 設(shè)計和過期策略edit_scope_guard 檢查改動只落在訂單模塊和測試目錄test_trigger 在每次改動后強制跑關(guān)聯(lián)測試evidence_check 檢查新測試里有沒有斷言緩存命中次數(shù)和淘汰行為delivery_signoff 確認文檔和回滾方案都齊了。最后才進入人工評審。同樣的任務(wù)治理前是“兩分鐘的自嗨”治理后是“帶證據(jù)鏈的交付”??雌饋砺说辽俨粫霈F(xiàn)那種“交付完才發(fā)現(xiàn)根本沒做對”的返工。治理層要的不是快是穩(wěn)。5. 落地四個月踩過的坑和給團隊的建議再好的設(shè)計落地時都會遇到意料之外的問題。這里把我實際踩過的四個坑完整擺出來包括當時的錯誤判斷和最終修正方式希望能幫你繞開。5.1 坑一Hook重入導(dǎo)致遞歸風(fēng)暴這是我遇到的第一個嚴重事故。我一開始把 command_guard 掛在所有外部命令之前同時把 test_trigger 掛在測試命令之后。結(jié)果出現(xiàn)了一個循環(huán)test_trigger 要執(zhí)行 pytest而 pytest 被 command_guard 檢查command_guard 在檢查過程中又要讀取測試日志而讀取日志這個動作本身又被某個 Hook 監(jiān)聽……最終的表現(xiàn)是 Claude Code 在一連串重復(fù)攔截后直接卡死進程被系統(tǒng)殺掉。解決方法是引入一個重入標記。在所有 Hook 腳本入口檢查環(huán)境變量 HARNESS_REENTRANT如果已經(jīng)存在就直接返回 0不做任何二次檢查。只有最外層調(diào)用才真正執(zhí)行邏輯。這個標記要隨環(huán)境變量傳遞給子進程否則不生效。if [ -n $HARNESS_REENTRANT ]; then exit 0 fi export HARNESS_REENTRANT1這個修復(fù)之后Hook 之間的互相調(diào)用不會再無限遞歸。它也給所有 Hook 定了一條隱式紀律一個 Hook 只做自己該做的那一件事不要在里面觸發(fā)別的工具命令。5.2 坑二狀態(tài)機被“退出碼0”騙了狀態(tài)機剛接入時我把 S3 到 S4 的遷移條件寫成“測試命令退出碼為 0”。上線當天就被打臉Claude Code 創(chuàng)建了一個測試文件里面只有一個空函數(shù)pytest 跑完確實退出碼 0狀態(tài)機乖乖把任務(wù)送到了人工評審。人工評審的人一眼就看出了問題但狀態(tài)機不會看。這件事讓我明白自動驗證必須落在證據(jù)層面而不是進程退出碼層面。后來我把 evidence_check 從“可選增強”提升為“強制前置條件”沒有斷言數(shù)和測試日志摘要的任務(wù)永遠到不了 S4。5.3 坑三超時設(shè)置讓Agent徹底變蠢我最初給每個 Hook 設(shè)置了 3 秒超時理由是“檢查動作要快”。結(jié)果發(fā)現(xiàn) Claude Code 稍微想久一點Hook 就超時失敗整個流程不斷被打斷。看起來是“治理層很嚴格”實際上是治理層把模型逼到?jīng)]法思考。后來我做了兩處調(diào)整。第一Hook 腳本只做輕量檢查超過百毫秒的活全部丟到異步隊列不阻塞主流程。第二區(qū)分兩類超時命令級超時控制單條命令可以嚴格任務(wù)級看門狗控制整個狀態(tài)停留時間必須寬松。正常的一次修 bug 過程可能包含多次嘗試把 S3 的 TTL 壓到三分鐘只會讓模型為了趕時間而找更多捷徑。5.4 坑四規(guī)則清單把上下文塞爆我早期還有個錯誤判斷就是覺得規(guī)則寫得越多越好。于是把幾十條禁止行為、樣例、邊界說明全部注入到上下文中希望模型“耳提面命”。結(jié)果上下文憑空多出幾千 token模型反而記不住重點關(guān)鍵約束經(jīng)常被忽略。后來的做法是把規(guī)則從 prompt 移到 Hook 代碼里。禁止改哪些目錄寫在 edit_scope_guard 的路徑清單里哪些命令危險寫在 command_guard 的黑名單里驗收標準怎么寫寫在 plan_validator 的校驗邏輯里。上下文里只保留少量原則性說明。這樣做的效果是規(guī)則不再靠模型“記住”來生效而是靠外部腳本強制攔截。只有當模型準備違反規(guī)則時規(guī)則才以證據(jù)形式回到它的視野里。5.5 建議漸進式灰度先裝4個Hook再上狀態(tài)機如果你準備在團隊里推這套治理層我建議不要一次性全部鋪開。第一天就上 16 個 Hook 和 8 關(guān)狀態(tài)機團隊會覺得“工具不好用”“太卡流程”然后迅速放棄。我推薦分三批走。第一批只開四個 Hookintent_guard、edit_scope_guard、command_guard、evidence_check。這四個能把最痛的四類問題管住需求不清、范圍蔓延、危險命令、假測試。用一周觀察日志看命中率也就是 Hook 真正攔截了多少次問題。第二批再把狀態(tài)機的八關(guān)完整跑起來。此時 Hook 已經(jīng)有了證據(jù)票據(jù)狀態(tài)遷移可以直接復(fù)用不需要改腳本。這一階段的重點是讓團隊適應(yīng)“任務(wù)過檢品線”的節(jié)奏接受人工評審不是額外負擔。第三批按團隊痛點開剩余 Hook。比如總有人忘更新文檔就開 delivery_signoff總有人引入不必要依賴就開 dependency_probe 和 tech_choice_review。最后再分享一個我在落地后期體會最深的小技巧不要用“任務(wù)通過率”來衡量治理層好壞要看“攔截命中率”。一個治理層如果每天都能攔下幾個本會被帶到交付階段的假測試、越界改動或危險命令說明它在實實在在干活。相反如果一整周都零命中不是團隊變自覺了而是規(guī)則設(shè)計得太寬松該收緊閾值了。讓 wow-harness 從“看得見”變成“攔得住”再從“攔得住”變成“不用攔也自然不偷懶”這是我認為最穩(wěn)妥的一條路。