Notizen aus dem Bau eines Eval-Harness für Entity Linking gegen ein hierarchisches Vokabular
Die meisten veröffentlichten Modellvergleiche beantworten eine Frage, die ich nicht habe. Sie sagen mir, dass ein Modell in Reasoning-Benchmarks gut abschneidet, zu 97 % gültiges JSON liefert oder 130 Tokens pro Sekunde schafft. Nützlicher Kontext. Aber als ich wissen musste, ob ein lokal gehostetes Modell ein unstrukturiertes Dokument nehmen und die darin genannten Dinge an den richtigen Knoten eines kontrollierten Vokabulars hängen kann — die Art von Aufgabe, die unter einer Menge unglamouröser Produktionsarbeit liegt —, half keine dieser Zahlen weiter.
Also habe ich einen kleinen Harness gebaut und ihn auf siebzehn Modelle losgelassen, die auf zwei Consumer-GPUs laufen. Was folgt, ist überwiegend das, was mich überrascht hat — einschließlich mehrerer Fälle, in denen nicht die Modelle falsch lagen, sondern das, was ich gebaut hatte.
Precision und Recall verdecken genau den Fehler, auf den es ankommt
Die Aufgabe: ein Dokument lesen, die Entitäten extrahieren und jede davon an einen Knoten einer Hierarchie binden. Die Hierarchie hat Tiefe — ganz oben breite Oberkategorien, darunter mehrere Zwischenebenen, ganz unten spezifische benannte Blattknoten.
Die naheliegenden Metriken sind Precision und Recall gegen ein Gold-Set. Sie sind nicht falsch, sie sind nur blind für das eine, worauf es tatsächlich ankommt. Wenn ein Dokument einen Blattknoten eindeutig belegt und das Modell stattdessen die Wurzel verlinkt, werten Precision und Recall das beide als Treffer. Es hat ja „eine korrekte Entität“ gefunden. Aber eine tiefe Hierarchie auf ihren obersten Knoten einzudampfen ist exakt der Fehler, der das Vokabular wertlos macht — es ist der Unterschied zwischen einer durchsuchbaren Struktur und einem Sack voller Tags.
Der Harness bewertet deshalb zwei Fehlermodi getrennt:
- Übergeneralisierung — es wurde ein Vorfahre verlinkt, obwohl das Dokument etwas Tieferes hergab. Der Verflachungsfehler.
- Überspezifizierung — es wurde eine Spezialisierung verlinkt, die das Dokument nicht hergibt. Der Erfindungsfehler.
Die beiden sind nicht symmetrisch. Übergeneralisierung macht die Ausgabe weniger nützlich. Überspezifizierung legt einem menschlichen Prüfer eine Behauptung vor, die in der Quelle nie stand, und das ist schlimmer: Es kostet Vertrauen, und Vertrauen ist das, was jemanden dazu bringt, sich auch den zweiten Schwung Vorschläge noch anzusehen.
Die Trennung hat die Rangfolge verändert. Zwei Modelle mit nahezu identischem Recall trennten sich sauber, sobald sichtbar wurde, dass das eine still verflachte und das andere nicht.
Vorfahren sind mitgemeint — sie einzufordern bestraft gutes Verhalten
Die erste Fassung der Gold-Daten führte die gesamte Vorfahrenkette auf. Belegte ein Dokument ein Blatt, erwartete das Gold-Set das Blatt und jeden Knoten darüber.
Das ist falsch, und es hat ein verwirrendes Ergebnis gebraucht, um es zu sehen. Ein Modell, das das Blatt verlinkt, hat die Vorfahren damit bereits behauptet — die Hierarchie kodiert das. Sie trotzdem einzufordern bestraft genau das Verhalten, das ich im Prompt verlangt hatte: den tiefsten belegten Knoten zu wählen. Meine besten Modelle wurden dafür abgewertet, dass sie richtig lagen.
Vorfahren gelten jetzt für den Recall als mitgemeint. Für die Precision nicht: Mitgemeinte Knoten bekommen Recall-Gutschrift, dürfen aber nie in den Precision-Zähler wandern. Ich weiß das, weil genau das eine Precision von 1,12 erzeugt hat — und eine unmögliche Zahl ist ein Geschenk. Sie meldet den Bug, statt still eine Rangfolge zu verschieben.
Die leere Antwort, die kerngesund aussieht
Der Fehler, der mich am meisten Zeit gekostet hat: Ein Modell liefert nichts zurück, und jede Metrik, die man normalerweise prüft, sagt, es sei alles in Ordnung.
Reasoning-fähige Modelle verbrauchen ihr Token-Budget für einen internen Trace. Geht das Budget aus, bevor der Trace fertig ist, bekommt man eine Antwort mit null Zeichen — während der Server munter gesunde Tokens pro Sekunde und eine volle Tokenzahl meldet. Wenn der Harness Struktur statt Inhalt bewertet — Tool-Calls zählen etwa, oder prüfen, dass ein Parse keine Exception geworfen hat —, rutscht eine leere Antwort glatt als Pass durch.
In einem früheren Datensatz, den ich daraufhin noch einmal durchgesehen habe, waren 36 % der Antworten einer Kategorie null Bytes lang, und der Fehler war wochenlang unsichtbar, weil die Durchsatzspalte die ganze Zeit normal aussah.
Der Fix ist trivial und gehört in jeden Harness: prüfen, dass die Antwort nicht leer ist, und leer als eigene Fehlerklasse behandeln statt als Parse-Fehler. Auf die Diagnose kommt es an — „hat nichts geliefert“ und „hat Kaputtes geliefert“ haben verschiedene Ursachen und verschiedene Fixes.
Gemessen habe ich mein eigenes Budget, nicht die Modelle
Verwandt damit, und peinlicher. Ich hatte eine Token-Obergrenze gesetzt, die mir großzügig vorkam. Mehrere Modelle scheiterten am Großteil der Suite.
Dann fiel mir auf, dass jeder einzelne Fehlschlag exakt auf der Obergrenze landete, mit null Zeichen, während jeder Erfolg bequem darunter blieb. Das ist keine Verteilung von Modellfähigkeit. Das ist eine Wand, die ich selbst dorthin gestellt habe.
Die Grenze anzuheben brachte ein Modell von einem brauchbaren Ergebnis aus sechs auf sechs aus sechs bei perfektem Recall — und es war ein Modell, das in meinen Notizen längst als „für diese Aufgabe ungeeignet“ abgeschrieben war. Geeignet war es. Mein Budget war es nicht.
Die allgemeine Lehre: Wenn Fehlschläge sich exakt auf einem Parameter sammeln, den man selbst gewählt hat, ist der Parameter verdächtig, nicht der Prüfling. Ein Histogramm darüber, wo die Fehlschläge landen, ist eine Prüfung von dreißig Sekunden, die mir eine falsche Schlussfolgerung erspart hätte.
Nachgemessen: der Scorer verschob die Rangfolge, das größere Kontextfenster nicht
Der erste Durchlauf hatte zwei Dinge vermengt, die ich auseinanderhalten wollte: Wie viel von der Rangfolge kam aus dem, was die Modelle taten, und wie viel aus dem, was mein Scorer daraus machte? Also habe ich ein Modell und eine einzige Aufgabe genommen und sie fünfmal je Kontextfenster laufen lassen — dasselbe kleine und ein sehr großes Fenster, dieselben fünf Seeds, beim Sampling-Preset. Fünf Züge je Zelle deshalb, weil dieser Lauf bei Temperature eins lief; ein einzelner Lauf wäre dort keine Messung.
Das erste Ergebnis betrifft nicht die Modelle, sondern mich. Ich habe exakt dieselben Antworten noch einmal bewertet — byte für byte dieselben Dateien, nichts neu erzeugt. Der alte Scorer ließ zwei der fünf Seeds bestehen, der neue keinen.
Das ist meine Regel, die sich unter unveränderter Eingabe bewegt, nicht das Modell. Als ich die beiden Scorer gegeneinander legte, hatten sich sechs von sieben Merkmalsmustern verschoben — drei verschärft, drei gelockert.
Um zu sehen, was den Einbruch wirklich auslöste, habe ich vom alten Scorer aus je eine Regel einzeln geändert (Basis: vier bestandene von zehn Zügen über beide Fenster). Nur die Temperature-Regex verschärft: zwei von zehn. Nur den geschenkten Punkt für ein Bedienelement gestrichen: zwei von zehn. Beides zusammen: null von zehn. Keine einzelne Änderung kippt das Ergebnis auf null — jede halbiert für sich, erst das Paar bringt die Null. Der Satz aus einer früheren Notiz, eine verschärfte Regex allein flippe keinen einzigen Zug, war schlicht falsch.
Woran das hängt, zeigt der schwächste Zug im Feld am deutlichsten. Er rechnet Auftrieb, Abkühlung, Temperatur und Dampf — und bekommt für alle vier null Punkte, weil seine Felder „temp“, „buoy“, „coolingRate“ und „nSteam“ heißen, vier Namen, die mein Prüfer nicht kennt.
Die Physik ist da. Meine Regex ist es nicht. Es ist dieselbe Fehlerform wie bei der Compliance-Prüfung weiter unten: eine Kontrolle, die korrektes Verhalten bestraft, weil sie es nicht wiedererkennt.
Der neue Scorer bringt zwei Golden-Kontrollen mit — eine Datei, die 16 von 20 erreichen muss, und eine, die bei 1 von 20 durchfallen muss. Der alte Scorer hatte gar keine. Aber man muss sehen, was sie leisten und was nicht: Sie sichern, dass der Scorer nicht kaputt ist. Diese Verzerrung haben sie nicht gefunden, weil sie die kanonischen Feldnamen benutzen. Gesehen habe ich sie erst, als ich die Artefakte von Hand las.
Das zweite, was ich trennen wollte, war das Kontextfenster. Über beide Fenster lag der Mittelwert der Punktzahl bei 12,8 gegen 12,6 von zwanzig, der Median der Laufzeit bei 83,7 gegen 84,1 Sekunden, der Median der Tokenzahl bei 5251 auf beiden Seiten. Drei der fünf Seed-Paare kamen byte-identisch zurück — effektiv also zwei unabhängige Stichproben je Zelle. Die 0,2 Punkte Abstand sind ein einziger Umfangspunkt, den ein Seed an einer Zeilenschwelle kippte. Einen Unterschied jenseits des VRAM habe ich in diesem Aufbau nicht gefunden — und bei fünf Zügen je Zelle, von denen drei Paare identisch sind, könnte ich einen kleinen auch gar nicht sehen. Das ist keine Gleichheit, das ist eine Nicht-Differenz.
Diese fünf Seeds liefen zweimal, einmal je Fenster. Die zehn Züge sind veröffentlicht: Prompt, Antwort, Denkspur, Bewertung und das erzeugte Artefakt liegen offen, sodass die Bewertung nachzurechnen ist. Drei Zahlen stehen aber nicht darin: Laufzeit, Tokenzahl und der Grund, aus dem ein Zug endete, liegen nur in meinem Lauf-Log, nicht in den Score-Dateien der einzelnen Züge. Wer genau diese drei nachprüfen will, kann es an den veröffentlichten Artefakten nicht. Die zehn Exemplare liegen hier.
Eine Architektur, die den Server mitreißt
Vier der Modelle, die ich ausprobiert habe, haben den Inference-Server schlicht abgeschossen — keine schlechte Antwort, ein abgestürzter Prozess, rund neunzehn Sekunden verbrannt pro Aufruf.
Interessant war die Zuschreibung. Dieser Absturz war zuvor als Folge von schema-constrained Decoding verbucht worden. Mein Harness benutzt Constrained Decoding überhaupt nicht und hat ihn trotzdem exakt reproduziert. Der eigentliche Auslöser war eine bestimmte Modellarchitektur im Zusammenspiel mit dem Context-Checkpointing der Runtime, im Server-Log sichtbar als ein Checkpoint, der unmittelbar vor einem CUDA-Fehler angelegt wird.
Daraus folgt zweierlei. Erstens lohnt sich ein billiger Architektur-Check, bevor ein Modell überhaupt geladen wird — er ist in Sekundenbruchteilen abgefragt und spart ein Modell, das sonst nur zwanzig Sekunden pro Aufruf verbrennt. Zweitens muss der Ausschluss architektur-genau sein. Die dichte Variante derselben Modellfamilie, ein Zeichen Unterschied im Architektur-String, läuft einwandfrei und lieferte zwei der vier besten Ergebnisse des ganzen Durchlaufs. Eine etwas zu breit gefasste Ausschlussregel hätte Spitzenreiter weggeworfen.
Eine Regelprüfung, die bei korrektem Verhalten anschlug
Der Harness enthält eine Compliance-Prüfung: Der Prompt verbietet dem Modell, eigene Bewertungen abzugeben, und der Harness verifiziert unabhängig davon, dass es sich daran gehalten hat. Klingt vernünftig.
Eines meiner Testdokumente enthält absichtlich Selbsteinschätzungs-Formulierungen, weil ein Dokument aus der echten Welt so etwas eben enthalten kann. Der Prompt verlangt außerdem, dass jede extrahierte Entität ein wörtliches Zitat aus der Quelle als Beleg mitführt.
Die Kollision sieht man kommen. Ein Modell, das die Quelle korrekt als Beleg zitierte, löste die Compliance-Prüfung aus — markiert wurde es, während es auf demselben Dokument perfekt abschnitt. Die Prüfung bestrafte exakt das Verhalten, das der Vertrag verlangte.
Der Fix besteht darin, zitierte Passagen vor dem Scan herauszuschneiden, damit die Prüfung anschlägt, wenn das Modell etwas in eigener Stimme behauptet, und nicht, wenn es die Eingabe getreu wiedergibt. Strukturelle Prüfungen bleiben davon unberührt: Ein Modell, das seiner Ausgabe ein Bewertungs-Feld hinzufügt, behauptet, es zitiert nicht — und das soll immer anschlagen.
Der weitergehende Punkt ist mehr wert als der Fix. Eine Compliance-Prüfung mit einem False-Positive-Pfad ist schlimmer als gar keine Prüfung, weil sie selbstbewusste Fehlurteile produziert, die einen auf erfundener Grundlage von guten Optionen wegsteuern. Sie hätte mich beinahe das beste Modell im Feld gekostet. Jede automatisierte Policy-Prüfung braucht ihre eigene Positiv- und Negativkontrolle, genau wie eine Messung.
Gelernt habe ich außerdem, zu speichern, wo die Prüfung angeschlagen hat. Ein späteres Modell löste dieselbe Regel bei perfekter Punktzahl aus, und ich konnte einen echten Verstoß nicht von einem weiteren False Positive unterscheiden, weil ich den Fundtext nicht aufgehoben hatte. Ein Urteil, das sich nicht nachprüfen lässt, ist keines.
Determinismus lohnt sich — einmal
Bei Temperature null waren zwei aufeinanderfolgende vollständige Durchläufe byte-identisch: gleiche Werte, gleiche Ergebnisse pro Dokument.
Das ist eine kleine Prüfung mit großer Wirkung. Sie bedeutet, dass ein einzelner Lauf eine belastbare Messung ist — solange die Temperature null ist — und dass jeder Unterschied zwischen zwei Läufen eine echte Veränderung im System ist und kein Sampling-Rauschen. Ohne sie braucht jeder Vergleich Wiederholungen und Fehlerbalken, und die Kosten der ganzen Übung vervielfachen sich. Einmal laufen lassen, das Ergebnis notieren, aufhören zu grübeln. Der Verlass darauf hält aber nur bei Temperature null: Eine spätere Nachprobe lief bewusst bei Temperature eins, und dort ist ein einzelner Lauf keine Messung mehr — sie brauchte fünf Züge je Zelle.
Was am Ende in den Zahlen stand
Die Schlagzeile ist unspektakulär und verallgemeinert sich vermutlich: Das schnellste Modell war nicht das beste, und das größte war es auch nicht.
Gewonnen hat ein mittelgroßes Modell bei rund 94 Tokens pro Sekunde — langsamer als mehrere Alternativen mit 130 und mehr, kleiner als die größten Kandidaten. Es hat gewonnen, weil es als einziges perfekten Recall, null Verflachungsfehler und einen Abschluss auf jedem Dokument zugleich schaffte. Drei weitere Modelle erreichten ebenfalls perfekten Recall, stießen aber bei je einem Dokument an die Token-Obergrenze — ein Zug, den der Harness nach eigener Regel als ungültig führt, nicht als Fehlschlag des Modells. Der Vorsprung des Siegers ist keiner an Fähigkeit, sondern einer an vollständiger Messung: Er blieb auf jedem Dokument im Budget.
Für eine Arbeitslast, deren Ausgabe ein Mensch prüft, ist dieser Tausch offensichtlich, sobald man ihn sehen kann: Durchsatz ist fast umsonst, ein Verflachungsfehler nicht. Sehen kann man ihn aber nur, wenn die Metriken die Fehlermodi auseinanderhalten. Auf einer generischen Scorecard sehen vier dieser Modelle austauschbar aus.
Das andere haltbare Ergebnis: Mehrere Modelle haben die Aufgabe gut genug erledigt. Der Abstand zwischen den besten paar Modellen ist kleiner als der Abstand zwischen einem guten und einem schlechten Harness. Ich habe deutlich mehr Zeit darauf verwendet, meine eigene Messung zu reparieren, als darauf, zwischen Modellen zu wählen — vier der gefundenen Bugs waren meine, und jeder einzelne davon hätte eine selbstbewusste, falsche Empfehlung erzeugt.
Für alle, die etwas Ähnliches bauen
- Die Fehlermodi bewerten, auf die es in der eigenen Domäne wirklich ankommt, nicht die, die sich leicht ausrechnen lassen.
- Nachsehen, wo sich die Fehlschläge sammeln, bevor man irgendetwas über Fähigkeit schließt.
- Auf nicht-leere Ausgabe prüfen. Immer.
- Abbrüche an der Token-Obergrenze als ungültige Züge führen, nie als Fehlschlag des Modells.
- Jeder automatisierten Prüfung — auch der Policy-Prüfung — eine Positiv- und eine Negativkontrolle geben.
- Den Beleg zu jedem Urteil aufheben, das man später einmal bestreiten könnte.
- Determinismus einmal bestätigen und einzelnen Läufen nur bei Temperature null vertrauen.
- Die Architektur prüfen, bevor man zwanzig Sekunden pro Aufruf damit verbrennt, es herauszufinden.
Nichts davon ist raffiniert. Nötig war alles davon, und das meiste habe ich beim ersten Mal falsch gemacht.
Beispiele zum Anfassen: dieselbe Disziplin, angewandt auf Game-Frontends
Ein Folge-Benchmark ließ lokale Modelle Casino-Präsentationsclients bauen — einen Slot und ein Crash-Spiel, bei denen der Server jedes Ergebnis besitzt und der Client es nur darstellen darf: kein Math.random, Geld als ganzzahlige Minor Units, das Grid wörtlich vom Server übernommen. Unten stehen unveränderte Modell-Ausgaben, eingebettet in einem Sandbox-Frame auf einer isolierten Origin. Selbst drehen, selbst auszahlen.
Ein First-Shot-Pass (TeichAI Fable-Distill, PixiJS)
Ein Zug, keine Reparaturen: 9/9 Vertragsprüfungen. Dafür hat das Modell 85.000 Zeichen nachgedacht, bevor es ein Byte geschrieben hat — Qualität und Latenz sind verschiedene Achsen, und genau davon handelt dieser Artikel.
Ein Crash-Client, der seinen eigenen Vertrag überlebt (qwen3.8, Phaser 3)
Wer mitten in der Runde auszahlt, bekommt den Gewinn in ganzzahligen Cents aus dem Server-Tickstream berechnet; nach dem Crash ist der Button tot und die Auszahlung unveränderlich.
Vertragsgrün und trotzdem falsch — der Fehlermodus, den kein Verifier sieht
Dieses Beispiel besteht nach einer Reparatur-Runde jede mechanische Prüfung. Sehen Sie es sich an. Das Layout klebt abgeschnitten in einer Ecke — ein Defekt, den keine Vertrags-Assertion fängt. Genau deshalb endet die Pipeline mit einem Sicht-Gate und nicht mit einem grünen Haken.
Alle sechs Beispiele, auch die DOM- und Pure-Canvas-Varianten: die komplette Galerie.