Software Architecture Kiến Trúc Phần Mềm · · 12 min read

Inside Alibaba's OpenCodeReview: Deterministic Engineering Meets LLM Agents at Scale Bên Trong OpenCodeReview Của Alibaba: Khi Kỹ Thuật Cơ Học Kết Hợp LLM Agent Quy Mô Lớn

A technical deep dive into Alibaba's open-source OpenCodeReview engine: sub-agent file bundling, 3-zone memory compression, async comment pools, and cross-file AST relocation. Mổ xẻ kiến trúc OpenCodeReview từ Alibaba: cơ chế gom nhóm file con, nén bộ nhớ 3 vùng, worker pool bất đồng bộ và kỹ thuật định vị dòng thay đổi xuyên file.

Written by Nguyen Cong Ben Nguyễn Công Ben
Alibaba OpenCodeReview Hybrid Architecture Diagram

1. 🔍 The Code Review Paradox & The Failure of Pure LLM Agents

Every engineering organization dreams of fully automated code reviews. When modern LLMs demonstrated fluent reasoning over programming languages, teams rushed to prompt frontier models with simple directives: “Review this Git diff and report any bugs.”

In production environments, however, pure LLM agents consistently hit a wall:

  1. Incomplete Coverage (“Corner Cutting”): When presented with large pull requests spanning 15+ files, general-purpose LLM agents selectively review a handful of files and silently skip the rest due to context window fatigue or attention dilution.
  2. Position Drift: Agents frequently hallucinate file paths or miscalculate hunk line offsets. A comment identifying a valid nil-pointer exception in service.go gets pinned to line 42 of service_test.go or an unrelated struct definition.
  3. Unstable Signal-to-Noise Ratio: Natural language prompt guidelines fluctuate wildly across minor phrasing changes. High recall is routinely accompanied by dozens of nitpicks on variable naming, formatting, or dead code in deleted hunks.
flowchart LR
    subgraph Naive ["Naive LLM Review Agent"]
        GitDiff["Raw Git Diff"] --> Prompt["Massive Prompt + Tools"]
        Prompt --> Agent["Single General LLM"]
        Agent --> Issues["❌ Position Drift\n❌ Skipped Files\n❌ ~9x Token Waste"]
    end

    subgraph Hybrid ["Alibaba OpenCodeReview Hybrid Architecture"]
        DiffIntake["Git Diff Engine"] --> Deterministic["Deterministic Boundary\n• Rule Sniffing\n• Smart Bundling\n• Churn Filter"]
        Deterministic --> IsolatedSubAgents["Parallel Sub-Agents\n+ 3-Zone Compression"]
        IsolatedSubAgents --> OffloadPool["Async Worker Pool\n+ String/Hunk Relocation"]
        OffloadPool --> FactChecker["Adversarial Fact-Check"]
        FactChecker --> HighPrecision["✅ Line-Accurate\n✅ 1/9 Token Cost\n✅ High F1 & Precision"]
    end

To address this challenge across thousands of repositories and millions of internal lines of code, Alibaba spent two years developing and validating OpenCodeReview (alibaba/open-code-review). Recently open-sourced under the Apache 2.0 license, the project demonstrates a foundational paradigm shift: Deterministic Engineering × LLM Agent Hybrid Architecture.

On Alibaba’s standardized benchmark (AACR-Bench, built from 50 popular open-source repositories, 200 real PRs across 10 programming languages, and 1,505 human-annotated defects), OpenCodeReview achieved significantly higher Precision and F1 scores than general-purpose agents, while consuming only ~1/9th of the tokens.

Here is an architectural teardown of how OpenCodeReview achieves this balance directly from its Go source code.


2. 🏛️ High-Level System Architecture

OpenCodeReview’s core philosophy is strict responsibility division: never delegate to a language model what can be guaranteed by deterministic algorithms.

flowchart TD
    A["Pull Request / Git Workspace"] --> B["Intake & Path Filtering\n(allowlist, binary & secret filters)"]
    B --> C["Rule Sniffer & AST Metadata\n(language-specific rulesets: Java, Go, TS, Rust...)"]
    
    C --> D{"Smart File Bundler\n(GroupingPlan)"}
    D -- "Small Churn (< 200 lines)" --> E["Single Review Bundle"]
    D -- "Large Changeset (>= 4 files)" --> F["LLM Semantic Grouping Subtask\n(Max 10 files / group)"]
    
    E --> G["Concurrent Sub-Agent Runner"]
    F --> G
    
    subgraph AgentLoop ["Isolated Sub-Agent Execution"]
        G --> H["3-Zone Memory Engine\n• Frozen Zone (0:2)\n• Compress Zone (Async @ 60%, Sync @ 80%)\n• Active Verbatim Zone (K-rounds)"]
        H --> I["Domain-Tuned Toolset\n(file_read, file_read_diff, code_search)"]
        I --> J["code_comment Tool Invocations"]
    end
    
    J --> K["Async CommentWorkerPool\n(Off critical LLM loop path)"]
    
    subgraph PostProcessing ["Precision & Anchoring Pipeline"]
        K --> L["Deterministic Relocation\n(Hunk match & In-memory cross-file match)"]
        L -- "Failed Match" --> M["Re-Location Subtask\n(Fast LLM /no_think snippet anchor)"]
        L -- "Success" --> N["Adversarial Fact-Checker\n(Filter Task: Diff vs Finding proof)"]
        M --> N
    end
    
    N --> O["Unified Review Manifest & SARIF / CLI Output"]

