架構總覽

從按下回車到 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 依序檢查:binary → user_exclude → user_include(立即保留)→ unsupported_ext → default_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_TASK、MAIN_TASK、MEMORY_COMPRESSION_TASK、REVIEW_FILTER_TASK、RE_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 個工具 · 評審規則

① 進階真實情境 Worked Example:大型重構 PR 的分層評審(review + scan 並行)

情境

你的團隊開了一顆 42 個檔案、跨 Go 後端與 TypeScript 前端的大型重構 PR(把舊 service 層拆成獨立的 domain 模組)。PR 同時改了路由、資料庫查詢與前端呼叫端,且包含 3 個安全敏感點(登入、權限、檔案上傳)。你要一次拿到:diff 正確性、安全掃描、效能疑點、整體品質評分。

Step 1 — 先 preview 確認範圍,別急著燒 token

ocr review --from main --to feature/repo-split --preview

確認五重門放行了哪些檔、擋了哪些(例如 *.pb.go 被排除是對的;*.test.ts 被排除意味著測試不在這次 diff 評審內——需要另外決定要不要納入)。

Step 2 — 帶背景脈絡跑正式 diff 評審

ocr review --from main --to feature/repo-split \
  --background "將 service 層拆分為獨立 domain module,行為不變,僅重組程式碼" \
  --format json --audience agent > review-diff.json

這條路徑只審「變更了什麼」,是重構 PR 的第一線。

Step 3 — 針對安全敏感目錄跑全檔 scan(補 diff 的盲點)

ocr scan --path internal/auth,internal/upload,web/src --exclude '**/generated/**' \
  --format json --audience agent > review-scan.json

diff 評審看不到「檔案長什麼樣」,重構最容易在搬移中偷偷改變了行為。對安全敏感目錄用 ocr scan 整檔審,抓「搬移後看起來沒變、其實變了」的問題。

Step 4 — 用 jq 把兩份 JSON 合併成分級報告

jq -s '[.[] | .comments[]] | group_by(.path) | map({path: .[0].path, count: length})' \
  review-diff.json review-scan.json

按檔案聚合,找出「同一檔在 diff 與 scan 都被點名」的高風險熱點。

為什麼選這條審查路徑

單靠 ocr review 無法回答「這個檔案整體品質如何」——它只對照 diff。review(變更)+ scan(現況)雙軌是大型重構的標準答案:diff 確保改動正確,scan 確保「搬移後的行為等價」。安全目錄單獨整檔掃,是因為遷移最容易在動線(auth flow、上傳權限)上悄悄退化,而這些退化在 diff 上往往只差一兩行、極難被察覺。

預期產出

review-diff.json   → 14 comments(9 檔)
review-scan.json   → 7 comments(3 檔:auth×3, upload×2, web/src×2)
合併報告          → internal/auth 是唯一雙軌熱點 → 優先人工複查

② 深入原理擴充:LSP diagnostics × AST analysis × AI review scoring

三種工具鏈的定位差異

  • LSP diagnostics——編譯器級即時錯誤(型別錯誤、未定義符號)。快、確定,但只看「這一檔能不能編」。OCR 不用 LSP——它的 file_read / code_search 就是讓 Agent 自己補上「型別與符號脈絡」。
  • AST analysis——把原始碼解析成語法樹做結構化掃描(未使用的 import、可疑的 control flow)。graphify 對 OCR 源碼做的就是 AST 抽取。它看不到語意,但能穩定地找到「形狀異常」。
  • AI review scoring——LLM 對代碼的品質給分與評論。能理解語意、跨檔脈絡,但非確定性,需要 REVIEW_FILTER_TASK 把可證偽的評論濾掉。

OCR 的定位是把三者的強項組起來:確定性工程負責形狀與位置(AST/正規),Agent 負責語意(AI scoring),過濾任務負責把確定性檢查套回 AI 產出。

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

// 重構前(main 分支)
func GetUser(id string) (*User, error) {
    u, err := db.QueryUser(id)
    if err != nil { return nil, err }
    return u, nil
}

