架構總覽

從按下回車到 JSON 落在終端,ocr review 內部如何運作

高層管線

ocr review
  → bootstrap         解析 LLM 端點(config → env → rc files)、載入模板/工具/規則
  → diff provider     git diff / ls-files / show → []model.Diff
  → filter & rules    五重門過濾(preview.go)+ 每檔選規則
  → subtask dispatch  每檔並行(concurrency=N):Plan 階段(可選)→ Main 迴圈 → Comments
  → output writer     同步行解析 + 評審過濾;渲染 text 或 JSON

編排邏輯在 internal/agent/,分布在 agent.go(主迴圈與分發)、compression.go(記憶壓縮)、preview.go(檔案過濾)、util.go(輔助)。兩個入口點:Agent.Run(管線頂部)與 Agent.dispatchSubtasks(per-file 扇出)。

diff provider

internal/diff/git.go 定義 Provider 結構,未匯出欄位 mode 選三種模式之一:

模式觸發返回
Workspace無參數staged + unstaged + untracked 變更
Commit--commitgit show <sha> 引入的變更
Range--from --tomerge-base(a,b)..b

每個 diff 帶 old/new path、hunk、插入/刪除計數、二進位旗標、重新命名偵測。untracked 檔從磁碟讀取當整檔新增處理。

五重門過濾

whyExcluded 依序檢查:binaryuser_excludeuser_include(立即保留)→ unsupported_extdefault_path。雜訊目錄(vendor/node_modules/…)在更早的 diff-provider 層過濾。

per-file 子任務:plan + main

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

Plan 階段(可選)

threshold := template.PlanModeLineThreshold     // 50
changeLines := d.Insertions + d.Deletions
if changeLines < threshold { skip plan }

小 diff 跳過 plan。較大 diff 做一次 PLAN_TASK 呼叫(不給工具),產出清單作為 {{plan_guidance}}

Main 迴圈

loop up to MAX_TOOL_REQUEST_TIMES (default 30):
    response = llm.complete(messages, tools)
    if response.toolCalls is empty: nudge "You did not successfully call any tools…"
    for each call: execute → collect result
    if any call was task_done: break
    addNextMessage(...)      # 可能觸發壓縮

五個退出條件:呼叫 task_done、30 輪耗盡、連續 3 輪無有效結果、context 取消、壓縮無法壓回閾值以下。

記憶壓縮

閾值(MAX_TOKENS=58888)動作
60%非同步背景壓縮,迴圈繼續
80%同步壓縮(下一個請求前)

三分區:frozen(前 2 則)、compress(摘要成一則)、active(最近可裝下的完整輪次)。

評論處理流水線

  1. 行解析(worker 內)——existing_code 滑動視窗匹配 → 精確行號。
  2. 重新定位(可選回退)——RE_LOCATION_TASK
  3. 評審過濾——REVIEW_FILTER_TASK 移除可證明為錯的評論。
  4. 二輪行解析——頂層對完整評論集重跑 diff.ResolveLineNumbers

模板與佔位符

internal/config/template/task_template.json 含五個 prompt:PLAN_TASKMAIN_TASKMEMORY_COMPRESSION_TASKREVIEW_FILTER_TASKRE_LOCATION_TASK。佔位符如 {{system_rule}}{{diff}}{{change_files}}{{requirement_background}}

持久化

~/.opencodereview/sessions/<encoded-repo-path>/<session-id>.jsonl

每行一個事件(prompt、LLM 回應、工具呼叫、結果、評論)。Web UI(ocr viewer)直接讀這些檔案——沒有資料庫,只有 append-only 日誌。

哪些不自動化

這些選擇讓運行按檔案確定性、成本可預測。

源碼地圖

關注點檔案
頂層命令分派cmd/opencodereview/main.go
review 參數解析cmd/opencodereview/flags.go
agent 編排與壓縮internal/agent/
檔案過濾 / 預覽internal/agent/preview.go
diff 載入internal/diff/git.go
規則解析鏈internal/config/rules/system_rules.go
工具註冊表internal/tool/
LLM 端點解析器internal/llm/resolver.go
會話 JSONL 寫入internal/session/persist.go
Web 檢視器internal/viewer/server.go
📖 教學解說:架構深度導讀

管線六階段的直覺理解

ocr review 想像成一條工廠管線:bootstrap(開機校準)→ diff provider(原料進貨)→ filter & rules(分揀品質管控)→ subtask dispatch(並行加工車間)→ output writer(成品包裝)。每個階段只做一件事,做完了交給下一站。這就是為什麼 OCR 的行為高度可預測——確定性管線 + Agent 動態決策。

五重門過濾的「為什麼」

五重門不是隨意堆的——它們有嚴格的因果順序binary(二進位沒意義)→ user_exclude(你說不要)→ user_include(你特別要求,立即保留)→ unsupported_ext(擴展名不在白名單)→ default_path(測試檔排除)。include 放在第三門是刻意的:它能繞過後面兩門,讓你強制保留本來會被排除的檔案。

per-file 子任務的並行模型

每個檔案一個 goroutine,但受 --concurrency(預設 8)限制。這不是「越多越好」——LLM API 有 rate limit,太多並行反而觸發 429。Plan 階段只在 diff ≥ 50 行時跑,是因為小 diff 的 plan 收益太低(多一次 LLM 呼叫但產不出有價值的指引)。

練習 / 驗收清單

  • 能畫出管線六階段的流程圖
  • 能解釋三種 diff 模式的觸發條件與差異
  • 能說出 plan+main 兩階段的進入條件與五個退出條件
  • 能解釋三分區壓縮的閾值與動作
  • 能描述評論處理流水線的五步
  • 能解釋「按檔案確定性」的設計取捨
看完這頁你應該能說出:管線六階段、三種 diff 模式、plan+main 兩階段與五個退出條件、三分區壓縮、評論處理流水線的五步、以及「按檔案確定性」的設計取捨。

延伸閱讀:程式碼對照 · 6 個工具 · 評審規則