The system operates across four primary subsystems:

  1. Intake & Deterministic Rule Sniffer: Resolves Git diffs, parses unified hunks, eliminates binary/generated noise, and associates targeted language rules.
  2. Smart Bundler & Sub-Agent Dispatcher: Partitions multi-file changesets into semantically coherent groups with isolated LLM contexts.
  3. 3-Zone LLM Conversation Engine: Regulates token consumption and active conversation memory with asynchronous background compaction.
  4. Asynchronous Post-Processing & Anchoring: Executes comment verification, cross-file hunk relocation, and adversarial fact-checking outside the main LLM tool loop.

3. 🧩 Mechanism 1: Smart File Bundling & Sub-Agent Divide-and-Conquer

When an agent is handed 30 changed files simultaneously, attention degrades non-linearly. OpenCodeReview solves this with semantic partitioning in internal/agent/grouping.go.

Threshold-Driven Partitioning (GroupingPlan)

Instead of unconditionally asking an LLM to group files, the engine first applies deterministic heuristics based on line churn:

// internal/agent/grouping.go
const maxFilesPerGroup = 10
const smallChangeSetLabel = "small change set"

func groupDiffs(ctx context.Context, diffs []model.Diff, client llm.LLMClient, 
    modelName string, tpl template.Template, tokenLimit int, sessOpts *groupingSessionOpts) groupDiffsResult {
    
    if len(diffs) <= 1 {
        return groupDiffsResult{groups: toSingleFileGroups(diffs)}
    }

    totalChanged, _ := diffsChurn(diffs)
    if strategy := tpl.GroupingPlan(len(diffs), totalChanged); strategy != template.GroupingViaLLM {
        return groupDiffsResult{groups: groupWithoutLLM(ctx, diffs, strategy, tpl, totalChanged, tokenLimit)}
    }
    // ... Fallback to LLM grouping only when files >= 4 and churn exceeds threshold
}

In task_template.json, the constants define sharp boundaries:

  • GROUPING_MIN_FILES: 4 files.
  • GROUPING_BUNDLE_LINE_THRESHOLD: 200 lines of changed code.
  • PLAN_MODE_LINE_THRESHOLD: 50 lines.

If a pull request modifies 12 files but changes only 80 lines total, OpenCodeReview avoids the grouping LLM call entirely. Conversely, when large changesets require semantic clustering, the grouping prompt transmits only file paths and change metadata (without diff bodies). The model responds with array indices rather than string paths to save completion tokens.

Each bundle then runs in an isolated MAIN_TASK sub-agent with its own context, enabling true multi-core concurrency bounded by CommentWorkerPool and semaphore limits.


4. 🧠 Mechanism 2: Three-Zone Memory Compression Engine

In complex reviews, sub-agents issue multiple tool calls (file_read, code_search, file_read_diff) to inspect call sites and type definitions. Without compaction, conversation histories explode past token limits.

OpenCodeReview implements a Three-Zone Memory Partitioning model in internal/llmloop/compression.go:

flowchart LR
    subgraph ConversationHistory ["Conversation History (Total Tokens)"]
        direction TB
        Z1["Zone 1: Frozen Zone\nIndex 0: System Prompt\nIndex 1: User Diff & Task Constraints\n(Immutable / Never Compressed)"]
        Z2["Zone 2: Compress Zone\nHistorical Assistant & Tool Result Pairs\n(Asynchronously Summarized at 60% Soft Limit)"]
        Z3["Zone 3: Active Zone\nLast K Complete Tool Rounds\n(Preserved Verbatim for Ongoing Reasoning)"]
    end

Dual-Threshold Triggering

The engine defines two strict operational thresholds relative to MaxTokens:

  • Soft Threshold (tokenSoftThreshold = 0.60): An asynchronous background goroutine is dispatched to summarize older rounds without blocking the agent’s tool execution.
  • Warning Threshold (tokenWarningThreshold = 0.80): If tokens exceed 80% before background compression finishes, execution halts and forces synchronous compression.
// internal/llmloop/compression.go
const (
    tokenSoftThreshold    = 0.60 // async background compression
    tokenWarningThreshold = 0.80 // immediate sync compression
)

func partitionMessages(messages []llm.Message, maxTokens int, prevSummaryTokenEstimate int) partitionResult {
    result := partitionResult{frozenEnd: 2}
    if len(messages) <= 2 {
        result.compressEnd = len(messages)
        return result
    }

    result.rounds = groupIntoRounds(messages, 2)
    result.activeCount = computeActiveZoneSize(result.rounds, messages, maxTokens, prevSummaryTokenEstimate)
    if result.activeCount >= len(result.rounds) {
        result.compressEnd = len(messages)
        result.activeCount = 0
        return result
    }

    // compressEnd marks the boundary right before the active zone begins
    activeStartIdx := len(result.rounds) - result.activeCount
    lastCompressRound := result.rounds[activeStartIdx-1]
    if len(lastCompressRound.toolIdxs) > 0 {
        result.compressEnd = lastCompressRound.toolIdxs[len(lastCompressRound.toolIdxs)-1] + 1
    } else {
        result.compressEnd = lastCompressRound.assistantIdx + 1
    }
    return result
}

