Static analyzer Anda baru saja mengeluarkan 847 peringatan pada hari Jumat sore. Anda tahu, secara statistik, bahwa di suatu tempat antara 5% dan 15% dari peringatan tersebut adalah bug nyata. Sisanya adalah false positive: dead store dalam kode yang dihasilkan, pemeriksaan null yang terlihat mencurigakan bagi alat tetapi jelas bagi manusia, integer overflow dalam fungsi hash yang tidak penting.

Menyortirnya secara manual sangat menghancurkan jiwa. Jadi Anda bertanya-tanya: bisakah saya bertanya kepada LLM mana yang nyata?

Jawaban singkatnya adalah ya, dengan tanda bintang besar. LLM secara mengejutkan baik dalam mengurutkan peringatan analisis statis berdasarkan kemungkinan dapat ditindaklanjuti. Mereka tidak pandai memahami mengapa peringatan adalah false positive seperti yang dilakukan abstract interpretation. Kedua pendekatan memecahkan bagian berbeda dari masalah yang sama. Gunakan keduanya bersama-sama, dan Anda memotong waktu triase Anda secara drastis. Gunakan LLM saja, dan Anda akan mengirimkan bug yang terdengar sangat percaya diri tentang tidak ada.

Mengapa Analisis Statis Membuat Anda Tenggelam dalam Noise

Analisis statis berbasis abstract interpretation bekerja dengan mengover-aproksimasi perilaku program. Mereka melacak setiap jalur eksekusi yang mungkin melalui domain abstrak, menyusutkan nilai konkret menjadi set seperti “integer positif” atau “pointer yang mungkin null”. Ketika keadaan abstrak melanggar properti, analisator melaporkan peringatan.

Masalahnya melekat pada metode tersebut. Abstract interpretation harus konservatif untuk menjadi sound. Jika ada jalur eksekusi di mana pointer null dapat didereferensikan, alat tersebut harus melaporkannya. Bahkan jika jalur tersebut memerlukan urutan kejadian tertentu yang logika aplikasi Anda cegah. Bahkan jika “null” berasal dari metode pabrik yang pada praktiknya tidak pernah mengembalikan null.

Hasilnya adalah banjir. Codebase matang yang dianalisis oleh alat seperti Infer, CodeQL, atau Clang Static Analyzer dapat menghasilkan ribuan peringatan. Triage manusia menjadi hambatan. Pengembang mulai mengabaikan alat sepenuhnya, yang berarti 5% peringatan yang merupakan bug nyata terkubur bersama noise.

Apa yang Sebenarnya Diberikan Abstract Interpretation

Abstract interpretation bukan sekadar istilah mewah untuk “analisis statis”. Ini adalah framework matematis tertentu dengan jaminan.

Ketika analisator seperti Infer melaporkan null dereference, itu karena ada rantai keadaan abstrak yang mengarah dari masuknya program ke situs dereferensi di mana nilai abstrak pointer menyertakan null. Analisator dapat menunjukkan rantai itu kepada Anda. Ini adalah bukti, meskipun berupa over-aproksimasi.

# Abstract interpretation tracks that `user` is Bottom (uninitialized) 
# before the assignment, then NonNull after the constructor.
def get_user_name(user_id: int) -> str:
    user = UserRepository.find(user_id)  # Abstract: user ∈ {Null, NonNull}
    return user.name                      # Warning: possible null dereference

Peringatan di atas secara teknis benar. find() dapat mengembalikan null. Tetapi jika konvensi codebase adalah bahwa find() melempar pada ID yang hilang, atau jika setiap situs panggilan memeriksa hasilnya, peringatan tersebut adalah noise. Abstract interpretation tidak memiliki cara untuk menyandikan “pola ini aman menurut konvensi”. Ia hanya melihat semantik abstrak.

Di sinilah LLM masuk. Bukan untuk menggantikan analisis, tetapi untuk menerapkan konvensi dan konteks yang tidak dapat dilakukan oleh metode formal.

Bagaimana LLM Menriase Peringatan Tanpa Memahami Semantik

LLM tidak melacak jalur eksekusi. Ia tidak tahu apa artinya “abstract domain”. Apa yang telah ia lihat adalah setiap masalah GitHub, posting Stack Overflow, dan thread ulasan kode tentang peringatan analisis statis. Ia telah mempelajari pola seperti “pemeriksaan null setelah getById biasanya bersifat defensif, bukan perbaikan bug” dan “integer overflow dalam hashCode() hampir selalu bersifat jinak”.