// 重構後(PR)——diff 上看起來「行為不變」
func GetUser(id string) (*User, error) {
    u, err := db.QueryUser(id)
    return u, err   // 省了兩行「多餘」檢查
}

diff 評審看到的是刪掉了兩行——Agent 可能判定「無害的簡化」,評論零條、PR 通過。真正的隱患:db.QueryUser 在「找不到使用者」時回傳 (nil, nil)(某些 ORM 的慣例)。重構前 if err != nil 會擋掉;重構後 u 是 nil、err 也是 nil,呼叫端直接解引用 u.Name 就 panic。這就是為什麼重構 PR 的 scan 軌 + 對「nil 回傳慣例」的規則會救你——把 "Check for nil-returning query functions before dereferencing" 寫進 rule.json,讓 Agent 在 Main 迴圈裡用 code_search 去驗證呼叫端。

③ 診斷式疑難排解

症狀可能原因解決方案
評論的 start_line: 0existing_code 被模型改寫,或 diff 格式異常導致三階段錨定全失敗到 viewer 看 re_location_task 泳道;把 existing_code 改成逐字精確片段
單一檔案耗盡 30 輪工具呼叫模型在盲目 file_read 探索,沒有聚焦傳 --background 給足脈絡、調低 --max-tools、換更強模型
記憶壓縮後評論品質驟降compress 區摘要把關鍵脈絡壓掉調高 --max-tokens 延後壓縮觸發;看 viewer 的 memory_compression_task 摘要內容
子 agent 失敗但 exit 0per-file 失敗被刻意隔離,聚合仍成功讀 JSON 的 warnings 陣列;把失敗檔的 --resume 單獨重跑
大檔 diff 直接被丟棄單檔超過 MAX_TOKENS 的 80%,在呼叫 LLM 前被丟棄調高 --max-tokens 或用 ocr scan --path 縮小到單檔審

④ 進階挑戰題

  1. 挑戰一:一顆 60 檔的 PR,你只想審 Go 後端、想繞過測試檔排除、但不想審前端。請寫出規則檔(include / exclude)與對應的 ocr review 命令。為什麼 include 能繞過測試排除,而 exclude 不能?
  2. 挑戰二:你在 viewer 的 main_task 泳道看到檔案以「工具執行錯誤」卡片結束,但 JSON 輸出是 exit 0 且零評論。這代表什麼?下一步該查哪個檔案、哪個泳道?
  3. 挑戰三:設計一套「重構等價性」規則:列出 3 條會讓 Agent 在重構 PR 上特別檢查「行為不變」的規則(例如:檢查被刪除的 if err != nil 是否對應到 nil-returning 函式)。

① 專案級端到端 Worked Example:把 OCR 擴大成「多團隊共用評審服務」

情境

三個團隊(後端、前端、資料)共用一套 OCR 評審基礎設施。你要設計「評審服務架構」:共用規則但各團隊可覆蓋、排程統一、資源(concurrency/token 預算)可調度、故障可隔離——把架構頁的「單次管線」放大成「組織級管線」。

Step 1 — 用 repo 結構表達「共用 + 覆蓋」

# 每個 repo 各自有專案規則(覆蓋全域)
services/api/.opencodereview/rule.json      # Go 後端規則
services/web/.opencodereview/rule.json      # React 前端規則
services/data/.opencodereview/rule.json     # Python/ETL 規則
# 全域 ~/.opencodereview/rule.json 只放「全公司共用」的 secrets/安全規則
# → 四層優先級鏈(rules.html)自動完成「共用+覆蓋」,不需自建系統

Step 2 — 用 concurrency 做資源調度

# 夜間全庫掃描(可高併發,off-peak)
ocr scan --concurrency 16 --max-tokens 120000 --format json --audience agent
# 白天 CI(限制併發,避免撞 LLM rate limit)
ocr review --from origin/main --to HEAD --concurrency 4

Step 3 — 用 telemetry 做服務健康監控

