Evaluierungs-Harness
laya.evals macht aus einem beschrifteten Datensatz einen wiederholbaren Score und aus einer Baseline ein
Gate, das Bestehen oder Durchfallen entscheidet, sodass eine Qualitätsänderung ein prüfbarer Diff statt
einer Handprüfung ist.
Die Metrik-Mathematik und der Datensatz-Parser sind reines Python plus numpy und importieren nie torch, sie laufen also ohne Gewichte. Einen Datensatz gegen einen Checkpoint laufen zu lassen braucht den Checkpoint und nimmt dessen normale Ladezeit in Anspruch.
Schnellstart
# check the format without a model
laya-evals validate research/evals/fixture.jsonl
# score a labelled set on one checkpoint, with thresholds and a baseline
laya-evals run data.jsonl --model english --device cpu \
--min-accuracy 0.8 --max-ece 0.05 --score-within 0.25 --slice language \
--json report.json --markdown report.md
# compare a saved report to a baseline
laya-evals compare report.json --baseline baseline.json --tolerance choice_accuracy=0.02
laya eval ... ist dasselbe über die Haupt-CLI, laya eval validate data.jsonl funktioniert also
ebenfalls.
Exit-Codes: 0 bei Erfolg, 1 wenn ein Schwellenwert oder eine Baseline-Toleranz verletzt wird, 2 bei
einem Nutzungsfehler. run gibt die Gesamtmetriken und alle angeforderten Slices nach stdout aus und
schreibt den vollständigen Bericht und eine Markdown-Zusammenfassung, wenn --json / --markdown
angegeben sind.
Shortlist-Fehler zuordnen
Für ein beschriftetes Choice-Set mit hoher Kardinalität nutzt
laya.evals_shortlist.evaluate_shortlist den bestehenden Pfad predict_shortlist und den regulären
Evaluierungs-Harness. Es beantwortet zwei getrennte Fragen: Hat das Retrieval das Gold-Label behalten,
und hat Laya es gewählt, wenn es vorhanden war? Dies ist eine optionale Python-API für Choice-Labels;
gewöhnliche laya-evals run-Berichte bleiben unverändert.
import laya
from laya.evals import Dataset
from laya.evals_shortlist import evaluate_shortlist
from laya.shortlist import embed_fn_from_agent
agent = laya.load()
dataset_path = "intents.jsonl"
dataset = Dataset.from_jsonl(dataset_path)
report = evaluate_shortlist(
agent, dataset, embed_fn_from_agent(agent), k=20,
checkpoint_id="my-checkpoint@revision", embedder_id="my-encoder@revision",
dataset_path=dataset_path,
)
print(report.overall)
print(report.cases[0]["shortlist_status"])
Verwende dieselbe Embedding-Funktion und denselben Checkpoint wie das Deployment, das gemessen wird.
Die beiden Bezeichner werden vom Aufrufer geliefert und sollten unveränderliche Revisionen benennen;
der Bericht kann die Gewichte hinter einem beliebigen Callable nicht ableiten. dataset_path
zeichnet das SHA256 der Datei neben dem bestehenden Frage-Fingerabdruck auf. Jeder Fall behält die
tatsächlichen Shortlist-Labels und eines von correct, retrieval_miss oder decision_miss.
shortlist_recall_at_k ist der Anteil der behaltenen Gold-Labels. shortlist_accuracy_on_recalled
sind korrekte Entscheidungen geteilt durch behaltene Fälle; es wird weggelassen, wenn keine behalten
wurden. Das bestehende choice_accuracy bleibt die End-to-End-Genauigkeit über alle Fälle,
einschließlich Retrieval-Fehlschlägen. Die Shortlist-Metriken erscheinen in denselben Sprache-,
Modell-, Frage- und Tag-Slices. Die Anfragelatenz umfasst Embedding und den Entscheidungsaufruf; der
Bericht isoliert keine Phasen-Timings. Bei k >= n läuft die ursprüngliche Frage durch und der
Retrieval-Recall ist 1, ohne den Embedder aufzurufen.
Dies reproduziert nicht die BANKING77-Ergebnisse aus issue #102: diese Zahlen hängen von dessen Datensatz, Checkpoint und Bi-Encoder ab. Diese API macht dieselbe Art der Diagnose auf einem eigenen beschrifteten Set des Aufrufers wiederholbar.
Einen ONNX-Export evaluieren
run --onnx PATH bewertet ein exportiertes ONNX-Modell über ONNXAgent statt über den torch-Router,
sodass ein ONNX-Deployment (einschließlich einer INT8-Kopie aus
scripts/export_onnx.py --quantize) durch dieselben Schwellenwerte und Baselines geprüft wird wie der
torch-Pfad:
python scripts/export_onnx.py --model convaiinnovations/laya --output laya.onnx --quantize
laya-evals run data.jsonl --onnx laya.int8.onnx --max-ece 0.05
--model benennt den Checkpoint, aus dem der Export stammt — eine Hub-ID oder ein lokaler Pfad, kein
Router-Kurzname wie english, da es auf diesem Pfad keinen Router gibt (Standard
convaiinnovations/laya). Seine Konfiguration und sein Tokenizer werden von dort geladen. Der Agent
bedient einen einzigen Checkpoint, eine Datensatzzeile, deren Feld model einen anderen benennt, schlägt
also mit einem klaren Fehler fehl, statt still vom falschen Modell beantwortet zu werden; --device gilt
nicht. --batch-size nutzt die Batch-API des Agenten, wenn er eine hat, und fällt sonst auf einen Aufruf
pro Zustand zurück; --sort-by-length wird an diese Batch-API weitergereicht; der Rückfall pro Zustand hat
keine Gruppe zum Umsortieren. Übergib --calibration PATH, um eine angepasste Kalibrierungskarte auf
ONNXAgent zu laden, sodass Kalibrierungs-Gates wie --max-ece gegen kalibrierte Wahrscheinlichkeiten
evaluieren. Der Block config des Berichts zeichnet den Pfad onnx und den Pfad calibration auf
(sofern gesetzt).
Gemessen auf research/evals/fixture.jsonl (12 beschriftete Zeilen, englischer Checkpoint, CPU):
| Runner | choice_acc | noul_acc | score_mae | ece | mean_conf | p50 ms |
|---|---|---|---|---|---|---|
| torch Router | 0.75 | 1.00 | 1.3418 | 0.1596 | 0.7304 | 116.8 |
--onnx fp32 |
0.75 | 1.00 | 1.3418 | 0.1596 | 0.7304 | 66.3 |
--onnx int8 |
0.75 | 1.00 | 1.3512 | 0.1658 | 0.7304 | 46.3 |
Der fp32-Export reproduziert die torch-Zahlen exakt, und die quantisierte Kopie verschiebt score_mae um
0.009 und ece um 0.006 — die Art von Drift, die compare --tolerance abfangen soll.
Datensatzformat
Ein JSON-Objekt pro Zeile (JSONL). Leerzeilen und Zeilen, die mit # beginnen, werden ignoriert.
| Feld | erforderlich | Bedeutung |
|---|---|---|
state |
ja | Text, E-Mail, Ticket oder JSON-Dokument, über das entschieden wird |
questions |
ja | ein Laya-Frage-Dict, genau wie es Router.predict akzeptiert |
expected |
ja | Ground Truth, indiziert nach Fragen-ID: ein Label für choice, eine Zahl für score, true/false für noul |
tags |
nein | Strings, nach denen slicebar ist |
language |
nein | ein Code, nach dem slicebar ist |
model |
nein | erzwingt einen Checkpoint für diese Zeile; --model überschreibt es. Eine Zeile, die nichts erzwingt, wird mit dem Checkpoint beschriftet, mit dem der Router geantwortet hat |
research/evals/dataset.template.jsonl enthält ein kommentiertes Beispiel.
Metriken
Jede Metrik wird pro Antwort berechnet, wo sie zutrifft, und über den Datensatz aggregiert:
| Metrik | gilt für | Bedeutung |
|---|---|---|
choice_accuracy |
choice |
Anteil, dessen gewähltes Label übereinstimmt |
noul_accuracy |
noul |
Anteil, dessen Boolescher Wert (Wahrscheinlichkeit >= 0.5) übereinstimmt |
score_mae |
score |
mittlerer absoluter Fehler |
score_within_<tol> |
score |
Anteil innerhalb einer absoluten Toleranz |
ece |
jede Antwort mit einer Konfidenz | erwarteter Kalibrierungsfehler, 15 Bins, berechnet auf answer["answer_confidence"], der kalibrierten Wahrscheinlichkeit, die Laya auf jedem Antworttyp meldet |
brier |
jede Antwort mit einer Konfidenz und einem bekannten Label | Brier-Score der Konfidenz als P(korrekt), mean((confidence - correct)**2); niedriger ist besser |
aurc |
jede Antwort mit einer Konfidenz und einem bekannten Label | Fläche unter der Risk-Coverage-Kurve: ein Risikowert pro verschiedenem Konfidenzniveau, jeder gewichtet mit den Antworten, die dieses Niveau umfasst; niedriger ist besser, und belohnt eine Konfidenz, die richtige vor falschen Antworten einordnet, statt nur kalibriert zu sein |
selective_accuracy@50, selective_accuracy@80 |
jede Antwort mit einer Konfidenz und einem bekannten Label | Genauigkeit über die Antworten, die ein Konfidenzschwellenwert am 50%- / 80%-Coverage-Punkt akzeptiert – was die Enthaltung auf dem am wenigsten sicheren Rand einbringt. Ein Schwellenwert kann eine Gruppe gleicher Konfidenzen nicht teilen, dies kann also mehr als den genannten Bruchteil abdecken; siehe Coverage-Schnitte |
mean_confidence |
jede Antwort mit einer Konfidenz | Mittelwert der gemeldeten answer["answer_confidence"] |
latency_p50_ms, latency_p95_ms |
pro Anfrage | Wandzeit, die jede Anfrage gewartet hat, informativ – siehe Batching |
cost_per_decision_p50_ms, cost_per_decision_p95_ms |
pro Entscheidung | die Wandzeit eines Aufrufs geteilt durch die Zeilen, die er trug, informativ |
Coverage-Schnitte und Gleichstände
Beide Coverage-Metriken schneiden auf einem Konfidenz-Schwellenwert, und ein Schwellenwert
akzeptiert jede Antwort auf ihrer eigenen Konfidenz. Ein Schnitt teilt also niemals eine Gruppe von
Antworten, die dieselbe Konfidenz teilen: Wenn coverage * n in eine solche Gruppe fällt, wird jedes
Mitglied der Gruppe akzeptiert. Die Anzahl der Antworten hinter dem Wert ist daher die obere Kante der
Gruppe statt des genannten Bruchteils – selective_accuracy@50 über einen Slice, dessen Konfidenzen
alle gleich sind, ist die eigene Genauigkeit dieses Slice, nicht die bessere Hälfte davon. Die Anzahl,
die das Gate ausgibt (n= in der Fehlermeldung einer Regel), ist die Größe des Slice, nicht die
akzeptierte Größe, eine sehr breite Gruppe ist aus der Meldung allein also nicht sichtbar.
Gleichstände sind der Normalfall, nicht eine Randerscheinung: Eine angepasste Temperatur kann dazu
führen, dass ein Bucket eine Punktmasse meldet, was laya.common.answer_confidence für das
ausgelieferte choice:11+ aufzeichnet, und ein echter Checkpoint erzeugte eine Gruppe von sechs
Zeilen bei genau 1.0 aus zwölf Antworten. Ein Schnitt an einem Zeilenindex stattdessen ließ beide
Metriken von der Reihenfolge abhängen, in der der Datensatz ankam – dieselben Zeilen, gemischt,
bewegten selective_accuracy@50 zwischen 0.000 und 1.000.
aurc integriert einen Risikowert pro verschiedenem Niveau, gewichtet mit den Antworten, die dieses
Niveau umfasst, es bleibt also eine Fläche unter der Risk-Coverage-Kurve statt eines Mittelwerts
ungleich großer Punkte.
Zwei Konsequenzen, für die es sich zu planen lohnt:
- Eine Zahl kann sich in beide Richtungen bewegen, weiter als eine Umsortierung es könnte. Wo eine
Gruppe den Schnitt überspannt, unterscheidet sich die Schwellenwert-Lesart von jeder
Zeilenindex-Lesart derselben Daten: gemessen über 400,001 Datensätze in Gleichstandsform, bis zu
0.500 für
selective_accuracy@50und 0.351 füraurc. Bei der obigen Zwölf-Antworten-Form – sechs korrekt, alle bei Konfidenz 1.0 – bewegt sichaurcum 0.327 (0.173 auf 0.500). Ein Gate, das bestand, kann fehlschlagen, und eines, das fehlschlug, kann bestehen; das vorherige Urteil hing von der Zeilenreihenfolge ab, auch bei einem absolutenmin- odermax-Limit, das von nichts abgelehnt wird, weil es einen einzelnen Lauf liest. - Eingecheckte Baselines neu erzeugen.
config.coverage_metric_definitionzeichnet auf, welche Definition einen Bericht erzeugt hat. Ein Vergleich einer Coverage-Metrik über zwei Definitionen hinweg wird an beiden Gates abgelehnt, die eine Baseline subtrahieren –--baseline --tolerance(EvalReport.compare) und eine relative Regel (max_drop/max_increase) unter--gate-policy– und wenn eine der beiden Seiten veraltet ist, nicht nur die Baseline: ein von einem älterenlayaerzeugter Kandidat trägt ein Zeilenordnungs-Artefakt, das besser lesen kann als die Wahrheit, sodass ein Gaten gegen eine korrekt neu erzeugte Baseline eine Regression durchwinken würde, die der korrekt bewertete Bericht nicht besteht. Ohne diese Ablehnung verbirgt eine veraltete Baseline eine echte Regression: ein Slice, der unter der alten Definition mit 0.033 aufgezeichnet wurde, liest 0.517 unter dieser, ein Kandidat, der echt um 0.217 gefallen ist, würde also einemax_dropvon 0.05 bestehen.eceundbrierschneiden nicht und bleiben vergleichbar.
Ohne Gleichstände in den Daten gibt es ein Niveau pro Antwort, und beide Metriken sind genau das, was sie schon immer waren – bit-identisch, nicht bloß ähnlich.
Füge ScoreWithin(0.25) zur Evaluator-Liste hinzu für eine Toleranzmetrik; das Standardset ist
choice_accuracy, noul_accuracy, score_mae, mean_confidence, plus ece. Von der CLI aus ist
dasselbe ein einziger Schalter: laya-evals run data.jsonl --score-within 0.25 meldet score_within_0.25
neben den Standardwerten, und der Schalter ist wiederholbar, --score-within 0.25 --score-within 0.5
meldet also beide.
Eine Toleranzmetrik braucht eine score-Antwort mit einem numerischen Label, auf einem Datensatz ohne
eine hat sie also keinen Wert: run benennt die Metrik, die es nicht berechnen konnte, statt eine stille
Null zu veröffentlichen, und ein --min / --max-Gate, das diese Metrik nennt, schlägt als fehlend fehl.
Die Toleranzen, um die ein Lauf gebeten wurde, werden im Block config des Berichts aufgezeichnet, eine
geprüfte Baseline sagt also, welche Spalten sie erwartet.
Batching und Timing
--batch-size N bewertet bis zu N aufeinanderfolgende Zeilen, die einen Checkpoint und ein Frageschema
teilen, in einem Aufruf. Beide Timing-Metriken stammen aus denselben Messungen und beantworten
verschiedene Fragen: jede Zeile eines Batches kehrt zurück, wenn der Batch es tut, ihre latency ist also
der ganze Aufruf, während ihr cost_per_decision 1/N davon ist. Batching erhöht daher latency_* und
senkt cost_per_decision_* bei einer unveränderten Menge von Entscheidungen, und
--max latency_p50_ms=... fragt, ob Anfragen schnell bedient wurden, nicht ob der Lauf günstig war. Ohne
--batch-size stimmen die beiden überein.
compare ignoriert jede *_ms-Metrik, sofern keine Toleranz sie nennt, diese lassen eine Baseline also
nie an Timing-Rauschen scheitern. Was der Harness tatsächlich getan hat – die angeforderte Batch-Größe,
die Runner-Form, zu der er sich auflöste, wie viele Zeilen sich einen Aufruf teilten und der größte Chunk
–, wird in config.timing des Berichts aufgezeichnet, weil der Schalter allein nicht sagt, ob etwas
gebatcht wurde. Diese Zähler erfassen die ausgegebenen Aufrufe, nicht die zurückgekehrten: mit
laya-evals run --on-error skip zählt ein Chunk, dessen Aufruf eine Ausnahme auslöste, weiterhin in rows_grouped und
max_chunk, neben seinen Einträgen in config.errored. Der Standard ist --on-error fail, der die
Ausnahme erneut auslöst, statt einen Bericht zu veröffentlichen, dessen Metriken nur die Aufrufe
abdecken, die zurückgekommen sind. Die beiden *_ms-Metriken zählen nur die
zurückgekehrten Aufrufe, ein fehlgeschlagener Aufruf trägt also nie eine Latenz bei, die er nicht gemessen
hat.
Gruppieren der Zeilen innerhalb eines Batches
--sort-by-length gruppiert ähnlich große Zeilen in denselben Forward-Pass, sodass jeder Pass auf ein
kürzeres Maximum auffüllt statt auf die längste Zeile darin. Es ist die Form der Aufrufe, nicht ihre
Antworten: die Ergebnisse kommen in derselben Reihenfolge zurück und bewerten identisch, weshalb
research/ 2.15x über 10,000 Tickets melden kann, ohne dass sich eine Entscheidung ändert.
Es muss mehr als einen Pass geben, um umzusortieren, es wirkt also nur mit einem --batch-size N, das
unter der Anzahl der Zeilen liegt, die der Lauf gruppiert. config.timing hält die beiden Aussagen
auseinander: sort_by_length ist, was die Kommandozeile gesagt hat, sort_by_length_sent ist, was beim
Runner ankam. Ein Lauf ohne --batch-size verlangt etwas, das nicht passieren kann, und sagt das mit
sent: false; ein Runner, dessen predict_batch älter ist als der Schalter, wird unsortiert bewertet,
statt mitten in einem langen Lauf TypeError auszulösen.
Das Enthaltungs-Gate bei einem Schwellenwert
--min-confidence T reicht den optionalen Enthaltungsschwellenwert des Kerns (#361) an jeden Aufruf
weiter, den der Lauf macht, sodass Router und ONNXAgent Antworten, deren answer_confidence unter
T fällt, mit low_confidence: True markieren, bevor der Harness sie sieht. Anders als das
Gruppieren ändert dies die Antworten, die bewerten: derselbe Lauf bei T=0 und T=0.7 ist ein anderes
Experiment, und ein precision@coverage-Sweep ist eine Reihe davon, keine einzelne driftende Baseline.
Der akzeptierte Bereich ist der laya.confidence.check_min_confidence des Kerns – [0.0, 1.0],
endlich, kein bool – statt einer Kopie hier, ein Wert, den das Gate selbst ablehnen würde, schlägt also
als Nutzungsfehler fehl (Exit 2), bevor ein Checkpoint lädt. 0.0 ist eine legitime Anfrage: es ist der
Kontrollarm für einen precision@coverage-Sweep, und eine Prüfung, die ihn verwerfen würde, würde den
eigenen Boden des Sweeps verbergen.
Ein Runner, dessen predict oder (für einen gebatchten Lauf) dessen predict_batch älter ist als das
Gate, wird mit einem benannten EvalError abgelehnt, nicht ohne den Schwellenwert bewertet. Das
stille Verwerfen einer Bewertungskontrolle ist die Art von Lüge, deren Verhinderung dieser Harness
dient: der Bericht würde eine precision@coverage-Zahl für eine Richtlinie veröffentlichen, die nie
lief. config.timing zeichnet sowohl die Anfrage als auch die Tatsache auf: min_confidence ist der
angeforderte Schwellenwert, min_confidence_sent sagt, ob irgendein Aufruf dieses Laufs ihn
tatsächlich trug.
Slices
compare und run melden Gesamtzahlen und, für --slice language|model|qid|tag, dieselben Metriken pro
Slice-Wert, sodass eine Regression in einer Sprache oder einer Frage sichtbar ist, ohne das Aggregat zu
lesen. Der model-Slice enthält den Checkpoint, der jede Zeile beantwortet hat: die eigene Wahl des
Router pro Anfrage, oder das model des Runners für einen Runner, der nicht routet.
Optionale Slice-Gates
Das Gesamt-Baseline-Gate kann bestehen, während ein kleinerer Sprach- oder Frage-Slice regressiert.
Um einen geprüften Slice zu einer CI-Anforderung zu machen, speichere eine JSON-Richtlinie wie
gates.json:
{
"version": 1,
"rules": [
{"slice": {"language": "zh"}, "metric": "choice_accuracy",
"min_count": 50, "max_drop": 0.05},
{"slice": {"qid": "intent"}, "metric": "ece",
"min_count": 50, "max": 0.10}
]
}
laya-evals run data.jsonl --baseline baseline.json --tolerance choice_accuracy=0.02 \
--gate-policy gates.json --json report.json
laya-evals compare report.json --baseline baseline.json \
--tolerance choice_accuracy=0.02 --gate-policy gates.json
Jede Regel wählt genau einen language-, model-, qid- oder tag-Wert und benennt die Metrik
genau so, wie sie im Slice-Bericht erscheint. Sie hat ein positives min_count und genau ein Limit:
min oder max prüft den Kandidatenwert; max_drop erlaubt höchstens diese Abnahme gegenüber der
Baseline; max_increase erlaubt höchstens diese Zunahme. Die letzten beiden erfordern --baseline.
Die Anzahl ist die Zahl der bewerteten Antworten für diese Metrik im gewählten Slice, in beiden
Berichten für eine relative Regel. Für ece ist es die Zahl der Antworten mit einer endlichen
Konfidenz und einem booleschen correct-Wert. Ein fehlender Slice oder eine fehlende Metrik, zu
wenige bewertete Antworten oder übersprungene/fehlerhafte Fälle lassen das optionale Gate
fehlschlagen. Relative Regeln erfordern außerdem, dass beide Berichte übereinstimmende
Lauf-Identitäten tragen, fehlende Evidenz kann also nicht als Bestehen erscheinen. Eine gemessene
Regression meldet den Slice, die Metrik, die Anzahlen, die Werte und das Limit. Ungültige
Richtlinien-Syntax beendet mit Exit 2, bevor ein Checkpoint lädt; ein Qualitätsfehler beendet mit Exit
- Die Richtlinie wird in
config.gate_policyeinesrun --json-Berichts aufgezeichnet.compare --gate-policywendet die auf dieser Kommandozeile angegebene Richtlinie auf die gespeicherten Messungen an. Wenn sie von der aufgezeichneten Richtlinie des Berichts abweicht, sagtcomparedas; eine ausdrückliche erneute Prüfung unter einer neuen Richtlinie ändert nicht die Richtlinie, unter der der ursprüngliche Lauf gemacht wurde.
Der reguläre Gesamtvergleich gilt weiterhin, einschließlich seiner Toleranz und seines
Legacy-Baseline-Verhaltens. Ohne --gate-policy verhalten sich Slice-Berichterstattung und -Vergleich
wie zuvor.
Identität des Laufs
run zeichnet auf, was es gemessen hat, im Block config des Berichts, sodass das Artefakt, das ein
Prüfer liest, für sich allein prüfbar ist:
| Schlüssel | Bedeutung |
|---|---|
schema |
die Form des Berichts, laya-evals-report/1, damit ein Konsument einen ablehnen kann, den er nicht lesen kann |
dataset |
der Pfad wie eingetippt – ein Name, kein Hash |
dataset_sha256 |
das sha256 der geparsten Datensatz-Bytes |
questions_sha256 |
ein Fingerabdruck des Frageschemas: ID, Typ, instructions und criteria jeder Frage, über den ganzen Datensatz |
laya_version |
das laya, das die Zahlen berechnet hat |
coverage_metric_definition |
welche Definition von aurc / selective_accuracy@* diesen Bericht erzeugt hat (siehe Coverage-Schnitte). Eine relative Gate-Regel auf einem der beiden lehnt eine Baseline ab, die unter einer anderen aufgezeichnet wurde, statt Zahlen zu subtrahieren, die nicht dasselbe bedeuten |
gate_policy |
die optionale Slice-Gate-Richtlinie, die von run --gate-policy angewendet wurde |
thresholds |
das Gate, das dieser Lauf angewendet hat: min, max und baseline_tolerance |
revisions |
der Commit, aus dem jeder Checkpoint, der geantwortet hat, geladen wurde (siehe unten) |
dataset ist ein Pfad, und ein Pfad ist keine Identität: ein Datensatz kann an Ort und Stelle bearbeitet,
verschoben oder unter demselben Namen neu geholt werden, und ein CI-Cache kann zwei Läufen denselben
Dateinamen und verschiedene Bytes geben. questions_sha256 deckt ab, was gefragt wurde, statt wie viele
Zeilen es gab, das Hinzufügen von Zuständen zu einer unveränderten Fragenmenge lässt den Fingerabdruck also
unberührt – dataset_sha256 bewegt sich weiterhin, und eine Zeile hinzuzufügen ist eine Änderung an den
Daten, nicht an der Frage.
Es deckt auch instructions ab, weil der Instruktionstext der Prompt ist. build_sequence rendert
"<type> question: <instructions>" in den tokenisierten Kopf, Agent lehnt eine Frage ohne Instruktion ab
(„add the text the model should answer”), und Layas eigene Frageidentität zählt sie bereits mit:
Router._question_schema und die Batch-Gruppierung dieses Harness schlüsseln beide auf dem ganzen
Frage-Dict, und tests/test_router_batch.py hält fest, dass allein das Umformulieren von instructions
eine Zeile in ihre eigene Batch-Gruppe verschiebt. Vergleicht sich eine umformulierte Instruktion also
weiterhin gleich mit einer Baseline? Nein – und das ist der Punkt. „Judge whether a refund is justified”
und „Be conservative and only approve explicit refund requests” stellen verschiedene Fragen, und das
Metrik-Gate kann es nur bemerken, wenn die Differenz zufällig eine Zahl weiter bewegt als die Toleranz, die
du genannt hast. Das Benennen einer choice-Option ist dasselbe Argument: criteria ist der
Entscheidungsraum, und die metamorphischen Prüfungen in research/eval/metamorphic.py existieren, weil das
Umbenennen eines Labels Antworten umdreht.
Am Instruktionstext wird nichts normalisiert außer dem einen Schritt, den die Engine selbst anwendet: ein
Nicht-String-instructions wird als json.dumps(ins, ensure_ascii=False) gehasht, passend zu
Agent._to_internal. Whitespace und Formulierung zählen also beide, und eine Umformulierung, die ein
Mensch als Redaktionsänderung betrachtet, wird als neues Experiment behandelt. Das ist der ehrliche
Standard – die Alternative ist eine Ähnlichkeitsheuristik zwischen einem Lauf und seiner Baseline, und
kein gängiges Evaluierungssystem hat eine.
Nichts Zeitabhängiges wird aufgezeichnet, ein Bericht ist für einen festen Runner also weiterhin byte-reproduzierbar.
REPORT_SCHEMA, questions_fingerprint(dataset) und file_fingerprint(path) sind öffentlich, ein
Aufrufer, der laya.evals.evaluate direkt steuert, bekommt also dieselbe Identität wie ein CLI-Lauf.
Baseline und CI-Gate
- Halte den Datensatz, einen Baseline-Bericht (die
--json-Ausgabe, die du geprüft hast) und die Toleranzen zusammen und eingecheckt, sodass eine Änderung ein prüfbarer Diff ist.--tolerance METRIC=VALUEist die maximal erlaubte absolute Drift für diese Metrik. laya-evals run ... --baseline baseline.json --tolerance ...beendet sich bei Drift mit einem Nicht-Null-Code, es lässt sich also unverändert in CI einhängen.laya.evals.EvalReport.compareundassert_regressionlegen dieselbe Logik für Tests offen.
Das Metrik-Gate beantwortet „haben sich die Zahlen bewegt”. Es kann nicht beantworten „waren das dieselben
Zahlen”, weil compare overall und nur overall liest – eine gegen einen Datensatz aufgezeichnete
Baseline würde also einen Kandidaten durchwinken, der auf einem anderen bewertet wurde, bei identischer
Arithmetik. EvalReport.comparable_to schließt das: es vergleicht schema, dataset_sha256 und
questions_sha256, und run --baseline und compare geben weiterhin jedes Delta aus und schlagen dann
mit einem Nicht-Null-Code fehl, der den Schlüssel und beide Werte nennt:
FAIL: baseline is not comparable: dataset_sha256 (dataset bytes): baseline is <sha>, this run is <sha>
Ein Schlüssel, der auf einer der beiden Seiten fehlt, ist unbekannt, kein Konflikt, jeder Bericht, der
vor der Existenz der Identität geschrieben wurde, vergleicht also weiterhin genau wie zuvor. Das schließt
die Baseline des geplanten Gates unten ein, die aus research/eval/ stammt und gar kein config.schema
hat.
Zwei CI-Oberflächen nutzen das:
- ein gewichtsfreier Job in
.github/workflows/ci.ymlführttests/test_evals.pyundtests/test_evals_api.pyaus, sodass Metrik-Mathematik, Datensatz-Parsing und die CLI bei jedem PR abgedeckt sind, ohne einen Checkpoint herunterzuladen; .github/workflows/evals.ymlläuft wöchentlich, vor einem Release und auf Anfrage: es evaluiert den englischen Checkpoint auf der englischen MASSIVE-Suite und vergleicht mitresearch/results/eval_english_51_languages.jsonmit den Toleranzen ausresearch/evals/thresholds.json. Es lädt den Bericht als Artefakt hoch und blockiert keinen PR.
Der Harness ist für eine feste Checkpoint-Revision deterministisch, ein Bericht ist also reproduzierbar.
run zeichnet den Datensatz, das Modell und das Gerät sowie die Timing-Fakten des
Laufs im Block config des Berichts auf, und revisions: den Commit, aus dem jeder Checkpoint, der
geantwortet hat, tatsächlich geladen wurde. --revision <SHA> pinnt diesen Commit für jeden Checkpoint,
den der Lauf lädt, und --revision english=<SHA> pinnt einen Checkpoint (wiederholbar) — was die Form ist,
die ein auto-routender Lauf will, da die drei Checkpoints drei Repositories sind und ein Commit nicht in
allen existieren kann. Ohne Pin nimmt der Lauf den Standardbranch des Checkpoints, und der Bericht sagt
weiterhin, welcher Commit geantwortet hat, eine Baseline-Drift lässt sich also den Gewichten oder dem Code
zuordnen. laya/revisions.py veröffentlicht geprüfte Commit-SHAs in PINNED_REVISIONS für Aufrufer, die
sich dafür entscheiden wollen. Mit --onnx gilt nur ein blankes --revision <SHA>, für den Download von
Konfiguration und Tokenizer.
Den echten beschrifteten Datensatz hinzufügen
Lege eine JSONL in research/evals/ und eine geprüfte Baseline daneben ab und richte dann einen Workflow
(oder research/evals/check_regression.py) auf beide. Das Format ist dasselbe wie das des Fixtures;
nichts im Harness weiß von MASSIVE.