Dieselbe Frage, zwei Listen

Ende September habe ich einem Coding-Agenten dieselbe Frage zweimal gestellt, an dieselbe Codebasis, mit zwei verschiedenen lokalen Modellen: „Kannst du mal das Projekt analysieren, ob es Verbesserungen gibt?“ Das Projekt ist ein Kundensystem aus drei Repositories: eine .NET-API, ein Vue-Frontend und ein Hintergrund-Worker. Bewusst offen gefragt, ohne Vorgabe, wonach zu suchen ist.

Beide Antworten waren korrekt. Ich habe zwölf Behauptungen, die nur eines der Modelle aufgestellt hat, gegen den Code geprüft: Branch-Abstände, Konfigurationszeilen, Dockerfile, Paketversionen, welche Dateien in Git liegen. Zwölf von zwölf stimmten. Und trotzdem war die Schnittmenge der beiden Listen klein. Das eine Modell sah den Prozess, das andere den Code. Wie es dazu kam, welche Technik dahintersteckt und was das für die Arbeit mit lokalen Modellen bedeutet, ist dieser Beitrag.

Der Aufbau

Der Rechner ist mein Apple M2 Max mit 96 GB, der Agent ist opencode 1.14 in der Desktop-App, mit denselben Werkzeugen für beide Läufe: Dateien lesen, Shell, Suche, Unteragenten. Die beiden Modelle:

Qwen3.8-27B, ein dichtes Modell in 4 Bit, gefahren über Splash. Splash ist eine Inferenz-Engine von Inco AI, die je Modell einen kleinen Entwurfs-Drafter mitbringt (DFlash 2): Der rät sieben Token voraus, das große Modell prüft sie in einem Durchgang. Offiziell läuft Splash erst ab M3; ich nutze den Community-Fork von paperniuk, der eigene Metal-Kernel für M1 und M2 mitbringt. Wie ich dort hingekommen bin, steht weiter unten.

Qwen3.8-Flash-Next, Qwens Architektur-Vorschau auf Qwen4: 180 Milliarden Parameter, davon 6 Milliarden pro Token aktiv, dazu eine 51-Milliarden-Nachschlagetabelle für Wortpaare, die nicht mitrechnet, sondern nur Speicher belegt. In der Unsloth-Quantisierung UD-IQ4_XS sind das 94 GB auf der Platte. Gefahren über den Unsloth-Fork von llama.cpp mit dem passenden MTP-Drafter und 131.000 Token Kontext.

Ein Modell mit 27 Milliarden Parametern gegen eines mit 180. Man würde erwarten, dass das große schlicht mehr findet. Das stimmt auch, aber nicht auf die Art, die ich erwartet hatte.

Was das kleine Modell sah

Das 27B hat die Aufgabe allein erledigt: 15 Shell-Befehle, ein Blick in die Projektdokumentation, 26 Minuten reine Arbeitszeit, am Ende 14 nummerierte Punkte in fünf Abschnitten mit einer empfohlenen Reihenfolge und Aufwandsschätzungen. Es hat sich verhalten wie ein Projektleiter, der ein fremdes Projekt übernimmt.

Was es fand, hätte ich in einem Code-Audit nicht unbedingt erwartet: Es gibt kein Build-Gate vor dem Deployment, jeder Push landet direkt auf dem Staging-System des Kunden. Ein Branch liegt 115 Commits hinter dem anderen. Ein Feature steht in der Dokumentation als „nicht deployt, wartet auf Freigabe“, im Code ist es längst in main. Die Node-Version im Dockerfile ist seit einem Jahr am Lebensende. Dazu die Klassiker: Zugangsdaten in einer versionierten Konfigurationsdatei, ein fehlendes .dockerignore, ein Login, der bei falschem Passwort mit 500 statt 401 antwortet.

Es hat außerdem einen Abschnitt „Gut so, beibehalten“ geschrieben. Das klingt nach Kleinigkeit, ist aber die Sorte Kontext, die ein Kunde braucht, um die Liste einzuordnen.

Was das große Modell sah

Das 180B hat anders gearbeitet. Nach sechs eigenen Shell-Befehlen hat es zwei Unteragenten gestartet, einen für Backend und Worker, einen für das Frontend, und deren Berichte zusammengefasst. Der Backend-Agent allein hat 86 Shell-Befehle in 49 Runden abgesetzt. Insgesamt 40 Minuten, rund 40 Funde mit Datei und Zeilennummer und je einem Fix-Vorschlag.

