Mencegah Konflik Kode Multi-Agent dan Lonjakan Token dengan Git Worktree dan Analisis AST

Computing/Software15분 읽기

TuBrief 편집팀 원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다. 원본 영상: Apa yang dapat kita pelajari dari eksperimen SQLite Rust Cursor · Maximilian Schwarzmüller

Ketika agen LLM yang berjalan lancar di versi demo diperluas ke skala swarm, Anda pasti akan menabrak dua tembok utama. Kode menjadi berantakan karena agen-agen saling menimpa berkas yang sama, atau biaya API membengkak hingga jutaan rupiah akibat memasukkan berkas yang tidak relevan ke dalam model besar.

Menjalankan beberapa proses model bahasa secara asal-asalan di ruang kerja yang sama akan memperburuk keadaan dengan cepat. Agen B membaca berkas belum selesai yang sedang diperbaiki oleh Agen A lalu menghasilkan kode yang ngawur, hingga akhirnya riwayat commit pun ikut hilang. Di sisi lain, menyalin seluruh repositori setiap kali menjalankan proses akan membuang-buang kapasitas disk dan memakan waktu beberapa menit hanya untuk inisialisasi.

Artikel ini membahas cara menyelesaikan masalah tersebut secara teknis melalui penggabungan isolasi berkas in-memory, routing berbasis analisis sintaksis, dan pipeline verifikasi statis.

Mencegah Mencegah Penimpaan Berkas dengan Git Worktree

Bottleneck yang terjadi saat menjalankan agen secara bersamaan di dalam codebase berukuran besar adalah perebutan sistem berkas (file system contention). Alih-alih melakukan full clone (Full Git Clone) terhadap seluruh repositori monolitik, penggunaan Git Worktree memungkinkan kita memisahkan direktori ringan berukuran beberapa MB hanya dalam waktu 1 detik sambil tetap berbagi metadata dan DB objek.

Namun, jika puluhan agen melakukan commit secara bersamaan, akan terjadi perebutan lock pada berkas indeks tingkat atas (.git/index.lock). Untuk mengendalikannya, diperlukan lapisan sandbox berbasis pemblokiran berkas (file locking).

`python
import os
import sys
import time
import subprocess
import shutil
from pathlib import Path
from typing import Optional, List
from filelock import FileLock, Timeout

class WorktreeSandboxManager:
def init(self, repo_path: str, base_branch: str = "main"):
self.repo_path = Path(repo_path).resolve()
self.base_branch = base_branch
self.worktrees_dir = self.repo_path / ".agent_worktrees"
self.locks_dir = self.repo_path / ".agent_locks"

    self.worktrees_dir.mkdir(exist_ok=True)
    self.locks_dir.mkdir(exist_ok=True)

def create_sandbox(self, agent_id: str, task_name: str) -> Path:
    branch_name = f"agent/{agent_id}-{task_name}"
    worktree_path = self.worktrees_dir / f"wt_{agent_id}"

    if worktree_path.exists():
        self.cleanup_sandbox(agent_id, force=True)

    cmd = [
        "git", "-C", str(self.repo_path),
        "worktree", "add", "-b", branch_name,
        str(worktree_path), self.base_branch
    ]
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        raise RuntimeError(f"Worktree 생성 실패: {result.stderr}")

    return worktree_path

def safe_git_commit(self, worktree_path: Path, commit_message: str, max_retries: int = 5) -> bool:
    lock_file_path = self.locks_dir / "git_index.lock"
    file_lock = FileLock(str(lock_file_path), timeout=10)

    for attempt in range(max_retries):
        try:
            with file_lock:
                add_res = subprocess.run(
                    ["git", "-C", str(worktree_path), "add", "."],
                    capture_output=True, text=True
                )
                if add_res.returncode != 0:
                    raise RuntimeError(f"Git add 실패: {add_res.stderr}")

                commit_res = subprocess.run(
                    ["git", "-C", str(worktree_path), "commit", "-m", commit_message],
                    capture_output=True, text=True
                )
                
                if commit_res.returncode == 0:
                    return True
                
                if "index.lock" in commit_res.stderr or "Unable to create" in commit_res.stderr:
                    backoff = (2 ** attempt) * 0.2
                    time.sleep(backoff)
                    continue
                else:
                    print(f"커밋 실패 (비 경합 에러): {commit_res.stderr}")
                    return False

        except (Timeout, RuntimeError) as e:
            backoff = (2 ** attempt) * 0.2
            time.sleep(backoff)

    return False

def cleanup_sandbox(self, agent_id: str, force: bool = False):
    worktree_path = self.worktrees_dir / f"wt_{agent_id}"
    if not worktree_path.exists():
        return

    status_res = subprocess.run(
        ["git", "-C", str(worktree_path), "status", "--porcelain"],
        capture_output=True, text=True
    )
    if status_res.stdout.strip() and not force:
        raise RuntimeError("커밋되지 않은 변경사항이 존재하여 Worktree를 삭제할 수 없습니다.")

    subprocess.run(
        ["git", "-C", str(self.repo_path), "worktree", "remove", "--force", str(worktree_path)],
        capture_output=True, text=True
    )
    if worktree_path.exists():
        shutil.rmtree(worktree_path, ignore_errors=True)

`