# 每個 repo 的 CI 都把遙測送到同一個 collector
env: { OCR_ENABLE_TELEMETRY: "1", OTEL_EXPORTER_OTLP_ENDPOINT: collector:4317, OTEL_SERVICE_NAME: ocr-platform }
# Grafana 依 OTEL_SERVICE_NAME / repo 標籤分組,看各團隊使用量與錯誤率

Step 4 — 用 session JSONL 做故障隔離與復盤

# 某團隊的評審「品質異常」→ 不猜,直接開 Session Viewer 復盤
ocr viewer
# 找該 session 的 main_task 泳道:工具錯誤卡片、壓縮次數、filter 移除數
# 若「某檔一直靜默」→ 看是否 task_done 前有 error badge(帶傷完成)

為什麼選這條架構

OCR 的「確定性管線」設計讓它天然適合被多團隊共用:規則用四層鏈表達覆蓋、資源用 concurrency 調度、健康用 telemetry 監控、故障用 JSONL 隔離。你不必自建「評審中台」——把 OCR 當成一個可組合的執行引擎,用 repo 結構 + 環境變數 + 遙測把組織需求映射上去,就是最省力的「平台化」。

預期產出

後端 repo: 專案規則生效(Go 特有),CI 每 PR ~2.8 萬 token
前端 repo: React 規則生效,每 PR ~1.5 萬 token
資料 repo: 夜間 scan 16 併發,30 分鐘掃完,0 rate-limit 撞車
Grafana: 三團隊共用一張健康面板,錯誤率 < 1%

② 效能 / 品質 / 安全深度:管線三面向

效能:concurrency 不是越大越好

管線的效能瓶頸不是 CPU,是 LLM rate limit。concurrency=16 遇 429 反而比 8 慢(重試 + 排隊)。經驗值:先觀察 telemetry 的 ocr.llm.requests_total 有沒有 429 標籤,再調 concurrency。另外 --max-tokens 決定了壓縮何時觸發——預算太小,同一顆 PR 會多出好幾次壓縮呼叫,反而更貴。

品質:過濾正確性 > 評論數量

架構層的品質風險在「審錯範圍」:五重門擋掉測試檔(預設)、但沒擋掉生成碼(若沒 exclude)。結果可能是「高品質地審了一堆生成碼」——Precision 數字漂亮,對真實程式的 Recall 是零。品質檢查不是看評論數,是每週對照 --preview 的檔案清單與 git ls-files,確認「該審的都審了」。

安全:session 資料是敏感資產

JSONL 記錄了「發給 LLM 的一切」(含 diff 程式碼)。~/.opencodereview/sessions/ 在多人共用伺服器上要設權限(700)。viewer 綁 localhost 預設——要從 LAN 存取才設 OCR_VIEWER_ALLOWED_HOSTS(並確認網路分割)。對外送的 diff 內容,內網模型是安全上限選項。

③ 文件間比較對照表

面向本文(架構總覽)相關文差異說明
管線步驟六階段高層管線 + 源碼地圖how-it-works.html本文講「零件怎麼排」,how-it-works 講「為什麼這樣設計」——互補而非重複
per-file 子任務plan/main 迴圈 + 五退出條件tools.htmltools 頁詳列六個工具的 schema 與各階段可用性——架構頁只講「迴圈」
評論流水線行解析 → 重定位 → 過濾 → 二輪faq.html / viewer.htmlviewer 頁用五條泳道可視化同一條流水線;FAQ 講「評論消失」的診斷
持久化JSONL append-onlycli.html(session)CLI 頁教 ocr session 命令操作;架構頁解釋「為何是檔案不是 DB」

④ 互動式檢核清單

進階驗收:你能設計並維運一套評審服務嗎?

  • - [ ] 能畫出管線六階段並指出每階段對應的源碼檔案
  • - [ ] 能用 repo 結構 + 四層規則鏈實作「共用規則 + 團隊覆蓋」
  • - [ ] 能用 telemetry 觀察 concurrency 是否撞 rate limit
  • - [ ] 能解釋「審錯範圍」為何比「評論少」更危險
  • - [ ] 能為 session 目錄與 viewer 設定正確的權限與存取邊界
  • - [ ] 能描述 plan/main 迴圈的五個退出條件各代表什麼