By guaranteeing that messages[0:2] (the system prompt and root diff instructions) remain permanently frozen, and that the $K$ most recent rounds remain intact, the agent never loses its identity or immediate working memory.


5. ⚡ Mechanism 3: Asynchronous CommentWorkerPool

In standard agent designs, when the model invokes code_comment, the runtime synchronously validates the arguments, resolves line numbers against Git, formats comments, and performs reflection checks before returning control to the LLM. This introduces latency bottlenecks.

OpenCodeReview detaches this workflow via CommentWorkerPool (internal/llmloop/pool.go):

// internal/llmloop/pool.go
type CommentWorkerPool struct {
    semaphore chan struct{}
    wg        sync.WaitGroup
    resultsMu sync.Mutex
    results   []model.LlmComment

    keysMu sync.Mutex
    keys   map[string]*sync.WaitGroup
}

func (p *CommentWorkerPool) SubmitFor(key string, f func() ([]model.LlmComment, error)) {
    p.keysMu.Lock()
    if p.keys == nil {
        p.keys = make(map[string]*sync.WaitGroup)
    }
    kwg := p.keys[key]
    if kwg == nil {
        kwg = &sync.WaitGroup{}
        p.keys[key] = kwg
    }
    kwg.Add(1)
    p.keysMu.Unlock()
    p.submit(f, kwg)
}

Keyed Synchronization (SubmitFor & AwaitKey)

Each subtask (reviewing a specific file group) submits comment post-processing jobs tagged with its group key. The main tool loop returns immediate confirmation to the model ("Comment recorded for line tracking"), allowing the agent to move to the next file without delay.

Before the subtask completes, AwaitKey(groupKey) halts only that group until its background post-processing tasks finish, preventing race conditions while maximizing pipeline parallelism.


6. 🎯 Mechanism 4: Deterministic Line Anchoring & Cross-File Relocation

Line drift is one of the most frustrating aspects of LLM code reviews: an agent spots a problem, but its line indices point to comments, imports, or adjacent functions.

OpenCodeReview resolves this through a three-stage locating pipeline:

flowchart TD
    C["Agent issues code_comment\n(Path, ExistingCode snippet, Feedback)"] --> R1{"Phase 1: In-Hunk Match\n(resolveFromHunk)"}
    R1 -- "Match Found" --> Done["Anchored to Diff Line"]
    R1 -- "Mismatch" --> R2{"Phase 2: In-File Content Match\n(resolveFromFileContent)"}
    R2 -- "Match Found" --> Done
    R2 -- "Mismatch" --> R3{"Phase 3: Cross-File Relocation\n(RelocateAcrossFiles)"}
    
    R3 -- "Unique match in another changed file" --> ReAnchor["Re-file comment path & lines\n(e.g., header -> source file)"]
    R3 -- "Ambiguous or 0 matches" --> R4{"Phase 4: LLM Re-Location\n(/no_think prompt)"}
    
    ReAnchor --> Done
    R4 -- "Snippet Re-extracted" --> R1
    R4 -- "Failed" --> Drop["Suppress Unanchored Comment"]

Deterministic Cross-File Matching

A common failure mode occurs when an agent reviews an interface (service.h) and uses file_read_diff to inspect its implementation (service.cpp). The agent spots a flaw in service.cpp but submits the comment under service.h.

Standard tools fail line resolution and either drop the comment or trigger an LLM prompt that hallucinates lines in the header. OpenCodeReview runs RelocateAcrossFiles first:

// internal/diff/resolver.go
func RelocateAcrossFiles(cm *model.LlmComment, diffs []model.Diff) (string, bool) {
    // Searches verbatim ExistingCode across in-memory diffs of all other files
    // If and only if an EXACT UNIQUE match is discovered, cm.Path, StartLine,
    // and EndLine are automatically re-anchored to the true target file.
    // Ambiguous (multiple) matches decline re-location to prevent false positives.
}

Conservative Adversarial Fact-Checking

After comments are anchored, they pass through REVIEW_FILTER_TASK (internal/config/template/prompts/review_filter_task_system.md). The prompt instructions encode a critical engineering principle:

“The two mistakes available to you are not equally bad: - Keeping an incorrect comment costs a reviewer a few seconds of attention. - Removing a correct comment silently destroys a real finding. When your evidence falls short of proof, approve.”

This asymmetric cost model prevents aggressive filtering from dropping subtle, multi-step vulnerabilities.


7. 📊 Technical Comparison: Architecture Tradeoffs

Capability / MetricNaive LLM Agent (e.g. Claude Code prompt)Traditional Linters (SonarQube, ESLint)Alibaba OpenCodeReview
Architectural ModelPure Prompt / Tool-Calling LoopStatic AST Pattern MatchingHybrid: Deterministic Pipeline × Agent
Token ConsumptionBaseline (9x higher)Zero (Non-LLM)~1/9th Tokens (AACR-Bench verified)
Position PrecisionFrequent line drift on refactored codeExact AST line resolutionExact Hunk + Cross-File AST Matching
Large PR ScalingHigh rate of skipped files / truncated contextO(N) linear parseSmart Bundling (<10 files / sub-agent)
Memory ManagementAd-hoc sliding context windowNot applicable3-Zone Partitioning (Frozen/Compress/Active)
Deep Semantic ReasoningHigh, but prone to false hallucinationsLow (Rigid heuristic rules)High (Focused prompt + rule sniffers)
Concurrency ModelSequential turn-by-turn callsMultithreaded parsingParallel Sub-Agents + Keyed Worker Pool