Urutan penerapannya sangat sederhana.

  1. Pasang paket filelock dan masukkan kelas WorktreeSandboxManager ke dalam proyek Anda.
  2. Sebelum memberikan tugas ke agen, panggil create_sandbox() untuk membuat direktori independen.
  3. Saat melakukan commit setelah tugas selesai, lewatkan prosesnya melalui safe_git_commit() untuk mengindari konflik lock menggunakan exponential backoff.

Dengan mengubah konfigurasi seperti ini, konflik penimpaan berkas akan hilang. Waktu yang terbuang untuk debugging pun berkurang lebih dari 5 jam per minggu.

Saat membawa kembali cabang yang telah diubah ke codebase utama, gunakan analisis Abstract Syntax Tree (AST) sebagai pengganti penggabungan baris teks biasa agar lebih aman. Penggabungan teks biasa bisa memicu konflik hanya karena perubahan posisi pernyataan import di bagian atas. Dengan melakukan parsing kode sumber menjadi node tree sintaksis menggunakan modul bawaan Python ast atau Tree-Sitter, lalu menggabungkannya berdasarkan fungsi atau kelas, tingkat kegagalan merge akan turun hingga mendekati 0%.

Memotong Biaya Token hingga 60% dengan Pengukuran Kompleksitas AST

Menggunakan Claude 3.5 Sonnet secara merata untuk semua jenis tugas akan membuat biaya tidak terkendali. Kita tidak boleh menilai kompleksitas hanya dari jumlah baris kode (LOC). Kode sepanjang 100 baris yang penuh dengan operator ternari rumit dan kondisi bersarang jauh lebih sulit daripada data class 500 baris yang hanya berisi komentar.

Dengan modul ast, Anda dapat menghitung jumlah node, kompleksitas siklomatis (cyclomatic complexity), serta kedalaman tree untuk diubah menjadi skor terukur.

`python
import ast

class CodeComplexityAnalyzer(ast.NodeVisitor):
def init(self):
self.node_count = 0
self.max_depth = 0
self.current_depth = 0
self.cyclomatic_complexity = 1

def generic_visit(self, node):
    self.node_count += 1
    self.current_depth += 1
    if self.current_depth > self.max_depth:
        self.max_depth = self.current_depth
    
    super().generic_visit(node)
    self.current_depth -= 1

def visit_If(self, node):
    self.cyclomatic_complexity += 1
    self.generic_visit(node)

def visit_For(self, node):
    self.cyclomatic_complexity += 1
    self.generic_visit(node)

def visit_While(self, node):
    self.cyclomatic_complexity += 1
    self.generic_visit(node)

def visit_ExceptHandler(self, node):
    self.cyclomatic_complexity += 1
    self.generic_visit(node)

def visit_BoolOp(self, node):
    self.cyclomatic_complexity += len(node.values) - 1
    self.generic_visit(node)

def calculate_ast_metrics(source_code: str) -> dict:
try:
tree = ast.parse(source_code)
analyzer = CodeComplexityAnalyzer()
analyzer.visit(tree)

    score = (analyzer.node_count * 0.2) + (analyzer.max_depth * 1.5) + (analyzer.cyclomatic_complexity * 3.0)
    return {
        "node_count": analyzer.node_count,
        "max_depth": analyzer.max_depth,
        "cyclomatic_complexity": analyzer.cyclomatic_complexity,
        "complexity_score": round(score, 2),
        "is_valid": True
    }
except SyntaxError as e:
    return {"is_valid": False, "error": str(e), "complexity_score": 9999}

`

