Metryki ewaluacji RAG: wyszukiwanie, reranking, generowanie

Tłumaczenie automatyczne Ten artykuł został automatycznie przetłumaczony z angielskiego oryginału.

Aktualizacja artykułu

Pierwotnie opublikowano 10 maja 2026 r. Artykuł sprawdzono i zaktualizowano 6 września 2026 r. Aktualizacja dodaje nowsze benchmarki wyszukiwania i kandydatów do rerankingu, zmienia zalecenia dotyczące narzędzi ewaluacyjnych oraz koryguje parametry API w przykładzie z judge’em.

System RAG z uszkodzonymi filtrami trafności może działać miesiącami bez wywołania alertu operacyjnego. Nadal zwraca odpowiedzi i mieści się w założonym limicie opóźnień, ale odpowiedzi opierają się na niepełnych dowodach. Recall@k względem oryginalnego kwalifikującego zbioru gold ujawnia tę utratę. Dashboardy opóźnień i dostępności — nie.

Dla inżynierów obsługujących lub ewaluujących wieloetapowe systemy RAG ten materiał mapuje awarie parsowania dokumentów, filtrowania, wyszukiwania, rerankingu i generowania na metrykę, która je identyfikuje. Pokazuje też, które kontrole należy uruchamiać przed wydaniem, a które monitorują ruch produkcyjny.

Chcesz pominąć wstęp i uruchomić kod?

Uruchamialne repozytorium slavadubrov/rag-evals-demo stosuje wybrane metryki do zbioru SciFact. make eval uruchamia cały zestaw, a make benchmark porównuje konfiguracje chunkingu, embeddingów i LLM. Notatniki 00–09 obejmują wyszukiwanie, filtrowanie, generowanie i przykłady systemowe; nie implementują wszystkich kontroli opisanych w tym materiale. Demo korzysta z osadzonego Qdrant, więc nie wymaga Dockera.

Materiał towarzyszący jest harnesssem edukacyjnym. Wersja sprawdzona 6 września 2026 r. nadal wymaga napraw w obsłudze brakujących zapytań, parsowaniu wyników judge’a i danych gold dotyczących autoryzacji. Jej pairwise judge nie otrzymuje także dostarczonego kontekstu potrzebnego do oceny wsparcia. Skorygowane kontrakty i kontrole inline poniżej nie naprawiają tego repozytorium; użyj jego notatników do prześledzenia workflow i zweryfikuj te przypadki, zanim przyjmiesz jego wyniki jako kryteria wydania.

TL;DR

  • Użyteczny stos ewaluacyjny obejmuje ingest, wyszukiwanie, ugruntowanie generacji, zgodność z ontologią oraz sygnały systemowe. RAGAS, TruLens, DeepEval, Arize Phoenix oraz TREC 2024 RAG Track oferują biblioteki lub publiczne protokoły ewaluacyjne. Nie wybierają jednak metryk za Ciebie.
  • W RAG opartym na metadanych i ontologii błędny tag lub kruchy predykat twardy może obniżyć recall do zera. Standardowy Recall@k wykrywa tę stratę, jeśli zachowuje oryginalny kwalifikujący zbiór gold. Metryka fałszywego wykluczenia przez filtr identyfikuje przyczynę. Faithfulness może nadal oceniać twierdzenia względem niepełnego kontekstu, ale nie potrafi zdiagnozować przyczyny związanej z filtrem ani wyszukiwaniem. Pusta odmowa może nie zawierać żadnych stwierdzeń i zwracać NaN, zależnie od implementacji.

Tabela decyzyjna ewaluacji RAG

Potraktuj tę tabelę jako punkt wyjścia przed wyborem frameworka. Właściwa metryka zależy od trybu awarii, który chcesz wykryć, a nie od nazwy narzędzia.

PytanieRodzina metrykUżyj, gdyUważaj na
Czy parsowanie zachowało źródło?Kompletność ekstrakcji, pokrycie tabel/rysunkówPDF-y, slajdy, skany i strony HTML trafiają do korpusuTekst może wyglądać poprawnie, a mimo to tracić podpisy, przypisy lub strukturę tabel
Czy wyszukiwanie znalazło właściwe dowody?Recall@k, nDCG@k, MRR, precision/recall kontekstuMożesz oznaczyć trafne chunki lub dokumentyTwardy filtr metadanych może usunąć właściwy dokument przed rozpoczęciem rankingu
Czy reranking poprawił shortlistę?Uplift rerankera, Precision@1, delta nDCGPo wyszukiwaniu działają cross-encodery lub rankery LLMMierz opóźnienie i koszt razem ze wzrostem jakości
Czy odpowiedź wykorzystała dowody?Faithfulness, groundedness, wsparcie cytowaniamiOdpowiedź cytuje dokumenty lub opiera fakty na kontekścieFaithfulness nie zdiagnozuje złego parsowania ani wyszukiwania
Czy system jest stabilny produkcyjnie?Drift, regeneracje, fallback, p95, koszt odpowiedziRuch zmienia się po wdrożeniuTelemetyka produkcyjna wymaga próbkowanej oceny ludzkiej dla kalibracji

Krótsze porównanie narzędzi znajdziesz w Best RAG Evaluation Tools: Ragas, DeepEval, and TruLens.

Część 1: Zdefiniuj sukces przed architekturą

Przygotuj zbiór ewaluacyjny przed diagramem architektury. Dzięki temu każda późniejsza decyzja dotycząca komponentu będzie miała mierzalny cel.

Nie możesz wybrać między BM25 a dense retrieval, chunkingiem rekurencyjnym a semantycznym ani Cohere Rerank a BGE, dopóki nie wiesz, co optymalizujesz. „Lepsze odpowiedzi” nie są metryką. Przykładowy wymóg wydaniowy może brzmieć: „faithfulness ≥ 0,85 na złotym zbiorze 200 zapytań obejmującym trzy najważniejsze intencje, przy p95 latency < 1,5 s i współczynniku fałszywego wykluczenia przez filtr < 2%”. Liczby są przykładowe; ważne jest, aby jakość, pokrycie, opóźnienie i filtrowanie miały jawne progi.

Zdefiniuj harness, zanim napiszesz kod wyszukiwania. Pierwszy harness będzie błędny i będziesz go poprawiać. Zmiana metryki jest znacznie tańsza niż zmiana systemu, który został już wdrożony.

Trzy warstwy pipeline’u i dwa tryby uruchamiania

Ewaluacja produkcyjna ma trzy warstwy pipeline’u. Ewaluacja ingestu sprawdza, czy korpus i indeks zachowują źródło. Ewaluacja w czasie zapytania sprawdza, czy przepisywanie zapytania, filtrowanie, wyszukiwanie, reranking i składanie kontekstu znalazły właściwe dowody. Ewaluacja odpowiedzi i produkcji sprawdza, czy odpowiedź wykorzystała te dowody oraz czy jakość utrzymuje się w ruchu produkcyjnym. Złożenie warstw w jeden wynik pozwala, aby błąd normalizacji zniknął w akceptowalnym wyniku odpowiedzi.

Trzy miejsca, w których system RAG może utracić dowody: korpus i indeks, ścieżka wyszukiwania oraz odpowiedź i ruch produkcyjnyTrzy miejsca, w których system RAG może utracić dowody: korpus i indeks, ścieżka wyszukiwania oraz odpowiedź i ruch produkcyjny

Warstwy opisują miejsce wystąpienia awarii. Offline i online opisują moment uruchomienia kontroli oraz dane, względem których jest wykonywana. Ewaluacja offline korzysta ze stałego zbioru danych ze znaną prawdą referencyjną; jest powtarzalna i należy ją stosować przy wyborze komponentów, porównaniach A/B oraz kontrolach CI, które mogą blokować zmianę. Ewaluacja online ocenia próbkowany ruch produkcyjny i rejestruje regeneracje, czas pozostawania, jawny feedback oraz rzeczywisty drift zapytań. Jest bardziej zaszumiona i trudniejsza do instrumentacji.

Korzystaj z obu trybów tam, gdzie przynoszą korzyści: stałe korpusy i zbiory zapytań zapewniają powtarzalność regresji, a próbkowane ślady produkcyjne ujawniają problemy ze świeżością i drift.

Ewaluacja komponentów a ewaluacja end-to-end

Typowe są dwa błędy. Ewaluacja wyłącznie end-to-end mówi, że system jest uszkodzony, ale nie wskazuje gdzie. Ewaluacja wyłącznie komponentów może pokazać, że każda część przechodzi testy, choć cały system nadal zawodzi. Rozwiązaniem jest kilka głównych metryk end-to-end do decyzji go/no-go oraz metryki komponentowe do diagnozy. Metryki wyszukiwania wykrywają regresje retrievera. Metryki generowania wykrywają regresje generatora. Poprawność odpowiedzi end-to-end wykrywa błędy integracji.

Referencyjne frameworki — przegląd z opinią

FrameworkNajlepszy wGdzie zawodzi
RAGASWspólny słownik dla faithfulness, answer relevancy oraz precision/recall kontekstu (metrics)Koszt LLM-judge’a; nieprzejrzyste składniki wyniku przy debugowaniu; zmiany wersji
ARESJudge w postaci klasyfikatora dopasowanego do zadania, jeśli koszt treningu i anotacji jest uzasadniony (paper); raportowane precision jest związane z benchmarkiemCięższa konfiguracja; trzeba rzeczywiście trenować modele
TruLensFunkcje feedbacku powiązane ze śladami i integracja z OpenTelemetry (project)Mniej gotowych metryk specyficznych dla RAG niż w RAGAS
DeepEvalIntegracja z runnerem testów i własne metryki (project)Intensywne użycie LLM-judge’a = skoki kosztów
Arize PhoenixTracing, eksperymenty na datasetach oraz gotowe lub własne ewaluatory RAG/agentów (evaluation docs)Rubryki domenowe i progi judge’a nadal wymagają lokalnej kalibracji
TREC 2024 RAG TrackPubliczny benchmark ewaluacji nuggetów (AutoNuggetizer), wsparcia i płynności na MS MARCO Segment v2.1Nie jest narzędziem runtime; to benchmark do kalibracji

Moim domyślnym stosem jest RAGAS jako słownik metryk, DeepEval do kontroli CI, Phoenix do trace’ów produkcyjnych oraz własny kod dla metryk zależnych od ontologii. Wybierz framework, który ułatwia tworzenie własnych metryk.

Przy wyborze benchmarku najpierw dopasuj zadanie, a dopiero potem analizuj leaderboard. BEIR, MTEB i MIRACL nadal są użytecznymi baseline’ami wyszukiwania. Dodaj testy dla możliwości, których nie obejmują:

  • Aktualny end-to-end RAG: TREC 2026 RAG Track korzysta z zapytań narracyjnych i ClimbMix-400b, zastępując MS MARCO v2.1, oraz odsyła do toolkit’u ewaluacyjnego RAGDoll. 6 września strona tracku nie ogłosiła daty zwrotu wyników i ocen. Udostępnione tematy są dostępne do eksperymentów; nie stanowią ukończonego, ocenionego leaderboardu 2026. Protokoły z lat 2024 i 2025 utrzymuj powiązane z ich własnymi korpusami i ocenami.
  • Pytania techniczne dotyczące zmieniającego się kodu: FreshStack łączy pytania zadane przez ludzi na Stack Overflow, korpusy repozytoriów i oceny nuggetów. Udostępniony snapshot i mechanizm budowania nowych korpusów to różne rzeczy; przypnij wersję repozytorium i datę pytania.
  • Obrazy zawierające część pytania lub dowodów: MM-BRIGHT rozdziela wyszukiwanie text-to-text, multimodal-to-text, multimodal-to-image i multimodal-to-multimodal. Oceniaj odpowiednie zadania osobno. Sam tekst z OCR może pominąć informacje przekazywane przez wykres lub screenshot.

Benchmarki te poszerzają zakres, ale nie zastępują kwalifikującego, wersjonowanego zbioru zapytań Twojej aplikacji.


Część 2: Zmapuj punkty ewaluacji

Pipeline RAG pogrupowany na ścieżki ingestu, czasu zapytania i odpowiedzi, z metrykami diagnostycznymi przy każdym etapiePipeline RAG pogrupowany na ścieżki ingestu, czasu zapytania i odpowiedzi, z metrykami diagnostycznymi przy każdym etapie

Użyj diagramu, aby przypisać objaw do pierwszej metryki diagnostycznej. Straty na wcześniejszych etapach ograniczają jakość późniejszych: złe parsowanie ogranicza wyszukiwanie, a złe wyszukiwanie ogranicza reranking i generowanie. Faithfulness mierzy odpowiedź, nigdy przyczynę upstream.


Część 3: Ewaluacja ingestu

Wiele produkcyjnych awarii RAG zaczyna się podczas ingestu. System działa na czystych dokumentach testowych, a następnie zawodzi na rzeczywistych PDF-ach, skanach, tabelach i nieuporządkowanych stronach korpusu.

Pozyskiwanie i parsowanie dokumentów