8. 💡 Actionable Engineering Lessons for Agent Builders

Studying Alibaba’s OpenCodeReview source code yields four valuable design patterns for production AI engineering:

1. Enforce Critical Paths in Code, Not System Prompts

Prompting an LLM to “remember to check all 25 files” or “ensure your line numbers match git diff offsets” fails under pressure. OpenCodeReview handles file filtering, exclusion patterns, rule sniffing, and line resolution entirely in deterministic Go code. The model is treated as a reasoning engine, not an arithmetic parser.

2. Segment Conversation Memory into Explicit Zones

Treating conversation history as an undifferentiated list of messages leads to catastrophic forgetfulness. By separating state into a Frozen Root Zone, a Background-Compressed Zone, and an Active Verbatim Zone, long-running agents maintain stability without losing context.

3. Decouple Reflection and Verification from the LLM Loop

Heavy post-processing tasks—such as comment validation, diff re-tracking, and test reproduction—should never run synchronously on the LLM’s critical response path. Using bounded worker pools and keyed synchronization allows the model to keep generating findings while background workers verify previous outputs.

4. Calibrate for Asymmetric Failure Costs

In verification and fact-checking subtasks, false rejections and false acceptances rarely carry equal weight. Clearly articulating cost asymmetries in verifier system prompts prevents autonomous agents from discarding valuable engineering insights.

1. 🔍 Nghịch Lý Của Code Review & Sự Thất Bại Của Agent Đơn Thuần

Bất kỳ tổ chức kỹ thuật phần mềm nào cũng hướng tới mục tiêu tự động hóa quy trình review mã nguồn (Code Review). Khi các mô hình ngôn ngữ lớn (LLM) hiện đại thể hiện khả năng hiểu code ấn tượng, nhiều nhóm phát triển đã thử nghiệm đưa Git diff trực tiếp vào LLM với các prompt tổng quát: “Hãy review đoạn diff này và chỉ ra các lỗi tiềm ẩn.”

Tuy nhiên, trong môi trường phát triển thực tế tại doanh nghiệp, cách tiếp cận dựa hoàn toàn vào Agent thông thường nhanh chóng bộc lộ 3 giới hạn nghiêm trọng:

  1. Bỏ sót file (Corner Cutting): Khi đối mặt với các Pull Request lớn gồm hơn 15 file, các agent đa năng thường có xu hướng “đốt cháy giai đoạn” — chỉ tập trung vào một vài file đầu tiên và bỏ sót hoàn toàn các file quan trọng phía sau do suy giảm chú ý trong ngữ cảnh dài.
  2. Lệch vị trí dòng (Position Drift): LLM thường xuyên gặp khó khăn trong việc tính toán chính xác offset của từng block thay đổi (diff hunk). Một nhận xét phát hiện đúng lỗi rò rỉ tài nguyên ở service.go nhưng lại bị gắn nhầm vào dòng 42 của service_test.go.
  3. Chất lượng phản hồi dao động: Các prompt viết bằng ngôn ngữ tự nhiên rất khó kiểm soát độ ổn định. Tỷ lệ phát hiện lỗi (Recall) cao thường kéo theo hàng tá bình luận vụn vặt về cách đặt tên biến, định dạng thụt dòng hoặc nhận xét sai trên các dòng code đã bị xóa.
flowchart LR
    subgraph Naive ["Agent LLM Thuần Túy"]
        GitDiff["Git Diff Thô"] --> Prompt["Prompt Lớn + Toolset Chung"]
        Prompt --> Agent["Single General LLM"]
        Agent --> Issues["❌ Lệch Vị Trí Dòng\n❌ Bỏ Sót File\n❌ Tốn Token Gấp ~9 Lần"]
    end

    subgraph Hybrid ["Kiến Trúc Lai Alibaba OpenCodeReview"]
        DiffIntake["Bộ Thu Thập Git Diff"] --> Deterministic["Ranh Giới Cơ Học Bất Biến\n• Nhận Diện Rule Theo Ngôn Ngữ\n• Gom Cụm File Thông Minh\n• Lọc Ngưỡng Churn"]
        Deterministic --> IsolatedSubAgents["Sub-Agent Song Song\n+ Nén Bộ Nhớ 3 Vùng"]
        IsolatedSubAgents --> OffloadPool["Worker Pool Bất Đồng Bộ\n+ Khớp Dòng Diff Xuyên File"]
        OffloadPool --> FactChecker["Bộ Phản Biện Đối Kháng"]
        FactChecker --> HighPrecision["✅ Khớp Dòng Chuẩn Xác\n✅ Tiết Kiệm 8/9 Token\n✅ F1 & Độ Chính Xác Vượt Trội"]
    end

