|
| 1 | +# Silent-fix classifier — model evaluation |
| 2 | + |
| 3 | +Evaluation of LLM backends for the training-free silent-fix classifier |
| 4 | +(`collection/llm_classify_fixes.py`). All numbers are on **one fixed 80-item |
| 5 | +eval set** (40 positive / 40 negative) so every model and ensemble is directly |
| 6 | +comparable. Runs on **Ollama Cloud** via the environment's `hermes-agent` |
| 7 | +(OpenAI-compatible `https://ollama.com/v1`); `claude -p` (Opus) is the baseline. |
| 8 | + |
| 9 | +_Last updated: 2026-07-02._ |
| 10 | + |
| 11 | +## Method |
| 12 | + |
| 13 | +- **Task:** given a code diff + dev artifacts (title/description) + a "graph-lite" |
| 14 | + hint (the security-sensitive subsystem touched), decide `is_security_fix` |
| 15 | + with a confidence, via a Chain-of-Thought prompt (LLM4VFD-style, arXiv |
| 16 | + 2501.14983). No training / fine-tuning. |
| 17 | +- **Labels:** unambiguous title-based ground truth — positives are explicit |
| 18 | + "fix panic/crash/overflow/race/consensus…" changes; negatives are clear |
| 19 | + feature/refactor/perf. Vendored-dep, revert, and test/CI-only changes are |
| 20 | + excluded from positives. (The LLM often reasons *more* precisely than these |
| 21 | + labels — attacker-reachability, prod-vs-test — so measured F1 slightly |
| 22 | + understates true quality.) |
| 23 | +- **Discipline:** every candidate is judged on precision/recall/F1 **and** the |
| 24 | + applied-confidence ranking; a good aggregate that ranks features above real |
| 25 | + fixes is rejected. (A TF-IDF classifier with CV-AUC 0.97 was killed this way.) |
| 26 | +- **Caveat:** at n=80 there is ~±0.05 run-to-run variance, so gaps under ~0.06 |
| 27 | + are not meaningful. "Speed" = wall-clock for all 80 items at the noted worker |
| 28 | + count against Ollama Cloud (network-bound, indicative not benchmark-grade). |
| 29 | + |
| 30 | +## Single-model results |
| 31 | + |
| 32 | +| engine · model | precision | recall | F1 | errors | speed (80 items) | notes | |
| 33 | +|---|---|---|---|---|---|---| |
| 34 | +| **openai · gemma4:31b** ⭐ **default** | **0.895** | 0.850 | **0.872** | 0 | ~137s (6w) | best F1; two clean runs identical (0.872) | |
| 35 | +| openai · devstral-2:123b | 0.78 | 0.90 | 0.84 / 0.84 | 0 | ~170s (8w) | consistent ×2, high recall | |
| 36 | +| openai · nemotron-3-nano:30b | 0.78 | 0.90 | 0.83 / 0.84 | 0 | ~150s (8w) | matches devstral, smaller | |
| 37 | +| _claude · opus (baseline)_ | 0.93 | 0.68 | 0.78 | 0 | — | highest precision, lower recall | |
| 38 | +| openai · qwen3-coder:480b | 0.90 | 0.65 | 0.75 | 0 | ~112s (8w) | precision-leaning | |
| 39 | +| openai · gpt-oss:120b | 0.74 | 0.78 | 0.76 | 1 | ~94s (8w) | balanced | |
| 40 | +| openai · qwen3-coder-next | 0.75 | 0.38 | 0.50 | 0 | ~55s (8w) | weak recall | |
| 41 | +| openai · gemma4:31b (run under contention) | 1.00 | 0.13 | 0.22 | **73** | ~45s | JSON truncated by concurrent-sweep API contention; not representative | |
| 42 | +| openai · kimi-k2.7-code | — | — | — | — | **>320s timeout** | fast on trivial prompts, very slow on real diffs | |
| 43 | +| openai · glm-5 / qwen3.5:397b / deepseek-v3.2 | — | — | — | — | ~20–45s **per call** | reasoning models, impractical at scale | |
| 44 | + |
| 45 | +**Winner: `gemma4:31b`** — near-Claude precision (0.895 vs 0.93) *and* higher |
| 46 | +recall (0.85 vs 0.68), best F1 (0.872, beats Claude's 0.78), 0 parse errors, |
| 47 | +reproducible. `--model` empty resolves to it for the `openai` engine. |
| 48 | + |
| 49 | +### Latency note (trivial probe vs real prompt) |
| 50 | +A one-line "reply ok" probe is misleading: gemma4:31b 0.5s, qwen3-coder-next |
| 51 | +0.7s, kimi 1.1s, gpt-oss:120b 2.1s — but on the full ~5k-char diff prompt kimi |
| 52 | +blows past 320s and glm-5/qwen3.5/deepseek run 20–45s **per call** (hours for a |
| 53 | +500-row production run). Pick the default on *real-prompt* speed × accuracy. |
| 54 | + |
| 55 | +## Multi-agent / consensus |
| 56 | + |
| 57 | +Computed offline by combining saved per-model predictions on the same eval set |
| 58 | +(no extra calls). |
| 59 | + |
| 60 | +| strategy | precision | recall | F1 | |
| 61 | +|---|---|---|---| |
| 62 | +| gemma4:31b solo (best single) | 0.895 | 0.850 | **0.872** | |
| 63 | +| majority(gemma, devstral, qwen) | 0.872 | 0.850 | 0.861 | |
| 64 | +| majority(gemma, devstral, nemotron) | 0.818 | 0.900 | 0.857 | |
| 65 | +| majority(5 models) | 0.854 | 0.875 | 0.864 | |
| 66 | +| **AND(gemma, qwen)** — precision-max | **0.929** | 0.650 | 0.765 | |
| 67 | +| AND(gemma, devstral) | 0.895 | 0.850 | 0.872 | |
| 68 | +| OR(gemma, devstral) — recall-max | 0.771 | 0.925 | 0.841 | |
| 69 | +| devstral + qwen: AND | 0.897 | 0.650 | 0.754 | |
| 70 | +| devstral + qwen: OR | 0.771 | 0.925 | 0.841 | |
| 71 | + |
| 72 | +**Finding: consensus does not beat the best single model here.** The models' |
| 73 | +errors are *correlated* (they read the same code signal), so ensembles only |
| 74 | +slide along the precision/recall curve a single confidence threshold already |
| 75 | +covers — majority votes score slightly *below* gemma solo. In particular |
| 76 | +**devstral + qwen are nested** (qwen's positives ⊂ devstral's), so their AND = |
| 77 | +qwen's operating point and OR = devstral's — zero synergy. |
| 78 | + |
| 79 | +**The one useful ensemble:** `AND(gemma, qwen)` reaches **0.929 precision** |
| 80 | +(Claude-level) — worth a 2× cost only when you want a precision-max sub-tier |
| 81 | +(e.g. auto-promote near-certain fixes). For general use, gemma solo dominates on |
| 82 | +cost and F1. |
| 83 | + |
| 84 | +## How to run |
| 85 | + |
| 86 | +```bash |
| 87 | +export OLLAMA_API_KEY="$(grep ^OLLAMA_API_KEY= ~/.hermes/.env | cut -d= -f2-)" |
| 88 | + |
| 89 | +# default engine (gemma4:31b) on the eval set |
| 90 | +uv run python collection/llm_classify_fixes.py --run \ |
| 91 | + --engine openai --base-url https://ollama.com/v1 --api-key-env OLLAMA_API_KEY |
| 92 | + |
| 93 | +# precision-leaning single model |
| 94 | +# ... --model qwen3-coder:480b |
| 95 | +# Anthropic baseline |
| 96 | +# ... --engine claude |
| 97 | +``` |
| 98 | + |
| 99 | +Diffs are served rate-limit-free by `collection/local_diffs.py` (bare blobless |
| 100 | +clone + persistent cache + delta `git fetch`), so a full off-Claude, |
| 101 | +cache-resumable classification run over the dataset is practical. |
0 commit comments