運作原理

確定性工程 × Agent 混合,為何比通用 agent 更省 token 又精準

一句話

OCR 讀你的 Git diff,把變更檔交給可呼叫工具的 LLM Agent,產生精確到行的結構化評論。它能讀完整檔案、搜尋 codebase、查看其他變更檔做脈絡,產出「深層評論」而非表面的 diff 回饋。

通用 Agent 的問題

如果你用過 Claude Code 這類通用 agent 的 review skill,一定遇過這些痛點:

問題現象
覆蓋不全較大的變更集,agent 傾向「偷工」——只挑部分檔案審,漏掉其他
位置漂移評論的檔案與行號常對不上實際程式碼
品質不穩自然語言驅動的 skill 難除錯,prompt 微調就會讓品質大幅波動

根因:純語言驅動的架構,對評審流程缺乏硬性約束。

核心設計:確定性工程 × Agent 混合

OCR 把「不能出錯的部分」交給工程邏輯,把「動態決策」交給 Agent。

確定性工程 —— 硬約束

Agent —— 動態決策

結果:同一個底層模型下,OCR 達到顯著更高的 Precision 與 F1,同時只消耗約 1/9 的 token、完成得更快。代價是 Recall 較低——這是刻意取捨:優先精準、降低雜訊。

執行流程一覽

ocr review
  → bootstrap         解析 LLM 端點、載入模板/工具/規則
  → diff provider     git diff → []model.Diff(workspace/commit/range)
  → filter & rules    五重門過濾 + 每檔選規則
  → subtask dispatch  每檔一個子 agent(concurrency=8):plan → main 迴圈
  → output writer     行解析 + 評審過濾,渲染 text 或 JSON

每檔子任務:plan + main

每個通過過濾的檔案,OCR 啟動一個子 agent,在自己的 goroutine 中跑,受 --concurrency(預設 8)約束。

記憶壓縮:三分區策略

長的工具呼叫迴圈終會溢出 context。OCR 用 MAX_TOKENS = 58888 的預算管理:

閾值動作
60%啟動非同步背景壓縮,當前迴圈繼續
80%下一個請求前同步執行壓縮

訊息分三區:frozen(前 2 則 system+user)、compress(摘要成一則)、active(最近可裝進預算的完整輪次)。compress 區以 XML 渲染交給 MEMORY_COMPRESSION_TASK,摘要包在 <previous_review_summary> 標籤內。

評論處理流水線

  1. 行解析——existing_code 用滑動視窗與 diff 比對,算出精確 start_line/end_line。
  2. 重新定位(可選回退)——行解析在複雜 diff 上失敗時,跑 RE_LOCATION_TASK 請模型重新錨定。
  3. 評審過濾——REVIEW_FILTER_TASK 對照 diff 檢查評論,移除可證明為錯的。
  4. 二輪行解析——頂層命令對完整評論集重跑,捕獲跨檔或重新定位的評論。

什麼不自動化

📖 教學解說:運作原理深度導讀

確定性 × Agent 混合的本質

問自己一個問題:哪些事情是「不能出錯」的?檔案選擇、規則匹配、排除邏輯——這些用工程程式碼(確定性)處理。哪些是「需要動腦」的?理解 diff、判斷缺陷、寫評論——這些用 LLM Agent。混合的關鍵是界線:確定性管線為 Agent 準備好精確的輸入,Agent 只需要專注在推理。

三分區壓縮的實務意義

frozen(前 2 則 system+user)永遠不壓縮——這是 Agent 的「記憶起點」。compress 區是已被摘要的歷史。active 是最近的完整輪次。壓縮發生在 60%(非同步,迴圈繼續)和 80%(同步,阻塞下一次請求)。實務意義:60% 時壓縮幾乎無感,80% 時你會感受到延遲。這就是為什麼 --max-tokens 很重要——越大的預算,壓縮觸發越晚。

「不自動化」的設計哲學

OCR 刻意不做的三件事都有同一個原因:確定性。

  • 端點發現不回退 → 避免猜錯 LLM 用什麼
  • 子 agent 失敗不重試 → 避免放大失敗、確保行為可預測
  • 無跨檔推理 → 避免複雜度爆炸、確保 per-file 確定性

這些選擇讓 OCR 的行為可重複、成本可預測。重試和跨檔推理是包住它的 CI 管線的責任。

練習 / 驗收清單

  • 能解釋「確定性 × Agent」比純語言 skill 強的原因
  • 能描述 plan+main 兩個階段各做什麼
  • 能解釋三分區壓縮如何避免溢出
  • 能說出 OCR 刻意不自動化的三件事
看完這頁你應該能說出:為什麼「確定性 × Agent」比純語言 skill 強、plan+main 兩個階段各做什麼、三分區壓縮如何避免溢出、以及哪三件事 OCR 刻意不自動化。

延伸閱讀:架構總覽 · 6 個工具 · 評審規則

① 進階真實情境 Worked Example:追蹤一顆「大檔 + 記憶壓縮」的完整旅程

情境