Để giải quyết bài toán này trên quy mô hàng chục nghìn kỹ sư nội bộ và hàng triệu dòng code mỗi tuần, Alibaba đã phát triển và thử nghiệm thực tế công cụ OpenCodeReview (alibaba/open-code-review) trong suốt 2 năm. Dự án vừa được mã nguồn mở theo giấy phép Apache 2.0, đánh dấu một bước chuyển dịch kiến trúc quan trọng: Kết hợp Kỹ Thuật Cơ Học Bất Biến với LLM Agent (Deterministic Engineering × Agent Hybrid).

Trên bộ benchmark thực tế AACR-Bench (gồm 50 repo mã nguồn mở phổ biến, 200 Pull Request thật trên 10 ngôn ngữ lập trình, với 1.505 lỗi được xác nhận bởi hơn 80 kỹ sư cấp cao), OpenCodeReview đạt điểm Precision và F1 vượt trội so với các agent đa năng phổ biến, trong khi chỉ tiêu thụ ~1/9 lượng token.

Dưới đây là phân tích chi tiết về kiến trúc kỹ thuật của hệ thống từ mã nguồn Go của dự án.


2. 🏛️ Sơ Đồ Kiến Trúc Tổng Thể

Triết lý cốt lõi của OpenCodeReview là phân định ranh giới trách nhiệm rõ ràng: Không bao giờ giao cho mô hình ngôn ngữ những việc mà thuật toán tất định (deterministic algorithms) có thể đảm bảo chính xác 100%.

flowchart TD
    A["Pull Request / Git Workspace"] --> B["Tiếp Nhận & Lọc Đường Dẫn\n(Loại bỏ file nhị phân, secret, file tự sinh)"]
    B --> C["Rule Sniffer & Metadata Ngôn Ngữ\n(Bộ quy tắc chuyên sâu: Java, Go, TypeScript, Rust...)"]
    
    C --> D{"Bộ Gom File Thông Minh\n(GroupingPlan)"}
    D -- "Thay Đổi Nhỏ (< 200 dòng)" --> E["Một Bundle Duy Nhất"]
    D -- "Thay Đổi Lớn (>= 4 files)" --> F["Subtask Phân Nhóm Bằng LLM\n(Tối đa 10 files / nhóm)"]
    
    E --> G["Bộ Điều Phối Sub-Agent Song Song"]
    F --> G
    
    subgraph AgentLoop ["Vòng Lặp Thực Thi Sub-Agent Độc Lập"]
        G --> H["Bộ Nhớ 3 Vùng (3-Zone Engine)\n• Frozen: Giữ nguyên prompt gốc (0:2)\n• Compress: Nén nền ở 60%, ép nén ở 80%\n• Active: Giữ nguyên K lượt trao đổi gần nhất"]
        H --> I["Tập Tool Chuyên Biệt\n(file_read, file_read_diff, code_search)"]
        I --> J["Gọi Tool code_comment"]
    end
    
    J --> K["CommentWorkerPool Bất Đồng Bộ\n(Tách khỏi luồng gọi LLM chính)"]
    
    subgraph PostProcessing ["Định Vị & Xác Minh Chính Xác"]
        K --> L["Định Vị Dòng Thay Đổi Cơ Học\n(Khớp diff hunk & khớp code xuyên file)"]
        L -- "Không Khớp" --> M["Subtask Re-Location\n(Dùng LLM /no_think trích xuất snippet)"]
        L -- "Khớp Thành Công" --> N["Bộ Phản Biện Đối Kháng\n(Review Filter Task: Chứng minh lỗi sai dựa trên diff)"]
        M --> N
    end
    
    N --> O["Tổng Hợp Manifest & Xuất Định Dạng SARIF / CLI"]

Quy trình xử lý chia thành bốn tầng kỹ thuật liên hoàn:

  1. Tiếp nhận & Nhận diện Quy tắc Cơ học: Trích xuất Git diff, phân tích các hunk thay đổi, loại bỏ các file rác/file cấu hình tự sinh, và ánh xạ các bộ rule đặc thù cho từng ngôn ngữ.
  2. Gom nhóm File & Điều phối Sub-Agent: Phân vùng PR thành các cụm file có liên quan ngữ nghĩa chặt chẽ với ngữ cảnh LLM độc lập.
  3. Quản lý Bộ nhớ Hội thoại 3 Vùng: Điều phối ngân sách token và nén lịch sử cuộc trò chuyện chạy nền bất đồng bộ.
  4. Hậu xử lý & Neo vị trí Dòng code: Thực thi định vị dòng, chuyển vùng nhận xét xuyên file và phản biện loại bỏ phát hiện giả ngoài luồng xử lý chính của LLM.

3. 🧩 Cơ Chế 1: Gom Nhóm File Thông Minh & Chiến Thuật Chia Để Trị

Khi nạp toàn bộ 30 file thay đổi vào một cửa sổ ngữ cảnh, chất lượng chú ý của LLM suy giảm nhanh chóng. OpenCodeReview giải quyết bài toán này bằng cơ chế phân cụm ngữ nghĩa tại internal/agent/grouping.go.

Phân Nhóm Dựa Trên Ngưỡng Churn (GroupingPlan)

Thay vì luôn dùng LLM để phân nhóm, hệ thống tính toán trước biến động dòng code (churn) bằng logic cơ học:

// internal/agent/grouping.go
const maxFilesPerGroup = 10
const smallChangeSetLabel = "small change set"