Letakkan penganalisis ini di pintu masuk pipeline backend dan tetapkan skor ambang batas routing pada angka 50.

Tugas dengan skor di bawah 50 seperti penulisan unit test, implementasi utilitas, atau definisi DTO akan dialihkan ke Claude 3.5 Haiku yang berbiaya sekitar $0,80 per satu juta token input. Hanya tugas skala besar seperti refactoring atau perancangan arsitektur dengan skor 50 ke atas yang dialihkan ke Claude 3.5 Sonnet seharga $3,00 per satu juta token. Memastikan Haiku menangani lebih dari 60% dari total traffic saja sudah dapat memangkas biaya API hingga 60%.

Masalah kebocoran token akibat konteks percakapan yang terlalu panjang dapat diatasi menggunakan middleware reset session. Kumpulkan akumulasi token, dan saat mencapai ambang batas, reset percakapan secara paksa. Pada saat ini, buat ringkasan menggunakan AST yang hanya mengambil simbol fungsi utama dan TODO yang tersisa, lalu masukkan ringkasan tersebut ke dalam prompt pertama sesi baru agar pekerjaan dapat dilanjutkan tanpa kehilangan konteks.

Meningkatkan Kecepatan Kerja hingga 40% dengan Chain Verifikasi Deterministik

Menggabungkan kode draf yang dibuat oleh agen langsung ke repositori dapat merusak proses build. Namun, memanggil LLM kembali hanya untuk memperbaiki salah ketik (typo) atau kesalahan tata bahasa (syntax error) sederhana akan memakan waktu lama dan membuang-buang uang.

Buatlah pipeline verifikasi bertahap yang menggabungkan linter, type checker, dan reviewer LLM.

`python
import ast
import subprocess
from pathlib import Path
from typing import Optional
from pydantic import BaseModel, Field

class ValidationResult(BaseModel):
is_success: bool = Field(description="검수 통과 여부")
failed_stage: Optional[str] = Field(default=None, description="실패한 검수 단계")
error_message: Optional[str] = Field(default=None, description="에러 메시지")
suggested_context: Optional[str] = Field(default=None, description="수정을 위해 주입할 콘텍스트")

class MultiLensReviewerChain:
def init(self, worktree_path: Path):
self.worktree_path = worktree_path

def run_stage1_ast_lint(self, file_path: Path) -> ValidationResult:
    try:
        with open(file_path, "r", encoding="utf-8") as f:
            code_content = f.read()
        ast.parse(code_content)
    except SyntaxError as e:
        return ValidationResult(
            is_success=False,
            failed_stage="Stage 1 (AST Syntax)",
            error_message=f"SyntaxError 발생 라인 {e.lineno}: {e.msg}",
            suggested_context=e.text
        )

    res = subprocess.run(["ruff", "check", str(file_path)], capture_output=True, text=True)
    if res.returncode != 0:
        return ValidationResult(
            is_success=False,
            failed_stage="Stage 1 (Ruff Linter)",
            error_message=res.stdout or res.stderr
        )

    return ValidationResult(is_success=True)

def run_stage2_type_check(self, file_path: Path) -> ValidationResult:
    res = subprocess.run(
        ["mypy", "--config-file", "mypy.ini", str(file_path)],
        capture_output=True, text=True, cwd=str(self.worktree_path)
    )
    if res.returncode != 0:
        return ValidationResult(
            is_success=False,
            failed_stage="Stage 2 (Mypy TypeChecker)",
            error_message=res.stdout
        )
    return ValidationResult(is_success=True)

def execute_pipeline(self, target_file_rel_path: str) -> ValidationResult:
    full_path = self.worktree_path / target_file_rel_path
    
    s1_res = self.run_stage1_ast_lint(full_path)
    if not s1_res.is_success:
        return s1_res

    s2_res = self.run_stage2_type_check(full_path)
    if not s2_res.is_success:
        return s2_res

    return ValidationResult(is_success=True)

`

