你的静态分析器刚刚在一个周五下午发出了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_BUG或UNCLEAR。关键在于给它足够的上下文。原始的警告消息表现很差。 surrounding 三十行代码改变了结果。
这个方法在哪里崩溃
LLM基于表面模式进行猜测。它无法验证指针在每条执行路径上都确实非空。它只是识别出代码看起来像是处理了空值的代码。
这产生了一种特定的失效模式:LLM自信地驳回它从未见过的微妙缺陷模式中的警告。抽象解释是故意保守的。它对任何可能发生的事情发出警告。LLM则是激进地宽容。它清除任何看起来没事的东西。
我们在一个自定义Closeable实现中的资源泄漏警告上看到了这一点。LLM将其分类为误报,因为代码类似于标准的try-with-resources模式。抽象解释器是正确的:close方法只在两个错误路径之一上被调用。LLM错过了这种不对称性,因为它没有进行路径敏感的推理。它进行的是模式识别。
你永远不应该仅仅基于LLM的分类来自动抑制警告。用它重新排序你的队列。对于LLM清除的任何东西,人工审查仍然重要。
构建混合分流流水线
实际实现是按顺序结合两种工具。
首先,运行你的抽象解释器并收集所有警告。Infer、CodeQL和Clang都输出SARIF或JSON等结构化格式。将它们解析为规范化模式。
其次,用源代码上下文丰富每条警告。获取 enclosing 函数,如果附近有导入或类型定义,也一并获取。LLM需要看到类型才能对惯例进行推理。
第三,运行LLM分类器。将警告批量处理以降低API成本。一个包含十条警告及其上下文的单一提示比十次单独调用更便宜,而且模型可以进行交叉引用推断。
第四,应用置信度阈值。高置信度分类为REAL_BUG的警告排在队列最前面。高置信度分类为FALSE_POSITIVE的警告进入二次审查列表,而不是垃圾桶。其他所有警告留在主队列中。
第五,将确认的误报反馈到抑制数据库中。随着时间的推移,你将建立一个针对自己代码库的模式语料库。未来的运行会更快更准确。
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美元。与工程时间相比这很便宜,但不是免费的。批处理和缓存至关重要。
非确定性意味着同一条警告在不同运行中可能得到不同分类。低温有帮助,但不能消除方差。不要构建依赖完美一致性的自动化。
从你最吵的检查器开始
你不需要第一天就分类每条警告。选择在你的代码库中产生最多误报的检查器。通常是空指针解引用、资源泄漏或整数溢出。为这一个类别构建流水线。衡量它正确降低优先级多少条警告。
如果它每周为你的团队节省一小时,扩展到下一个检查器。如果没有,你学到了一些关于你的代码库是否有足够一致的惯例让模式匹配发挥作用的东西。
目标不是取代抽象解释。目标是停止把每条警告都当作可能是埋在噪音山里的唯一真正缺陷。让形式化方法找到缺陷。让LLM分类这座山。
如果你想实验,从OpenAI Batch API和SARIF解析器开始。脚本不到五十行。你赢回的时间可以花在点击误报之外的事情上。