Skip to content

Commit c547448

Browse files
committed
fix(cr-agent): _apply_verdicts 兼容FP/false_positive多种verdict格式(修复降噪失效) (#92)
修复 PR #200 reviewer(CongkeChen)发现的 Critical bug: - DENOISING_PROMPT 要求 LLM 返回 "verdict":"TP|FP"(TP=真问题, FP=误报) - 原 _apply_verdicts 只在 verdict == "false_positive" 时剔除 - 真 LLM 返回 "FP" 不匹配 "false_positive" → 降噪失效 - 现在兼容多种格式:FP, false_positive, false positive, False Positive, 误报 新增测试:test_real_mode_fp_verdict_compatibility() 验证真 LLM 返回 "FP" 格式时正确剔除误报
1 parent e049361 commit c547448

2 files changed

Lines changed: 84 additions & 1 deletion

File tree

examples/skills_code_review_agent/agent/llm_layer.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,9 @@ def _apply_verdicts(findings: list[Finding], verdicts: list[dict]) -> list[Findi
413413

414414
if verdict:
415415
# 如果被标记为 false_positive,跳过(剔除误报)
416-
if verdict.get("verdict") == "false_positive":
416+
# 兼容多种格式:FP, false_positive, false positive, False Positive, 误报
417+
_v = str(verdict.get("verdict", "")).strip().lower().replace(" ", "_")
418+
if _v in ("false_positive", "fp", "false", "误报"):
417419
continue
418420

419421
# 更新 source 和置信度(IMP-2:区分降噪确认和补召回新增)

examples/skills_code_review_agent/tests/test_llm_layer.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,87 @@ def test_prepare_diff_context_redaction_order():
534534
assert "... [截断]" in result, "长行应该被截断"
535535

536536

537+
def test_real_mode_fp_verdict_compatibility():
538+
"""测试真模式下 LLM 返回 'FP' 格式(按 DENOISING_PROMPT 指示)能正确剔除误报
539+
540+
Bug: _apply_verdicts 原本只检查 verdict == "false_positive",
541+
但 DENOISING_PROMPT 要求 LLM 返回 "TP|FP" 格式,导致真 LLM 返回 "FP" 时无法识别剔除。
542+
修复后应兼容多种格式:FP, false_positive, false positive, False Positive, 误报
543+
"""
544+
findings = [
545+
Finding(severity=Severity.HIGH,
546+
category="security",
547+
file="auth.py",
548+
line=5,
549+
title="Hardcoded password",
550+
evidence="password = 'admin123'",
551+
recommendation="Use env var",
552+
confidence=0.9,
553+
source="rule",
554+
rule_id="SECRET002"),
555+
Finding(severity=Severity.LOW,
556+
category="style",
557+
file="util.py",
558+
line=15,
559+
title="Long line",
560+
evidence="return 'a' * 1000",
561+
recommendation="Break line",
562+
confidence=0.5,
563+
source="rule",
564+
rule_id="STYLE002"),
565+
Finding(severity=Severity.MEDIUM,
566+
category="bug",
567+
file="parser.py",
568+
line=20,
569+
title="Potential null pointer",
570+
evidence="obj.method()",
571+
recommendation="Add null check",
572+
confidence=0.7,
573+
source="rule",
574+
rule_id="BUG003"),
575+
]
576+
577+
files = [DiffFile(path="auth.py", status="modified", hunks=[], added_lines=[])]
578+
579+
# Mock LLM 返回多种 verdict 格式(按 DENOISING_PROMPT 要求返回 "TP|FP")
580+
mock_verdicts = [
581+
{
582+
"rule_id": "SECRET002",
583+
"file": "auth.py",
584+
"line": 5,
585+
"verdict": "TP", # 真 LLM 按 prompt 返回 TP(true_positive 简写)
586+
"reason": "Real password hardcoded"
587+
},
588+
{
589+
"rule_id": "STYLE002",
590+
"file": "util.py",
591+
"line": 15,
592+
"verdict": "FP", # 真 LLM 按 prompt 返回 FP(false_positive 简写)
593+
"reason": "Style preference, not a real issue"
594+
},
595+
{
596+
"rule_id": "BUG003",
597+
"file": "parser.py",
598+
"line": 20,
599+
"verdict": "false positive", # 兼容完整拼写(带空格)
600+
"reason": "False alarm due to null check earlier"
601+
},
602+
]
603+
604+
with patch('agent.llm_layer._call_llm_for_classification') as mock_llm:
605+
mock_llm.return_value = mock_verdicts
606+
607+
# 设置环境变量
608+
with patch.dict(os.environ, {'OPENAI_API_KEY': 'test_key'}):
609+
result = enhance(findings, files, dry_run=False)
610+
611+
# 验证:应该剔除 FP 和 false positive 格式的误报,只保留 TP
612+
assert len(result) == 1
613+
assert result[0].rule_id == "SECRET002"
614+
assert result[0].source == "rule+llm"
615+
assert result[0].confidence > 0.9 # 置信度应该提升
616+
617+
537618
if __name__ == "__main__":
538619
# 运行测试前先确认 llm_layer.py 存在
539620
import sys

0 commit comments

Comments
 (0)