Dokumentation

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@50 und 0.351 für aurc. Bei der obigen Zwölf-Antworten-Form – sechs korrekt, alle bei Konfidenz 1.0 – bewegt sich aurc um 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 absoluten min- oder max-Limit, das von nichts abgelehnt wird, weil es einen einzelnen Lauf liest.
  • Eingecheckte Baselines neu erzeugen. config.coverage_metric_definition zeichnet 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 älteren laya erzeugter 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 eine max_drop von 0.05 bestehen. ece und brier schneiden 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

  1. Die Richtlinie wird in config.gate_policy eines run --json-Berichts aufgezeichnet. compare --gate-policy wendet die auf dieser Kommandozeile angegebene Richtlinie auf die gespeicherten Messungen an. Wenn sie von der aufgezeichneten Richtlinie des Berichts abweicht, sagt compare das; 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=VALUE ist 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.compare und assert_regression legen 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.yml führt tests/test_evals.py und tests/test_evals_api.py aus, sodass Metrik-Mathematik, Datensatz-Parsing und die CLI bei jedem PR abgedeckt sind, ohne einen Checkpoint herunterzuladen;
  • .github/workflows/evals.yml läuft wöchentlich, vor einem Release und auf Anfrage: es evaluiert den englischen Checkpoint auf der englischen MASSIVE-Suite und vergleicht mit research/results/eval_english_51_languages.json mit den Toleranzen aus research/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.