Dokümanlar menüsü
Değerlendirme: scorer & LLM-judge
Run'ları journal trace'inden deterministik puanlar.
Ne işe yarar / ne zaman kullanılır#
@gnldev/evals, bir agent run'ının çıktısını journal trace'inden (kaydedilmiş model/araç geçmişinden) puanlar — canlı çağrıya değil, journal'a bakar. Bu yüzden puanlama deterministiktir ve replayable'dır: aynı journal her koştuğunda kural-tabanlı scorer'lar (exactMatch, contains, regex, embeddingSimilarity) aynı puanı verir; scoreRun ile çağrılan LLM-judge sonucu bile journal'a memoize edildiği için resume'da tekrar model çağrısı yapmaz, ilk seferki puanı döner.
Tipik senaryo: bir agent'ı bir dataset'teki N test-case üzerinden koşturup her case'i skorla, ortalamayı al — bunun için evalDataset kullanılır. Suite bir journal ile çağrılırsa her case durable şekilde memoize edilir: suite ortasında çökerse tamamlanan case'ler tekrar koşmaz, kalanlar kaldığı yerden devam eder (resumable eval suite).
Kurulum / import#
Paket @gnldev/durable'a peer-dependency ile bağlıdır (journal okuma/yazma için); LLM-judge için de ai (AI SDK) gerekir.
pnpm add @gnldev/evals @gnldev/durable aiimport { scoreRun, exactMatch, contains, regexScore, embeddingSimilarity, llmJudge, evalDataset } from '@gnldev/evals';
import {
faithfulness, hallucination, answerRelevancy, toxicity, bias, completeness, contextPrecision, toneConsistency,
} from '@gnldev/evals';
import { createDatasetsManager } from '@gnldev/evals';Adım adım kullanım#
1) Tamamlanmış bir run'ı puanla — scoreRun, bir JournalReader üzerinden run'ın journal trace'ini okur, son model entry'sinin metnini çıkarır ve verilen scorer'ları bu metne uygular:
import { scoreRun, exactMatch, llmJudge } from '@gnldev/evals';
const res = await scoreRun(reader, runId, [
exactMatch(),
llmJudge({ model, rubric: 'Is the answer correct and concise?' }),
], { expected: 'Paris' });
// res.output -> the last model output in the journal
// res.scores -> { 'exact-match': { score, reason }, 'llm-judge': { score, reason } }reader, Journal arayüzünü de karşılıyorsa (ör. SqliteStorage örneği) scoreRun her scorer sonucunu `${runId}:proc:eval:${name}` anahtarıyla journal'a memoize eder — böylece aynı run tekrar puanlanırsa (veya resume edilirse) llmJudge bile ikinci kez model çağrısı yapmaz. Bunu { memo: false } ile kapatabilirsin.
2) Bir dataset üzerinde toplu değerlendirme — evalDataset, agent'ı dataset'teki her case'te koşturur, skorlar ve scorer başına ortalamayı toplar:
import { evalDataset } from '@gnldev/evals';
const report = await evalDataset({
dataset: { id: 'qa-suite', cases: [
{ id: 'c1', input: 'What is the capital of France?', expected: 'Paris' },
] },
run: async (input, { runId }) => {
// run the agent for this case; return { output } or a string
return runAgent(input, { runId });
},
scorers: [exactMatch()],
journal, // when given, case results are memoized -> a resumable suite
});
// report.aggregate -> { 'exact-match': 0.8 } (0..1 ortalama)journal parametresi verildiğinde her case `evalds:${dataset.id}` altında `case:${caseId}` anahtarıyla memoize edilir; suite ortasında çökme durumunda tamamlanmış case'ler journal'dan döner, koşulmaz.
3) Kural-tabanlı scorer'lar tek başına — Scorer arayüzünü uygulayan bu fonksiyonlar scoreRun/evalDataset olmadan da doğrudan çağrılabilir:
const exact = exactMatch();
await exact.score({ output: 'Paris', expected: 'Paris' }); // { score: 1, reason: 'exact match' }
const emb = embeddingSimilarity(
(text) => embed({ model: embeddingModel, value: text }).then((r) => r.embedding),
{ threshold: 0.85 },
);
await emb.score({ output: '...', expected: '...' });4) Hazır scorer'lar — 8 hazır scorer, llmJudge üzerine kurulu rubric'lerdir (faithfulness, hallucination, answerRelevancy, toxicity, bias, completeness, contextPrecision, toneConsistency) — hepsi generateText'i doğrudan çağırmaz, llmJudge'ı sarar; bu yüzden scoreRun/dataset memoizasyonu bedava gelir.
import { faithfulness, hallucination, toxicity } from '@gnldev/evals';
const res = await scoreRun(reader, runId, [
faithfulness({ model }), // sample.context gerektirir — yoksa { score: 0, reason: 'context gerekli: ...' }
hallucination({ model }), // needs context; DIRECTION: 1.0 = NO hallucination (good)
toxicity({ model }), // needs neither context nor input; it scores the output alone
], { context: retrievedChunks });Yön semantiği (tüm scorer'larda ortak): yüksek skor = iyi sonuç. Bu, isimden ters sezilebilecek scorer'larda (hallucination, toxicity, bias) kafa karıştırabilir: ör. hallucination skoru 1.0 "halüsinasyon YOK" demektir, "halüsinasyon VAR" değil.
Bağlam gerektiren scorer'lar (faithfulness, hallucination, contextPrecision) sample.context, soru gerektirenler (answerRelevancy, completeness) sample.input ister — eksikse sessizce 1.0 vermez, { score: 0, reason: '...' } döner. Her scorer ayrıca sampleFields ile llmJudge'a hangi ek alanların (input/context) prompt'a ekleneceğini açıkça bildirir — toxicity/bias/toneConsistency bilerek sampleFields: [] kullanır (alakasız bağlamla kirlenmesin diye — ör. bağlam zehirliyse çıktı temiz olsa bile yanlış düşük toxicity skoru çıkmasın).
5) Dataset versiyonlama + deney karşılaştırma — createDatasetsManager, evalDataset'in üstüne journal-tabanlı bir katman ekler: dataset'in versiyon geçmişini tutar, deneyleri (experiment) idempotent kaydeder ve iki deneyi karşılaştırır (regresyon/iyileşme).
import { createDatasetsManager } from '@gnldev/evals';
const manager = createDatasetsManager(journal); // journal 'listKeys' desteklemeli
await manager.saveDataset(dataset); // no new version is opened when the content is unchanged (hash comparison)
const exp1 = await manager.runExperiment({
datasetId: 'qa-suite', run: runAgent, scorers: [exactMatch()], experimentId: 'baseline',
});
const exp2 = await manager.runExperiment({
datasetId: 'qa-suite', run: runAgentV2, scorers: [exactMatch()], experimentId: 'candidate', label: 'yeni prompt',
});
// a second call with the same experimentId does NOT re-run evalDataset — it returns the recorded result (idempotent)
const diff = await manager.compare('qa-suite', 'baseline', 'candidate');
// diff.aggregate -> per scorer { baseline, candidate, delta }
// diff.regressions / diff.improvements -> how many cases got worse or betterKoşması hiçbir şeye mal olmayan scorer’lar#
Buradaki iki aile hiç model çağırmaz. Dört metin scorer’ı — contentSimilarity, keywordCoverage, textualDifference, answerSimilarity — çıktı ve beklenenin saf fonksiyonlarıdır: maliyet yok, gecikme yok, üzerine düşünülecek non-determinizm yok. Bir LLM-judge’ın hem yavaş hem pahalı olacağı büyük bir regresyon suite’i için doğru varsayılan bunlardır.
Trajectory scorer’ları başka bir soruyu cevaplar: "cevap iyi mi" değil, "ajan makul bir yol izledi mi". createTrajectoryScorer, tool-çağrısı dizisini dört boyuta karşı skorlar — içermesi gereken sıralı bir alt dizi, sırası önemsiz ama bulunması gereken tool’lar, hiç bulunmaması gerekenler, ve bir çağrı bütçesi. Bütçeyi aşmak ya da yasak bir tool’a dokunmak skoru sıfırlamaz, oransal olarak düşürür.
Regresyon diff’inin kullandığı karar dizisinin aynısını okur (buildDecisionSequence); böylece "karar noktası" bütün sistemde tek bir şey demektir, iki pakette birbirine benzeyen iki şey değil.
import { createTrajectoryScorer, trajectoryScorerFor, contentSimilarity } from '@gnldev/evals';
const path = createTrajectoryScorer({
expectedTools: ['searchDocs', 'summarise'], // ordered subsequence; extras in between are fine
requiredTools: ['citeSource'], // must appear, order irrelevant
forbiddenTools: ['deleteRecord'], // a hit degrades the score proportionally
maxToolCalls: 8, // over budget degrades, it does not zero
});
// bound to a reader: the sample carries a runId and the scorer looks the run up
const fromJournal = trajectoryScorerFor(reader, { requiredTools: ['citeSource'] });
await evalDataset({ dataset, run, scorers: [path, contentSimilarity()], journal });API referansı#
scoreRunJournal'dan run'ın son model çıktısını okuyup verilen scorer'larla puanlar; journal yazılabilirse sonuçları memoize eder.
createTrajectoryScorerTool-çağrısı dizisi üzerinde saf bir Scorer: expectedTools (sıralı ALT DİZİ — aradaki fazladan çağrılar sorun değil), requiredTools (sırasız), forbiddenTools, maxToolCalls ve boyut başına ağırlıklar. Model çağrısı yok, yani aynı dizi hep aynı skoru alır.
trajectoryScorerForJournalReader’a bağlanmış Scorer uyarlaması: örneğe bir runId verirsiniz, çalıştırmayı kendisi bulur.
scoreTrajectoryscoreTrajectory(reader, runId, opts) — çalıştırmayı okur, karar dizisini yeniden kurar ve skorlar. Tek seferlik bir fonksiyondur, Scorer değildir.
scoreToolSequenceAynı skorlama, düz bir string[] tool adı dizisi üzerinde tek seferlik fonksiyon olarak — elle kurulmuş bir diziyle birim testinde kullanılabilir.
contentSimilarityÇıktı ile beklenen arasında model-free token-örtüşmesi benzerliği.
keywordCoverageÇıktının bir anahtar kelime listesini ne kadar kapsadığı. Liste verilmezse anahtar kelimeler sample.expected’tan türetilir.
textualDifferenceModel-free farklılık skoru — benzerliğin ters çerçevelenmiş hâli.
answerSimilarityÇıktı ile beklenen arasında model-free cevap düzeyinde benzerlik.
ScoreRunResultscoreRun dönüşü: { runId, output, scores }.
exactMatchÇıktı, expected'a (trim'li) birebir eşit mi.
containsÇıktı verilen alt-diziyi (veya expected'ı) içeriyor mu.
regexScoreÇıktı verilen regex ile eşleşiyor mu.
embeddingSimilarityoutput/expected'ı embed fonksiyonuyla vektörleştirip kosinüs benzerliği (opsiyonel eşik ile pass/fail) döner.
llmJudgeAI SDK modelinden çıktıyı verilen rubric'e göre 0.0-1.0 puanlamasını ister ve SCORE/REASON formatını ayrıştırır.
LlmJudgeOptions{ model, rubric?, name?, sampleFields? } — LlmJudge girdisi. 'sampleFields' örneğin hangi parçalarının hakemin istemine gireceğini seçer (varsayılan ['input','context']); bağlamdan etkilenmemesi gereken bir puanlayıcı — örneğin toksisite — bilerek [] geçer.
faithfulness(opts: JudgeScorerOptions) → Scorer. Çıktının sample.context'e sadakatini ölçer; context yoksa 0 + gerekçe. YÖN: 1.0 = tamamen sadık.
hallucinationÇıktıda bağlamla çelişen/uydurma iddia var mı ölçer; context gerektirir. YÖN (isimden ters): 1.0 = halüsinasyon YOK.
answerRelevancyÇıktının sample.input'a (soru) ne kadar odaklı yanıt verdiğini ölçer; input gerektirir.
toxicityÇıktıda hakaret/nefret söylemi var mı ölçer; bağlam/girdi gerektirmez. YÖN (isimden ters): 1.0 = zehirli DEĞİL.
biasÇıktıda grup temelli önyargı var mı ölçer; bağlam/girdi gerektirmez. YÖN (isimden ters): 1.0 = önyargı YOK.
completenessÇıktının sample.input'un tüm yönlerini eksiksiz kapsayıp kapsamadığını ölçer; input gerektirir.
contextPrecisionGetirilen bağlam parçalarının ne kadarının alakalı/gerekli olduğunu (retrieval kalitesi) ölçer; context gerektirir.
toneConsistency(opts: ToneConsistencyOptions) → Scorer. Çıktı boyunca ton tutarlılığını (opsiyonel expectedTone ile) ölçer.
JudgeScorerOptions{ model, name? } — 8 hazır scorer'ın ortak fabrika seçenekleri.
ToneConsistencyOptionsJudgeScorerOptions + { expectedTone? } — toneConsistency'e özel.
evalDataset{ dataset, run, scorers, journal?, scope? } alır; her case'i koşturur, skorlar, scorer başına ortalamayı aggregate olarak döner.
Dataset{ id, cases: DatasetCase[] }.
DatasetCase{ id, input, expected?, metadata? }.
EvalDatasetResult{ datasetId, cases: EvalCaseResult[], aggregate }.
Scorer{ name, score(sample) } — sample: { output, expected?, input? }; score 0.0-1.0 döner.
createDatasetsManager(journal) → { saveDataset, getDataset, listVersions, runExperiment, getExperiment, listExperiments, compare }. journal 'listKeys' desteklemeli.
DatasetVersion{ version, at, dataset } — saveDataset/getDataset dönüşü; içerik değişmediyse yeni versiyon açılmaz (hash karşılaştırma).
ExperimentRecord{ id, datasetId, datasetVersion, at, label?, result } — runExperiment kaydı; aynı experimentId ile ikinci koşu idempotent (yeniden koşulmaz).
RunExperimentOptions{ dataset?, datasetId?, run, scorers, experimentId?, label?, now? } — runExperiment girdisi.
ExperimentDiff{ datasetId, baseline, candidate, aggregate, changes, regressions, improvements } — compare() dönüşü; changes en kötü regresyon başta sıralanır.
scoreRun üzerinden çağrılırsa journal'a memoize edilir (Journal/JournalReader geçirildiğinde) — llmJudge(...).score(...) doğrudan çağrılırsa her seferinde gerçek model isteği yapılır.embeddingSimilarity, embed sağlayıcısı bekler — GNL kendi embed fonksiyonunu içermez; AI SDK'nin embed()'i (veya eşdeğeri) dışarıdan verilmelidir.createDatasetsManager'ın runExperiment'ı, aynı dataset'in FARKLI deneylerinin birbirinin memoized case'ini görmemesi için evalDataset'e her deneye özel bir scope (`${datasetId}:exp:${experimentId}`) verir — aynı deneyin crash-resume'u ise kaldığı yerden devam eder (resumable suite korunur).