func groupDiffs(ctx context.Context, diffs []model.Diff, client llm.LLMClient, 
    modelName string, tpl template.Template, tokenLimit int, sessOpts *groupingSessionOpts) groupDiffsResult {
    
    if len(diffs) <= 1 {
        return groupDiffsResult{groups: toSingleFileGroups(diffs)}
    }

    totalChanged, _ := diffsChurn(diffs)
    if strategy := tpl.GroupingPlan(len(diffs), totalChanged); strategy != template.GroupingViaLLM {
        return groupDiffsResult{groups: groupWithoutLLM(ctx, diffs, strategy, tpl, totalChanged, tokenLimit)}
    }
    // Chỉ kích hoạt LLM Grouping khi số file >= 4 và tổng dòng thay đổi vượt ngưỡng
}

Các ngưỡng quy định tại task_template.json:

  • GROUPING_MIN_FILES: 4 file.
  • GROUPING_BUNDLE_LINE_THRESHOLD: 200 dòng thay đổi.
  • PLAN_MODE_LINE_THRESHOLD: 50 dòng.

Nếu một PR sửa đổi 12 file nhưng chỉ thay đổi 80 dòng code, OpenCodeReview gom toàn bộ vào một nhóm mà không tốn lượt gọi LLM nào. Ngược lại, với các PR lớn cần phân cụm ngữ nghĩa, prompt gom nhóm chỉ truyền danh sách đường dẫn và metadata (không truyền nội dung diff chi tiết). Mô hình phản hồi bằng danh sách chỉ số mảng (index) thay vì chuỗi đường dẫn dài, giúp tối ưu chi phí token phản hồi.

Mỗi nhóm file sau đó được chạy như một sub-agent độc lập với context riêng biệt, cho phép mở rộng xử lý song song trên nhiều CPU core.


4. 🧠 Cơ Chế 2: Kiến Trúc Nén Bộ Nhớ 3 Vùng (Three-Zone Memory)

Trong quá trình review sâu, agent liên tục gọi các công cụ ngữ cảnh (file_read, code_search, file_read_diff) để tra cứu khai báo kiểu dữ liệu hoặc hàm liên quan. Nếu không nén, lịch sử hội thoại sẽ nhanh chóng tràn giới hạn token.

OpenCodeReview áp dụng mô hình phân vùng bộ nhớ 3-Zone Memory Engine tại internal/llmloop/compression.go:

flowchart LR
    subgraph ConversationHistory ["Lịch Sử Hội Thoại (Tổng Lượng Token)"]
        direction TB
        Z1["Vùng 1: Frozen Zone (Băng giá)\nVị trí 0: System Prompt\nVị trí 1: Yêu cầu nhiệm vụ & Diff gốc\n(Bất biến / Tuyệt đối không nén)"]
        Z2["Vùng 2: Compress Zone (Khu vực nén)\nCác cặp Assistant + Kết quả Tool cũ\n(Tóm tắt bất đồng bộ khi chạm ngưỡng 60%)"]
        Z3["Vùng 3: Active Zone (Hiện hành)\nK lượt gọi tool hoàn chỉnh gần nhất\n(Giữ nguyên vẹn cho suy luận tiếp diễn)"]
    end

Cơ Chế Hai Ngưỡng Kích Hoạt (Dual-Threshold)

Hệ thống định nghĩa hai mốc giới hạn tính theo tỷ lệ của MaxTokens:

  • Ngưỡng Mềm (tokenSoftThreshold = 0.60): Khi lịch sử chạm 60% ngân sách, một goroutine chạy nền được kích hoạt để tóm tắt các vòng gọi tool cũ mà không làm gián đoạn luồng suy luận của agent.
  • Ngưỡng Cảnh Báo (tokenWarningThreshold = 0.80): Nếu số lượng token chạm 80% trước khi quá trình nén nền hoàn tất, hệ thống sẽ chặn luồng thực thi và tiến hành nén đồng bộ ngay lập tức để tránh lỗi tràn context.
// internal/llmloop/compression.go
const (
    tokenSoftThreshold    = 0.60 // nén nền bất đồng bộ
    tokenWarningThreshold = 0.80 // nén đồng bộ bắt buộc
)

func partitionMessages(messages []llm.Message, maxTokens int, prevSummaryTokenEstimate int) partitionResult {
    result := partitionResult{frozenEnd: 2}
    if len(messages) <= 2 {
        result.compressEnd = len(messages)
        return result
    }

    result.rounds = groupIntoRounds(messages, 2)
    result.activeCount = computeActiveZoneSize(result.rounds, messages, maxTokens, prevSummaryTokenEstimate)
    if result.activeCount >= len(result.rounds) {
        result.compressEnd = len(messages)
        result.activeCount = 0
        return result
    }

    // compressEnd xác định điểm phân chia ngay trước Active Zone
    activeStartIdx := len(result.rounds) - result.activeCount
    lastCompressRound := result.rounds[activeStartIdx-1]
    if len(lastCompressRound.toolIdxs) > 0 {
        result.compressEnd = lastCompressRound.toolIdxs[len(lastCompressRound.toolIdxs)-1] + 1
    } else {
        result.compressEnd = lastCompressRound.assistantIdx + 1
    }
    return result
}

