你的靜態分析器剛剛在一個週五下午發出了847條警告。從統計上看,其中大約5%到15%是真正的缺陷。其餘都是誤報:生成程式碼中的無效儲存、對工具來說可疑但對人類來說顯而易見的空值檢查、無關緊要的雜湊函式中的整數溢位。

手動逐一排查令人心力交瘁。於是你想:能不能直接問LLM哪些是真的?

簡短的答案是:可以,但有個大大的星號。LLM在按可操作性對靜態分析警告排序方面出奇地擅長。但它們無法像抽象詮釋那樣理解為什麼一條警告是誤報。這兩種方法解決的是同一問題的不同部分。結合使用,能大幅削減你的分流時間。單獨使用LLM,你會發布一些”非常自信地聲稱自己不存在”的缺陷。

為什麼靜態分析讓你淹沒在噪音中

基於抽象詮釋的靜態分析器透過過度近似程式行為來工作。它們追蹤透過抽象領域的每一條可能的執行路徑,將具體值摺疊成如”正整數”或”可能為空指標”的集合。當抽象狀態違反某個屬性時,分析器就會報告警告。

這個問題是該方法固有的。抽象詮釋為了健全性必須保持保守。如果存在任何空指標可能被解引用的執行路徑,工具就必須報告它。即使該路徑需要應用程式邏輯阻止的特定事件序列。即使這個”空值”來自一個實際上從不返回空值的工廠方法。

結果是洪水。一個成熟的程式碼庫經過Infer、CodeQL或Clang Static Analyzer等工具分析後,可能產生數千條警告。人工分流成為瓶頸。開發者開始完全忽略這個工具,這意味著那5%真正是缺陷的警告被噪音埋沒。

抽象詮釋真正給你的東西

抽象詮釋不僅僅是”靜態分析”的花哨說法。它是一個帶有保證的特定數學框架。

當Infer這樣的分析器報告空指標解引用時,是因為存在一條從程式入口到解引用位置的抽象狀態鏈,該位置上指標的抽象值包含空值。分析器可以向你展示這條鏈。這是一個證明,albeit 是過度近似的。

# 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

上述警告在技術上是正確的。find()可能返回空值。但如果程式碼庫的慣例是find()在ID缺失時丟擲異常,或者每個呼叫點都檢查結果,那這條警告就是噪音。抽象詮釋無法編碼”這個模式按慣例是安全的”。它只能看到抽象語義。

這就是LLM的用武之地。不是取代分析,而是應用形式化方法無法應用的慣例和上下文。

LLM如何在不理解語義的情況下分流警告

LLM不追蹤執行路徑。它不知道”抽象領域”是什麼意思。它看過的是每一個關於靜態分析警告的GitHub問題、Stack Overflow貼文和程式碼審查執行緒。它學到了諸如”getById後的空值檢查通常是防禦性的,不是缺陷修復”、“hashCode()中的整數溢位幾乎總是無害”等模式。

這是大規模的模式匹配。而對於分流來說,模式匹配正是你需要的。

以下是實用方法:給LLM提供警告、surrounding 函式和評分標準。讓它將每條警告分類為”可能是真的”、“可能是誤報”或”需要人工審查”。

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

在我們對一個生產級Java程式碼庫的實驗中,這個簡單的流水線正確地將78%的誤報分類為FALSE_POSITIVE,並將91%的確認缺陷標記為REAL_BUGUNCLEAR。關鍵在於給它足夠的上下文。原始的警告訊息表現很差。surrounding 三十行程式碼改變了結果。

這個方法在哪裡崩潰

LLM基於表面模式進行猜測。它無法驗證指標在每條執行路徑上都確實非空。它只是識別出程式碼看起來像是處理了空值的程式碼。

這產生了一種特定的失效模式:LLM自信地駁回它從未見過的微妙缺陷模式中的警告。抽象詮釋是故意保守的。它對任何可能發生的事情發出警告。LLM則是激進地寬容。它清除任何看起來沒事的東西。

我們在一個自訂Closeable實作中的資源洩漏警告上看到了這一點。LLM將其分類為誤報,因為程式碼類似於標準的try-with-resources模式。抽象詮釋器是正確的:close方法只在兩個錯誤路徑之一上被呼叫。LLM錯過了這種不對稱性,因為它沒有進行路徑敏感的推理。它進行的是模式識別。

你永遠不應該僅僅基於LLM的分類來自動抑制警告。用它重新排序你的queue。對於LLM清除的任何東西,人工審查仍然重要。

建構混合分流流水線

實際實現是按順序結合兩種工具。

首先,執行你的抽象詮釋器並收集所有警告。Infer、CodeQL和Clang都輸出SARIF或JSON等結構化格式。將它們解析為規範化模式。

其次,用原始碼上下文豐富每條警告。獲取enclosing函式,如果附近有匯入或型別定義,也一併獲取。LLM需要看到型別才能對慣例進行推理。

第三,執行LLM分類器。將警告batch 處理以降低API成本。一個包含十條警告及其上下文的單一提示比十次單獨呼叫更便宜,而且模型可以進行交叉引用推斷。

第四,應用置信度閾值。高置信度分類為REAL_BUG的警告排在queue最前面。高置信度分類為FALSE_POSITIVE的警告進入二次審查列表,而不是垃圾桶。其他所有警告留在主queue中。

第五,將確認的誤報回饋到抑制資料庫中。隨著時間的推移,你將建立一個針對自己程式碼庫的模式語料庫。未來的執行會更快更準確。

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))

那直接更好地訓練分析器呢?

這是更好的長期修復方案,你應該追求。抽象詮釋可以透過更精確的領域、使用者註釋或上下文敏感分析來細化。像Infer這樣的工具支援自訂模型來編碼你的API慣例。

問題是時間。為每個內部API編寫自訂模型需要團隊可能沒有的工程投入。LLM分流流水線用一天指令碼編寫給你80%的收益。自訂分析器模型用一個月領域工程給你95%的收益。

兩者都做。用LLM流水線爭取喘息空間,然後將節省的分流時間投入到適當的分析器配置中。

誠實的局限性

基於LLM的分流有你需要計劃的真正約束。

上下文視窗限制了你能包含多少程式碼。一個500行函式深處的警告可能無法容納其完整上下文。你需要啟發式方法來提取相關切片。

API成本會累積。用GPT-4o分類10,000條警告每次執行大約3-5美元。與工程時間相比這很便宜,但不是免費的。batch 處理和快取至關重要。

非確定性意味著同一條警告在不同執行中可能得到不同分類。低溫有幫助,但不能消除變異。不要建構依賴完美一致性的自動化。

從你最吵的檢查器開始

你不需要第一天就分類每條警告。選擇在你的程式碼庫中產生最多誤報的檢查器。通常是空指標解引用、資源洩漏或整數溢位。為這一個類別建構流水線。衡量它正確降低優先順序多少條警告。

如果它每週為你的團隊節省一小時,擴展到下一個檢查器。如果沒有,你學到了一些關於你的程式碼庫是否有足夠一致的慣例讓模式匹配發揮作用的東西。

目標不是取代抽象詮釋。目標是停止把每條警告都當作可能是埋在噪音山裡的唯一真正缺陷。讓形式化方法找到缺陷。讓LLM分類這座山。

如果你想實驗,從OpenAI Batch API和SARIF parser開始。指令碼不到五十行。你贏回的時間可以花在點擊誤報之外的事情上。