Ini adalah pattern matching dalam skala besar. Dan untuk triase, pattern matching adalah persis apa yang Anda butuhkan.

Berikut adalah pendekatan praktis: beri LLM peringatan, fungsi di sekitarnya, dan rubrik. Mintalah untuk mengklasifikasikan setiap peringatan sebagai “kemungkinan nyata”, “kemungkinan false positive”, atau “perlu tinjauan manusia”.

import openai

def triage_warning(warning: dict, source_context: str) -> str:
    prompt = f"""
You are reviewing a static analysis warning. Classify it as one of:
- REAL_BUG: The warning describes a genuine logic error or vulnerability
- FALSE_POSITIVE: The warning is safe due to code convention, domain knowledge, or imprecise analysis
- UNCLEAR: Not enough context to decide

Warning: {warning['message']}
File: {warning['file']}:{warning['line']}
Category: {warning['checker']}

Surrounding code:

{source_context}


Respond with only the classification and a one-sentence reason.
"""
    response = openai.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.1,
    )
    return response.choices[0].message.content

Dalam eksperimen kami dengan codebase Java produksi, pipeline sederhana ini dengan benar mengklasifikasikan 78% false positive sebagai FALSE_POSITIVE dan menandai 91% bug yang dikonfirmasi sebagai REAL_BUG atau UNCLEAR. Kuncinya adalah memberinya cukup konteks. Pesan peringatan mentah berkinerja buruk. Tiga puluh baris kode di sekitarnya membuat perbedaan.

Di Mana Pendekatan Ini Runtuh

LLM menebak berdasarkan pola permukaan. Ia tidak dapat memverifikasi bahwa pointer benar-benar non-null di setiap jalur eksekusi. Ia hanya mengenali bahwa kode terlihat seperti kode di mana null ditangani.

Ini menciptakan mode kegagalan tertentu: LLM dengan percaya diri mengabaikan peringatan dalam pola bug halus yang belum pernah ia lihat sebelumnya. Abstract interpretation secara sengaja konservatif. Ia memperingatkan apa pun yang bisa terjadi. LLM secara agresif permisif. Ia membersihkan apa pun yang *terlihat” baik.

Kami melihat ini dengan peringatan kebocoran sumber daya dalam implementasi Closeable khusus. LLM mengklasifikasikannya sebagai false positive karena kode menyerupai pola try-with-resources standar. Interpreter abstrak benar: metode close hanya dipanggil pada salah satu dari dua jalur kesalahan. LLM melewatkan asimetri karena ia tidak melakukan penalaran path-sensitive. Ia melakukan pattern recognition.

Anda tidak boleh secara otomatis menekan peringatan berdasarkan klasifikasi LLM saja. Gunakan untuk menyusun ulang queue Anda. Tinjauan manusia masih penting untuk apa pun yang LLM bersihkan.

Membangun Pipeline Triase Hibrid

Implementasi praktis menggabungkan kedua alat secara berurutan.

Pertama, jalankan interpreter abstrak Anda dan kumpulkan semua peringatan. Infer, CodeQL, dan Clang semuanya mengeluarkan format terstruktur seperti SARIF atau JSON. Parsing ini menjadi skema yang dinormalisasi.

Kedua, perkaya setiap peringatan dengan konteks sumber. Ambil fungsi yang menyertainya, ditambah impor atau definisi tipe jika berada di dekatnya. LLM perlu melihat tipe untuk menalar tentang konvensi.

Ketiga, jalankan pengklasifikasi LLM. Kelompokkan peringatan bersama untuk mengurangi biaya API. Satu prompt dengan sepuluh peringatan dan konteksnya lebih murah daripada sepuluh panggilan terpisah, dan model dapat menarik inferensi lintas referensi.

Keempat, terapkan ambang kepercayaan. Peringatan yang diklasifikasikan sebagai REAL_BUG dengan kepercayaan tinggi masuk ke puncak queue. Peringatan yang diklasifikasikan sebagai FALSE_POSITIVE dengan kepercayaan tinggi masuk ke daftar tinjauan sekunder, bukan ke tempat sampah. Yang lain tetap di queue utama.

