AI Coding Agent 交出一個大型 PR,測試全綠、功能也能跑,你就可以放心合併嗎?未必。它仍可能留下 Code Sloppiness:把同一段驗證流程複製三次,或把後續需求一層層塞進原本已經很複雜的函式;今天看似省時,下一輪修改卻會同時碰到更多地方。
這篇 Code Sloppiness 教學會帶你看懂 Verbosity 與 Erosion 兩個可觀測指標,建立可比較的 baseline,再把它們做成「先警告、後收緊」的 CI Budget。讀完後,你會有一套能放進 Coding Agent 工作流的稽核方法,也知道哪些情況一定要交回人類判斷。
本文寫給會複製終端指令、但第一次替 Agent 建品質 gate 的讀者。先統一幾個詞:PR 是一包等待審查的變更;repo 是程式碼與版本歷史的專案;baseline 是拿來對照的起點快照;CI 是通常在推送或 PR 後自動執行的檢查流程;checkpoint 是一段連續任務中的可驗收存檔點;fixture 則是為了驗證工具而刻意做小的測試專案。
先說結論:Code Sloppiness 不是「程式不能跑」,而是測試過關後仍在累積的冗長重複與複雜度集中。記住這條式子:可靠合併 = 功能測試綠燈 + Sloppiness 趨勢不惡化 + 人工抽查。CI 管的是同一個 repo 的變化,不是拿一個分數替程式碼判生死。
Code Sloppiness 是什麼?先拆成兩種債
SlopCodeBench v2 論文用「code slop」這個口語名詞包住兩個自行操作化的指標:Verbosity 看冗餘與重複程式碼占多少;Structural Erosion 看複雜度是否集中在已經難改的函式。本文只沿用這個窄定義,不把它當整體軟體品質標準。兩者都介於 0 與 1,越低代表這兩項局部警報越少。
Verbosity:新增了多少可以少寫的程式碼?
論文的核心概念可寫成 Verbosity = 被 AST 規則或 clone detector 標記的不重複行數 ÷ 全部 SLOC。AST 是程式語法的樹狀結構,SLOC 是有效原始碼行,clone detector 則是相似碼偵測器。AST 規則找手工整理的冗長模式,clone detector 找重複片段;同一行被兩邊命中只算一次。它不是單純計算 PR 行數:一個長但必要的解析器,和三份幾乎相同的驗證器,意義完全不同。
Erosion:複雜度是否堆進少數函式?
論文先為每個函式計算 mass(f) = CC(f) × √SLOC(f),再以 CC > 10 的函式 mass 占全部 mass 的比例作為 Erosion。CC 是 cyclomatic complexity,也就是控制流程中的獨立路徑數;平方根會降低純行數的權重,讓分支複雜度更突出。門檻 10 來自既有複雜度分級,但它只是量測約定,不是「第 11 個分支突然變成爛碼」的自然定律。

