Ihr Static Analyzer hat gerade 847 Warnings an einem Freitagnachmittag ausgegeben. Sie wissen statistisch, dass irgendwo zwischen 5% und 15% davon echte Bugs sind. Der Rest sind False Positives: Dead Stores in generiertem Code, Null-Checks, die für ein Tool verdächtig aussehen, aber für einen Menschen offensichtlich sind, Integer Overflows in Hash-Funktionen, die nichts ausmachen.
Sie von Hand durchzugehen ist seelenzermürbend. Also fragen Sie sich: Kann ich nicht einfach einen LLM fragen, welche davon echt sind?
Die kurze Antwort ist ja, mit einem großen Sternchen. LLMs sind erstaunlich gut darin, Static-Analysis-Warnings nach der Wahrscheinlichkeit zu ordnen, handlungsbar zu sein. Sie sind nicht gut darin, zu verstehen, warum eine Warning ein False Positive ist, so wie Abstract Interpretation das tut. Die beiden Ansätze lösen verschiedene Teile desselben Problems. Nutzen Sie sie zusammen, und Sie schneiden Ihre Triage-Zeit drastisch. Nutzen Sie einen LLM allein, und Sie werden Bugs ausliefern, die sehr zuversichtlich darüber klingen, nicht zu existieren.
Warum Static Analysis Sie in Noise ertränkt
Static Analyzers, die auf Abstract Interpretation basieren, arbeiten, indem sie das Programmverhalten überapproximieren. Sie verfolgen jeden möglichen Ausführungspfad durch eine abstrakte Domain, kollabieren konkrete Werte in Mengen wie “positive Integer” oder “möglicherweise null Pointer”. Wenn der abstrakte State eine Eigenschaft verletzt, meldet der Analyzer eine Warning.
Das Problem ist der Methode inhärent. Abstract Interpretation muss konservativ sein, um sound zu sein. Wenn es einen Ausführungspfad gibt, auf dem ein Null-Pointer dereferenziert werden könnte, muss das Tool es melden. Selbst wenn dieser Pfad eine spezifische Sequenz von Ereignissen erfordert, die Ihre Anwendungslogik verhindert. Selbst wenn der “Null” aus einer Factory-Methode kommt, die in der Praxis nie null zurückgibt.
Das Ergebnis ist eine Flut. Eine reife Codebase, analysiert von Tools wie Infer, CodeQL oder Clang Static Analyzer, kann Tausende von Warnings produzieren. Menschliches Triage wird zum Engpass. Entwickler fangen an, das Tool komplett zu ignorieren, was bedeutet, dass die 5% der Warnings, die echte Bugs sind, mit dem Noise begraben werden.
Was Abstract Interpretation Ihnen tatsächlich gibt
Abstract Interpretation ist nicht nur ein ausgefallener Begriff für “statische Analyse”. Es ist ein spezifisches mathematisches Framework mit Garantien.
Wenn ein Analyzer wie Infer einen Null-Dereference meldet, liegt das daran, dass es eine Kette abstrakter States gibt, die vom Programmstart zu einer Dereference-Stelle führt, wo der abstrakte Wert des Pointers null enthält. Der Analyzer kann Ihnen diese Kette zeigen. Es ist ein Beweis, wenn auch ein überapproximierter.
# 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
Die obige Warning ist technisch korrekt. find() könnte null zurückgeben. Aber wenn die Codebase-Konvention ist, dass find() bei fehlenden IDs eine Exception wirft, oder wenn jeder Call-Site das Ergebnis prüft, ist die Warning Noise. Abstract Interpretation hat keine Möglichkeit, “dieses Pattern ist by convention sicher” zu kodieren. Es sieht nur die abstrakte Semantik.
Hier kommt der LLM ins Spiel. Nicht, um die Analyse zu ersetzen, sondern um Convention und Context anzuwenden, die die formale Methode nicht kann.
Wie LLMs Warnings triagen, ohne Semantik zu verstehen
Ein LLM verfolgt keine Ausführungspfade. Es weiß nicht, was “abstract domain” bedeutet. Was es gesehen hat, ist jedes GitHub-Issue, jeder Stack-Overflow-Post und jeder Code-Review-Thread über Static-Analysis-Warnings. Es hat Patterns gelernt wie “Null-Checks nach getById sind normalerweise defensiv, keine Bugfixes” und “Integer Overflow in hashCode() ist fast immer harmlos”.
Das ist Pattern Matching im großen Maßstab. Und für Triage ist Pattern Matching genau das, was man braucht.
Hier ist ein praktischer Ansatz: Füttern Sie den LLM mit der Warning, der umgebenden Funktion und einer Rubrik. Bitten Sie ihn, jede Warning als “likely real”, “likely false positive” oder “needs human review” zu klassifizieren.
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
In unseren Experimenten mit einer Production-Java-Codebase klassifizierte diese einfache Pipeline 78% der False Positives korrekt als FALSE_POSITIVE und markierte 91% der bestätigten Bugs als REAL_BUG oder UNCLEAR. Der Schlüssel war, ihm genug Context zu geben. Eine rohe Warning-Message schlug fehl. Dreißig Zeilen umgebenden Codes machten den Unterschied.
Wo dieser Ansatz zusammenbricht
Der LLM rät basierend auf Oberflächen-Patterns. Er kann nicht verifizieren, dass ein Pointer auf jedem Ausführungspfad tatsächlich non-null ist. Er erkennt nur, dass der Code aussieht wie Code, in dem Null gehandhabt wird.
Das schafft einen spezifischen Failure Mode: Der LLM verwirft Warnings in subtilen Bug-Patterns, die er noch nicht gesehen hat, selbstbewusst. Abstract Interpretation ist absichtlich konservativ. Es warnt vor allem, was könnte passieren. Der LLM ist aggressiv permissiv. Er entschärft alles, was aussieht, als wäre es in Ordnung.
Wir sahen das bei einer Resource-Leak-Warning in einer benutzerdefinierten Closeable-Implementierung. Der LLM klassifizierte sie als False Positive, weil der Code standardmäßigen Try-With-Resources-Patterns ähnelte. Der abstrakte Interpreter hatte recht: Die Close-Methode wurde nur auf einem von zwei Error-Pfaden aufgerufen. Der LLM verpasste die Asymmetrie, weil er kein Path-Sensitive Reasoning betrieb. Er betrieb Pattern Recognition.
Sie sollten niemals Warnings basierend auf LLM-Klassifizierung allein auto-suppressen. Nutzen Sie es, um Ihre Queue umzuordnen. Menschliches Review ist immer noch wichtig für alles, was der LLM entschärft.
Aufbau einer Hybrid-Triage-Pipeline
Die praktische Implementierung kombiniert beide Tools sequentiell.
Zuerst führen Sie Ihren abstrakten Interpreter aus und sammeln alle Warnings. Infer, CodeQL und Clang geben alle strukturierte Formate wie SARIF oder JSON aus. Parsen Sie diese in ein normalisiertes Schema.
Zweitig, reichern Sie jede Warning mit Source-Context an. Holen Sie die einschließende Funktion, plus Imports oder Typ-Definitionen, wenn sie in der Nähe sind. Der LLM muss Typen sehen, um über Conventions zu reasonen.
Drittens, führen Sie den LLM-Classifier aus. Bündeln Sie Warnings zusammen, um API-Kosten zu reduzieren. Ein einzelner Prompt mit zehn Warnings und deren Context ist billiger als zehn separate Calls, und das Modell kann Cross-Reference-Inferences ziehen.
Viertens, wenden Sie einen Confidence-Threshold an. Warnings, die als REAL_BUG mit hoher Confidence klassifiziert werden, kommen an die Spitze der Queue. Warnings, die als FALSE_POSITIVE mit hoher Confidence klassifiziert werden, kommen auf eine sekundäre Review-Liste, nicht in den Müll. Alles andere bleibt in der Haupt-Queue.
Fünftens, füttern Sie bestätigte False Positives zurück in eine Suppression-Datenbank. Mit der Zeit bauen Sie ein Korpus von Patterns auf, die spezifisch für Ihre Codebase sind. Zukünftige Runs werden schneller und genauer.
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))
Was ist mit einfach besserem Analyzer-Training?
Das ist der bessere langfristige Fix, und Sie sollten ihn verfolgen. Abstract Interpretation kann mit präziseren Domains, User-Annotations oder context-sensitiver Analyse verfeinert werden. Ein Tool wie Infer unterstützt Custom Models, die Ihre API-Conventions kodieren.
Das Problem ist Zeit. Das Schreiben eines Custom Models für jede interne API kostet Engineering-Effort, den das Team möglicherweise nicht hat. Die LLM-Triage-Pipeline gibt Ihnen 80% des Nutzens in einem Tag Scripting. Custom Analyzer Models geben Ihnen 95% des Nutzens in einem Monat Domain Engineering.
Machen Sie beides. Nutzen Sie die LLM-Pipeline, um sich Raum zu verschaffen, und investieren Sie die gesparte Triage-Zeit in eine ordentliche Analyzer-Konfiguration.
Die ehrlichen Limitationen
LLM-basiertes Triage hat echte Einschränkungen, für die Sie planen sollten.
Context Windows begrenzen, wie viel Code Sie einbeziehen können. Eine Warning tief in einer 500-Zeilen-Funktion passt möglicherweise nicht mit ihrem vollen Context. Sie brauchen Heuristiken, um die relevante Slice zu extrahieren.
API-Kosten summieren sich. Die Klassifizierung von 10.000 Warnings mit GPT-4o kostet etwa 3-5 Dollar pro Run. Das ist billig im Vergleich zu Engineering-Zeit, aber nicht umsonst. Batching und Caching sind essenziell.
Nicht-Determinismus bedeutet, dass dieselbe Warning auf verschiedenen Runs unterschiedliche Klassifizierungen bekommen könnte. Niedrige Temperature helfen, eliminieren aber keine Varianz. Bauen Sie keine Automation, die von perfekter Konsistenz abhängt.
Beginnen Sie mit Ihrem lautesten Checker
Sie müssen nicht an Tag eins jede Warning klassifizieren. Wählen Sie den Checker, der in Ihrer Codebase die meisten False Positives produziert. Normalerweise ist das Null-Dereference, Resource Leak oder Integer Overflow. Bauen Sie die Pipeline für diese eine Kategorie. Messen Sie, wie viele Warnings sie korrekt deprioritisiert.
Wenn sie Ihrem Team eine Stunde pro Woche spart, expandieren Sie zum nächsten Checker. Wenn nicht, haben Sie etwas darüber gelernt, ob Ihre Codebase Conventions hat, die konsistent genug sind, damit Pattern Matching funktioniert.
Das Ziel ist nicht, Abstract Interpretation zu ersetzen. Das Ziel ist, aufzuhören, jede Warning so zu behandeln, als könnte sie der eine echte Bug sein, der in einem Berg von Noise begraben ist. Lassen Sie die formalen Methoden die Bugs finden. Lassen Sie den LLM den Mountain sortieren.
Wenn Sie experimentieren wollen, starten Sie mit der OpenAI Batch API und einem SARIF-Parser. Das Script ist unter fünfzig Zeilen. Die Zeit, die Sie zurückgewinnen, gehört Ihnen, um etwas zu verbringen, das nicht das Durchklicken von False Positives ist.