4. R3. Ranking i metryki wyszukiwania

ENHANCER+ · Moduł 6 · Ewaluacja RAG · Rozdział

W R3 pytasz, czy system znalazł dowody potrzebne do odpowiedzi. Nie uruchamiasz generowania. Utrzymujesz stałe pytanie samodzielne, indeks i parametry wyszukiwania, dzięki czemu nie mieszasz problemu retrievalu z problemem formułowania odpowiedzi.

Od wektora do uporządkowanej listy

retrieve korzysta z funkcji M3 i M4. Tworzy embedding pytania modelem zgodnym z indeksem, porównuje go z wektorami fragmentów i zwraca pierwsze top_k wyników. Znormalizowany rekord ma chunk_id, text, metadata i score. W aktualnym indeksie używana jest metryka cosine i normalizacja wektorów.

Wartość score służy do uporządkowania i filtrowania fragmentów. Wynik 0,81 nie oznacza 81% prawdopodobieństwa poprawnej odpowiedzi. Fragment o wysokim podobieństwie może dotyczyć tego samego systemu, ale nie zawierać potrzebnej procedury. Nie porównuj surowych score między różnymi modelami i indeksami jako wspólnej skali jakości.

Hit@1, Hit@k i pozycja

Dla jednego pytania Hit@k odpowiada na pytanie, czy w pierwszych k pozycjach znajduje się co najmniej jeden wymagany albo jawnie dopuszczony fragment. Wynik jest logiczny. Dopiero dla wielu pytań liczysz odsetek. expected_rank oznacza pozycję pierwszego takiego fragmentu w zapisanym rankingu, liczoną od 1.

Oczekiwany fragmentRankingHit@1Hit@3Pozycja
BA, B, Cfalsetrue2
BB, A, Ctruetrue1
DA, B, Cfalsefalsenull
Brak oczekiwanego źródłaA, B, Cnullnullnull

To dane kontrolowane do nauki metryki, nie wynik modelu. Przy top_k=3 nie wolno obliczyć Hit@5 z trzech zachowanych pozycji. Możesz zwiększyć top_k w nowej serii albo wcześniej jawnie zapisać szerszy ranking. Brak fragmentu w pierwszej trójce nie dowodzi jego braku na pozycji czwartej.

R3 · Do pliku kod/moj_m6.py · zastąp całą definicję

def hit_at_k(test_case, ranking, k, recorded_top_k):
    if type(k) is not int or not 1 <= k <= recorded_top_k:
        raise ValueError('k musi mieścić się w zapisanym top_k.')
    expected = {
        cid
        for group in test_case['expected_sources']
        for cid in group['required_chunk_ids'] + group.get('alternative_chunk_ids', [])
    }
    if not expected:
        return None
    return any(row['chunk_id'] in expected for row in ranking[:k])

recorded_top_k opisuje szerokość żądanego i zachowanego rankingu. Jeżeli indeks ma mniej rekordów niż k, lista może być krótsza i nadal kompletna dla danego żądania. Nie uzupełniaj jej fikcyjnymi pozycjami. Dla pytań bez oczekiwanego źródła funkcja zwraca None: to N/D, nie niepowodzenie retrievalu.

Trafienie fragmentu a pokrycie grup

Hit@k nie mierzy kompletności odpowiedzi wieloźródłowej. Załóżmy, że test wymaga dwóch grup. Pierwsza zawiera restart i kontrolę stanu, druga przywrócenie kopii. Obecność wyłącznie pierwszej daje Hit@k=true, any_group=true, all_groups=false i pokrycie grup 0,5.

Wymagane fragmenty jednej grupy łączysz warunkiem „wszystkie”. Grupy także muszą być spełnione wszystkie, jeśli oceniasz pełne pokrycie. Alternatywa zastępuje całą daną grupę, więc nie wymagasz jednocześnie wszystkich alternatyw. Taką semantykę zapisujesz jawnie przed testem.

R3 · Do pliku kod/moj_m6.py · zastąp całą definicję

def source_metrics(test_case, ranking, context):
    groups = test_case['expected_sources']
    def coverage(items):
        ids = {row['chunk_id'] for row in items}
        hits = [
            set(g['required_chunk_ids']) <= ids
            or bool(set(g.get('alternative_chunk_ids', [])) & ids)
            for g in groups
        ]
        if not hits:
            return {'any_group': None, 'all_groups': None, 'group_coverage': None}
        return {
            'any_group': any(hits),
            'all_groups': all(hits),
            'group_coverage': sum(hits) / len(hits),
        }
    return {'ranking': coverage(ranking), 'context': coverage(context)}

Funkcja liczy metryki osobno dla rankingu oraz końcowego kontekstu. Możliwa jest sytuacja: ranking.all_groups=true, ale context.all_groups=false. Wtedy wyszukiwanie znalazło źródło, a dalszy etap go nie przekazał. Przyczyny szukasz w progu lub filtrowaniu, a nie od razu w modelu embeddingowym.

Agregacja i właściwy mianownik

Dla jednego wybranego run_id odfiltruj błędy wykonania. Następnie zbierz wartości Hit@k dla przypadków z oczekiwanym źródłem. Nie dopisuj zer za None, pominięte tury bez RAG i błędy techniczne. Raportuj ich liczby oddzielnie, bo samo wykluczenie bez ujawnienia może upiększyć wynik.

Poniższy fragment wykonaj po utworzeniu retrieval_results w następnej części „Zapis pełnego rankingu i wykonanie R3”. Notebook warsztatowy zachowuje tę kolejność.

Notebook warsztatu · przykład wywołania

completed = [r for r in retrieval_results if r['status'] == 'completed']
values = [r['hit_at_k'] for r in completed if r['hit_at_k'] is not None]
print({
    'hits': sum(values),
    'evaluated_questions': len(values),
    'hit_rate': sum(values) / len(values) if values else None,
    'not_applicable': sum(r['hit_at_k'] is None for r in completed),
    'not_completed': len(retrieval_results) - len(completed),
})

Średnia odwrotność pozycji, MRR, jest rozszerzeniem: dla znalezionego fragmentu liczysz 1 / expected_rank, dla nieznalezionego 0, a przypadki bez oczekiwanego fragmentu wyłączasz. Jeśli zachowujesz wyłącznie top-k, nazwij tę miarę MRR@k. Nie przedstawiaj jej jako oceny rankingu obejmującego cały indeks.

Ćwiczenie R3

Dla dwóch wymaganych grup zbuduj ranking, który daje Hit@1=true i all_groups=false. Następnie dodaj brakujący dowód na pozycji czwartej. Porównaj metryki przy top_k równym 3 i 5. Uzupełnij funkcje metryk i sprawdź, że pytanie bez oczekiwanego fragmentu zwraca None.

Sprawdź się: potrafisz wyjaśnić wysokie Hit@k przy niewystarczającym kontekście. W każdym odsetku podajesz licznik, mianownik oraz zakres k.

Lokalne chatboty, agenci i systemy RAG z własną bazą wiedzy · Moduł 6 · Wydanie 29.09.2026