研究發現了什麼?又沒有證明什麼?
在 2026 年 5 月修訂的 v2 實驗裡,研究者讓 15 個模型/Agent 組合連續處理 36 個人工設計的 Python 問題,共 196 個 checkpoint。Verbosity 在 75.5% 的 Agent 軌跡上升,Erosion 在 77% 的軌跡上升;Agent checkpoint 的平均值分別是 0.44 與 0.68,對照的開源 Python repo HEAD 樣本是 0.19 與 0.34。
這組差距值得當警報,卻不能直接翻譯成「AI 程式碼品質差兩倍」。兩邊不是同一題由人與 Agent 對打:任務、repo 年齡與開發流程都不同;實驗還會在每個 checkpoint 清除對話狀態、不提供測試回饋。更重要的是,論文附錄中 Erosion 與下一個 checkpoint 是否通過的相關係數只有 −0.018。它描述局部結構,沒有證明降低分數會提高正確率。
研究的 anti-slop 與 plan-first 提示能降低初始指標;整體而言,多數設定的品質仍從起點到終點惡化,論文明列 GPT-5.4 anti-slop 為改善的例外。這些提示平均每個 checkpoint 成本增加 12.1%。所以這篇不會教你用 prompt 追分;我們要做的是把指標當煙霧警報器,再由測試與 reviewer 找真正的火。
實作第一步:建立可比較的 repo baseline
最容易犯的錯,是拿不同工具版本、不同 exclude、不同目錄算出的分數畫成一條線。截至 2026 年 9 月 12 日,SlopCodeBench 官方 runner也固定 checker 版本;因為規則集或計分實作一變,數字就可能失去可比性。你的 baseline 至少要保存:commit SHA、工具版本、完整命令、掃描路徑、exclude、設定檔 hash 與原始 JSON。
1. 固定範圍與版本
截至 2026 年 9 月 12 日,下面使用獨立發布的 scb-check 0.2.0,並用 uvx 建立隔離環境。先從真正由團隊維護的 src/ 開始;generated code、vendor、migration、snapshot 與大型 fixture 分開報告。把掃描政策寫進專用設定檔並 commit,不能為了讓分數漂亮而在 PR 裡悄悄改 exclude。
# scb-check.toml;以下路徑只是示例,要依 repo 調整
exclude = ["src/generated/**", "vendor/**", "tests/fixtures/**"]
context = 1
uvx scb-check==0.2.0 --version
set +e
uvx scb-check==0.2.0 check --report --include-all \
--config scb-check.toml src > scb-head.json
rc=$?
set -e
# v0.2.0:0 代表沒有 finding,1 代表有 finding;2 才是用法/設定失敗
if [ "$rc" -gt 1 ]; then exit "$rc"; fi
--include-all 會改變被納入的 finding 集合,必須在 base 與 head 保持一致。還要注意版本語意:論文 runner 目前釘住 0.1.3;獨立工具 0.2.0 的 Verbosity 組件與 exit code 已有差異,因此本文實作數字不能和論文均值混算。
2. 用最小 fixture 確認兩個指標不是同一件事
本文先在 Darwin arm64 上用 uvx scb-check==0.2.0 跑三個刻意簡化的 Python fixture,保留完整命令與 JSON:單一 13 SLOC 函式得到 V=0、E=0;把相同邏輯複製成兩檔 26 SLOC 後得到 V=1、E=0;另一個只有一個、但 CC 超過 10 的 25 SLOC 函式得到 V=0、E=1。這證實命令能把「重複」與「複雜度集中」分開,不是重現 36 題 benchmark,也不是 production threshold。
同一份 clone fixture 在 0.1.3 仍算出 V=1,但 process exit code 是 0;0.2.0 則是 1。這正是 CI 必須固定版本、也不能只抄網路 shell snippet 的理由。
3. 每個 Agent checkpoint 都留下同一組證據
- CP0:在人類已維護、測試通過的 base commit 建 baseline。
- CP1~CPn:Agent 每完成一個可驗收需求就 commit,再跑相同 checker 與測試。
- 記錄:保存
V、E、flagged SLOC、clone LOC、high-CC mass、測試結果與人工 disposition(對每個警報寫下接受、修正或誤判)。 - 比較:計算
ΔV = V_head − V_base、ΔE = E_head − E_base;連續任務再看(Vn − V0) ÷ n與(En − E0) ÷ n的簡單斜率。
4. 用 comparator 把差值轉成 warning 或 block
下面的最小腳本同時比較兩個比例與兩個原始量;budget 從環境變數傳入,避免把文章中的數字誤當全產業標準。SLOP_MODE=warn 只留下 GitHub annotation,block 才回傳 exit code 1。任何欄位缺漏、非數字或 analyzer exit code 大於 1,則一律視為工具失敗,而不是「沒有債務」。
# scripts/slop_budget.py
import json, math, os, sys
if len(sys.argv) != 3:
raise SystemExit("usage: slop_budget.py BASE.json HEAD.json")
base = json.load(open(sys.argv[1], encoding="utf-8"))
head = json.load(open(sys.argv[2], encoding="utf-8"))
limits = {
"verbosity": "MAX_DELTA_V",
"erosion": "MAX_DELTA_E",
"verbosity_flagged_loc": "MAX_DELTA_FLAGGED_LOC",
"high_cc_mass": "MAX_DELTA_HIGH_CC_MASS",
}
mode = os.environ.get("SLOP_MODE", "warn")
if mode not in {"warn", "block"}:
raise SystemExit("SLOP_MODE must be warn or block")
violations = []
for metric, env_name in limits.items():
try:
delta = float(head[metric]) - float(base[metric])
budget = float(os.environ[env_name])
except (KeyError, TypeError, ValueError) as error:
raise SystemExit(f"invalid {metric}/{env_name}: {error}")
if not math.isfinite(delta) or not math.isfinite(budget) or budget < 0:
raise SystemExit(f"invalid numeric policy for {metric}")
print(f"{metric}: delta={delta:.6f}, budget={budget:.6f}")
if delta > budget:
violations.append(metric)
if violations:
print("::warning::Sloppiness budget exceeded: " + ", ".join(violations))
if mode == "block":
raise SystemExit(1)
在 CI 先取 origin/main 與 PR 的共同祖先,使用兩個乾淨 worktree 跑同一版 checker。base 與 head 各用自己根目錄下的設定檔,先以 cmp 要求兩份 bytes 完全相同,確保相對 exclude 在兩棵 tree 都有相同語意;若 policy 真的要改,應另走審核。0.2.0 的 finding exit code 1 必須被保留成可比較的 JSON;只有大於 1 才中止 pipeline。CI checkout 也要取得完整的比較 ref,例如 GitHub Actions 使用 fetch-depth: 0:
set -euo pipefail
BASE_SHA="$(git merge-base HEAD origin/main)"
BASE_DIR="$(mktemp -d)"
git worktree add --detach "$BASE_DIR" "$BASE_SHA"
trap 'git worktree remove --force "$BASE_DIR"' EXIT
BASE_CONFIG="$BASE_DIR/scb-check.toml"
HEAD_CONFIG="scb-check.toml"
test -f "$BASE_CONFIG" && test -f "$HEAD_CONFIG"
cmp -s "$BASE_CONFIG" "$HEAD_CONFIG" || {
echo "scb-check policy changed; review separately" >&2
exit 2
}
shasum -a 256 "$HEAD_CONFIG"
run_scb () {
set +e
uvx scb-check==0.2.0 check --report --include-all \
--config "$3" "$1" > "$2"
rc=$?
set -e
if [ "$rc" -gt 1 ]; then return "$rc"; fi
}
run_scb "$BASE_DIR/src" scb-base.json "$BASE_CONFIG"
run_scb src scb-head.json "$HEAD_CONFIG"
# Shadow mode:0 只代表「任何上升都顯示」,不是 production 標準
MAX_DELTA_V=0 MAX_DELTA_E=0 \
MAX_DELTA_FLAGGED_LOC=0 MAX_DELTA_HIGH_CC_MASS=0 \
SLOP_MODE=warn python scripts/slop_budget.py scb-base.json scb-head.json
等 advisory 期累積足夠的人工 disposition,再把四個環境變數改成團隊核准的 repo-specific budget,最後才把 SLOP_MODE 改為 block。baseline 要由 reviewer 確認後固定;CI 不應在每次 PR 自動把 head 寫回 baseline,否則新增債務會被當場洗白。
只看比例仍可能被分母稀釋:多加一批未被標記的行,V 可能下降,但真正的重複行並沒有消失。因此 raw component 也要一起保存。若你還在設計 Agent 的 workspace、權限與事件紀錄,先補上 AI Agent Harness;功能正確性則用 Hidden Oracle 與 Mutation Test 另設 gate。
把 CI Budget 做成三段式 ratchet
本文的 CI Budget 是額外的工程 policy layer。SlopCodeBench v2 定義量測,scb-check 0.2.0 輸出報告;本文再加入 base/head 比較,讓 legacy repo 保留經審查的基準,但不讓新 PR 無聲增加同類債務。每個 repo 自己定義 budget_V、budget_E、新 clone LOC 與 high-CC mass 上限,不能把論文的人類均值直接搬來當紅線。
階段 A:只警告,先量 false positive
前兩到四週把每個 finding 標成「接受、需重構、規則誤判、刻意例外」,同時記錄 reviewer 花多久處理。scanner crash、設定錯誤或漏掃則要明確失敗;不要用整個 job 的 continue-on-error 把工具壞掉也塗成綠色。
階段 B:只阻擋新增債務
當一條規則的誤判已可控,就比較 base 與 head:ΔV ≤ budget_V 且 ΔE ≤ budget_E 才通過;同時檢查 raw component,防止分母遊戲。這和 Sonar 的 new-code 思路相近:先守住新變更,而不是要求一個老 repo 在第一天還清所有債。
階段 C:高風險區才設 hard gate
付款、權限、資料遷移等模組可設更窄的新增債務預算;一般 UI adapter 或 generated schema 則可能只警告。hard gate 應擋「新 clone 超出已校準 budget」「高複雜度 mass 明顯增加」「分析器沒有完成」,而不是看到任一 CC > 10 就拒絕合併。大型 PR 也應先按可獨立驗收的需求拆 checkpoint;Google 的 Small CL 指南同樣把自包含變更放在固定行數之前。
6 種誤判,看到紅燈先問什麼?
- Generated code:來源模板可維護嗎?若可,生成結果應獨立報告。
- Migration/schema:重複是否是不可逆歷史紀錄,而不是抽象化對象?
- 測試 fixture:保留明確案例是否比共用 helper 更容易讀?
- Parser/protocol dispatch:高分支是否直接對應一份穩定規格?拆函式會不會只把耦合藏起來?
- 相似但會分岔的業務流程:現在硬做 DRY,是否反而把兩個生命週期綁死?
- AST 規則命中:規則是否同時通過正例、反例與真實 PR 抽查?
Cyclomatic complexity 看不到命名、模組邊界、依賴方向、資料流、安全性與商業正確性;clone detector 也不知道兩段相似碼是否應共享抽象。這些也是這場 HN 討論最有力的質疑:局部指標很容易修,全域架構債才可能最昂貴。處理方式不是丟掉量測,而是保留獨立的 architecture review;也不要把一次 Agent 過度修改和跨 checkpoint 的結構惡化混成同一個問題。
給 Coding Agent 的實際工作流
- 由人類把一個大型需求拆成可獨立驗收的 checkpoint,固定 seed commit、工具版本與掃描設定。
- Agent 完成 CP1 後,先跑功能測試,再產生 sloppiness JSON;兩者不互相代替。
- CI 留下 base/head 差值與 raw component,advisory 期不自動要求 Agent 追分。
- Reviewer 抽查最大的 clone 與 high-CC function,寫下接受或重構理由。
- 下一個 checkpoint 延續同一條時間序列;只有經校準的新增債務規則才進 hard gate。
- 每月回看實際結果:返工時間、review latency、escaped defect 是否改善;若只剩分數變漂亮,就停下來重審 policy。
這套流程處理的是 repo-level 趨勢;如果你更在意 reviewer 能否快速理解變更,可搭配 Cognitive Debt 認知債與 PR Style Drift CI。想系統化練習 Agent harness、測試與 release gate,也可以從 AlphaLab 課程開始。
Code Sloppiness 常見問題
1. Code Sloppiness 高,代表程式有 bug 嗎?
不代表。Verbosity 與 Erosion 是局部靜態 proxy;bug 要靠契約測試、整合測試、security check 與真實執行證據判定。
2. 測試全綠,還需要看這兩項嗎?
需要,但用途不同。測試回答目前行為是否符合已寫出的判準;sloppiness 趨勢提示下一輪修改可能變得更難。
3. CC 超過 10 就應該拆函式嗎?
不應自動拆。先看它是否對應 parser、protocol dispatch 或清楚的決策表,再檢查拆分會不會只移動複雜度、增加耦合。
4. 所有 clone 都應該 DRY 嗎?
不應。兩段現在相似、未來卻各自演化的流程,過早共用抽象可能製造錯誤耦合;clone finding 是 review 起點。
5. 可以直接採用論文的人類平均值當 CI 上限嗎?
不可以。你的語言、架構、generated code 與規則版本都不同;應先用同一個 repo 的 base/head 分布校準。
6. 為什麼還要記 raw LOC 與 mass?
為了防止比例失真。Verbosity 與 Erosion 都有分母,只看比例可能把新增債務藏在更多普通程式碼裡。
7. JavaScript、Rust 專案也能照做嗎?
可以沿用 baseline/checkpoint/budget 方法,但量測能力不完全相同。截至 scb-check 0.2.0 的官方支援表,Python 有完整 AST/structural 規則;列出的其他語言主要使用 clone、SLOC 與 complexity。導入前先用自己的 fixture 驗證 coverage。
8. CI 一開始就該 hard fail 嗎?
通常先 advisory。收集誤判與例外類型後,只對「分析器失敗」和已校準的新債務條件 hard fail;否則團隊很快會學會忽略警報。
給新手的 5 個重點
- 先分證據:測試看功能,Verbosity 看冗餘,Erosion 看複雜度集中。
- 先鎖版本:同一工具、規則、掃描範圍與 exclude 才能比較。
- 先看差值:管 base 到 head 的變化,不用陌生 repo 的絕對分數審判自己。
- 比例加原始量:同步看 flagged LOC、clone LOC 與 high-CC mass。
- 人類保留裁量:誤判、刻意重複與全域架構只能靠 context 決定。
接著閱讀
左右滑動查看更多推薦
結語:把分數當煙霧警報,不是法官
Code Sloppiness 最有用的地方,不是替「好程式碼」發一張單一證書,而是讓你看見 Agent 連續修改時,冗餘與複雜度正在往哪裡走。今天就選一個小型 repo:固定 scb-check 版本,在 base 與下一個 Agent checkpoint 各留一份 JSON,先做 warning 與人工 disposition。當你能解釋每一個紅燈,再讓其中少數變成 CI Budget。
更多 Coding Agent、模型與實作教學,可到 AlphaLab AI 專區。






