OCR 讀你的 Git diff,把變更檔交給可呼叫工具的 LLM Agent,產生精確到行的結構化評論。它能讀完整檔案、搜尋 codebase、查看其他變更檔做脈絡,產出「深層評論」而非表面的 diff 回饋。
如果你用過 Claude Code 這類通用 agent 的 review skill,一定遇過這些痛點:
| 問題 | 現象 |
|---|---|
| 覆蓋不全 | 較大的變更集,agent 傾向「偷工」——只挑部分檔案審,漏掉其他 |
| 位置漂移 | 評論的檔案與行號常對不上實際程式碼 |
| 品質不穩 | 自然語言驅動的 skill 難除錯,prompt 微調就會讓品質大幅波動 |
根因:純語言驅動的架構,對評審流程缺乏硬性約束。
OCR 把「不能出錯的部分」交給工程邏輯,把「動態決策」交給 Agent。
message_en.properties 與 message_zh.properties),每個 bundle 以子 agent 隔離執行——大變更集也穩、可並行。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
每個通過過濾的檔案,OCR 啟動一個子 agent,在自己的 goroutine 中跑,受 --concurrency(預設 8)約束。
PLAN_MODE_LINE_THRESHOLD)才跑。單次 LLM 呼叫、不給工具,產出清單作為 main prompt 的指引。MAX_TOOL_REQUEST_TIMES)。收集 code_comment 呼叫作為評審評論。記憶體超過預算時觸發三分區壓縮。長的工具呼叫迴圈終會溢出 context。OCR 用 MAX_TOKENS = 58888 的預算管理:
| 閾值 | 動作 |
|---|---|
| 60% | 啟動非同步背景壓縮,當前迴圈繼續 |
| 80% | 下一個請求前同步執行壓縮 |
訊息分三區:frozen(前 2 則 system+user)、compress(摘要成一則)、active(最近可裝進預算的完整輪次)。compress 區以 XML 渲染交給 MEMORY_COMPRESSION_TASK,摘要包在 <previous_review_summary> 標籤內。
existing_code 用滑動視窗與 diff 比對,算出精確 start_line/end_line。RE_LOCATION_TASK 請模型重新錨定。REVIEW_FILTER_TASK 對照 diff 檢查評論,移除可證明為錯的。file_read_diff/code_search 理解,但不對「其他檔」的發現發表評論。問自己一個問題:哪些事情是「不能出錯」的?檔案選擇、規則匹配、排除邏輯——這些用工程程式碼(確定性)處理。哪些是「需要動腦」的?理解 diff、判斷缺陷、寫評論——這些用 LLM Agent。混合的關鍵是界線:確定性管線為 Agent 準備好精確的輸入,Agent 只需要專注在推理。
frozen(前 2 則 system+user)永遠不壓縮——這是 Agent 的「記憶起點」。compress 區是已被摘要的歷史。active 是最近的完整輪次。壓縮發生在 60%(非同步,迴圈繼續)和 80%(同步,阻塞下一次請求)。實務意義:60% 時壓縮幾乎無感,80% 時你會感受到延遲。這就是為什麼 --max-tokens 很重要——越大的預算,壓縮觸發越晚。
OCR 刻意不做的三件事都有同一個原因:確定性。
這些選擇讓 OCR 的行為可重複、成本可預測。重試和跨檔推理是包住它的 CI 管線的責任。
你有一檔 400 行的重構檔(internal/checkout/order.go),想要逐段觀察:plan 階段產出什麼指引、main 迴圈裡 Agent 怎麼用工具、記憶壓縮在什麼時候介入、最後評論如何被定位與過濾。這比「跑完看結果」能讓你真正理解管線。
ocr review --path internal/checkout/order.go --concurrency 1 \
--format json --audience agent
ocr session list | head -3
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——看它列的檢查清單合不合理。
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 ← 錨定順利,沒觸發
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 的短期記憶在縮水」。
# 長迴圈: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 區摘要品質受模型影響 | 調大預算延後壓縮;視需要拆成多個小檔審 |
Max tool requests reached),你會調 --max-tools、調 --max-tokens、還是拆檔?各自的 trade-off 是什麼?