Es hat sich verhalten wie ein Auditor. Rechteprüfungen, die nur im Frontend existieren, sodass jeder eingeloggte Nutzer per HTTP-Aufruf Dinge tun kann, die die Oberfläche ihm nicht anbietet. Eingabevalidierung, die global abgeschaltet ist, weil ein Filter registriert, aber nirgends angewendet wird. Zugangsdaten für ein Admin-Dashboard direkt im Quellcode. Eine JWT-Prüfung, bei der Aussteller und Zielgruppe nicht geprüft werden. N+1-Datenbankabfragen mit Zeilenangaben, Klassen mit 1.500 Zeilen, eine Testdatei mit einem Admin-Token, die ohne .dockerignore ins Produktionsimage gewandert wäre.

Die Sicherheitspunkte des großen Modells wiegen schwerer als alles, was das kleine gefunden hat. Aber das große hat das fehlende Build-Gate nicht gesehen, die Branch-Drift nicht, den Widerspruch zwischen Dokumentation und Code nicht. Es hat im Code gelesen, nicht im Projekt.

Die Gegenprobe

Bevor ich aus zwei Listen eine Aussage mache, will ich wissen, ob sie stimmen. Sprachmodelle erfinden Zeilennummern mit derselben Sicherheit, mit der sie richtige nennen. Also habe ich zwölf Punkte herausgegriffen, die jeweils nur eines der Modelle behauptet hat, und im Code nachgesehen: die Node-Version im Dockerfile, den Commit-Abstand der Branches, ob das angeblich nicht deployte Feature in main liegt, die Zeile mit der abgeschalteten Validierung, die Zeilen mit der halboffenen JWT-Prüfung, die Zugangsdaten im Dashboard-Code, den Fallback in der Frontend-Konfiguration, die versionierten Backup-Dateien, die Paketversionen, das fehlende .dockerignore in allen drei Repositories.

Zwölf Treffer. Das große Modell hatte seine Liste mit „alle Angaben verifiziert“ überschrieben, und das hielt stand. Das kleine hatte keine solche Behauptung aufgestellt, war aber ebenso präzise. Kein Modell hat halluziniert. Sie haben an verschiedenen Stellen hingeschaut.

Warum zwei Blickwinkel

Drei Dinge unterscheiden die Läufe, und keines davon ist die Modellgröße allein.

Erstens die Delegation. Das große Modell hat sich entschieden, Unteragenten zu starten; das kleine hat alles selbst gemacht. Das war keine Einstellung des Agenten, sondern eine Entscheidung des Modells. Unteragenten bekommen einen engen Auftrag („Analysiere API und Worker“) und liefern deshalb Tiefe im Code. Der Hauptagent, der alles selbst macht, behält den Blick auf das Ganze, hat aber weniger Zeit pro Datei.

Zweitens die Frage, was ein Modell als „Verbesserung“ versteht. Das 27B hat die Projektdokumentation gelesen und mit dem Repository abgeglichen; daraus kommen die Doku-gegen-Realität-Funde. Das 180B hat die Doku auch gelesen, aber die Codemuster stärker gewichtet.

Drittens schlicht Kapazität: 40 Funde mit Zeilennummern brauchen viele Lesevorgänge. Der Backend-Unteragent hat 45.000 Token Eingabe verarbeitet und 20.000 Token geschrieben. Das kleine Modell hätte das auch gekonnt, hat es aber nicht versucht.

Für die Praxis heißt das: Bei offenen Aufgaben ist die Frage „welches Modell ist besser“ die falsche. Zwei Modelle, die verschieden hinschauen, ergeben zusammen eine Liste, die keines allein geliefert hätte. Und die beiden Punkte, die der Kunde zuerst hören musste, kamen von je einem Modell.

Der Weg dorthin: ein Watchdog und ein Fork-Maintainer

Dass beide Modelle überhaupt in einer langen Agentensitzung durchgehalten haben, war zwei Tage vorher nicht selbstverständlich. Der Splash-Fork ist auf dem M2 Max zunächst bei etwa 43.000 Token Kontext abgestürzt, reproduzierbar. Der Absturz-Mitschnitt nannte den Grund: kIOGPUCommandBufferCallbackErrorImpactingInteractivity. macOS bricht einen GPU-Auftrag ab, der die Grafikeinheit zu lange festhält, während der Bildschirm sie braucht. Ein Block von 2.048 Token beim Einlesen des Verlaufs dauerte auf dem M2 14 bis 17 Sekunden. Kein Speicherproblem, ein Zeitproblem.

Ich habe das mit Trace und Vorschlag im GitHub-Thread des Forks gemeldet: die Einlese-Aufträge auf wenige Sekunden begrenzen. Der Maintainer hatte dasselbe auf seinem M1 Max gesehen und veröffentlichte wenige Stunden später eine Version, die jeden Auftrag auf etwa fünf Sekunden deckelt. Auf meinem M2 Max reichte das nicht; unter Bildschirmlast brach es weiter ab. Also habe ich die Konstante im Quellcode geändert und drei Werte durchgemessen, jeweils in einer echten Agentensitzung mit kaltem Einlesen des gesamten Verlaufs:

