GNL
Dokümanlar menüsü
Core · Ücretsiz@gnldev/evals

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.

npm/pnpm
pnpm add @gnldev/evals @gnldev/durable ai
import
import { 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:

scoreRun ile puanlama
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:

evalDataset ile suite koşturma
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:

doğrudan scorer çağrısı
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.

hazır scorer'lar
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).

createDatasetsManager
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 better

Koş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.

bir trajectory scorer’ı
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ı#

fnscoreRun

Journal'dan run'ın son model çıktısını okuyup verilen scorer'larla puanlar; journal yazılabilirse sonuçları memoize eder.

fncreateTrajectoryScorer

Tool-ç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.

fntrajectoryScorerFor

JournalReader’a bağlanmış Scorer uyarlaması: örneğe bir runId verirsiniz, çalıştırmayı kendisi bulur.

fnscoreTrajectory

scoreTrajectory(reader, runId, opts) — çalıştırmayı okur, karar dizisini yeniden kurar ve skorlar. Tek seferlik bir fonksiyondur, Scorer değildir.

fnscoreToolSequence

Aynı skorlama, düz bir string[] tool adı dizisi üzerinde tek seferlik fonksiyon olarak — elle kurulmuş bir diziyle birim testinde kullanılabilir.

fncontentSimilarity

Çıktı ile beklenen arasında model-free token-örtüşmesi benzerliği.

fnkeywordCoverage

Çıktının bir anahtar kelime listesini ne kadar kapsadığı. Liste verilmezse anahtar kelimeler sample.expected’tan türetilir.

fntextualDifference

Model-free farklılık skoru — benzerliğin ters çerçevelenmiş hâli.

fnanswerSimilarity

Çıktı ile beklenen arasında model-free cevap düzeyinde benzerlik.

typeScoreRunResult

scoreRun dönüşü: { runId, output, scores }.

fnexactMatch

Çıktı, expected'a (trim'li) birebir eşit mi.

fncontains

Çıktı verilen alt-diziyi (veya expected'ı) içeriyor mu.

fnregexScore

Çıktı verilen regex ile eşleşiyor mu.

fnembeddingSimilarity

output/expected'ı embed fonksiyonuyla vektörleştirip kosinüs benzerliği (opsiyonel eşik ile pass/fail) döner.

fnllmJudge

AI 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.

typeLlmJudgeOptions

{ 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.

fnfaithfulness

(opts: JudgeScorerOptions) → Scorer. Çıktının sample.context'e sadakatini ölçer; context yoksa 0 + gerekçe. YÖN: 1.0 = tamamen sadık.

fnhallucination

Çıktıda bağlamla çelişen/uydurma iddia var mı ölçer; context gerektirir. YÖN (isimden ters): 1.0 = halüsinasyon YOK.

fnanswerRelevancy

Çıktının sample.input'a (soru) ne kadar odaklı yanıt verdiğini ölçer; input gerektirir.

fntoxicity

Çıktıda hakaret/nefret söylemi var mı ölçer; bağlam/girdi gerektirmez. YÖN (isimden ters): 1.0 = zehirli DEĞİL.

fnbias

Çıktıda grup temelli önyargı var mı ölçer; bağlam/girdi gerektirmez. YÖN (isimden ters): 1.0 = önyargı YOK.

fncompleteness

Çıktının sample.input'un tüm yönlerini eksiksiz kapsayıp kapsamadığını ölçer; input gerektirir.

fncontextPrecision

Getirilen bağlam parçalarının ne kadarının alakalı/gerekli olduğunu (retrieval kalitesi) ölçer; context gerektirir.

fntoneConsistency

(opts: ToneConsistencyOptions) → Scorer. Çıktı boyunca ton tutarlılığını (opsiyonel expectedTone ile) ölçer.

typeJudgeScorerOptions

{ model, name? } — 8 hazır scorer'ın ortak fabrika seçenekleri.

typeToneConsistencyOptions

JudgeScorerOptions + { expectedTone? } — toneConsistency'e özel.

fnevalDataset

{ dataset, run, scorers, journal?, scope? } alır; her case'i koşturur, skorlar, scorer başına ortalamayı aggregate olarak döner.

typeDataset

{ id, cases: DatasetCase[] }.

typeDatasetCase

{ id, input, expected?, metadata? }.

typeEvalDatasetResult

{ datasetId, cases: EvalCaseResult[], aggregate }.

typeScorer

{ name, score(sample) } — sample: { output, expected?, input? }; score 0.0-1.0 döner.

fncreateDatasetsManager

(journal) → { saveDataset, getDataset, listVersions, runExperiment, getExperiment, listExperiments, compare }. journal 'listKeys' desteklemeli.

typeDatasetVersion

{ version, at, dataset } — saveDataset/getDataset dönüşü; içerik değişmediyse yeni versiyon açılmaz (hash karşılaştırma).

typeExperimentRecord

{ id, datasetId, datasetVersion, at, label?, result } — runExperiment kaydı; aynı experimentId ile ikinci koşu idempotent (yeniden koşulmaz).

typeRunExperimentOptions

{ dataset?, datasetId?, run, scorers, experimentId?, label?, now? } — runExperiment girdisi.

typeExperimentDiff

{ datasetId, baseline, candidate, aggregate, changes, regressions, improvements } — compare() dönüşü; changes en kötü regresyon başta sıralanır.

İpucu
LLM-judge yalnızca 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.
Dikkat
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.
Kapsam
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).