Co mierzyć:

  • Kontrola sensowności długości ekstrakcji: extracted_chars / expected_chars na klasę dokumentu wykrywa podejrzane zmiany długości, ale zduplikowany lub błędny tekst nadal może otrzymać wynik 1,0. Porównuj wyrównany tekst z ręcznie oczyszczonym odniesieniem pod kątem pominięć i substytucji, a następnie osobno sprawdzaj przypisy, podpisy, zawartość tabel i kolejność czytania.

  • Dokładność OCR: CER (Character Error Rate) i WER (Word Error Rate), standardowe metryki mowy/OCR:

    CER=S+D+IN,WER=Sw+Dw+IwNw\text{CER} = \frac{S + D + I}{N}, \qquad \text{WER} = \frac{S_w + D_w + I_w}{N_w}

    gdzie SS, DD, II oznaczają odpowiednio substytucje, usunięcia i wstawienia na poziomie znaków, a NN to liczba znaków w tekście referencyjnym (z indeksem dolnym ww dla wersji słownej). Nie stosuj jednego progu CER do całego korpusu. Kalibruj go według klasy dokumentu i późniejszej utraty jakości odpowiedzi. Tekst drukowany, pismo odręczne i materiały wielojęzyczne mają różne profile błędów. Obliczaj go za pomocą jiwer (jiwer.cer(refs, hyps), jiwer.wer(refs, hyps)) lub HuggingFace evaluate. Dla korpusów ewaluacyjnych dostępne są publiczne benchmarki FUNSD i SROIE.

    from jiwer import cer, wer
    
    refs = ["Mars has two moons, Phobos and Deimos."]
    hyps = ["Mars has two m00ns, Phobos and Deirnos."]
    
    print(f"CER = {cer(refs, hyps):.3f}")  # CER = 0.105
    print(f"WER = {wer(refs, hyps):.3f}")  # WER = 0.286
  • Wierność ekstrakcji tabel: TEDS (Tree-Edit-Distance-based Similarity) mierzy podobieństwo przewidywanego drzewa tabeli HTML do drzewa referencyjnego, znormalizowane rozmiarem większego drzewa. Z Zhong et al., 2020 (PubTabNet):

    TEDS(Ta,Tb)=1EditDist(Ta,Tb)max(Ta,Tb)\text{TEDS}(T_a, T_b) = 1 - \frac{\text{EditDist}(T_a, T_b)}{\max(|T_a|, |T_b|)}

    TEDS uwzględnia zarówno strukturę (wiersze, kolumny, scalenia), jak i zawartość komórek. TEDS-S pomija zawartość i ocenia wyłącznie strukturę. Implementacja referencyjna: teds.py PubTabNet (wewnętrznie korzysta z apted). Korpusy ewaluacyjne obejmują PubTabNet, FinTabNet i SciTSR. Naiwne parsery często zawodzą na tabelach. Wykonaj benchmark, zanim im zaufasz.

  • Zachowanie układu / struktury: kolejność nagłówków, integralność list i kolejność czytania w wielokolumnowych PDF-ach. Użyj oznaczonego benchmarku DocLayNet. Porównanie „prosto z pudełka” może obejmować parser elementów, taki jak unstructured, bibliotekę PDF, taką jak pymupdf, oraz wybrany pipeline Docling. Docling udostępnia ścieżki standardową i VLM; zapisz, którą testujesz.

Porównuj różne rodziny parserów, na przykład baseline oparty na Tesseract, model OCR oparty na VLM oraz kandydata dostawcy. Użyj warstwowej próbki rzeczywistych klas dokumentów przy stałym DPI, obejmującej czyste skany, zdjęcia, tabele, tekst wielojęzyczny, matematykę i pismo odręczne. Raportuj CER lub WER dla każdej klasy oraz TEDS dla stron z tabelami.

Czyszczenie i normalizacja

  • Dokładność usuwania boilerplate’u: precision/recall względem ręcznie oznaczonych fragmentów boilerplate’u. Zbyt agresywne usuwanie usuwa istotną treść, a zbyt pobieżne zanieczyszcza embeddingi. Narzędzia do porównania: trafilatura, jusText, Resiliparse. Barbaresi (2021) porównuje Trafilatura z baseline’ami, w tym jusText; Resiliparse jest osobnym kandydatem, a nie systemem ocenianym w tej pracy.

  • Normalizacja Unicode: odsetek dokumentów generujących identyczne wyniki NFC i NFKC (obliczany za pomocą biblioteki standardowej unicodedata.normalize) jest użytecznym sygnałem driftu form kompatybilności. Nie wykrywa on niewidocznych/dom yślnie ignorowanych punktów kodowych ani podobnych znaków z różnych skryptów: te pierwsze skanuj jawnie, a w przypadku drugich stosuj politykę lub detektor Unicode confusables.

  • Dokładność detekcji języka: F1 na oznaczonej próbce wielojęzycznej. Jest kluczowa dla indeksów wielojęzycznych. Użyj fasttext-langdetect (lid.176 Facebooka), lingua-py lub cld3. FLORES-200 dostarcza tekst ewaluacyjny w 200 językach, ale to produkcyjna mieszanka językowa powinna określać wycinek testowy.

  • Skuteczność deduplikacji (MinHash / LSH): precision/recall detektora near-duplicate’ów względem ręcznie oznaczonego zbioru. Idea polega na estymacji podobieństwa Jaccarda J(A,B)=ABABJ(A, B) = \frac{|A \cap B|}{|A \cup B|} między zbiorami shingle’i dokumentów za pomocą kk hashy losowych permutacji (Broder, 1997) oraz grupowaniu near-duplicate’ów przez banding LSH (Indyk & Motwani, 1998). Przeszukaj liczbę hashy i próg Jaccarda na swoim korpusie. Śledź osobno współczynnik błędnych scaleń (psuje odpowiedzi) oraz przeoczonych scaleń (marnuje miejsce w indeksie). datasketch udostępnia implementację używaną poniżej; jej parametry są przykładowe:

    from datasketch import MinHash, MinHashLSH
    
    def shingles(text: str, k: int = 5) -> set[str]:
        text = text.lower()
        return {text[i:i + k] for i in range(len(text) - k + 1)}
    
    def to_minhash(text: str, num_perm: int = 128) -> MinHash:
        m = MinHash(num_perm=num_perm)
        for s in shingles(text):
            m.update(s.encode("utf-8"))
        return m
    
    docs = {
        "d1": "Mars has two moons, Phobos and Deimos.",
        "d2": "Mars has two moons, Phobos and Deimos!",   # near-dup
        "d3": "Curiosity rover landed on Mars in 2012.",
    }
    
    lsh = MinHashLSH(threshold=0.8, num_perm=128)
    for did, text in docs.items():
        lsh.insert(did, to_minhash(text))
    
    print(sorted(lsh.query(to_minhash(docs["d1"]))))  # ['d1', 'd2']
  • Usuwanie PII: precision i recall obliczane osobno dla każdego typu encji (e-maile, SSN, nazwiska, adresy). Błędy recall tworzą ryzyko compliance, a błędy precision pogarszają jakość odpowiedzi. Punkt operacyjny ustal wraz z zespołem prawnym. Kandydackie narzędzia obejmują Microsoft Presidio, scrubadub lub dostrojony model NER na oznaczonym zbiorze.

Chunking kontroluje jakość wyszukiwania

Chunking decyduje, które dowody dotrą do odpowiedzi. W benchmarku dostawcy NVIDIA z 2025 r. chunking na poziomie strony osiągnął najwyższą średnią dokładność odpowiedzi end-to-end w testowanej konfiguracji, która zachowywała tabele i wykresy jako całe jednostki. Mierzy to dokładność odpowiedzi, a nie recall wyszukiwania. Traktuj ten wynik jako dowód dla testowanego korpusu, a nie uniwersalnego zwycięzcę.

Chunking semantyczny grupuje sąsiednie zdania według podobieństwa embeddingów i tnie na granicach o niskim podobieństwie. SemanticChunker LangChain oraz SemanticSplitterNodeParser LlamaIndex implementują tę strategię. Może ona poprawić recall względem stałych okien, gdy istotne są granice tematyczne.

RecursiveCharacterTextSplitter LangChain domyślnie próbuje kolejno podwójnych znaków nowej linii, pojedynczych znaków nowej linii, spacji, a następnie pojedynczych znaków: ["\n\n", "\n", " ", ""]. Nie wykrywa granic zdań, chyba że skonfigurujesz odpowiednie separatory lub użyjesz splittera uwzględniającego zdania. Wybierz rozmiar okna i overlap pasujące do struktury dokumentów, a następnie porównaj je na złotym zbiorze.

Metryki do śledzenia:

  • Spójność chunków: coherence=cos(si,sj)withincos(si,sj)across boundary\text{coherence} = \overline{\cos(s_i, s_j)}_{\text{within}} - \overline{\cos(s_i, s_j)}_{\text{across boundary}}, gdzie sis_i to embeddingi zdań. Zdrowe chunki są wewnętrznie podobne, a na granicach — niepodobne. Obliczaj ją za pomocą sentence-transformers oraz scikit-learn’s cosine_similarity.
  • Jakość granic: ręcznie oznaczone „czy to sensowny punkt podziału?” na próbce oraz kontrola strukturalna, czy chunki nie dzielą tabel, list ani numerowanych sekcji.
  • Optymalny rozmiar chunka: przetestuj rozmiary tokenów (128, 256, 512, 1024) i wykreśl Recall@k względem rozmiaru na złotym zbiorze. Wybierz punkt przegięcia. Nie wybieraj rozmiaru tylko dlatego, że tak podano w tutorialu.
  • Skuteczność overlapu: usuń kilka wartości procentowych overlapu i zmierz Recall@k. Zwiększaj overlap tylko do momentu, w którym lokalna krzywa recall się wypłaszcza lub koszt duplikacji przewyższa zysk.
  • Wierność atrybucji chunka: odsetek chunków zachowujących weryfikowalny wskaźnik źródła (numer strony, anchor sekcji, ID dokumentu). Jest to wymagane do audytowalności.
  • Chunking późny a wczesny: late chunking (Günther et al., 2024) osadza cały dokument, a dopiero potem go segmentuje, zachowując globalny kontekst (implementacja referencyjna w jina-embeddings-v3). Contextual Retrieval (Anthropic, 2024) dodaje do każdego chunka kontekst wygenerowany przez LLM. Oba podejścia zwiększają koszt. Przetestuj je na własnym korpusie przed wdrożeniem.

Moja opinia: chunking strukturalny (podział według nagłówków, tabel i sekcji — implementowany przez parsery takie jak unstructured.io lub przez przejście po AST już wygenerowanym przez parser) jest niedoceniany. Jeśli dokumenty mają strukturę, wykorzystaj ją, zanim dodasz heurystyki podobieństwa. Rekurencyjne dzielenie znakowe to baseline; chunking semantyczny uzasadnia narzut głównie w przypadku nieustrukturyzowanej prozy.

Ekstrakcja i wzbogacanie metadanych

  • Precision/recall/F1 dla NER: dla każdego typu encji na oznaczonym podzbiorze. Standard CoNLL/MUC. Oblicz za pomocą seqeval (from seqeval.metrics import f1_score) dla wersji uwzględniającej tagi BIO/IOB lub scikit-learn dla porównań zbiorów spanów. CoNLL-2003 i OntoNotes 5.0 to kanoniczne korpusy referencyjne.
  • F1 ekstrakcji relacji: jeszcze ważniejsze dla systemów opartych na ontologii. Ręcznie oznacz zbiór warstwowo według typu relacji i klasy dokumentu. TACRED i DocRED to publiczne benchmarki; przykładowe implementacje obejmują pipeline’y relacji opennre i spaCy.
  • Dokładność ekstrakcji tytułów / nagłówków: exact match oraz znormalizowane podobieństwo Levenshteina (1edit_dist(a,b)max(a,b)1 - \frac{\text{edit\_dist}(a, b)}{\max(|a|, |b|)}) względem prawdy referencyjnej — python-Levenshtein lub rapidfuzz zapewniają oba wyniki w jednym wywołaniu.
  • Zachowanie hierarchicznych metadanych: odsetek chunków, które poprawnie zachowują sekcję nadrzędną, dokument nadrzędny i ścieżkę przodków. To ta metryka decyduje, czy RAG potrafi odpowiedzieć na pytania typu „co mówi element podrzędny polityki X?”.

Generowanie embeddingów

  • Benchmarki wyboru modelu: użyj wyników zadań retrieval w MTEB, BEIR dla generalizacji zero-shot oraz MIRACL dla wielojęzycznego wyszukiwania jako punktów odniesienia. Retrieval w MTEB zwykle raportuje nDCG@10; inne rodziny zadań korzystają z innych metryk. Pakiet Python MTEB uruchamia benchmarki lokalnie. Transfer wyniku z angielskiego MTEB do języka o mniejszych zasobach traktuj jako hipotezę do przetestowania na oznaczonym zbiorze tego języka.
  • Ewaluacja domenowa: nie traktuj pozycji w ogólnym benchmarku jako wyniku domenowego. Zbuduj domenowy złoty zbiór na podstawie macierzy pokrycia i poziomu niepewności, który możesz zaakceptować w decyzji. Następnie uszereguj modele kandydujące na tym zbiorze za pomocą ranx lub pytrec_eval. Zbiór domenowy może odwrócić kolejność z leaderboardu, dlatego publikuj wycinek danych, protokół wyszukiwania i przedział ufności wraz z wynikiem.
  • Wykrywanie driftu embeddingów: porównuj stałe okno referencyjne z embeddingami kroczącymi za pomocą MMD lub zwalidowanego klasyfikatora reference-versus-current. KL wymaga jawnego estymatora rozkładu prawdopodobieństwa i nie może być bezpośrednio zastosowane do surowych współrzędnych embeddingów. Mierz także stabilność nearest neighbors dla stałego zbioru sond. evidently i alibi-detect implementują detektory modelowe i statystyczne. Badanie porównawcze Evidently to jedna z ewaluacji dostawcy; porównaj metody na znanych zmianach we własnych embeddingach.
  • Wiele wektorów a jeden wektor: late interaction zachowuje reprezentacje na poziomie tokenów zamiast redukować każdy dokument do jednego wektora; ColBERT to kanoniczny projekt, z implementacjami referencyjnymi w RAGatouille i PyLate. Bogatsza reprezentacja zwiększa koszt indeksu i wyszukiwania. Przed wdrożeniem porównaj jakość, zajętość storage’u i opóźnienie z baseline’em jednego wektora na tym samym zbiorze domenowym.