Bằng cách bảo toàn nguyên vẹn messages[0:2] (system prompt và diff gốc) và giữ $K$ lượt trao đổi gần nhất trong Active Zone, agent duy trì được định hướng nhiệm vụ mà không bị hiện tượng “mất trí nhớ tạm thời”.


5. ⚡ Cơ Chế 3: Tách Rời Hậu Xử Lý Với CommentWorkerPool

Trong thiết kế agent thông thường, mỗi khi mô hình gọi tool code_comment, runtime sẽ dừng lại để kiểm tra định dạng dòng, tính toán offset trên file thật, và chạy kiểm tra phản xạ trước khi trả quyền điều khiển về cho LLM. Điều này tạo ra độ trễ tích lũy rất lớn.

OpenCodeReview giải phóng luồng chính bằng CommentWorkerPool (internal/llmloop/pool.go):

// internal/llmloop/pool.go
type CommentWorkerPool struct {
    semaphore chan struct{}
    wg        sync.WaitGroup
    resultsMu sync.Mutex
    results   []model.LlmComment

    keysMu sync.Mutex
    keys   map[string]*sync.WaitGroup
}

func (p *CommentWorkerPool) SubmitFor(key string, f func() ([]model.LlmComment, error)) {
    p.keysMu.Lock()
    if p.keys == nil {
        p.keys = make(map[string]*sync.WaitGroup)
    }
    kwg := p.keys[key]
    if kwg == nil {
        kwg = &sync.WaitGroup{}
        p.keys[key] = kwg
    }
    kwg.Add(1)
    p.keysMu.Unlock()
    p.submit(f, kwg)
}

Đồng Bộ Theo Khóa Nhóm (SubmitFor & AwaitKey)

Mỗi subtask (khi review một cụm file) đẩy công việc xử lý nhận xét vào pool kèm theo khóa định danh (key). Luồng chính phản hồi ngay lập tức cho mô hình ("Đã ghi nhận nhận xét"), giúp agent tiếp tục đọc các file khác mà không phải chờ đợi.

Chỉ khi subtask hoàn tất lượt review của nhóm, lệnh AwaitKey(groupKey) mới chặn lại để chờ các tác vụ hậu xử lý của riêng nhóm đó về đích, đảm bảo an toàn luồng dữ liệu mà vẫn tối ưu hóa năng suất xử lý.


6. 🎯 Cơ Chế 4: Định Vị Dòng Cơ Học & Chuyển Vùng Nhận Xét Xuyên File

Hiện tượng lệch dòng (Position Drift) là rào cản lớn nhất của các công cụ review bằng AI: agent nhận ra lỗi thực tế nhưng dòng code được gắn thẻ lại trỏ vào phần comment, import hoặc hàm bên cạnh.

OpenCodeReview áp dụng quy trình định vị đa tầng:

flowchart TD
    C["Agent gọi code_comment\n(Path, đoạn code ExistingCode, nội dung góp ý)"] --> R1{"Tầng 1: Khớp Trong Hunk Diff\n(resolveFromHunk)"}
    R1 -- "Khớp Thành Công" --> Done["Gắn Chính Xác Dòng Diff"]
    R1 -- "Không Khớp" --> R2{"Tầng 2: Khớp Trong Nội Dung File Mới\n(resolveFromFileContent)"}
    R2 -- "Khớp Thành Công" --> Done
    R2 -- "Không Khớp" --> R3{"Tầng 3: Chuyển Vùng Xuyên File\n(RelocateAcrossFiles)"}
    
    R3 -- "Khớp duy nhất tại một file thay đổi khác" --> ReAnchor["Đổi lại đường dẫn Path & dòng đích\n(VD: từ file header sang file mã nguồn)"]
    R3 -- "Trùng nhiều nơi hoặc 0 kết quả" --> R4{"Tầng 4: LLM Re-Location\n(Prompt nhẹ /no_think)"}
    
    ReAnchor --> Done
    R4 -- "Trích xuất lại snippet thành công" --> R1
    R4 -- "Thất Bại" --> Drop["Loại bỏ nhận xét không thể neo vị trí"]

Xử Lý Hiện Tượng Nhận Xét Nhầm File (RelocateAcrossFiles)

Một lỗi phổ biến là khi agent đang review file khai báo giao diện (service.h), sau đó dùng tool file_read_diff để kiểm tra file hiện thực (service.cpp). Agent phát hiện lỗi trong logic của service.cpp nhưng lại vô tình gửi nhận xét kèm nhãn service.h.

Các công cụ thông thường sẽ báo lỗi không tìm thấy dòng code hoặc kích hoạt LLM đoán mò vị trí. OpenCodeReview giải quyết bài toán này trước tiên bằng thuật toán cơ học:

// internal/diff/resolver.go
func RelocateAcrossFiles(cm *model.LlmComment, diffs []model.Diff) (string, bool) {
    // Quét chuỗi verbatim ExistingCode trên bộ nhớ diff của TẤT CẢ các file thay đổi khác.
    // Nếu và chỉ nếu tìm thấy DUY NHẤT một vị trí khớp, cm.Path, StartLine,
    // và EndLine sẽ tự động được điều chuyển sang file thực tế.
    // Nếu tìm thấy nhiều vị trí giống nhau (ambiguous), hệ thống từ chối chuyển vùng để tránh đoán sai.
}