Tahap 1 menangkap kesalahan sintaksis menggunakan parsing AST dan Ruff, kemudian Tahap 2 menyelaraskan tipe data menggunakan Mypy. Hanya kode yang lolos dari semua alat verifikasi statis ini yang dikirim ke Tahap 3, yaitu deep reviewer Claude 3.5 Sonnet. Ini menghilangkan pemanggilan ulang LLM akibat masalah sepele seperti kurang kurung atau type error, sehingga kecepatan penyelesaian pipeline meningkat 40%.

Circuit breaker sangat penting untuk mencegah agen terjebak dalam loop saat gagal melewatin verifikasi. Batasi percobaan ulang untuk kesalahan yang sama maksimal 3 kali. Jika nilai hash dari pesan kesalahan benar-benar sama seperti sebelumnya, sistem harus menilai bahwa agen telah terjebak dalam hallucination loop dan segera menghentikan eksekusi.

Shared Memory dan Rollback Transaksi

Untuk mengelola secara terpusat berkas dan cabang mana yang sedang diubah oleh setiap agen, setidaknya Anda memerlukan skema SQLite.

`sql
CREATE TABLE agent_sessions (
agent_id TEXT PRIMARY KEY,
worktree_path TEXT NOT NULL,
current_status TEXT CHECK(current_status IN ('IDLE', 'RUNNING', 'LINTING', 'FAILED', 'COMPLETED')),
assigned_task TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE file_locks (
file_path TEXT PRIMARY KEY,
locked_by_agent TEXT NOT NULL,
ast_symbol_node TEXT,
lock_acquired_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(locked_by_agent) REFERENCES agent_sessions(agent_id)
);

CREATE TABLE context_events (
event_id INTEGER PRIMARY KEY AUTOINCREMENT,
source_agent TEXT NOT NULL,
event_type TEXT CHECK(event_type IN ('FILE_MUTATED', 'INTERFACE_CHANGED', 'ROLLBACK_TRIGGERED')),
affected_path TEXT NOT NULL,
payload_json TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`

Setiap kali seorang agen berhasil melakukan commit pada modul bersama, event FILE_MUTATED akan dipublikasikan. Agen lain yang menerima notifikasi ini akan langsung memperbarui definisi simbol AST yang mereka rujuk ke versi terbaru.

Jika agen tertentu gagal dalam verifikasi dan berada dalam kondisi tidak dapat dipulihkan, lakukan atomic rollback ke SHA commit snapshot yang dicatat saat pertama kali memulai tugas.

bash git -C .agent_worktrees/wt_agent_01 reset --hard <SNAPSHOT_COMMIT_SHA> git -C .agent_worktrees/wt_agent_01 clean -fd

Dengan menghubungkan direktori terisolasi, routing model berbasis sintaksis, pipeline verifikasi statis, dan DB status seperti ini, Anda dapat menjalankan agent swarm kelas produksi secara stabil tanpa perlu khawatir tentang konflik berkas atau membengkaknya biaya.

관련 영상

Apa yang dapat kita pelajari dari eksperimen SQLite Rust Cursor

Maximilian Schwarzmüller32:24

커뮤니티의 다른 글

댓글

로그인하고 댓글을 남겨 보세요.

아직 댓글이 없습니다.