Budowa indeksu

  • Recall@k przy aproksymacji: porównaj indeks approximate-nearest-neighbour (ANN) z dokładnym baseline’em brute-force przy tym samym k — w FAISS jest to IndexHNSWFlat (lub IndexIVFFlat) względem IndexFlatIP/IndexFlatL2. Ustal akceptowalną utratę recall na podstawie budżetu jakości downstream. Projekt ann-benchmarks śledzi krzywe Pareto recall–QPS dla różnych bibliotek.
  • Strojenie HNSW: HNSW (Hierarchical Navigable Small World) to warstwowy graf bliskości; zob. Malkov & Yashunin, 2018. Jest implementowany w hnswlib, IndexHNSWFlat FAISS i większości baz wektorowych. HNSW udostępnia trzy parametry: M (fan-out grafu), efConstruction (szerokość kandydatów podczas budowy) oraz efSearch (szerokość kandydatów podczas zapytania). Zacznij od wartości domyślnych udokumentowanych przez bibliotekę, a następnie przeszukaj parametry do momentu spełnienia wymagań zbioru ewaluacyjnego na krzywej recall–latency.
  • Strojenie IVF: IVF (Inverted File index — partycjonuje wektory za pomocą k-means na nlist komórek, a podczas zapytania skanuje nprobe najbliższych komórek; zob. IndexIVFFlat i IndexIVFPQ FAISS). Przeszukaj nlist i nprobe względem recall i opóźnienia wyszukiwania dokładnego. Osobno testuj zapytania filtrowane, ponieważ rodziny indeksów i bazy wektorowe różnie implementują przechodzenie po filtrach.
  • Opóźnienie świeżości aktualizacji: czas od zatwierdzenia dokumentu do jego dostępności w wyszukiwaniu. Śledź p50 i p99. W systemach objętych wymaganiami regulacyjnymi śledź także odsetek zapytań obsłużonych na nieaktualnych indeksach.

Część 4: Ewaluacja w czasie zapytania

Ścieżka czasu zapytania zawiera metryki diagnozujące drogę wyszukiwania. Sam Recall@k nie pokaże, czy przyczyną awarii było przepisywanie, filtrowanie, reranking czy składanie kontekstu.

Rozumienie i przepisywanie zapytań

  • Jakość rozszerzania zapytania: uplift Recall@k na złotym zbiorze, dla zapytania rozszerzonego względem surowego. Przed testem zdefiniuj minimalny użyteczny zysk i jego niepewność. Jeśli rozszerzenie nie spełnia tego wymogu, nie uzasadnia kosztu ani opóźnienia. Klasyczne baseline’y PRF (pseudo-relevance feedback), takie jak RM3 i Bo1, nadal są użytecznymi kontrolami zdroworozsądkowymi; rozszerzanie oparte na LLM powinno je przewyższać.
  • Ewaluacja HyDE: HyDE (Gao et al., 2022) generuje hipotetyczną odpowiedź za pomocą LLM, osadza ją i wyszukuje względem niej. Dodaje opóźnienie generowania i nową powierzchnię awarii. Mierz Recall@10 osobno dla wycinków in-domain, out-of-domain i o niskiej pewności, a następnie zdecyduj, czy HyDE powinno znaleźć się na domyślnej ścieżce, jako fallback czy nigdzie.
  • Generowanie zapytań multi-query: suma Recall@k dla N przepisań względem pojedynczego zapytania. Przeszukaj N i wybierz punkt na froncie recall–latency. Implementacje: MultiQueryRetriever LangChain i QueryFusionRetriever LlamaIndex.
  • Dokładność klasyfikacji intencji: standardowe precision/recall/F1 dla każdej intencji (obliczane za pomocą sklearn.metrics.classification_report), ale operacyjnie najważniejsza jest poprawność routingu — czy wywoływany jest właściwy pipeline downstream?
  • Routing adaptacyjny: Adaptive-RAG (Jeong et al., NAACL 2024) pokazuje, że nie każde zapytanie wymaga tej samej strategii wyszukiwania. Śledź dokładność routera jako problem klasyfikacji względem oznaczonego zbioru „bez wyszukiwania / one-shot / iteracyjnie”.

Wyszukiwanie iteracyjne wymaga budżetowego testu end-to-end

Agent może wyszukać wynik, przeanalizować go i wyszukać ponownie zamiast pobierać jedną stałą listę top-k. Aktualna domena wiedzy tau3 udostępnia konfigurowalne wyszukiwanie RAG i agentic shell-based search, dzięki czemu jest to konkretna ścieżka ewaluacji, a nie tylko szkic architektury. Do lokalnego porównania zapewnij wyszukiwaniu one-shot i iteracyjnemu ten sam kwalifikujący korpus oraz jawne limity czasu, tokenów modelu i wywołań narzędzi. Rejestruj każde zapytanie i dowody widziane w każdej turze; oceniaj końcowe pokrycie dowodów, poprawność odpowiedzi, wsparcie cytowaniami i awarie po wyczerpaniu budżetu. Większa liczba wywołań wyszukiwania jest użyteczna tylko wtedy, gdy dodatkowe dowody poprawiają odpowiedź w ramach tych limitów.

Metryki wyszukiwania

To metryki bazowe. Jeśli ich nie śledzisz, nie możesz stwierdzić, czy wyszukiwanie się poprawia.

MetrykaCo mierzyKiedy stosować
Recall@kułamek trafnych dokumentów zapytania zwróconych w top kgdy pominięcie dowolnej części zbioru trafnych dokumentów ma znaczenie
Precision@kodsetek trafnych dokumentów w top-kgdy wąskim gardłem jest okno kontekstu
MRRśrednią z 1/rankingu pierwszego trafnego dokumentugdy użytkownicy patrzą tylko na top-1 lub top-3
nDCG@kzysk ważony trafnością i dyskontem pozycjistandardowa metryka wyszukiwania dla stopniowanej trafności
MAPśrednią po zapytaniach z average precisiongdy interesuje Cię cała lista rankingowa
Hit Rate@kczy co najmniej jeden trafny dokument znajduje się w top kuśredniaj wynik binarny po zapytaniach jako szybką kontrolę
Coverageodsetek dokumentów gold kiedykolwiek zwróconych dla wszystkich zapytańwykrywa systemowe luki w indeksie

Wzory, dla odniesienia (trafność binarna ze zbiorem trafnych RqR_q dla zapytania qq oraz reli=1\text{rel}_i = 1, jeśli ii-ty zwrócony dokument należy do RqR_q):

Recall@k=Rq{d1,,dk}Rq,Precision@k=Rq{d1,,dk}k\text{Recall@k} = \frac{|R_q \cap \{d_1, \dots, d_k\}|}{|R_q|}, \quad \text{Precision@k} = \frac{|R_q \cap \{d_1, \dots, d_k\}|}{k} RRq=1rank of first relevant doc,MRR=1QqQRRq\text{RR}_q = \frac{1}{\text{rank of first relevant doc}}, \quad \text{MRR} = \frac{1}{|Q|} \sum_{q \in Q} \text{RR}_q DCG@k=i=1k2reli1log2(i+1),nDCG@k=DCG@kIDCG@k\text{DCG@k} = \sum_{i=1}^{k} \frac{2^{\text{rel}_i} - 1}{\log_2(i + 1)}, \quad \text{nDCG@k} = \frac{\text{DCG@k}}{\text{IDCG@k}}

Dla stopniowanej trafności reli{0,1,2,}\text{rel}_i \in \{0, 1, 2, \dots\}; binarne nDCG jest szczególnym przypadkiem używanym w kodzie poniżej. MAP to średnia po zapytaniach z APq=1Rqi:reli=1Precision@i\text{AP}_q = \frac{1}{|R_q|}\sum_{i: \text{rel}_i = 1} \text{Precision@}i. Wyprowadzenia znajdziesz w rozdziale 8 książki Manning, Raghavan, Schütze, Introduction to Information Retrieval.

W kodzie produkcyjnym użyj ranx, pytrec_eval lub ir_measures — implementują całą rodzinę metryk TREC i poprawnie obsługują stopniowaną trafność. Ustal cele wydaniowe na podstawie realistycznego złotego zbioru, downstreamowej jakości odpowiedzi i kosztu pominięcia. Nie dziedzicz progów z tutorialu.

W tym materiale k oznacza unikalne ID dokumentów. Odrzucaj duplikaty, zamiast przyznawać powtórzonemu dokumentowi dodatkowy zysk. W eksperymentach z chunkingiem określ, czy k liczy chunki, czy deduplikowane dokumenty nadrzędne, i porównuj także dowody dostarczone w ramach stałego budżetu tokenów. Ten sam recall dokumentów może ukrywać bardzo różną jakość kontekstu.

Harness testowy dla tych metryk jest krótki. Możesz uruchomić go w notatniku, zanim wybierzesz bazę wektorową.

from math import log2
from statistics import mean

# synthetic gold set: query_id -> set of relevant doc ids
gold = {
    "q1": {"d3"},
    "q2": {"d7", "d2"},
    "q3": {"d11"},
    "q4": {"d5"},
}

# ranked retrieval results: query_id -> ranked list of doc ids (top-10)
runs = {
    "q1": ["d8", "d3", "d1", "d4", "d2", "d9", "d6", "d10", "d12", "d13"],
    "q2": ["d2", "d6", "d4", "d7", "d1", "d3", "d8", "d11", "d5", "d9"],
    "q3": ["d11", "d2", "d3", "d4", "d1", "d6", "d7", "d8", "d10", "d12"],
    "q4": ["d1", "d2", "d3", "d6", "d8", "d9", "d10", "d12", "d13", "d14"],
}

def recall_at_k(ranked, gold_set, k):
    if k <= 0 or len(ranked) != len(set(ranked)):
        raise ValueError("require positive k and unique document IDs")
    if not gold_set:
        return 0.0
    hit = sum(1 for d in ranked[:k] if d in gold_set)
    return hit / len(gold_set)

def reciprocal_rank(ranked, gold_set):
    if len(ranked) != len(set(ranked)):
        raise ValueError("require unique document IDs")
    # MRR contribution per query: 1/rank of the first relevant doc.
    for rank, d in enumerate(ranked, start=1):
        if d in gold_set:
            return 1.0 / rank
    return 0.0

def ndcg_at_k(ranked, gold_set, k):
    if k <= 0 or len(ranked) != len(set(ranked)):
        raise ValueError("require positive k and unique document IDs")
    # binary relevance: rel ∈ {0, 1}
    gains = [1.0 if d in gold_set else 0.0 for d in ranked[:k]]
    dcg = sum(g / log2(i + 2) for i, g in enumerate(gains))
    # ideal DCG: all gold docs ranked first, capped by k
    n_gold_in_topk = min(k, len(gold_set))
    idcg = sum(1.0 / log2(i + 2) for i in range(n_gold_in_topk))
    return dcg / idcg if idcg else 0.0

K = 5
print(f"Recall@{K}: {mean(recall_at_k(runs.get(q, []), gold[q], K) for q in gold):.3f}")
print(f"MRR:       {mean(reciprocal_rank(runs.get(q, []), gold[q]) for q in gold):.3f}")
print(f"nDCG@{K}:  {mean(ndcg_at_k(runs.get(q, []), gold[q], K) for q in gold):.3f}")
# Recall@5: 0.750
# MRR:       0.625
# nDCG@5:    0.627
assert recall_at_k([], {"d1"}, 5) == 0.0
for score in (lambda ids: recall_at_k(ids, {"d1"}, 2),
              lambda ids: reciprocal_rank(ids, {"d1"}),
              lambda ids: ndcg_at_k(ids, {"d1"}, 2)):
    try:
        score(["d1", "d1"])
    except ValueError:
        pass
    else:
        raise AssertionError("duplicate IDs must fail")

Agregacja iteruje po wszystkich oczekiwanych zapytaniach, traktując brakujący run jako pusty. Ten przykład przypisuje zero zapytaniom z pustym zbiorem gold; w rzeczywistym zestawie oznacz je jako osobny wycinek answerability z własnym mianownikiem, zamiast traktować zero jako zmierzony recall. Raportuj timeouty i brakujące runy wraz z wynikami.