Bộ Lọc Phản Biện Đối Kháng Với Chi Phí Bất Đối Xứng

Sau khi đã định vị dòng, các nhận xét được đưa qua REVIEW_FILTER_TASK (internal/config/template/prompts/review_filter_task_system.md). Hướng dẫn hệ thống tại đây nêu rõ một nguyên tắc thực tế:

“Hai sai lầm có thể xảy ra không có mức độ nghiêm trọng như nhau: - Giữ lại một nhận xét chưa chuẩn xác chỉ tốn của người review vài giây để bỏ qua. - Xóa bỏ một nhận xét đúng sẽ âm thầm phá hủy một phát hiện lỗi thật sự và không ai nhận ra điều đó. Vì vậy, khi bằng chứng không đủ để CHỨNG MINH nhận xét là sai, hãy phê duyệt (approve).”

Tư duy chi phí bất đối xứng này giúp bộ lọc loại trừ các phát hiện vô căn cứ mà không triệt tiêu các lỗi tinh vi có giá trị kỹ thuật cao.


7. 📊 Bảng So Sánh Kỹ Thuật Toàn Diện

Tiêu Chí Kỹ ThuậtAgent Đa Năng Thông Thường (Prompt / Claude Code)Linter Tĩnh Truyền Thống (SonarQube, ESLint)Alibaba OpenCodeReview
Mô hình Kiến trúcVòng lặp Prompt / Tool-Calling thuần túyPhân tích cây cú pháp trừu tượng (AST) tĩnhMô hình Lai: Cơ Học Bất Biến × Agent
Tiêu Thụ TokenRất cao (Gấp ~9 lần)Bằng 0 (Không dùng LLM)Chỉ tốn ~1/9 Token (Kiểm chứng trên AACR-Bench)
Độ Chuẩn Xác Vị Trí DòngDễ bị lệch dòng trên code tái cấu trúcTuyệt đối theo dòng ASTKết hợp Hunk Matching + Chuyển Vùng Xuyên File
Khả Năng Xử Lý PR LớnDễ bỏ sót file do nghẽn contextXử lý tuyến tính O(N)Gom nhóm thông minh (<10 file/sub-agent)
Quản Lý Ngữ Cảnh Bộ NhớCửa sổ trượt đơn giản (Sliding Window)Không áp dụngPhân Vùng 3 Vùng (Frozen / Compress / Active)
Khả Năng Hiểu Ngữ Nghĩa SâuTốt nhưng dễ bị ảo giác thông tinKém (Chỉ bắt theo quy tắc cứng cố định)Rất tốt (Kết hợp rule ngữ cảnh chuyên sâu)
Mô Hình Xử Lý Song SongTuần tự từng lượt gọiĐa luồng cơ họcSub-Agent song song + Worker Pool theo khóa

8. 💡 Bài Học Thực Tiễn Cho Kỹ Sư Phát Triển Hệ Thống Agent

Việc mổ xẻ mã nguồn OpenCodeReview của Alibaba mang lại 4 bài học kiến trúc quan trọng cho cộng đồng kỹ sư xây dựng hệ thống phần mềm ứng dụng AI:

1. Hiện Thực Hóa Ranh Giới Bất Biến Bằng Mã Nguồn, Không Phải Prompt

Việc yêu cầu LLM trong prompt “hãy nhớ đọc đủ 25 file” hoặc “hãy tính toán đúng số dòng” sẽ luôn có xác suất lỗi trong thực tế. OpenCodeReview đưa toàn bộ logic lọc file, nhận diện rule, và tính toán tọa độ dòng vào mã nguồn Go. LLM chỉ được dùng đúng cho thế mạnh của nó: suy luận logic và phát hiện bất thường nghiệp vụ.

2. Phân Tầng Vòng Đời Của Bộ Nhớ Ngữ Cảnh

Xem toàn bộ lịch sử trò chuyện là một chuỗi phẳng đồng nhất sẽ dẫn đến tình trạng mất định hướng hoặc tràn token. Phân chia rõ ràng thành Vùng Băng Giá (Frozen), Vùng Nén Chạy Nền (Compress), và Vùng Hoạt Động Nguyên Bản (Active) là mẫu thiết kế chuẩn mực để xây dựng các agent chạy lâu dài.

3. Tách Rời Các Tác Vụ Xác Minh Khỏi Luồng Suy Luận Chính

Các bước tính toán nặng như định vị dòng diff, kiểm tra phản xạ, hoặc chạy thử nghiệm đối kháng không nên chạy đồng bộ trên luồng hội thoại của LLM. Sử dụng worker pool có giới hạn tài nguyên và đồng bộ theo khóa giúp mô hình tiếp tục làm việc mà không lãng phí thời gian chờ đợi.

4. Thiết Kế Hệ Thống Theo Chi Phí Lỗi Bất Đối Xứng

Trong các tác vụ hậu kiểm hoặc fact-checking, hậu quả của việc “bỏ sót lỗi thật” thường nghiêm trọng hơn nhiều so với việc “để lọt một cảnh báo nghi ngờ”. Định hình rõ ràng nguyên tắc này trong logic thuật toán và prompt kiểm thử sẽ giúp hệ thống đạt được sự cân bằng tối ưu giữa Precision và Recall.