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 扇出)。
internal/diff/git.go 定義 Provider 結構,未匯出欄位 mode 選三種模式之一:
| 模式 | 觸發 | 返回 |
|---|---|---|
Workspace | 無參數 | staged + unstaged + untracked 變更 |
Commit | --commit | git show <sha> 引入的變更 |
Range | --from --to | merge-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 層過濾。
每個通過過濾的檔案啟動一個子 agent,在自己的 goroutine 中跑,受 --concurrency(預設 8)約束。
threshold := template.PlanModeLineThreshold // 50
changeLines := d.Insertions + d.Deletions
if changeLines < threshold { skip plan }
小 diff 跳過 plan。較大 diff 做一次 PLAN_TASK 呼叫(不給工具),產出清單作為 {{plan_guidance}}。
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(最近可裝下的完整輪次)。
existing_code 滑動視窗匹配 → 精確行號。RE_LOCATION_TASK。REVIEW_FILTER_TASK 移除可證明為錯的評論。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 放在第三門是刻意的:它能繞過後面兩門,讓你強制保留本來會被排除的檔案。
每個檔案一個 goroutine,但受 --concurrency(預設 8)限制。這不是「越多越好」——LLM API 有 rate limit,太多並行反而觸發 429。Plan 階段只在 diff ≥ 50 行時跑,是因為小 diff 的 plan 收益太低(多一次 LLM 呼叫但產不出有價值的指引)。
你的團隊開了一顆 42 個檔案、跨 Go 後端與 TypeScript 前端的大型重構 PR(把舊 service 層拆成獨立的 domain 模組)。PR 同時改了路由、資料庫查詢與前端呼叫端,且包含 3 個安全敏感點(登入、權限、檔案上傳)。你要一次拿到:diff 正確性、安全掃描、效能疑點、整體品質評分。
ocr review --from main --to feature/repo-split --preview
確認五重門放行了哪些檔、擋了哪些(例如 *.pb.go 被排除是對的;*.test.ts 被排除意味著測試不在這次 diff 評審內——需要另外決定要不要納入)。
ocr review --from main --to feature/repo-split \
--background "將 service 層拆分為獨立 domain module,行為不變,僅重組程式碼" \
--format json --audience agent > review-diff.json
這條路徑只審「變更了什麼」,是重構 PR 的第一線。
ocr scan --path internal/auth,internal/upload,web/src --exclude '**/generated/**' \
--format json --audience agent > review-scan.json
diff 評審看不到「檔案長什麼樣」,重構最容易在搬移中偷偷改變了行為。對安全敏感目錄用 ocr scan 整檔審,抓「搬移後看起來沒變、其實變了」的問題。
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 是唯一雙軌熱點 → 優先人工複查
file_read / code_search 就是讓 Agent 自己補上「型別與符號脈絡」。REVIEW_FILTER_TASK 把可證偽的評論濾掉。OCR 的定位是把三者的強項組起來:確定性工程負責形狀與位置(AST/正規),Agent 負責語意(AI scoring),過濾任務負責把確定性檢查套回 AI 產出。
// 重構前(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: 0 | existing_code 被模型改寫,或 diff 格式異常導致三階段錨定全失敗 | 到 viewer 看 re_location_task 泳道;把 existing_code 改成逐字精確片段 |
| 單一檔案耗盡 30 輪工具呼叫 | 模型在盲目 file_read 探索,沒有聚焦 | 傳 --background 給足脈絡、調低 --max-tools、換更強模型 |
| 記憶壓縮後評論品質驟降 | compress 區摘要把關鍵脈絡壓掉 | 調高 --max-tokens 延後壓縮觸發;看 viewer 的 memory_compression_task 摘要內容 |
| 子 agent 失敗但 exit 0 | per-file 失敗被刻意隔離,聚合仍成功 | 讀 JSON 的 warnings 陣列;把失敗檔的 --resume 單獨重跑 |
| 大檔 diff 直接被丟棄 | 單檔超過 MAX_TOKENS 的 80%,在呼叫 LLM 前被丟棄 | 調高 --max-tokens 或用 ocr scan --path 縮小到單檔審 |
include / exclude)與對應的 ocr review 命令。為什麼 include 能繞過測試排除,而 exclude 不能?main_task 泳道看到檔案以「工具執行錯誤」卡片結束,但 JSON 輸出是 exit 0 且零評論。這代表什麼?下一步該查哪個檔案、哪個泳道?if err != nil 是否對應到 nil-returning 函式)。三個團隊(後端、前端、資料)共用一套 OCR 評審基礎設施。你要設計「評審服務架構」:共用規則但各團隊可覆蓋、排程統一、資源(concurrency/token 預算)可調度、故障可隔離——把架構頁的「單次管線」放大成「組織級管線」。
# 每個 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)自動完成「共用+覆蓋」,不需自建系統
# 夜間全庫掃描(可高併發,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
# 每個 repo 的 CI 都把遙測送到同一個 collector
env: { OCR_ENABLE_TELEMETRY: "1", OTEL_EXPORTER_OTLP_ENDPOINT: collector:4317, OTEL_SERVICE_NAME: ocr-platform }
# Grafana 依 OTEL_SERVICE_NAME / repo 標籤分組,看各團隊使用量與錯誤率
# 某團隊的評審「品質異常」→ 不猜,直接開 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%
管線的效能瓶頸不是 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,確認「該審的都審了」。
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.html | tools 頁詳列六個工具的 schema 與各階段可用性——架構頁只講「迴圈」 |
| 評論流水線 | 行解析 → 重定位 → 過濾 → 二輪 | faq.html / viewer.html | viewer 頁用五條泳道可視化同一條流水線;FAQ 講「評論消失」的診斷 |
| 持久化 | JSONL append-only | cli.html(session) | CLI 頁教 ocr session 命令操作;架構頁解釋「為何是檔案不是 DB」 |