Uruchamiaj szybki podzbiór oparty na pokryciu przy każdym PR, a pełny złoty zbiór przed wydaniem. Blokuj merge, gdy prerejestrowana metryka przekroczy budżet regresji.

Repozytorium towarzyszące przypina dokładne liczby powyżej (Recall@5 = 0.750, MRR = 0.625, nDCG@5 = 0.627) jako test jednostkowy w tests/test_retrieval_metrics.py; notebook 01 przeszukuje Recall@k / MRR / nDCG na rzeczywistym indeksie SciFact, a harness o kształcie produkcyjnym znajduje się w evaluation/retrieval.py.

Wyszukiwanie hybrydowe i reciprocal rank fusion

BM25 to rzadki scorer leksykalny łączący dopasowanie dokładnych terminów, ważenie terminów i normalizację długości. Jest dostępny w rank_bm25, Elasticsearch, OpenSearch i większości silników wyszukiwania.

Reciprocal Rank Fusion (Cormack, Clarke i Buettcher, SIGIR 2009) łączy rankingi BM25 i dense według pozycji. Oryginalne ustawienie k=60 jest użytecznym baseline’em. RRF nie zależy od score’ów, dzięki czemu unika normalizacji między ścieżkami wymaganej przy interpolacji liniowej. Przy dostatecznie dużym oznaczonym zbiorze, pozwalającym oszacować stabilną różnicę, przetestuj także kombinację wypukłą i dostrój α.

Moja hipoteza mówi, że wyszukiwanie hybrydowe wraz z rerankerem cross-encoder może pomóc w korpusach technicznych, logowych i kodowych. Zysk może być niewielki w korpusach silnie semantycznych. Porównuj wyniki ze ścieżką wyłącznie dense i wyłącznie sparse, ponieważ słaba konfiguracja fuzji może działać gorzej niż którekolwiek z wejść. Towarzyszący notebook SciFact to jeden ograniczony test, a nie wynik ogólny.

Implementacja mieści się w kilku liniach.

from collections import defaultdict

# two retrieval lanes: dense embeddings and BM25.
dense  = ["d3", "d7", "d1", "d4", "d2", "d9", "d10"]
sparse = ["d2", "d3", "d8", "d1", "d11", "d4", "d6"]

def rrf(rankings: list[list[str]], k: int = 60) -> list[tuple[str, float]]:
    """Reciprocal Rank Fusion (Cormack et al., SIGIR 2009).

    score(d) = sum over rankings of 1 / (k + rank(d))
    Score-agnostic: only rank position matters. k=60 is the canonical default.
    """
    scores: dict[str, float] = defaultdict(float)
    if k <= 0:
        raise ValueError("k must be positive")
    for ranking in rankings:
        if len(ranking) != len(set(ranking)):
            raise ValueError("each ranking must contain unique document IDs")
        for rank, doc in enumerate(ranking, start=1):
            scores[doc] += 1.0 / (k + rank)
    return sorted(scores.items(), key=lambda kv: kv[1], reverse=True)

fused = rrf([dense, sparse], k=60)
for doc, score in fused[:5]:
    print(f"{doc}  score={score:.5f}")
# d3  score=0.03252   <- rank 1 dense, rank 2 sparse
# d2  score=0.03178   <- rank 5 dense, rank 1 sparse
# d1  score=0.03150
assert [doc for doc, _ in fused[:3]] == ["d3", "d2", "d1"]
try:
    rrf([["d1", "d1"]])
except ValueError:
    pass
else:
    raise AssertionError("one lane must not vote twice for a document")

Zwróć uwagę, czego RRF nie robi: nigdy nie analizuje surowych score’ów podobieństwa. Retriever dense zwracający cosine 0,98 i ścieżka BM25 zwracająca score 17,4 nie są bezpośrednio porównywalne. Normalizacja z-score i min-max usuwa afiniczne różnice skali, ale żadna z nich nie kalibruje score’a jako trafności. Wartości znormalizowane nadal zależą od wartości odstających, kształtu rozkładu i zbioru kandydatów, a więc wpływają na wynik fuzji. Każdą kombinację opartą na score’ach waliduj na oznaczonych zapytaniach.

RRF korzysta wyłącznie z rangi. Jeśli retriever umieszcza dokument na pozycji 2, głos ma wartość 1 / (60 + 2), niezależnie od surowego score’a, który doprowadził do tej pozycji.

Hybrid + RRF na SciFact: notebook 02 porównuje dense, BM25 i RRF z deltami dla poszczególnych zapytań. Fuser o kształcie produkcyjnym znajduje się w retrieval/hybrid_rrf.py; tests/test_rrf.py przypina kanoniczną kolejność d3 / d2 / d1 przy k=60.

Reranking

  • ΔnDCG / ΔMRR: uplift względem braku rerankingu na złotym zbiorze, na głębokości faktycznie używanej przez aplikację. Obliczaj go, uruchamiając metryki wyszukiwania z rerankerem i bez niego na identycznych zbiorach kandydatów.
  • Cross-encoder a bi-encoder: bi-encoder osadza zapytanie i dokument niezależnie (po jednym wektorze na stronę) i ocenia je iloczynem skalarnym; cross-encoder konkaten uje zapytanie i dokument oraz wykonuje pojedynczy forward pass, który wspólnie uwzględnia oba elementy. Cross-encodery wymieniają forward pass dla każdego kandydata na bogatszą interakcję zapytanie–dokument. Implementacja referencyjna: sentence-transformers CrossEncoder. Benchmarkuj trafność i opóźnienie na nazwanym sprzęcie, dla określonego batch size i głębokości kandydatów; nie przenoś wyniku jednego modelu lub zarządzanej usługi do innego środowiska.
  • Listwise a pointwise: pointwise ocenia każdą parę (zapytanie, dokument) niezależnie; listwise ocenia całą listę kandydatów wspólnie, aby model mógł je porównać. Oceniaj oba podejścia na tych samych zbiorach kandydatów. Kalibruj próg score’a osobno dla modelu i korpusu, zamiast traktować opublikowany przykład jako przenośny.
from sentence_transformers import CrossEncoder

reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")  # Reproducible baseline, not a latest-model ranking

query = "How do I rotate database credentials in production?"
candidates = [
    "Production database credentials are rotated via Vault every 30 days.",
    "The new logo was unveiled at the all-hands meeting.",
    "To rotate prod DB creds, run the `rotate-secrets` GitHub Action.",
]

scores = reranker.predict([(query, c) for c in candidates])
ranked = sorted(zip(candidates, scores), key=lambda x: -x[1])
for doc, score in ranked:
    print(f"{score:+.3f}  {doc}")

Powyższy kod BGE to mały baseline. W aktualnym porównaniu uwzględnij Cohere Rerank 4.0 fast/pro do zarządzanego, wielojęzycznego rankingu tekstu lub Qwen3-VL-Reranker 2B/8B, gdy zapytania lub dokumenty zawierają obrazy, screenshoty albo wideo. Jawnie określ modalności zadania, głębokość kandydatów, limity wejścia, instrukcje i sprzęt. Ogólny checkpoint generacyjny Qwen3.8 nie jest tym samym modelem co wyspecjalizowany reranker Qwen3-VL.

Reranker często pomaga podstawowemu pipeline’owi RAG, ale nie gwarantuje poprawy. Zmierz jego ΔPrecision@1 i ΔnDCG na złotym zbiorze, a następnie zachowaj go tylko wtedy, gdy zysk przekracza budżet opóźnienia i kosztu. Porównaj ten zmierzony zysk z mniejszymi zmianami w wyszukiwaniu, zanim wybierzesz kolejną optymalizację.

ΔnDCG i ΔPrecision@1 dla cross-encodera na SciFact: notebook 03; moduł: retrieval/reranker.py.

Budowa kontekstu i lost-in-the-middle

Wiele awarii typu „dobre wyszukiwanie, zła odpowiedź” zaczyna się podczas budowania kontekstu.

  • Trafność kontekstu: Ragas ContextRelevance ocenia dostarczoną listę kontekstu za pomocą dwóch promptów i znormalizowanych ocen; nie jest to wynik dla pojedynczego chunka. Do diagnostyki chunków oceniaj jawnie każdą parę zapytanie–chunk, na przykład cross-encoderem, i raportuj rozkład względem lokalnie skalibrowanego progu.
  • Pokrycie cytowaniami dostarczonego kontekstu: liczba różnych dostarczonych chunków, do których są cytowania, podzielona przez liczbę różnych dostarczonych chunków. Przypadki pustego kontekstu raportuj osobno. Ten obserwowalny proxy mówi, które chunki otrzymały cytowania, a nie których model użył wewnętrznie ani czy cytowania wspierają jego twierdzenia. Sprawdzaj wsparcie cytowaniami osobno i porównuj pokrycie z jakością odpowiedzi oraz kosztem tokenów.
  • Wykrywanie lost-in-the-middle: syntetyczna ewaluacja, w której złoty chunk umieszczasz na pozycjach {first, middle, last} długiego kontekstu i mierzysz poprawność odpowiedzi. Cytowane badanie Liu et al. (TACL 2024) raportuje degradację w kształcie litery U w swoich warunkach długiego kontekstu. Ten sam wzorzec w aktualnym modelu traktuj jako hipotezę do przetestowania. Możliwe ograniczenia: najpierw wykonaj reranking, a następnie uporządkuj top-k tak, aby najwyżej oceniony chunk znalazł się na początku lub na końcu (LongContextReorder LangChain robi dokładnie to) albo agresywnie skompresuj chunki środkowe. Mierz wynik za pomocą ewaluacji warstwowej względem pozycji, a nie tylko agregatu. Działająca ewaluacja warstwowa względem pozycji znajduje się w notebook 06 (moduł: evaluation/lost_in_middle.py).
  • Kompresja kontekstu: raportuj współczynnik kompresji (tokeny wejściowe / tokeny wyjściowe) wraz z poprawnością odpowiedzi. Narzędzia obejmują ContextualCompressionRetriever LangChain i LongLLMLingua. Z góry określ największą akceptowalną utratę poprawności wynikającą z ryzyka aplikacji i budżetu tokenów, a następnie odrzucaj konfiguracje, które ją przekraczają.

Część 5: Współczynnik fałszywego wykluczenia przez filtr

Ta metryka ma własną sekcję, ponieważ zagregowane wyniki wyszukiwania nie potrafią przypisać pominięcia filtrowi trafności. Oceniaj filtry kwalifikujące osobno względem uprawnień wywołującego; dokument spoza tego zbioru musi pozostać wykluczony.

Twardy predykat trafności, taki jak product = Y AND locale = en-US, może obniżyć efektywny recall do zera wśród dokumentów, które wywołujący może zobaczyć. Poprawnie zaimplementowany Recall@k wykrywa stratę, ponieważ jego mianownik pozostaje oryginalnym zbiorem trafnych dokumentów, do których użytkownik ma dostęp. Nie mówi jednak, czy przyczyną pominięcia był filtr, retriever czy ranker. Faithfulness ocenia twierdzenia względem pobranego kontekstu. Może nadal przyznać wynik twierdzeniom wspartym przez ten niepełny kontekst, ale nie potrafi zdiagnozować przyczyny związanej z filtrem lub wyszukiwaniem. Pusta odmowa może nie zawierać stwierdzeń i zwracać NaN, zależnie od implementacji; nie traktuj tego jako dowodu, że faithfulness zaakceptowało odmowę.

Wyróżniona gałąź to typowa awaria: właściwy dokument istnieje, ale filtr usuwa go przed wyszukiwaniem. Recall@k rejestruje spadek; tylko współczynnik wykluczeń przypisuje go predykatowi.

Ciche awarie RAG zmapowane od korpusu źródłowego przez filtrowanie, ranking i generowanie do metryki identyfikującej każde źródłoCiche awarie RAG zmapowane od korpusu źródłowego przez filtrowanie, ranking i generowanie do metryki identyfikującej każde źródło

Metryka