你有一檔 400 行的重構檔(internal/checkout/order.go),想要逐段觀察:plan 階段產出什麼指引、main 迴圈裡 Agent 怎麼用工具、記憶壓縮在什麼時候介入、最後評論如何被定位與過濾。這比「跑完看結果」能讓你真正理解管線。

Step 1 — 只審一檔,把 concurrency 降到 1(好觀察)

ocr review --path internal/checkout/order.go --concurrency 1 \
  --format json --audience agent

Step 2 — 找 session id

ocr session list | head -3

Step 3 — 從 JSONL 抽出 plan 階段的產出

jq -r 'select(.type=="llm_request" and (.data.task|tostring|contains("PLAN"))) | .data.messages[-1].content' \
  ~/.opencodereview/sessions/*/$(ls -t ~/.opencodereview/sessions/*/ | head -1)*.jsonl

plan 指引會寫進 main 的 prompt——看它列的檢查清單合不合理。

Step 4 — 觀察記憶壓縮何時介入

jq -r 'select(.type=="llm_request") | .data.task' < session.jsonl | sort | uniq -c
1  PLAN_TASK
14 MAIN_TASK
2  MEMORY_COMPRESSION_TASK   ← 在長迴圈中壓縮了 2 次
1  REVIEW_FILTER_TASK
0  RE_LOCATION_TASK          ← 錨定順利,沒觸發

Step 5 — 看 filter 移除了什麼

jq -r 'select(.type=="tool_call" and .data.tool=="code_comment") | .data.result' < session.jsonl | jq '.comments | length'

為什麼選這條審查路徑

審「一檔、低併發、高 token」是為了讓管線的每個階段都被觸發且可觀察:400 行 ≥ 50 行門檻所以 plan 會跑;長迴圈所以壓縮會介入;評論多了所以 filter 會工作。用 --path 限制到單檔 + --concurrency 1,你看到的 JSONL 就是這檔的完整人生——這是最便宜的「管線教學示範」。

預期產出

plan: 檢查清單 5 項
main: 14 輪(file_read×6, code_search×3, code_comment×2, task_done×1)
compression: 2 次(60% 非同步 + 80% 同步)
filter: 5 條 code_comment → 3 條存活
→ 你在報告看到的 3 條,是這整個旅程的收斂結果

② 深入原理擴充:記憶壓縮是「品質風險點」不是「省錢技巧」

壓縮的語意是「摘要」,摘要會丟資訊

compress 區把歷史輪次壓成一則 <previous_review_summary> 摘要。對 Agent 而言,「第 3 輪看過某檔的某段」變成「摘要裡的一句話」。壓縮不是無損的——這正是 60%(非同步)與 80%(同步)兩級設計的意義:非同步壓縮讓迴圈繼續,80% 才阻塞。但無論哪級,壓縮都代表「Agent 的短期記憶在縮水」。

「看起來通過 review 但其實有隱患」的案例

# 長迴圈:Agent 在第 5 輪透過 code_search 發現了某個跨檔風險
# 第 9 輪觸發壓縮 → 摘要只寫「檢查了 auth flow」
# 第 14 輪 Agent 只記得「auth flow 檢查過了」,實際上該檢查的細節已從記憶消失
# → 最終評論少了那條跨檔發現,但整顆 review 看起來「正常完成」

隱患不是評論出錯,而是「被壓縮掉的發現」看起來像「沒發現」——報告照常綠燈。防禦:對大檔/長迴圈調高 --max-tokens(壓縮觸發越晚越好);並在 viewer 檢查 memory_compression_task 的摘要內容——如果壓縮後的摘要明顯遺漏了早期輪次的關鍵脈絡(工具呼叫記錄、檔案名),就代表該加大預算或拆檔。

③ 診斷式疑難排解

症狀可能原因解決方案
同一檔每次重審評論數不同非確定性 LLM + 壓縮時機因 token 浮動而不同固定 --max-tokens 與模型;用 -c <sha> 固定範圍
檔案很大時常觸發同步壓縮(卡頓)80% 同步壓縮阻塞了迴圈調高 --max-tokens;或拆檔/縮小 --path
plan 階段看起來「沒有產出指引」變更行數 < 50,plan 自動跳過這是設計;要看到 plan 需大檔(≥50 行變更)
RE_LOCATION_TASK 頻繁觸發錨定在複雜 diff 上失敗,系統在「救場」改善 existing_code 唯一性;檢查 diff 格式
壓縮摘要遺漏早期脈絡compress 區摘要品質受模型影響調大預算延後壓縮;視需要拆成多個小檔審

④ 進階挑戰題

  1. 挑戰一:畫出 60% 與 80% 兩級壓縮在「時間軸」上的差異——哪個會讓你感受到延遲?為什麼設計成兩級?
  2. 挑戰二:解釋「plan 是唯讀分析」如何影響 plan 的工具可用性(對照 tools.html 的可用性表)。
  3. 挑戰三:一檔在 30 輪內跑不完(Max tool requests reached),你會調 --max-tools、調 --max-tokens、還是拆檔?各自的 trade-off 是什麼?