GrenzeErgebnisKontextEinlesen bei 40–50K Verlauf
5 s (Fork-Standard)Abbruch bei ~45K––
1 s9 Anfragen, kein Abbruchbis ~96K105 tok/s
2 s38 Anfragen, kein Abbruchbis ~96K111 tok/s
3 s62 Anfragen, kein Abbruchbis ~116K122 tok/s
Prefill-Grenze im Splash-Fork auf dem M2 Max, Qwen3.8-27B, gemessen in opencode-Sitzungen.

Die Schwelle dieses Rechners liegt also zwischen drei und fünf Sekunden, und von zwei auf drei Sekunden gewinnt man nur noch fünf bis zehn Prozent. Die Zahlen sind zurück im Thread; ein anderer Nutzer mit einem M2 Ultra sieht denselben Abbruch bei extremen Kontextlängen und hat eine dynamische Anpassung vorgeschlagen. So sieht Open Source aus, wenn es gut läuft: Messung, Gegenmessung, Release, Gegenmessung, innerhalb von drei Tagen.

Das 180B braucht den ganzen Rechner

Flash-Next in llama.cpp hatte das Watchdog-Problem nicht. llama.cpp verteilt einen Rechengraphen auf mehrere GPU-Aufträge, Splash hält einen Auftrag am Stück; deshalb kam Flash auch mit 60.000 Token kaltem Einlesen ohne Abbruch durch. Beim Einlesen war es mit 245 bis 272 Token pro Sekunde fast doppelt so schnell wie das 27B über Splash, beim Schreiben mit 18 bis 27 Token pro Sekunde gleichauf. Für ein Modell, das siebenmal größer ist, ist das bemerkenswert; die 6 Milliarden aktiven Parameter machen es möglich.

Zwei Haken. Der erste: Speicher. Die 96 GB des Mac reichen nur, wenn sonst nichts läuft. Mein LM-Studio-Server, an dem die n8n-Automationen hängen, musste für die Dauer des Tests aus, sonst hätte die nächste Automations-Anfrage das 35B-Modell nachgeladen und den Speicher gesprengt. Während der Sitzung waren 81 von 83,5 GB GPU-Budget belegt, dazu 26 GB Swap, die aber, wie sich zeigte, fast nur aus komprimierten Nullseiten bestanden. Der zweite Haken: Parallelität. Sobald ein zweiter Unteragent seinen Kontext einliest, fällt der schreibende auf ein halbes Token pro Sekunde, weil llama.cpp Einlesen und Schreiben in eine Runde packt. Splash regelt das mit kleineren Blöcken im Parallelbetrieb besser.

Das ist der eigentliche Unterschied für den Alltag: Das 27B über Splash lässt neben sich 40 GB für andere Dienste übrig und verträgt vier Agenten gleichzeitig. Das 180B liefert das tiefere Audit, aber der Rechner gehört ihm dann allein.

Was ich mitnehme

Erstens: Bei offenen Prüfaufgaben zwei Modelle laufen lassen und die Differenz der Listen lesen. Die Schnittmenge ist das Offensichtliche, die Differenz ist der Wert.

Zweitens: Die Gegenprobe ist Pflicht, und sie ist billig. Zwölf Behauptungen im Code nachsehen hat zehn Minuten gedauert. Ohne sie wäre „beide fehlerfrei“ eine Vermutung.

Drittens: Das Werkzeug darunter entscheidet mit. Ein Modell, das bei 43.000 Token abstürzt, kann keine Codebasis lesen, egal wie gut es ist. Dass es nach drei Tagen stabil bis 116.000 Token lief, lag an einem Absturz-Mitschnitt, einem GitHub-Kommentar und einem Maintainer, der zuhört.

Und viertens: Was beide Modelle nicht wussten, ist, welche der 40 Punkte der Kunde zuerst hören muss und welche er sich für später aufheben kann. Das blieb Handarbeit, und ich vermute, das bleibt noch eine Weile so.

Alle Messungen: Apple M2 Max, 38 GPU-Kerne, 96 GB, macOS 26.6. Splash-Fork paperniuk/splash auf Basis 1.1.0, Qwen3.8-27B 4 Bit mit DFlash-2-Drafter. Unsloth-llama.cpp Build 11030, Qwen3.8-Flash-Next UD-IQ4_XS mit MTP-Drafter, 131K Kontext. opencode 1.14, Desktop-App. Die Befunde des Kundenprojekts sind hier bewusst nur als Kategorien genannt.