Podręcznik uczestnika z objaśnieniami, przykładami i ćwiczeniami do samodzielnej nauki do Modułu 6

3. R2. Kontrakty przypadków i scenariuszy

3.1. Walidacja zestawu i przygotowanie pytania

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

Walidator powinien zatrzymać test, którego nie można jednoznacznie wykonać lub ocenić. Nie powinien obiecywać, że sprawdził prawdziwość wszystkich faktów. Struktura danych podlega kontroli automatycznej, a zgodność kontraktu ze źródłem wymaga lektury człowieka.

Co kontroluje pakiet

Pełny walidator w m6_tools sprawdza m.in. wymagane pola, unikalność identyfikatorów, typy danych, liczebność zestawu, obecność T101/T106/S001, kolejne numery tur, źródła, narzędzia i kryteria. Raport ma pole valid oraz listę errors. Niepoprawny typ danych powinien dać błąd raportu, a nie ciche pominięcie przypadku.

Twoja funkcja rozszerza tę kontrolę o związki między polami: brak narzędzia nie powinien jednocześnie planować RAG, wymagane fragmenty powinny zgadzać się z sekcją, a wewnątrz grupy nie powinno być powtórzeń. Ten podział pozwala korzystać ze wspólnego kontraktu kursowego i uczyć się walidacji zależności.

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

def validate_test_cases(cases, chunks):
    report = reference.validate_test_cases(cases, chunks)
    if not report['valid']:
        return report
    known = {c['chunk_id']: c for c in chunks}
    errors = report['errors']
    for t in flatten_cases(cases):
        prefix = f"{t['test_id']}/{t['turn_id']}: "
        tool = t['expected_tool']
        if tool is None and t['retrieval_question'] is not None:
            errors.append(prefix + 'Brak narzędzia, ale zaplanowano RAG.')
        if tool and tool['name'] == 'answer_from_documents':
            if t['retrieval_question'] is None:
                errors.append(prefix + 'Brak pytania do izolowanego R3.')
        for group in t['expected_sources']:
            ids = group['required_chunk_ids'] + group.get('alternative_chunk_ids', [])
            if len(ids) != len(set(ids)):
                errors.append(prefix + 'Powtórzony fragment w grupie.')
            # Sekcja identyfikuje główne źródło. Alternatywa może mieć inną sekcję.
            for cid in group['required_chunk_ids']:
                if known[cid]['metadata'].get('section') != group.get('section'):
                    errors.append(prefix + 'Sekcja niezgodna z ' + cid)
        if t['critical_conditions'] and not t['required_facts']:
            errors.append(prefix + 'Warunek krytyczny wymaga jawnego oczekiwania.')
    report['valid'] = not errors
    return report

Notebook warsztatu · przykład wywołania

chunks = ref.read_jsonl(ROOT / 'dane/semantic_index/chunks.jsonl')
validation = w.validate_test_cases(cases, chunks)
ref.save_json(E / 'test_cases_validation.json', validation)
assert validation['valid'], validation['errors']
(E / 'test_cases.jsonl').write_text(
    ''.join(json.dumps(c, ensure_ascii=False) + '\n' for c in cases),
    encoding='utf-8')
contracts = {(t['test_id'], t['turn_id']): t for t in w.flatten_cases(cases)}

Jeżeli raport jest niepoprawny, popraw zestaw i ponów kontrolę. Nie usuwaj błędnych wierszy z wyników dopiero na końcu. Zapis raportu umożliwia później wykazanie, jaki zestaw rzeczywiście poddano wykonaniu.

Czego walidator nie rozstrzyga

Identyfikator może istnieć i mieć właściwy dokument, a mimo to fragment może nie odpowiadać na pytanie. Lista required_facts może zawierać prawdziwe zdanie, które nie jest potrzebne w tym przypadku. Nawet poprawne typy danych nie potwierdzają, że warunek krytyczny jest dobrze wybrany. Dlatego przed pomiarem otwórz wszystkie wymagane źródła i spróbuj zbudować odpowiedź wyłącznie z ich treści.

Przy migracji pytań z M3 zachowaj pierwotny identyfikator w dodatkowym polu, np. source_question_id. Nowy test M6 dostaje własny test_id. Historia pochodzenia nie zastępuje sprawdzenia, że intencja, źródła i warunki są nadal te same.

Przygotowanie pytania do R3

W tym warsztacie przygotowanie jest jawne i deterministyczne: odczytujesz retrieval_question z kontraktu. Nie prosisz LLM o parafrazę. Taka decyzja pozwala zbadać wyszukiwanie przy stałym pytaniu. Ocena resolvera rozmowy pozostaje zadaniem R4.

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

def prepare_search_question(test_case):
    question = test_case.get('retrieval_question')
    if question is None:
        return None
    if not isinstance(question, str) or not question.strip():
        raise ValueError('Pytanie retrieval_question musi być niepustym tekstem.')
    return question

None oznacza, że tura nie wymaga izolowanego retrievalu, np. prośba o wcześniej zapisane źródła. Nie zamieniaj tego na pusty tekst i nie uruchamiaj wyszukiwania „na wszelki wypadek”. Nie usuwaj również białych znaków lub nie poprawiaj sformułowania pytania po jego zatwierdzeniu bez nowej wersji kontraktu: dla checkpointu nawet mała zmiana wejścia może oznaczać inne żądanie.

Sprawdź nie tylko poprawny przypadek

W kontrolowanej kopii danych zmień jeden chunk_id na nieistniejący. Następnie powtórz test_id, usuń T101 i dodaj nieznany argument narzędzia. Każda próba powinna zakończyć się valid=False. Nie modyfikuj oryginalnego pliku podczas tych sprawdzeń. Po kontroli uruchom walidację niezmienionego zestawu i oczekuj valid=True.

Sprawdź się: raport błędu wskazuje konkretną przyczynę, a funkcja przygotowania pytania nie wykonuje połączenia z modelem ani nie zmienia samodzielnie intencji testu.

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