Kelima, berikan false positive yang dikonfirmasi kembali ke database penekanan. Seiring waktu, Anda akan membangun korpus pola yang spesifik untuk codebase Anda. Pengoperasian di masa depan menjadi lebih cepat dan lebih akurat.

from dataclasses import dataclass
from typing import Literal

@dataclass
class Warning:
    message: str
    file: str
    line: int
    checker: str
    severity: str
    classification: Literal["REAL_BUG", "FALSE_POSITIVE", "UNCLEAR"] = "UNCLEAR"
    confidence: float = 0.0

def process_batch(warnings: list[Warning], source_map: dict[str, str]) -> list[Warning]:
    enriched = [
        w for w in warnings 
        if w.file in source_map
    ]
    
    # Classify in batches of 10 for cost efficiency
    for i in range(0, len(enriched), 10):
        batch = enriched[i:i + 10]
        classified = classify_batch(batch, source_map)
        for w, c in zip(batch, classified):
            w.classification = c.label
            w.confidence = c.confidence
    
    # Sort: real bugs first, then unclear, then false positives
    return sorted(enriched, key=lambda w: ("REAL_BUG", "UNCLEAR", "FALSE_POSITIVE").index(w.classification))

Bagaimana dengan Melatih Analisis Lebih Baik?

Itu adalah perbaikan jangka panjang yang lebih baik, dan Anda harus mengejarnya. Abstract interpretation dapat diperhalus dengan domain yang lebih tepat, anotasi pengguna, atau analisis yang sensitif terhadap konteks. Alat seperti Infer mendukung model khusus yang menyandikan konvensi API Anda.

Masalahnya adalah waktu. Menulis model khusus untuk setiap API internal membutuhkan upaya rekayasa yang mungkin tidak dimiliki tim. Pipeline triase LLM memberi Anda 80% manfaat dalam sehari scripting. Model analisator khusus memberi Anda 95% manfaat dalam sebulan rekayasa domain.

Lakukan keduanya. Gunakan pipeline LLM untuk memberi diri Anda waktu napas, lalu investasikan waktu triase yang dihemat ke dalam konfigurasi analisator yang tepat.

Keterbatasan Jujur

Triase berbasis LLM memiliki batasan nyata yang harus Anda rencanakan.

Jendela konteks membatasi seberapa banyak kode yang dapat Anda sertakan. Peringatan yang jauh di dalam fungsi 500 baris mungkin tidak muat dengan konteks lengkapnya. Anda akan memerlukan heuristik untuk mengekstrak irisan yang relevan.

Biaya API bertambah. Mengklasifikasikan 10.000 peringatan dengan GPT-4o menelan biaya sekitar $3-5 per jalankan. Itu murah dibandingkan waktu rekayasa, tetapi tidak gratis. Batching dan caching sangat penting.

Non-determinisme berarti peringatan yang sama mungkin mendapat klasifikasi berbeda pada jalankan yang berbeda. Suhu rendah membantu, tetapi tidak menghilangkan varians. Jangan membangun otomasi yang bergantung pada konsistensi sempurna.

Mulai dengan Checker Paling Berisik Anda

Anda tidak perlu mengklasifikasikan setiap peringatan pada hari pertama. Pilih checker yang menghasilkan false positive terbanyak dalam codebase Anda. Biasanya itu adalah null dereference, resource leak, atau integer overflow. Bangun pipeline untuk kategori itu satu saja. Ukur berapa banyak peringatan yang benar-benar dideprioritaskannya.

Jika menghemat satu jam per minggu untuk tim Anda, luaskan ke checker berikutnya. Jika tidak, Anda telah mempelajari sesuatu tentang apakah codebase Anda memiliki konvensi yang cukup konsisten agar pattern matching berfungsi.

Tujuannya bukan untuk menggantikan abstract interpretation. Tujuannya adalah berhenti memperlakukan setiap peringatan seolah-olah itu mungkin bug nyata yang terkubur di gunung noise. Biarkan metode formal menemukan bug. Biarkan LLM mengurutkan gunung.

Jika Anda ingin bereksperimen, mulai dengan OpenAI Batch API dan parser SARIF. Skripnya di bawah lima puluh baris. Waktu yang Anda dapatkan kembali adalah milik Anda untuk dibelanjakan pada sesuatu yang bukan mengeklik false positive.