filter_false_exclusion_rate =
    (# queries where all gold docs were excluded by metadata filter) /
    (# queries with at least one entitlement-eligible gold doc)

Ta definicja na poziomie zapytania zlicza katastroficzne wykluczenia: żaden kwalifikujący się trafny dokument nie przetrwał. Przed oceną przetnij zbiór gold każdego zapytania ze zbiorem uprawnień wywołującego; dokument niekwalifikujący się nie jest fałszywym wykluczeniem. Dla zapytań z wieloma dokumentami gold standardowy Recall@k nadal ujawnia częściową stratę; jeśli ta granica ma znaczenie, dodaj współczynnik wykluczeń na poziomie dokumentu. Do obliczenia obu współczynników potrzebujesz (a) prawdziwych ID dokumentów dla każdego zapytania ewaluacyjnego oraz (b) instrumentacji logującej zastosowane predykaty filtrów, a nie tylko końcowe wyniki. Ustal cel na podstawie kosztu wykluczenia poprawnej odpowiedzi i przedziału ufności próbki produkcyjnej.

Poniżej znajduje się działająca implementacja. Porównuje poprawny standardowy recall z niepoprawnym ewaluatorem, który redefiniuje trafność po filtrowaniu.

# A small worked example where relevance predicates remove eligible documents.
docs = [
    {"id": "d1", "tenant": "acme",   "locale": "en-US"},
    {"id": "d2", "tenant": "acme",   "locale": "en-GB"},
    {"id": "d3", "tenant": "globex", "locale": "en-US"},
    {"id": "d4", "tenant": "acme",   "locale": "en-US"},
    {"id": "d5", "tenant": "acme",   "locale": "de-DE"},
    {"id": "d6", "tenant": "acme",   "locale": "fr-FR"},
]

# Tenant isolation is an eligibility invariant, not a relevance experiment.
# Verify it independently before evaluating relevance preferences.
eligible_docs = [d for d in docs if d["tenant"] == "acme"]
assert {d["id"] for d in eligible_docs} == {"d1", "d2", "d4", "d5", "d6"}

queries = [
    # the gold doc lives in en-GB but the dynamic filter forced en-US
    {"qid": "q1", "gold": {"d2"}, "filter": lambda d: d["locale"] == "en-US"},
    # the gold doc is correctly within the tenant filter
    {"qid": "q2", "gold": {"d4"}, "filter": lambda d: d["tenant"] == "acme"},
    # the gold doc lives in fr-FR but the dynamic filter forced en-US
    {"qid": "q3", "gold": {"d6"}, "filter": lambda d: d["locale"] == "en-US"},
    # the gold doc passes the filter (de-DE locale match)
    {"qid": "q4", "gold": {"d5"}, "filter": lambda d: d["locale"] == "de-DE"},
]

def filter_false_exclusion_rate(queries, eligible_docs):
    n_with_gold, n_excluded = 0, 0
    eligible_ids = {d["id"] for d in eligible_docs}
    for q in queries:
        eligible_gold = q["gold"] & eligible_ids
        if not eligible_gold:
            continue
        n_with_gold += 1
        survivors = {d["id"] for d in eligible_docs if q["filter"](d)}
        if not (eligible_gold & survivors):
            n_excluded += 1
    if not n_with_gold:
        raise ValueError("false-exclusion rate needs queries with eligible gold")
    return n_excluded / n_with_gold, n_with_gold

rate, eligible_query_count = filter_false_exclusion_rate(queries, eligible_docs)
print(f"filter_false_exclusion_rate = {rate:.2%}")
# filter_false_exclusion_rate = 50.00%
print(f"eligible queries = {eligible_query_count} / {len(queries)}")
# eligible queries = 4 / 4

# Correct Recall@k keeps the original eligible gold set as its denominator.
def standard_recall_at_k(queries, eligible_docs, k=10):
    recalls = []
    eligible_ids = {d["id"] for d in eligible_docs}
    for q in queries:
        eligible_gold = q["gold"] & eligible_ids
        if not eligible_gold:
            continue
        # demo only: the survivor set stands in for a ranked run.
        # A real harness ranks the survivors first, then slices to k.
        survivors = [d for d in eligible_docs if q["filter"](d)][:k]
        survivor_ids = {d["id"] for d in survivors}
        recalls.append(len(eligible_gold & survivor_ids) / len(eligible_gold))
    return sum(recalls) / len(recalls) if recalls else 0.0

print(f"standard recall@10 = {standard_recall_at_k(queries, eligible_docs):.2%}")
# standard recall@10 = 50.00%

# INVALID: rebuilding the gold set after filtering changes the question.
# It drops queries whose relevant documents did not survive, then scores 100%.
def invalid_recall_over_filtered_gold(queries, eligible_docs, k=10):
    recalls = []
    all_doc_ids = {d["id"] for d in eligible_docs}
    for q in queries:
        all_survivors = {d["id"] for d in eligible_docs if q["filter"](d)}
        filtered_gold = q["gold"] & all_doc_ids & all_survivors
        if not filtered_gold:
            continue
        top_k_ids = set(list(all_survivors)[:k])
        recalls.append(len(filtered_gold & top_k_ids) / len(filtered_gold))
    return sum(recalls) / len(recalls) if recalls else 0.0

invalid = invalid_recall_over_filtered_gold(queries, eligible_docs)
print(f"INVALID recall (filtered gold) = {invalid:.2%}")
# INVALID recall (filtered gold) = 100.00%

assert rate == 0.5
assert standard_recall_at_k(queries, eligible_docs) == 0.5
assert invalid == 1.0

# An unauthorized-only gold document belongs in an entitlement test, not this metric.
unauthorized_only = [
    {"qid": "q5", "gold": {"d3"}, "filter": lambda d: d["locale"] == "en-US"},
]
assert eligible_query_count == 4
assert filter_false_exclusion_rate(queries + unauthorized_only, eligible_docs) == (rate, 4)
for unscorable in ([], unauthorized_only):
    try:
        filter_false_exclusion_rate(unscorable, eligible_docs)
    except ValueError:
        pass
    else:
        raise AssertionError("an unmeasured exclusion rate must not pass as zero")

Funkcja zwraca współczynnik oraz liczbę zapytań z kwalifikującym się gold. Jeśli mianownik jest pusty, zgłasza wyjątek zamiast raportować uspokajające zero; wymagany check wydaniowy powinien traktować to jako brak dowodu. Zapytania, dla których wszystkie dokumenty gold są nieautoryzowane, pozostają w osobnych testach uprawnień.

Połowa zapytań traci dokument gold przez filtr, więc poprawny Recall@10 spada do 50%. Ten wynik wykrywa objaw, ale nie potrafi wskazać przyczyny. Współczynnik fałszywego wykluczenia pokazuje, że predykat usunął dwie odpowiedzi, zanim uruchomiono retriever. Celowo niepoprawny ewaluator raportuje 100% tylko dlatego, że usuwa te awarie ze swojego zbioru gold. Żaden model nie odzyska dokumentu odfiltrowanego wcześniej.

Wynik 50% powyżej jest odtworzony jako test jednostkowy w repozytorium towarzyszącym: tests/test_filter_exclusion.py::test_50_percent_exclusion_rate. Notebook 04 uruchamia go na SciFact z syntetycznymi metadanymi, aby można było zobaczyć, jak rzeczywisty filtr zeruje recall; metryka runtime (z towarzyszącym precision/recall predykatu) znajduje się w evaluation/filter_exclusion.py.

Metryka towarzysząca: precision i recall predykatu

Gdy filtrowanie jest dynamiczne (na przykład LLM wyodrębnia predykaty filtrów z zapytania), traktuj ekstraktor predykatów jak model klasyfikacyjny i oceniaj go w ten sposób. Mierz precision i recall predykatów względem oznaczonego zbioru par (query, correct predicate). Współczynnik błędów predykatu nie przekłada się bezpośrednio na taki sam ubytek recall wyszukiwania; zmierz, jak często błędy te wykluczają dokument gold. Gdy twardy filtr usunie dokument gold, żaden reranking już nie pomoże.

Filtry kwalifikujące a preferencje trafności

Autoryzacja, izolacja tenantów, jurysdykcja prawna i stan publikacji określają, czy dokument może wejść do zbioru kandydatów. Zachowaj je jako twarde filtry i waliduj niezależnie; Recall@k, współczynnik fałszywego wykluczenia i precision trafności nie upoważniają do ich rozluźnienia.

Dla preferencji trafności, takich jak locale, świeżość lub wersja, porównaj twardy predykat z miękkim boostem na tych samych, odłożonych zapytaniach kwalifikujących się:

For each relevance preference F:
  hard_recall_F  = retrieval_recall@k with F as a hard filter
  soft_recall_F  = retrieval_recall@k with F as a +0.X rerank boost
  hard_precision = relevant_in_top_k / k under hard filter
  soft_precision = relevant_in_top_k / k under soft boost
  precision_gain = hard_precision - soft_precision
  precision_gain_CI = paired-query uncertainty interval for precision_gain
  exclusion_rate = % of queries where the gold doc was filtered out (hard)

Pre-register Δ_min, ε, and recall_loss_max for this workflow.
Use a hard predicate only if:
  lower_bound(precision_gain_CI) >= Δ_min
  AND exclusion_rate <= ε
  AND hard_recall - soft_recall >= -recall_loss_max.
Otherwise prefer a soft boost.

Wybierz minimalny użyteczny zysk precision, przedział niepewności, ε oraz limit utraty recall na podstawie szkody wynikającej z wykluczenia kwalifikującej się odpowiedzi, korzyści z większego precision i rozmiaru odłożonej próbki. Są to lokalne kryteria wydaniowe, a nie uniwersalne progi. Osobny artykuł o tym kompromisie jest planowany; zobacz zapowiedzi na końcu.


Część 6: Ewaluacja generowania

Metryki wyszukiwania mówią, że system mógłby odpowiedzieć poprawnie. Nie mówią, że odpowiedział. Metryki generowania wypełniają tę lukę.

Faithfulness i groundedness

Faithfulness w RAGAS rozkłada odpowiedź na atomowe twierdzenia (krótkie, samodzielne stwierdzenia faktów), a następnie weryfikuje każde z nich względem pobranego kontekstu za pomocą LLM judge’a:

faithfulness=claims supported by contexttotal claims\text{faithfulness} = \frac{|\text{claims supported by context}|}{|\text{total claims}|}

Faithfulness sprawdza wsparcie w dostarczonych dowodach; nie ustala, czy dowody są poprawne lub wystarczające do odpowiedzi na pytanie. Odpowiedzi bez twierdzeń rejestruj osobno i raportuj ich liczbę, zamiast przypisywać pustej odpowiedzi idealną faithfulness.

Aktualna dokumentacja Ragas zaleca poniżej API collections. W projekcie uv zainstaluj ragas i openai za pomocą uv add ragas openai, ustaw OPENAI_API_KEY i zapisz to jako skrypt uruchamiany za pomocą uv run. Przykład wykonuje wywołania do dostawcy i generuje opłaty; wynik jest rezultatem judge’a, a nie deterministyczną stałą oczekiwaną. Przypnij rozwiązane zależności w lockfile.

import asyncio

from openai import AsyncOpenAI, omit
from ragas.llms import llm_factory
from ragas.metrics.collections import Faithfulness

async def main():
    async with AsyncOpenAI() as client:
        llm = llm_factory(
            "gpt-5.6-terra", client=client, reasoning_effort="none",
            max_tokens=omit, max_completion_tokens=1024,
            temperature=omit, top_p=omit,
        )
        scorer = Faithfulness(llm=llm)
        result = await scorer.ascore(
            user_input="How many moons does Mars have?",
            response="Mars has two moons, Phobos and Deimos.",
            retrieved_contexts=["Mars has two moons named Phobos and Deimos."],
        )
        print(result.value)

if __name__ == "__main__":
    asyncio.run(main())

Przykład używa GPT-5.6 Terra jako aktualnego kandydata ze structured outputs; fabryka Ragas przekazuje argumenty modelu. Ragas 0.4.3 nie rozpoznaje kropkowanych nazw modeli GPT w swoim mapperze limitów tokenów. Przykład jawnie pomija starsze max_tokens i domyślne parametry próbkowania oraz podaje max_completion_tokens; przy aktualizacji adaptera sprawdź wysyłane żądanie. Wyłączenie reasoning sprawia, że konfiguracja jest jawna, ale jej nie waliduje. Porównaj fałszywe akceptacje, fałszywe odrzucenia i koszt judge’a z etykietami ludzkimi, zanim zastąpisz nim tańszego skalibrowanego judge’a.

Poniżej ten sam loop rozwinięty z deterministycznym judge’em zastępczym, aby pokazać kształt całego przepływu.

def extract_claims(answer: str) -> list[str]:
    # Production: an LLM call that decomposes the answer.
    # Demo: split on sentence-final punctuation.
    return [c.strip() for c in answer.replace("?", ".").replace("!", ".").split(".") if c.strip()]

def verify_claim(claim: str, context: str) -> bool:
    # Production: an NLI (natural-language inference) model or LLM judge.
    # Demo: a deterministic stand-in so the example runs offline.
    entailed_pairs = {
        "Mars has two moons": True,
        "Phobos and Deimos orbit Mars": True,
        "Mars has a thick atmosphere": False,  # unsupported by context
        "Curiosity landed in 2012": True,
    }
    for k, v in entailed_pairs.items():
        if k.lower() in claim.lower() or claim.lower() in k.lower():
            return v
    words = [w.lower() for w in claim.split() if len(w) > 3]
    return all(w in context.lower() for w in words) if words else False

context = (
    "Mars has two moons, Phobos and Deimos. NASA's Curiosity rover "
    "landed on Mars in 2012."
)
answer = (
    "Mars has two moons. Phobos and Deimos orbit Mars. "
    "Mars has a thick atmosphere. Curiosity landed in 2012."
)

claims = extract_claims(answer)
verdicts = [(c, verify_claim(c, context)) for c in claims]
faithfulness = sum(1 for _, ok in verdicts if ok) / len(verdicts) if verdicts else None
for c, ok in verdicts:
    print(f"  [{'✓' if ok else '✗'}] {c}")
print(f"faithfulness = {faithfulness:.2f}" if faithfulness is not None else "no_claims")
# faithfulness = 0.75   (one unsupported claim about the atmosphere)
assert faithfulness == 0.75
assert extract_claims("") == []

Struktura ma znaczenie. W produkcji verify_claim staje się modelem NLI lub wywołaniem LLM. Zachowaj strukturę extract–verify–aggregate, ale waliduj ekstrakcję i entailment osobno oraz rejestruj przypadki bez twierdzeń i nieudane oceny. Stand-in offline powyżej jest zahardkodowany do tych przykładów; nie jest detektorem faktyczności.

End-to-end extraction + verification twierdzeń dla wygenerowanych odpowiedzi SciFact: notebook 05; moduł: evaluation/faithfulness.py. Repozytorium uruchamia ten sam loop przez dwie rodziny judge’ów — własny model generatora i judge’a z innej rodziny (RAG_EVALS_JUDGE_MODEL) — oraz deterministyczny baseline leksykalny, dzięki czemu można zobaczyć, gdzie rodziny się rozchodzą.

Celowaną alternatywą dla LLM-as-judge jest HHEM-2.1-Open (Hughes Hallucination Evaluation Model, Vectara), klasyfikator dostrojony do wykrywania halucynacji. Karta modelu dokumentuje checkpoint, surowy wynik 0–1 i wyniki balanced accuracy na AggreFact oraz RAGTruth. Nie publikuje domyślnej granicy decyzyjnej, więc trzeba wybrać ją samodzielnie. Traktuj te dane jako dowód z karty modelu, a nie gwarancję dla własnego korpusu: skalibruj próg na lokalnych etykietach i porównaj z wybranym judge’em przed wdrożeniem.

Ewaluacja atomowych faktów

FActScore (Min et al., EMNLP 2023) rozkłada długie generacje na atomowe fakty, wyszukuje dowody dla każdego faktu, oznacza każdy jako supported / not-supported i raportuje odsetek wspartych faktów:

FActScore=supported atomic factstotal atomic facts\text{FActScore} = \frac{|\text{supported atomic facts}|}{|\text{total atomic facts}|}

Implementacja referencyjna: shmsw25/FActScore. Metryka dobrze działa dla biografii, podsumowań i innych długich wypowiedzi. Uważaj: powtarzalne, banalne fakty mogą zawyżać wynik, a pojedynczo prawdziwe stwierdzenia mogą tworzyć mylącą odpowiedź. MontageLie (EMNLP 2025) testuje tę słabość przez zwodnicze relacje i kolejność prawdziwych stwierdzeń. VeriScore obsługuje twierdzenia z wymaganymi modyfikatorami; filtr Core pomaga zapobiegać dopisywaniu faktów.

Poprawność cytowań

Śledź precision cytowań (czy cytowane fragmenty rzeczywiście wspierają twierdzenie) oraz recall cytowań (czy cytowane są twierdzenia, które powinny być cytowane):

cite_precision=cited spans that support a claimcited spans,cite_recall=claims with at least one supporting cited spanclaims that should be cited\text{cite\_precision} = \frac{|\text{cited spans that support a claim}|}{|\text{cited spans}|}, \quad \text{cite\_recall} = \frac{|\text{claims with at least one supporting cited span}|}{|\text{claims that should be cited}|}

TREC 2024 RAG Track definiuje powtarzalny protokół support evaluation. Thakur et al. (SIGIR 2025) raportują około 56% zgodności z ocenami ludzkimi wykonywanymi od zera oraz 72% w innym warunku, w którym ludzie po edycji poprawiali predykcje LLM. Ten drugi wynik dotyczy anotacji wspomaganej, a nie niezależnego dowodu poprawy dokładności judge’a. Zawsze zachowuj warunek anotacji przy tej liczbie. Zautomatyzowane przybliżenie oferuje ALCE (Gao et al., EMNLP 2023), implementujące precision/recall cytowań z weryfikacją opartą na NLI.

Poprawność, kompletność i odmowa odpowiedzi

  • Poprawność odpowiedzi względem referencji: exact match lub token-F1 mogą pasować do krótkich odpowiedzi. W przypadku dłuższych odpowiedzi sprawdzaj relacje faktograficzne, encje, ilości, negacje i wymagane informacje względem zweryfikowanych referencji. BERTScore i cosine embeddingów mierzą podobieństwo; błędna liczba lub negacja może zachować wysoki wynik. AnswerCorrectness Ragas łączy porównanie faktograficzne z podobieństwem, zamiast utożsamiać te pojęcia.
  • Kompletność przez nuggety: nugget to istotna jednostka informacji, przy czym nuggety kluczowe odróżnia się od opcjonalnych, użytecznych. Pytanie o datę założenia może wymagać roku; nazwisko założyciela nie jest automatycznie wymagane. AutoNuggetizer konstruuje i udoskonala nuggety z ocenianych zbiorów dokumentów, a następnie sprawdza ich obecność w wygenerowanych odpowiedziach. Początkowy raport TREC 2024 obejmował 21 tematów i 45 runów. Przegląd TREC 2025, opublikowany w marcu 2026 r. rozszerza protokół o zapytania narracyjne i ocenia trafność wyszukiwania, kompletność odpowiedzi i atrybucję. Są to publiczne protokoły ewaluacyjne, a nie dowód, że każdy produkcyjny system RAG potrzebuje tej samej rubryki nuggetów.
  • Zachowanie przy odmowie: oznacz, czy dostarczone dowody pozwalają na odpowiedź, a następnie mierz poprawne odmowy wśród wszystkich odmów oraz odmowy w przypadkach, w których system powinien się wstrzymać. NoMIRACL (Findings of EMNLP 2024) testuje odporność względem trafnych i nietrafnych dostarczonych fragmentów; nie dowodzi, że cały korpus nie zawiera odpowiedzi. Własny zestaw rozdziel pominięcia wyszukiwania od rzeczywiście zapytań spoza zakresu.

Weryfikacja po generowaniu

Najtańsze zyski niezawodności często pochodzą z deterministycznych kontroli po generowaniu, a nie z większych modeli.

  • Flaga nieznanej encji: rejestruj ciągi encji nazwanych w odpowiedzi, których brakuje w znormalizowanym ciągu kontekstu (na przykład spaCy’s ents oraz exact matching). To tani, domenowy sygnał nieznanych ciągów encji, a nie kontrola ugruntowania: nie ustala tożsamości, relacji, negacji, czasu ani proweniencji. Zmierz precision i recall na lokalnych etykietach, zanim użyjesz go do akceptacji wydania, i zachowaj entailment na poziomie twierdzeń lub przegląd ludzki do weryfikacji.

    def unseen_entity_flag(answer: str, context: str, entities: list[str]) -> bool:
        return any(entity.lower() not in context.lower() for entity in entities)
    
    context = "Paris was not founded in 1994."
    answer = "Paris was founded in 1994."
    assert not unseen_entity_flag(answer, context, ["Paris"])
    claim_supported = False  # human label or an NLI verdict, not the lexical flag
    assert not claim_supported
  • Weryfikacja twierdzeń: wyodrębnij twierdzenia, uruchom NLI względem kontekstu, a każde poniżej progu oznacz lub odrzuć. Modele NLI-as-faithfulness: cross-encoder/nli-deberta-v3-large, MoritzLaurer/DeBERTa-v3-large-mnli-fever-anli-ling-wanli. Dodaje opóźnienie. Warto w domenach wysokiego ryzyka.

  • Self-consistency (Wang et al., ICLR 2023): próbkuj wiele generacji przy temperaturze > 0; raportuj współczynnik zgodności (np. odsetek generacji zgodnych z odpowiedzią modalną lub parowy BERTScore); wybierz liczbę próbek na podstawie krzywej stabilność–koszt i kieruj odpowiedzi o niskiej zgodności do przeglądu ludzkiego.

  • Kalibracja pewności: zbierz werbalizowaną pewność („Jak bardzo jesteś pewien, w skali 0–1?”) i porównaj ją z rzeczywistą poprawnością na zbiorze ewaluacyjnym. Wykreśl krzywą kalibracji i raportuj Expected Calibration Error: ECE=m=1MBmnacc(Bm)conf(Bm)\text{ECE} = \sum_{m=1}^{M} \frac{|B_m|}{n} |\text{acc}(B_m) - \text{conf}(B_m)|, gdzie BmB_m to przedziały pewności. Implementacje: netcal, torchmetrics.CalibrationError. Model raportujący pewność 0,9 powinien być poprawny w około 90% porównywalnych przypadków; mierz różnicę, zamiast zakładać kalibrację.


Część 7: Ewaluacja RAG opartego na ontologii

Standardowe metryki powyżej obejmują RAG dla otwartego korpusu. Jeśli RAG wyszukuje względem ustrukturyzowanej ontologii, taksonomii lub grafu wiedzy, metryki te są konieczne, ale niewystarczające. Przykłady obejmują produkty w katalogu, jednostki chorobowe w SNOMED, komponenty w BOM oraz techniki bezpieczeństwa w MITRE ATT&CK. Trzeba także mierzyć warstwę ontologii.

Dokładność linkowania encji

Pierwszym zadaniem jest mapowanie wzmianki w zapytaniu na encję ontologii („Aspirin” → wikidata:Q18216, „the 737” → aircraft:Boeing_737).

  • Precision/recall/F1 na poziomie wzmianek: standardowe metryki względem złotych spanów wzmianek (oblicz za pomocą seqeval lub comparatora zbiorów spanów).
  • Dokładność dezambiguacji: spośród poprawnie wykrytych wzmianek, jaki odsetek mapuje się na właściwe ID encji? Publiczne punkty odniesienia obejmują ReFinED, REL i GENRE; benchmarki takie jak AIDA-CoNLL i BELB pokazują, że wyniki różnią się zależnie od systemu i domeny.
  • Obsługa NIL: precision/recall dla „encji nie ma w ontologii”. Mierz osobno nadmierne linkowanie do podobnych, lecz błędnych encji oraz poprawne wstrzymanie się.

Ewaluacja uwzględniająca hierarchię

Zwykła dokładność traktuje „przewidziano Sedan, gdy prawdą jest Hatchback” tak samo jak „przewidziano Sedan, gdy prawdą jest Submarine”. Te błędy nie są równoważne.

  • Hierarchiczne precision/recall/F1 (Kosmopoulos et al., 2015): uwzględniaj wspólnych przodków w DAG ontologii. Dla P^q\hat{P}_q będącego przewidywanym węzłem wraz ze wszystkimi przodkami oraz TqT_q będącego prawdziwym węzłem wraz ze wszystkimi przodkami:

    hP=qP^qTqqP^q,hR=qP^qTqqTq,hF1=2hPhRhP+hRhP = \frac{\sum_q |\hat{P}_q \cap T_q|}{\sum_q |\hat{P}_q|}, \quad hR = \frac{\sum_q |\hat{P}_q \cap T_q|}{\sum_q |T_q|}, \quad hF1 = \frac{2 \cdot hP \cdot hR}{hP + hR}

    Zaimplementuj za pomocą networkx na grafie ontologii: uzupełnij każdą predykcję i każdą etykietę o przodków, a następnie oblicz przecięcia zbiorów powyżej.

  • Podobieństwo Wu-Palmera między przewidywaną a złotą encją w taksonomii (Wu & Palmer, 1994):

    WuP(c1,c2)=2depth(LCA(c1,c2))depth(c1)+depth(c2)\text{WuP}(c_1, c_2) = \frac{2 \cdot \text{depth}(\text{LCA}(c_1, c_2))}{\text{depth}(c_1) + \text{depth}(c_2)}

    gdzie LCA to najniższy wspólny przodek w taksonomii. Jest dostępne od ręki w NLTK dla WordNet (from nltk.corpus import wordnet as wn; wn.synset("car.n.01").wup_similarity(wn.synset("truck.n.01"))); dla własnych taksonomii oblicz LCA za pomocą networkx.

  • Współczynnik pomyłek rodzeństwo/rodzic: osobno śledź pomyłki na rzecz rodzeństwa, rodziców i dzieci — count_sibling / total_errors, count_parent / total_errors, count_descendant / total_errors. Użyj zweryfikowanych przykładów, aby sprawdzić, czy błędy rodzeństwa wynikają z niejednoznacznych wzmianek, a błędy rodziców z nadmiernej generalizacji.

Współczynnik fałszywego wykluczenia przez filtr — ponownie, tym razem krytycznie

W systemach opartych na ontologii twarde filtry często pochodzą z samej ontologii („wyszukuj tylko dokumenty oznaczone kategorią X”). Metryka współczynnika wykluczeń (zdefiniowana w Części 5) staje się podstawowym sygnałem poprawności. Błędna predykcja kategorii może wyzerować recall; współczynnik wykluczeń przypisuje tę stratę filtrowi.

Zgodność generowania z ograniczeniami

Gdy output musi być zgodny z ontologią (każda nazwa encji w odpowiedzi musi być poprawnym członkiem ontologii, a każdy predykat musi pochodzić ze słownika zamkniętego), mierz:

  • Współczynnik poprawności schematu: odsetek outputów, które można sparsować i zwalidować względem schematu ontologii. Waliduj za pomocą jsonschema lub pydantic. JSONSchemaBench to publiczny benchmark ustrukturyzowanych danych wyjściowych; dla schematów specyficznych dla ontologii zbuduj własny walidator.
  • Zgodność ze słownikiem: odsetek nazwanych encji w outputcie, które są poprawnymi ID ontologii — jednowierszowa kontrola przynależności do zamkniętego słownika.
  • Zgodność semantyczna: syntaktycznie poprawny output nadal może wybrać błędną, lecz poprawną encję. Łącz zgodność z downstreamową poprawnością odpowiedzi.

Frameworki constrained decoding (Outlines, XGrammar, Guidance, OpenAI Structured Outputs) służą do wymuszania poprawności schematu. JSONSchemaBench porównuje efektywność, pokrycie i jakość implementacji. Uruchom ponownie przypadki odpowiadające Twoim schematom i backendowi servingowemu, ponieważ pokrycie i opóźnienie zależą od obu.

Audytowalność

W systemach opartych na ontologii, których odpowiedzi podlegają przeglądowi:

  • Kompletność cytowań: odsetek twierdzeń faktograficznych mających co najmniej jedno weryfikowalne cytowanie.
  • Głębokość proweniencji: odsetek cytowań prowadzących aż do dokumentu źródłowego ze stabilnym ID, a nie tylko do hashy chunków.
  • Współczynnik reprodukowalności: ponowne uruchomienie tego samego zapytania na stałym snapshocie zwraca tę samą odpowiedź. Przypnij wersję modelu, runtime, konfigurację dekodowania i seed, a następnie ustal wymaganą częstotliwość powtórzeń na podstawie potrzeb audytowalności workflow. Sama temperatura zero nie gwarantuje determinizmu. Błąd może pochodzić z generowania, runtime’u servingowego lub dowolnego wcześniejszego etapu.

Część 8: Ewaluacja na poziomie systemu

Holistyczna jakość odpowiedzi

  • LLM-as-judge (Zheng et al., NeurIPS 2023): skalowalne podejście do ewaluacji opartej na modelu. G-Eval (Liu et al., EMNLP 2023) generuje kroki ewaluacji na podstawie zadania i kryteriów, a następnie waży poziomy ocen według prawdopodobieństw tokenów: score=ip(si)si\text{score} = \sum_i p(s_i)\,s_i. Są to prawdopodobieństwa, a nie log-probabilities. Zgodność zależy od judge’a, zadania, promptu i zbioru kalibracyjnego.
  • Preferencja pairwise: pokaż judge’owi odpowiedź A i B oraz zarejestruj preferencję. Zastępuje ocenę absolutną decyzją porównawczą, ale nadal wymaga kalibracji względem preferencji ludzkich. MT-Bench raportował zgodność judge’a GPT-4 powyżej 80% zarówno z preferencjami ludzi, jak i zgodnością człowiek–człowiek w warunkach tego benchmarku; nie przenoś tego wyniku do innej domeny bez kalibracji.

LLM-as-judge ma realne biasy:

  • Bias pozycji: mierz wrażliwość na kolejność na przypadkach oznaczonych przez ludzi dla wybranego judge’a i zadania. Randomizacja lub agregacja po zamianie kolejności może pomóc niektórym parom model–zadanie, ale zachowaj to rozwiązanie tylko wtedy, gdy poprawia lokalną zgodność z ludźmi; badanie kontrolowane z 2026 r. wykazało, że zamiana pozycji szkodziła w jego przypadkach adversarial.
  • Bias rozwlekłości: judge’e mogą mylić długość z jakością. Cytowane badanie kontrolowane z 2026 r., wersja 2 wykazało niejednorodne zachowanie dla par z rozszerzeniem: trzech judge’ów preferowało dłuższe odpowiedzi, Claude preferował zwięzłe, a GPT-4o był w przybliżeniu neutralny. Wszystkie pięć modeli dobrze radziło sobie z kontrolami truncation. Wyniki są związane z benchmarkiem, więc określ w judge’u sposób traktowania kompletności i wypełniaczy, a następnie raportuj wyniki kontrolowane względem długości na własnej rubryce.
  • Ryzyko self-preference: Zheng et al. zaobserwowali w swoich danych o 10% wyższy współczynnik zwycięstw GPT-4 nad sobą oraz o 25% wyższy współczynnik zwycięstw Claude-v1 nad sobą, ale uznali, że ograniczona ilość danych i małe różnice nie pozwalają ustalić biasu self-enhancement. Porównuj judge’e z tej samej i z różnych rodzin z lokalnymi etykietami ludzkimi; wybierz lepiej skalibrowanego judge’a, zamiast zakładać, że którekolwiek ustawienie jest bezpieczne.

Praktyczny przepis: wybierz judge’a na danych kalibracyjnych oznaczonych przez ludzi, zamaskuj tożsamości modeli, zmierz wrażliwość na kolejność i określ politykę długości w rubryce. Powtarzaj przypadki tylko wtedy, gdy dodatkowe próbki istotnie zmniejszają niepewność. W ewaluacjach wysokiego ryzyka porównuj judge’e z tej samej i z różnych rodzin oraz analizuj rozbieżności względem etykiet ludzkich.

Schema-Guided Reasoning dla judge’ów

Swobodny output jest jednym ze źródeł zmienności uruchomień judge’a. Dwa uruchomienia dla tej samej odpowiedzi mogą zorganizować rubrykę inaczej i wygenerować różne wyniki. Schema-Guided Reasoning (SGR) czyni tę rubrykę jawną: zdefiniuj etapy ewaluacji jako schemat Pydantic, a następnie użyj constrained output przez Outlines, XGrammar, structured outputs w vLLM lub OpenAI response_format, aby wymusić obsługiwane ograniczenia schematu. Kolejność pól może ułatwić recenzentowi inspekcję rekordu, ale nie dowodzi, że model rozumował w tej kolejności.

W ewaluacji RAG schemat rozkłada ocenę na jawne, audytowalne pola, zamiast pozwalać modelowi przeskoczyć od razu do liczby:

from pydantic import BaseModel, Field, ValidationError, model_validator
from typing import Literal

class FaithfulnessJudgment(BaseModel):
    extracted_claims: list[str] = Field(
        description="Atomic factual claims in the answer, one per item."
    )
    supported_claims: list[str] = Field(
        description="Subset of extracted_claims that are entailed by the context."
    )
    unsupported_claims: list[str] = Field(
        description="Subset that is NOT entailed by the context."
    )
    outcome: Literal["scored", "no_claims"]
    failure_mode: Literal[
        "none", "fabrication", "overgeneralization", "wrong_entity", "stale_fact"
    ]
    rationale: str

    @model_validator(mode="after")
    def require_claim_partition(self):
        for claims in (self.extracted_claims, self.supported_claims, self.unsupported_claims):
            if any(not claim.strip() for claim in claims):
                raise ValueError("claims must contain non-whitespace text")
        extracted = set(self.extracted_claims)
        supported = set(self.supported_claims)
        unsupported = set(self.unsupported_claims)
        if not extracted:
            if supported or unsupported or self.outcome != "no_claims":
                raise ValueError("an empty extraction must be a no_claims outcome")
            return self
        if (
            len(extracted) != len(self.extracted_claims)
            or len(supported) != len(self.supported_claims)
            or len(unsupported) != len(self.unsupported_claims)
            or supported & unsupported
            or supported | unsupported != extracted
            or self.outcome != "scored"
        ):
            raise ValueError("verdict lists must be a complete, disjoint claim partition")
        return self

    @property
    def score(self) -> float | None:
        if not self.extracted_claims:
            return None
        return len(self.supported_claims) / len(self.extracted_claims)

valid = FaithfulnessJudgment(
    extracted_claims=["Mars has two moons", "Mars has a thick atmosphere"],
    supported_claims=["Mars has two moons"],
    unsupported_claims=["Mars has a thick atmosphere"],
    outcome="scored",
    failure_mode="fabrication",
    rationale="The atmosphere claim lacks support.",
)
assert valid.score == 0.5
empty = FaithfulnessJudgment(
    extracted_claims=[],
    supported_claims=[],
    unsupported_claims=[],
    outcome="no_claims",
    failure_mode="none",
    rationale="Score refusal correctness separately.",
)
assert empty.score is None
try:
    FaithfulnessJudgment(
        extracted_claims=["Mars has two moons"],
        supported_claims=["Mars has two moons"],
        unsupported_claims=["Mars has two moons"],
        outcome="scored",
        failure_mode="none",
        rationale="Contradictory verdict.",
    )
except ValidationError:
    pass
else:
    raise AssertionError("overlapping verdicts must fail validation")

for blank in ("", " ", "\t\n"):
    for verdict_field in ("supported_claims", "unsupported_claims"):
        fields = {"extracted_claims": [blank], "supported_claims": [], "unsupported_claims": []}
        fields[verdict_field] = [blank]
        try:
            FaithfulnessJudgment(
                **fields, outcome="scored", failure_mode="none", rationale="Blank claim."
            )
        except ValidationError:
            pass
        else:
            raise AssertionError("blank claims must fail validation")

Ten przykładowy kontrakt oblicza wynik dopiero wtedy, gdy listy werdyktów tworzą kompletny i rozłączny podział wyodrębnionych twierdzeń. Wynik bez twierdzeń nie ma wyniku faithfulness i musi pozostać poza agregatem faithfulness; raportuj jego liczbę, a poprawność odmowy oceniaj osobno, aby abstencje nie poprawiały po cichu średniej. Zachowaj te kontrole semantyczne w produkcji; constrained output gwarantuje kształt, a nie bezstronny werdykt. Model Pydantic sprawia również, że zmiana rubryki jest widoczna jako diff w kodzie, podczas gdy kalibracja ludzka testuje sam osąd.

Działa to dla każdej rubryki opartej na judge’u, nie tylko dla faithfulness. Preferencja pairwise, wsparcie cytowań i poprawność odmowy również korzystają z tego podejścia.

Uproszczony judge rubrykowy oraz przykłady pairwise, biasu pozycji i rodzin modeli znajdują się w notebook 07; moduł: evaluation/llm_judge.py. W sprawdzanej wersji funkcja nazwana g_eval żąda pojedynczej oceny całkowitej; nie generuje kroków ewaluacji ani nie oblicza wyników ważonych prawdopodobieństwami, więc nie odtwarza protokołu G-Eval. Przeszukiwanie benchmarku (make benchmark w repozytorium) podłącza trzy modele (gpt-5-mini, claude-haiku-4-5, gemini-2.5-flash) do rotacyjnego pairwise A/B: każdy judge ocenia odpowiedzi dwóch pozostałych rodzin modeli. Taka topologia obsługuje wyniki pairwise cross-family; pomiar self-preference wymaga także warunków same-family i cross-family porównanych z lokalnymi etykietami ludzkimi.

Opóźnienie i koszt

  • p50, p95, p99 na każdym etapie pipeline’u. Percentyl SLO i próg alertu wybieraj na podstawie ścieżki użytkownika, wolumenu ruchu i budżetu błędów.
  • Time-to-first-token względem całkowitego czasu generowania. W przypadku streaming UX użytkowników interesuje TTFT.
  • Podział na etapy: wyszukiwanie, reranking, generowanie i post-processing. Użyj trace’a do zlokalizowania ogona rozkładu, zamiast zakładać, który etap go powoduje; przy porównywaniu uruchomień rejestruj urządzenie rerankera i batch size.
  • Łącznie $/zapytanie = embedding + wyszukiwanie + reranking + generowanie + zamortyzowany storage. Śledź p50 i p99; to ogon rozkładu pochłania budżet.
  • Współczynniki trafień cache na poziomie cache embeddingów, cache wyszukiwania i KV-cache. Ustal osobne cele na podstawie obserwowanej powtarzalności, polityki unieważniania i kosztu unikniętego na każdej warstwie.

p50/p95/p99 dla poszczególnych etapów wraz z ich rozbiciem są wbudowane w notebook 08 i runnerze evaluation/latency.py; raport benchmarku łączy latency z faithfulness w jednej macierzy, którą można ponownie uruchomić za pomocą make benchmark.

Testy A/B

  • Jednostka randomizacji: wybierz ją na podstawie estymandu, efektu przeniesienia i interferencji. Używaj przypisania per user lub per session, gdy wielokrotna ekspozycja może zmieniać zachowanie albo powodować niespójny UX. Przypisanie per query jest uzasadnione tylko wtedy, gdy te efekty są pomijalne, a analiza modeluje obserwacje powtarzane.
  • Metryki główne, guardraile i eksploracyjne: prerejestruj je. Wybierz miarę główną na podstawie wyniku produktowego; proxy satysfakcji obejmują łapki, regeneracje i dwell. Latency i koszt traktuj jako guardraile, gdy ograniczają doświadczenie.
  • Rozmiar próby: wykonaj analizę mocy przed uruchomieniem na podstawie minimalnego wykrywalnego efektu, wariancji bazowej, jednostki przypisania i reguły zatrzymania.

Część 9: Budowa zbioru testowego

Metryka jest tak dobra, jak zbiór testowy, na którym działa. Jeśli złoty zbiór obejmuje trzy intencje, a ruch produkcyjny dwanaście, Recall@10 mierzy tylko te trzy intencje. Co gorsza, zbiór testowy przeuczony na łatwe pytania („Jaka jest polityka zwrotów firmy?”) może zaakceptować system, który zawodzi na trudnych („Czy przysługuje mi zwrot za częściowe anulowanie zgodnie z unijnym aktem o usługach cyfrowych z 2023 r., jeśli płatność w EUR pochodziła z Irlandii?”). Wynik agregatowy rośnie, a system nadal zawodzi w ważnej części ruchu produkcyjnego.

Niepełne etykiety trafności mogą zniekształcić recall w obu kierunkach. Jeśli rzeczywisty zbiór trafnych dokumentów to {a, b}, ale etykiety zawierają tylko {a}, pobranie {a} otrzymuje wynik 1,0 zamiast 0,5; pobranie {b} otrzymuje zero zamiast 0,5. Wersjonuj oceny i sprawdzaj nowo pobrane, nieocenione dowody przed interpretacją różnicy.

Najpierw buduj zbiór testowy wokół rzeczywistego rozkładu zapytań i ich trudności. Następnie dobierz metryki reagujące na docelowe tryby awarii i dostrajaj system względem nich.

Syntetyczne generowanie zapytań

Użyj LLM do generowania pytań z korpusu:

  • Per-chunk: „Wygeneruj 3 pytania, które użytkownik mógłby zadać i na które ten chunk odpowiada”.
  • Multi-hop: wylosuj dwa chunki i wygeneruj pytanie wymagające obu.
  • Adversarial: generuj pytania z encjami zakłócającymi, parafrazami near-duplicate i niejednoznacznymi wzmiankami.

Generowanie testów w Ragas korzysta ze scenariuszy opartych na grafie, obejmujących zapytania single-hop i multi-hop oraz konkretne lub abstrakcyjne potrzeby informacyjne. DataMorgana generuje konfigurowalne syntetyczne benchmarki według kategorii użytkowników i pytań. Dane syntetyczne są przydatne przy cold starcie i testach pokrycia. Nie zastąpią rzeczywistych zapytań użytkowników.

Budowa złotego datasetu

Dane przygotowane przez ludzi zakotwiczają złoty zbiór.

  1. Próbkuj rzeczywiste zapytania użytkowników (lub symulowane, jeśli system nie został jeszcze uruchomiony), warstwowo według intencji.
  2. Poproś SME o odpowiedź na każde pytanie i wskazanie dokumentu lub dokumentów zawierających odpowiedź.
  3. Ustal rozmiar zbioru na podstawie macierzy pokrycia i przedziału ufności potrzebnego do decyzji wydaniowych; pokrycie jest ważniejsze niż zapożyczona liczba zapytań.
  4. Ponownie przygotuj zbiór, gdy uzasadniają to cykl wydań, sygnały driftu, ryzyko domenowe i możliwości anotacji.

Oddziel zapytania deweloperskie, kalibrację judge’a, odłożony pomiar wydaniowy i próbki monitoringu. Przed podziałem grupuj wspólne dokumenty źródłowe i sesje. Gdy zapytanie lub etykieta wpływa na strojenie, staje się danymi deweloperskimi. Zachowaj nietknięte przypadki do walidacji wybranej konfiguracji i judge’a.

Rejestruj wersje korpusu, zapytań, etykiet trafności i polityk, jednostkę k, budżet dostarczonego kontekstu, wersje scorerów i judge’ów oraz reguły agregacji. Porównuj sparowane delty per query na tej samej populacji, wraz z niepewnością i licznościami wycinków. Uwzględniaj każde oczekiwane zapytanie: timeout, brakujący wynik lub nieocenialny rezultat judge’a musi pozostać widoczny w rozliczeniu kompletności i błędów. Nie pozwól, aby wynik wydania poprawił się przez ciche pomijanie awarii. ARES oferuje badawcze podejście wykorzystujące automatycznych judge’ów, walidację ludzką i prediction-powered inference dla estymacji systemu przy ograniczonej anotacji; zweryfikowany lokalny zestaw może zacząć prościej.

Zbiory testów adversarial

  • Kontrfaktyczne przypadki: zamień kluczowe encje w zapytaniu. Czy system pobiera właściwe chunki dla zmienionego zapytania?
  • Distractory: zapytania, dla których korpus zawiera wiarygodną, lecz błędną odpowiedź, której nie należy pobrać. To właśnie testuje RGB (Chen et al., AAAI 2024): odporność na szum, odrzucanie negatywów, integrację informacji i odporność kontrfaktyczną.
  • Negacje i kwantyfikatory: zapytania z „nie”, „z wyjątkiem” i „tylko”. Dense retrievery często mają z nimi problemy.
  • Out-of-scope: zapytania bez odpowiedzi w korpusie. System powinien powiedzieć „Nie wiem”, a nie halucynować. NoMIRACL dostarcza testy trafności/answerability na poziomie fragmentów; dodaj osobne etykiety out-of-scope na poziomie korpusu. Jawnie oceniaj abstencję dla produkcyjnych typów zapytań.

Pokrycie i ciągła ewaluacja

  • Zbuduj macierz pokrycia: intencja zapytania × typ dokumentu × gałąź ontologii. Jedno zapytanie na komórkę to punkt wyjścia do inwentaryzacji pokrycia, a nie wystarczająca moc statystyczna dla decyzji wydaniowej. Puste komórki ujawniają braki; rozmiar wypełnionych wycinków dobierz do akceptowalnej niepewności.
  • Uruchamiaj ograniczony, szybki podzbiór regresji przy każdym PR i pełny zestaw według wolniejszego harmonogramu.
  • Planuj pełną ewaluację złotego zbioru na podstawie cyklu wydań i kosztu ewaluacji; uruchamiaj ją na release candidate’ach.
  • Planuj ewaluację driftu na podstawie wolumenu ruchu, oczekiwanych zmian i ryzyka. Korzystaj z kroczącej próbki produkcyjnej i warstwuj ją według feedbacku, zamiast po cichu zmieniać rozkład docelowy.

Część 10: Monitoring produkcyjny

Zestaw ewaluacyjny wdrażany wraz z systemem opisuje go w momencie startu. Ruch produkcyjny zmienia się później.

Implicytny i jawny feedback

  • Traktuj zdarzenia implicytne jako sygnały kandydackie, a nie pozytywne lub negatywne KPI jakości, dopóki nie skorelują się z zaślepioną oceną lub jawnym feedbackiem na lokalnej próbce.
  • Click-through / open rate cytowanych źródeł (jeśli UI je udostępnia).
  • Dwell time odpowiedzi.
  • Współczynnik regeneracji: odsetek odpowiedzi, o które użytkownik pyta ponownie lub prosi o ich ponowne wygenerowanie. Traktuj go jako jeden z sygnałów niezadowolenia i skalibruj względem ocenionych rozmów.
  • Współczynnik kopiowania / udostępniania / eksportu: kandydackie sygnały implicytne, które mogą oznaczać użyteczność, weryfikację, przekazanie dalej lub niezadowolenie. Zmierz ich związek i przedział ufności, zanim przypiszesz im kierunek.
  • Wzorce follow-upów: używaj „Na pewno?” lub „A co z X?” jako warstw do przeglądu, a następnie oznaczaj ich związek z brakiem zaufania lub nierozwiązaną potrzebą.
  • Łapki w górę/dół z opcjonalnymi kategoriami powodów (błędna, niepełna, nie na temat, szkodliwa, wolna). Edycje inline mogą zachować więcej kontekstu diagnostycznego; oceń tę wartość na ocenionych próbkach.

Wykrywanie driftu

  • Drift zapytań: porównuj embeddingi zapytań z oknem referencyjnym za pomocą MMD lub zwalidowanego klasyfikatora reference-versus-current. KL wymaga zdefiniowanego estymatora prawdopodobieństwa, na przykład wybranych histogramów; surowe współrzędne embeddingów nie są prawdopodobieństwami. Kalibruj alerty na znanych zmianach, a następnie analizuj dotknięte wycinki.
  • Drift embeddingów: przypnij reprezentację i zbiór sond, a następnie mierz stabilność sąsiadów i jakość wyszukiwania. Różne wersje modelu nie muszą mieć tych samych wymiarów ani bazy współrzędnych, więc cosine między wersjami może być pozbawiony znaczenia. Migruj enkodery zapytań i dokumentów razem, oceniaj nowy indeks i zachowuj wersjonowane snapshoty do rollbacku.
  • Drift wydajności: śledź metryki równoważne produkcji (współczynnik regeneracji według intencji) w czasie. Nagłe i stopniowe zmiany sugerują różne hipotezy, ale ich kształt nie ustala przyczyny; sprawdzaj zmiany danych, ruchu, dostawcy, polityki i wdrożenia.

Ewaluacja shadow i human-in-the-loop

Uruchamiaj system kandydujący równolegle z produkcją, porównuj outputy offline i nie udostępniaj ich użytkownikom. Może to ujawnić regresje przed wdrożeniem. Shadow inference nadal zużywa zasoby i może wywoływać narzędzia: izoluj zasoby, blokuj zapisy i sprawdzaj, czy porównanie nie pogarsza latency produkcji.

W przypadku przeglądu human-in-the-loop (HITL):

  • Kieruj outputy o niskiej pewności do kolejki przeglądu.
  • Dodaj losową próbkę ruchu produkcyjnego do zaślepionego przeglądu; częstotliwość dobierz do wolumenu ruchu, ryzyka i możliwości recenzentów.
  • Nadpróbkuj outputy z łapką w dół do przeglądu obok próbki losowej.
  • Używaj ocenionych outputów do rozszerzania złotego zbioru.

Minimalny zestaw guardraili

Priorytety alertów i progi wybieraj na podstawie szkody dla użytkownika, SLO oraz zwalidowanej skuteczności detektora. Sygnały kandydackie obejmują:

  1. Wynik Faithfulness/HHEM poniżej progu na kroczącej próbce produkcyjnej.
  2. p95 latency powyżej SLO.
  3. Współczynnik fałszywego wykluczenia przez filtr powyżej progu (na podstawie próbki).
  4. Współczynnik regeneracji poza lokalnie skalibrowanym pasmem kontrolnym uwzględniającym rozmiar okna, wolumen ruchu, sezonowość i budżet fałszywych alertów.
  5. Koszt/zapytanie powyżej budżetu.

Użyj czasu wdrożenia jako wskazówki diagnostycznej, a następnie zweryfikuj ją względem trace’ów i dotkniętych wycinków. Wdrożenie może zbiec się ze zmianą ruchu, a zmiana u dostawcy lub danych może nastąpić bez wydania aplikacji. Alerty są dowodami do zbadania; ich wyprzedzenie względem raportów użytkowników warto mierzyć.


Zastrzeżenia

  • Cele są lokalne, a nie uniwersalne. Każda liczba oznaczona w tym przewodniku jako przykładowa jest konfiguracją lub wynikiem demonstracyjnym, a nie progiem wydaniowym. Kalibruj progi do domeny, stawek, niepewności zbioru ewaluacyjnego i oczekiwań użytkowników.
  • Przestrzeń frameworków szybko się zmienia. Wersje HHEM, nazwy metryk RAGAS, karty modeli i kolejność na leaderboardach mogą zmienić się po publikacji. Sprawdź ponownie źródła i wykonaj benchmark przed podjęciem zobowiązania.
  • Liczby zgodności LLM-as-judge mają zastrzeżenia. Wynik 80% GPT-4 względem ludzi pochodzi z warunków MT-Bench / Chatbot Arena. Nie dowodzi zgodności w niszowej domenie ani na wycinku adversarial. Traktuj judge’ów jako mnożnik siły, a nie zamiennik wyrywkowej kontroli.
  • Uplifty z benchmarków dostawców często nie są niezależnie odtwarzalne. Odtwórz je na własnych danych, zanim uwierzysz w liczbę, zwłaszcza w przypadku nowszych rerankerów i systemów OCR.
  • Żadna metryka nie zastępuje oglądania outputów. Planuj zaślepiony przegląd losowej próbki produkcyjnej odpowiednio do ruchu, ryzyka i możliwości recenzentów. Metryki skalują ten nawyk; nie zastępują go.

Nadchodzące części serii

To był indeks. Planuję następujące materiały:

  • Soft Boosts vs. Hard Filters: szczegółowe omówienie współczynnika fałszywego wykluczenia przez filtr, z kodem, rzeczywistymi przykładami produkcyjnymi i frameworkiem decyzyjnym.
  • Chunking Is the Hidden Variable: kontrolowany eksperyment porównujący chunking rekurencyjny, semantyczny, późny i strukturalny na trzech korpusach.
  • Reranker Selection in 2026: BGE vs. Cohere vs. ZeRank vs. aktualne modele cross-encoder, porównanie head-to-head pod względem kosztu, latency i upliftu.
  • Ontology-Grounded RAG: An End-to-End Walkthrough: budowa pełnego harnessu ewaluacyjnego dla systemu wyszukiwania opartego na encjach.
  • LLM-as-Judge Without the Self-Preference Trap: praktyczne przepisy na bezstronną automatyczną ewaluację.
  • Online Evaluation in Production: wzorce instrumentacji, polityki alertów i dashboardy wykrywające rzeczywiste regresje.

Referencje

Frameworki i benchmarki

Wyszukiwanie i ranking

Generowanie, faithfulness i judge’e

Drift i produkcja

Kod towarzyszący

  • slavadubrov/rag-evals-demo — uruchamialny harness wybranych metryk z tego artykułu na korpusie SciFact oraz benchmarkowy sweep chunking × embedding × LLM. Notatniki 00–09, testy jednostkowe przypinające powyższe przykłady i indeks z osadzonym Qdrant, dzięki czemu całość działa bez Dockera.