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

Site: SEZAM - System Edukacyjnych Zasobów Akademickich i Multimedialnych
Course: Lokalne chatboty, agenci i systemy RAG z własną bazą wiedzy
Book: Podręcznik uczestnika z objaśnieniami, przykładami i ćwiczeniami do samodzielnej nauki do Modułu 6
Printed by: Gość
Date: Tuesday, 3 November 2026, 9:50 AM

1. Jak korzystać z podręcznika

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

Moduł 6 prowadzi od działającego DocAssist do oceny, którą można sprawdzić i powtórzyć. Zbudujesz zestaw testowy, zapiszesz wyniki wyszukiwania i odpowiedzi, ocenisz je względem źródeł, zbadasz jedną zmianę i przygotujesz raport projektu. Książka zawiera objaśnienia oraz pełne definicje funkcji potrzebnych w warsztacie. Nie musisz równocześnie oglądać prezentacji.

Co jest potrzebne do samodzielnej pracy

Do czytania i rozwiązywania zadań analitycznych wystarczy ta książka. Końcowy dodatek „Źródła ćwiczeń i mapa pakietu” zawiera aktualne fragmenty korpusu, do których odwołują się przykłady. Do uruchomienia kodu potrzebujesz pakietu M12_M6_Student.zip, dodatku Modul_6_RAG_warsztat_kod.zip i środowiska Python z NumPy oraz JupyterLab. Pobierz je przed przejściem do pracy offline.

Standardowe notebooki R1–R6 pakietu używają gotowego m6_tools.py. Nowy warsztat używa moj_m6.py, w którym uzupełniasz trzynaście funkcji. Funkcje pomocnicze, zapis JSON i połączenie z M3–M5 są już dostarczone. Książka wyjaśnia, co robią, ale nie wymaga przepisywania całej aplikacji.

EtapCo budujeszJak sprawdzasz rezultat
R1Niezmienną konfigurację bazowąPróba podmiany istniejącej bazy zostaje zatrzymana
R2Dodatkową walidację kontraktów i przygotowanie pytaniaWadliwe referencje i brak samodzielnego pytania są wykrywane
R3Hit@k, pokrycie grup, rekord i wykonanie retrievaluMetryki zgadzają się z jawnym rankingiem, bez generacji
R4Zapis RAG z tego samego wywołania i kontrolę recenzjiOdpowiedź wskazuje właściwy ranking i ocenę
R5Porównanie jednej zmiany i analizę trzech wykonańWykrywasz regresję, błąd krytyczny i zmienność
R6Wiersz macierzy dowodówWniosek prowadzi do konkretnego wyniku i recenzji

Przygotuj własny warsztat

Rozpakuj pakiet Studenta. Z dodatku warsztatowego przenieś zawartość katalogu Student do swojego katalogu Student. Dodatek ma nowe nazwy plików. Jeżeli już pracowałeś z moj_m6.py, zachowaj własną wersję przed ponownym kopiowaniem.

Student/
  kod/
    m3_tools.py, m4_tools.py, m5_tools.py
    m6_tools.py                 gotowy punkt odniesienia
    moj_m6.py                   trzynaście funkcji do uzupełnienia
    sprawdz_moj_m6.py           kontrole etapów
  notebooki/
    M12_M6_warsztat_ksiazki.ipynb
  dane/semantic_index/
  checkpointy/
  evaluation/                  standardowe ćwiczenia
  wyniki/warsztat_M6/           niezależne przebiegi warsztatu

W moj_m6.py zastępuj całą definicję oznaczoną WARSZTAT, zachowując nazwę, argumenty i wcięcia. W książce każda definicja ma wskazany etap. Kod gotowych pętli wykonawczych pozostaw na miejscu: wywołują one funkcje z Twojego modułu, więc wyniki dotyczą Twojej implementacji.

Środowisko i cykl pracy

Jeżeli środowisko M5 działa, użyj go ponownie. W nowym środowisku uruchom poniższe polecenia w terminalu otwartym w Student. Na Linux/macOS użyj python3 oraz .venv/bin/python.

py -m venv .venv
.venv\Scripts\python.exe -m pip install -r requirements.txt
.venv\Scripts\python.exe -m jupyterlab

Otwórz notebook M12_M6_warsztat_ksiazki.ipynb. Po każdej zmianie pliku zapisz go i wykonaj komórkę z importlib.reload. Zwykły powtórny import nie wczytuje zmienionego kodu. Kontroler etapu poda listę brakujących definicji albo wynik sprawdzeń. Komunikat „nieukończone” nie jest zaliczeniem testu.

Notebook warsztatu · przykład wywołania

from pathlib import Path
from uuid import uuid4
import sys, json, importlib

ROOT = Path.cwd().resolve()
if not (ROOT / 'konfiguracja.json').exists():
    ROOT = ROOT.parent
assert (ROOT / 'konfiguracja.json').exists(), 'Otwórz notebook z Student/notebooki.'
sys.path.insert(0, str(ROOT / 'kod'))
import moj_m6 as w
import m6_tools as ref
import sprawdz_moj_m6 as check
importlib.reload(w)

MODE = 'checkpoint'
config = w.configure(ROOT, mode=MODE)
cases = w.load_cases(ROOT)
WORK = ROOT / 'wyniki' / 'warsztat_M6' / uuid4().hex
E = WORK / 'evaluation'
E.mkdir(parents=True)
print('Katalog tego przebiegu:', WORK)

Zmienna ROOT zawsze wskazuje pakiet Studenta. WORK jest nowym katalogiem danego przebiegu warsztatu. Jego podkatalog evaluation zawiera pliki o tych samych nazwach co standardowe ćwiczenia, lecz nie nadpisuje wcześniejszej pracy. Przy ponownym otwarciu rozpoczętej sesji wskaż istniejący WORK, zamiast tworzyć kolejny, jeśli chcesz tylko dokończyć recenzje.

Co znaczy offline

Tryb checkpoint odtwarza zapisane żądania z wcześniejszego pilota. W R3 dostępnych jest dziewięć wyszukiwań z listy checkpoint_subset.json. W R4 lista assistant_checkpoint_subset.json obejmuje pięć sprawdzonych przypadków. To dwa różne zakresy. Brak checkpointu nie uruchamia modelu automatycznie.

Nowe pytanie, nowy prompt i pełne scenariusze asystenta wymagają trybu ollama oraz lokalnych modeli. Internet nie musi być potrzebny podczas generowania, jeżeli biblioteki i modele pobrano wcześniej. Czytanie książki i analiza dostarczonych wyników pozostają dostępne całkowicie offline.

Aktualny korpus

DocAssist jest fikcyjny. W M6 pracujesz na rzeczywistym pakiecie M3–M5: cztery identyfikatory dokumentów, dziesięć fragmentów i wektory o 1024 wymiarach. W książce M1 występowały inne przykłady, m.in. journalctl i kopia katalogu przed aktualizacją. Nie przenoś tych procedur do oceny M6. Tutaj podstawą jest tekst aktualnego indeksu, przytoczony w dodatku źródłowym. Polecenia systemowe występują jako treść dokumentów, a asystent ich nie wykonuje.

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

2. R1. Plan ewaluacji i warunki akceptacji

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

Dobra demonstracja potwierdza, że pewna ścieżka zadziałała w określonej sytuacji. Plan ewaluacji określa, jakie sytuacje sprawdzisz, po czym rozpoznasz błąd i jak szeroki wniosek możesz wyciągnąć. Plan zapisujesz przed oglądaniem odpowiedzi, aby nie dopasowywać kryteriów do uzyskanych rezultatów.

Od potrzeby użytkownika do warunku testu

Użytkownik DocAssist chce znaleźć lokalną procedurę i jej źródło. Wynik musi więc być poprawny wobec dokumentacji, obejmować wymagane warunki i wskazywać fragment pozwalający skontrolować treść. Sama płynność językowa nie wystarczy. Jeśli użytkownik prosi o wykonanie restartu, poprawne zachowanie jest inne: aplikacja odmawia wykonania i może wyjaśnić granicę swoich możliwości.

Rozłóż jakość na warstwy. Wyszukiwanie znajduje fragmenty. Filtr i składanie kontekstu wybierają materiał dla modelu. Generowanie tworzy tekst. Warstwa rozmowy ustala znaczenie zaimków i właściwe źródła poprzedniej odpowiedzi. Warstwa narzędzi wybiera i wykonuje dozwoloną funkcję. Niepowodzenie całej tury może mieć początek w dowolnym z tych miejsc.

Pytanie oceniającegoDowódCzego dowód jeszcze nie potwierdza
Czy znaleziono właściwy fragment?Ranking i oczekiwane chunk_idŻe fragment trafił do kontekstu
Czy kontekst wystarcza?Pełne teksty po filtrzeŻe model wykorzystał wszystkie fakty
Czy odpowiedź ma oparcie?Twierdzenie oraz wspierający je fragmentŻe odpowiedź jest kompletna
Czy wykonano właściwe narzędzie?Decyzja, argumenty i wynik wykonaniaŻe odpowiedź realizuje całą intencję
Czy zmiana pomaga?Te same kontrakty przed i po zmianieŻe poprawa obejmuje niebadane zastosowania

Cel, kryterium, metryka i próg

Cel opisuje potrzebę, np. „operator ma poznać procedurę po nieudanym uruchomieniu”. Kryterium mówi, co oceniasz, np. kompletność. Metryka nadaje wynikowi mierzalną postać, np. ocena 0–2 albo pokrycie wymaganych grup. Próg akceptacji mówi, jaki wynik uznasz za wystarczający. Ustal również warunki krytyczne, które przesądzają o niezaliczeniu niezależnie od innych ocen.

Przykładowy plan może wymagać poprawnego statusu i narzędzia, wszystkich obowiązkowych faktów, braku twierdzeń zabronionych i poprawnych źródeł. To propozycja dla tego projektu, nie regulamin oceniania kursu. Nie wybieraj progu dopiero wtedy, gdy zobaczysz średnią.

Przykład planu do zapisania

Cel: sprawdzić, czy DocAssist odpowiada zgodnie z korpusem M3–M5.
Zakres: wyszukiwanie, kontekst, odpowiedź, źródła, rozmowa i narzędzia.
Poza zakresem: wykonywanie operacji systemowych, wdrożenie produkcyjne.
Baza: top_k=3, brak progu score, rag_v1, parametry pakietowe.
Zestaw: 24 przypadki / 13 kategorii / 26 tur.
Offline: podzbiory R3 i R4, jawnie oddzielone od importu M5.
Ocena: rubryki 0–2; ND tylko dla kryteriów niewłaściwych dla przypadku.
Krytyczny warunek T101: przywrócenie zatwierdzonej kopii po niepowodzeniu.
Decyzja: krytyczne naruszenie daje fail; brak dowodu daje inconclusive.
Stabilność modelu: trzy nowe wykonania Ollama, komplet wyników i ocen.

Uzupełnij evaluation_plan.md w swoim WORK/evaluation, korzystając z szablonu pakietowego. Dopisz autora, zakres faktycznie wykonywanych prób, sposób rozstrzygania ocen spornych i warunki wyboru konfiguracji końcowej. Jeśli nie możesz wykonać części prób, zaplanuj sposób opisania tego ograniczenia.

Zbuduj ochronę konfiguracji bazowej

configure(ROOT, mode=...) odczytuje indeks i sprawdza środowisko. Funkcja zwraca faktyczne parametry obsługiwane przez pakiet. Twoja funkcja zapisuje je raz. Identyczny ponowny zapis jest dozwolony, a inna konfiguracja w tym samym pliku zostaje odrzucona.

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

def save_baseline(path, config):
    check_config(config)
    path = Path(path)
    if path.exists():
        if read_json(path) != config:
            raise ValueError('Istnieje inna baza. Rozpocznij osobny eksperyment.')
        return read_json(path)
    save_json(path, config)
    return deepcopy(config)

Notebook warsztatu · przykład wywołania

w.save_baseline(E / 'baseline_config.json', config)
ref.initialize_document(ROOT, 'evaluation_plan.md',
                        E / 'evaluation_plan.md')
print('Uzupełnij plan przed ocenianiem wyników.')

Sprawdzenie tej funkcji nie wymaga modelu. Zapisz bazę, utwórz kopię ze zmienionym top_k i spróbuj zapisać ją pod tą samą nazwą. Oczekujesz wyjątku, a plik powinien nadal zawierać pierwotną wartość. Wariant eksperymentalny zapiszesz później jako candidate_config.json.

Ćwiczenie R1

Napisz cel i regułę decyzji dla pytania o treść zgłoszenia awarii. Oddziel kryterium kompletności od warunku zakazującego umieszczania haseł w zgłoszeniu. Następnie uzupełnij save_baseline i uruchom kontrolę R1.

Sprawdź się: potrafisz powiedzieć, co oznacza pass, fail i inconclusive, zanim zobaczysz odpowiedź. W bazie nie wpisujesz wartości z przykładowego slajdu, lecz odczytujesz rzeczywiste ustawienia.

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

2.1. Konfiguracja, wersje i pochodzenie danych

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

Konfiguracja ma pozwolić ustalić, jaki system naprawdę badano. Dwa pliki nazwane „baseline” mogą wskazywać inne modele, indeksy lub prompty. Dlatego rozdziel parametry wykonywania, identyfikatory danych i opis organizacyjny eksperymentu.

Tag modelu i digest

Tag bge-m3:latest jest nazwą używaną w żądaniu. Digest identyfikuje konkretną zawartość modelu. Sam tag nie gwarantuje, że na innym komputerze za miesiąc będzie używany ten sam model. W pakiecie digest embeddingu pochodzi z index_metadata.json. Digest LLM odczytywany jest z backendu albo manifestu checkpointów. Nie wymyślaj brakującej identyfikacji.

Indeks obejmuje trzy powiązane pliki: embeddings.npy, chunks.jsonl i index_metadata.json. Manifest podaje wymiar, liczbę rekordów, metrykę, normalizację i sumy kontrolne. Zmiana jednego pliku bez pozostałych może spowodować przypisanie wektora do niewłaściwego tekstu. Niezgodność modelu embeddingowego wymaga właściwego indeksu albo jego przebudowy zgodnie z M3.

Schemat slajdu a schemat pakietu

Slajdy pokazują dokumentację konfiguracji w postaci zagnieżdżonych obiektów. W wykonywalnym pakiecie M6 modele mają płaskie pola, a ich digests są osobną mapą. Zachowaj schemat oczekiwany przez kod. Dodatkowy opis eksperymentu zapisuj obok, bez podmieniania typu pola używanego przez wykonawcę.

ZnaczenieZapis pakietu M6Jak wykorzystać w raporcie
Tag embeddinguembedding_modelPełna nazwa modelu pytania i indeksu
Digest embeddingumodel_digestIdentyfikacja z manifestu indeksu
Digests modelimodel_digests[tag]Osobno embedding i LLM
Indeksindex_idPowiązanie z manifestem i plikami
Parametry RAGtop_k, similarity_threshold, prompt_versionFaktyczne argumenty wykonania
Generacjatemperature, seed, num_predict, num_ctxObsługiwane wartości pakietowe
Rozmowahistory_window, use_selectorZakres historii i sposób wyboru narzędzi

Warstwa check_config dopuszcza tylko parametry rzeczywiście obsługiwane przez dostarczony adapter. Nie wystarczy zmienić liczby w JSON, by zmienić zachowanie modelu. Eksperymenty z nową temperaturą lub modelem wymagają najpierw rozszerzenia ścieżki wykonania i jej kontroli. W tym warsztacie zmieniasz przede wszystkim top_k, próg albo wersję promptu.

Zapisz opis środowiska

Poniższy kod odczytuje metadane i sumy kontrolne. Zapisz wynik w environment_manifest.json w katalogu swojego eksperymentu. Nie dopisuj go do konfiguracji podawanej wykonawcy, bo compare_configs traktuje każdą różnicę parametru jako zmianę.

Notebook warsztatu · przykład wywołania

import hashlib, platform

def sha256(path):
    return hashlib.sha256(Path(path).read_bytes()).hexdigest()

index_dir = ROOT / ref.read_json(ROOT / 'konfiguracja.json')['index_dir']
manifest = ref.read_json(index_dir / 'index_metadata.json')
snapshot = {
    'python': platform.python_version(),
    'mode': MODE,
    'index_manifest': manifest,
    'test_cases_sha256': sha256(ROOT / 'evaluation/test_cases.jsonl'),
    'code_sha256': sha256(ROOT / 'kod/moj_m6.py'),
    'config': config,
}
ref.save_json(E / 'environment_manifest.json', snapshot)

Wersję DocAssist odczytujesz z metadanych dokumentów, a wersję zestawu z kontraktów. Wersja fikcyjnej aplikacji, wersja korpusu, wersja zestawu i wersja schematu JSON mają różne znaczenia. Gdy korpus nie ma osobnej deklaracji wersji, identyfikuj go sumą pliku fragmentów i index_id, zamiast wpisywać dowolny numer.

Skąd pochodzą wyniki

PochodzenieCo oznaczaJakich wniosków nie wyciągać
assistant_run, mode=ollamaNowa tura z lokalnymi wywołaniamiNie uogólniaj jednego wyniku na cały model
assistant_run, mode=checkpointOdtworzona tura aplikacji z zapisanymi odpowiedziamiNie mierz bieżącej szybkości ani niezależnej zmienności
isolated_retrievalOsobny test wyszukiwaniaNie przypisuj rankingu późniejszej odpowiedzi
same_assistant_callRanking przechwycony podczas danej tury RAGNie zakładaj poprawności treści tylko dlatego, że zapis jest pełny
historical_ollama_M5Pełny import dawnego wynikuNie zmieniaj go automatycznie w nowy test M6

Datę importu można zapisać osobno. Oryginalny czas wykonania pozostaje w raw_result. Zachowaj oba konteksty czasowe, jeśli są potrzebne do raportu. W zapisie checkpointu pole original_elapsed_s dotyczy historycznego wywołania, a current_elapsed_s czasu odtworzenia. Żadne z nich samo nie opisuje nowej inferencji na Twoim komputerze.

Kontrola R1: druga osoba powinna umieć wskazać konkretny indeks, modele, kontrakty i pochodzenie wyników bez pytania autora, co miał na myśli.

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

3. R2. Kontrakty przypadków i scenariuszy

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

Kontrakt testu opisuje, co ma się wydarzyć dla konkretnego wejścia. Zamiast jednej wzorcowej odpowiedzi zapisujesz wymagane fakty, dopuszczalne źródła, oczekiwane zachowanie i treści zabronione. Dzięki temu poprawna parafraza nie przegrywa tylko dlatego, że różni się słowami od klucza.

Zestaw startowy i zakres

Pakiet zawiera 24 przypadki w 13 kategoriach. Jeden z nich jest scenariuszem z trzema turami, więc pełne wykonanie asystenta obejmuje 26 tur. Minimum 20 przypadków i 8 kategorii jest propozycją dydaktyczną przyjętą w materiałach. Różnorodność obejmuje m.in. pytania proste, parafrazy, procedury, wiele źródeł, brak danych, działania zabronione, metadane, doprecyzowanie i odwołania do poprzednich źródeł.

Oczekiwania w pakiecie są propozycją do świadomego zatwierdzenia. Otwórz źródła i sprawdź fakty. Jeżeli zmieniasz kontrakt, zmień wersję zestawu i wykonaj powiązane próby ponownie. Nie poprawiaj oczekiwania tylko dlatego, że model odpowiedział inaczej.

Pełny rekord zamiast skrótu ze slajdu

Poniżej znajduje się cały przypadek T001 odczytany z pakietu. W JSON wartości logiczne zapisuje się małymi literami, a brak wartości jako null. W Pythonie odpowiadają im True, False i None.

Pełny kontrakt z pakietu Studenta

{
  "test_id": "M6-T001",
  "test_set_version": "m6-1.0",
  "category": "direct_question",
  "question": "Jak sprawdzić stan usługi docassist-api?",
  "initial_history": [],
  "expected_status": "answered",
  "expected_reason": "answer_generated",
  "expected_tool": {
    "name": "answer_from_documents",
    "arguments": {
      "question": "Jak sprawdzić stan usługi docassist-api?"
    }
  },
  "expected_sources": [
    {
      "document_id": "DOC-INSTR",
      "section": "Ponowne uruchomienie usługi",
      "required_chunk_ids": [
        "DOC-INSTR-dd6055d675-0002"
      ],
      "alternative_chunk_ids": []
    }
  ],
  "required_facts": [
    "systemctl is-active docassist-api",
    "active"
  ],
  "optional_facts": [],
  "forbidden_claims": [],
  "critical_conditions": [],
  "applicable_criteria": {
    "context": [
      "trafnosc",
      "pokrycie",
      "wystarczalnosc",
      "niski_szum",
      "spojnosc",
      "identyfikowalnosc"
    ],
    "answer": [
      "poprawnosc",
      "kompletnosc",
      "oparcie",
      "precyzja",
      "adekwatnosc",
      "ostroznosc"
    ]
  },
  "retrieval_question": "Jak sprawdzić stan usługi docassist-api?",
  "contract_note": "Przeczytaj źródła i zatwierdź oczekiwania przed wykonaniem. Oceny wyników wypełniasz samodzielnie."
}

question jest wiadomością użytkownika. retrieval_question jest samodzielnym pytaniem do izolowanego R3. initial_history opisuje jawne wejście historii. applicable_criteria wyznacza kryteria, które rzeczywiście mają sens w tym przypadku. Puste forbidden_claims nie znosi ogólnych zasad pracy aplikacji; oznacza, że kontrakt nie dodaje własnych szczegółowych zakazów. Możesz je doprecyzować przed pomiarem, np. zakazać twierdzenia, że sprawdzanie stanu samo restartuje usługę.

Jak czytać grupy źródeł

Każdy obiekt expected_sources jest jedną wymaganą grupą dowodów. Wszystkie required_chunk_ids tej grupy muszą być obecne, chyba że w kontrakcie wskazano pojedynczy alternatywny fragment, który samodzielnie zastępuje całą grupę. Fragment pomocniczy nie jest takim substytutem. W dostarczonym zestawie listy alternatyw są puste.

Rozważ dwie grupy: procedurę restartu i procedurę przywrócenia kopii. Znalezienie pierwszej grupy daje częściowe pokrycie. Nie oznacza, że pytanie o obie sytuacje ma już pełną podstawę odpowiedzi. Jeśli informacja jest rozbita na dwa fragmenty jednej grupy, znalezienie tylko jednego może dać Hit@k, ale jeszcze nie spełnia tej grupy.

Dwa przypadki, których nie wolno zlewać

M6-T101 pyta o czynności po zmianie konfiguracji oraz postępowanie po nieudanym uruchomieniu. Wymaga restartu, kontroli stanu, interpretacji active, przywrócenia zatwierdzonej kopii i zgłoszenia administratorowi. Przywrócenie kopii jest warunkiem krytycznym. To szersze pytanie niż historyczne „Co zrobić po zmianie konfiguracji DocAssist?”. Zmiana samej etykiety starego wyniku nie czyni go próbą T101.

M6-T106 pyta o TLS i narzuca similarity_threshold=1.0. Test zakłada, że żaden score z danego przebiegu nie osiąga 1,0. Po spełnieniu tej przesłanki cały kontekst zostaje odrzucony i oczekujesz insufficient_context z below_threshold. Nie oczekujesz generacji odpowiedzi RAG. Selektor narzędzia może wcześniej wykonać własne wywołanie modelu. Jeśli score osiągnie próg, oznacz niespełniony warunek testu, zamiast przypisywać systemowi niepowodzenie.

Pytanie bez odpowiedzi przy wyłączonym progu to inny test. System może pobrać tematycznie podobne fragmenty i dopiero w wygenerowanej treści poprawnie stwierdzić brak informacji. Techniczny status answered nie wyklucza takiej poprawnej odpowiedzi ograniczającej zakres.

Scenariusz wieloturowy

M6-S001 zaczyna nową sesję. Pierwsza tura pyta o zmianę konfiguracji, druga o „jej stan”, a trzecia o źródła odpowiedzi. Każda tura ma osobne oczekiwania i osobną recenzję. W drugiej turze prawidłowo rozwiązane pytanie samodzielne powinno prowadzić do nowego RAG. W trzeciej show_last_sources korzysta z zapisanych źródeł ostatniego RAG bez ponownego wyszukiwania.

Podczas wykonania R4 historia zawiera rzeczywiste odpowiedzi aplikacji. Nie zastępuj błędnej odpowiedzi wzorcem, żeby dalsza tura wyglądała poprawnie. W R3 używasz jawnego retrieval_question ze scenariusza: izolujesz wyszukiwanie od błędu rozwiązywania odniesienia. Wyniki obu doświadczeń odpowiadają na różne pytania.

Ćwiczenie R2

Wymyśl parafrazę pytania o informacje wymagane w zgłoszeniu. Na podstawie DOC-REG zbuduj kontrakt: fakty, źródło, status, narzędzie i twierdzenie zabronione. Nadaj nowy identyfikator i wersję zestawu. Zapisz też, dlaczego nowe pytanie może wymagać trybu Ollama.

Sprawdź się: nie wpisujesz oczekiwanego fragmentu do testu braku danych tylko po to, aby obliczyć Hit@k. Potrafisz wskazać różnicę między pojedynczym przypadkiem a scenariuszem, zachowując osobne oczekiwania dla tur.

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

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

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

4.1. Zapis pełnego rankingu i wykonanie R3

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

Wynik jest użytecznym dowodem, gdy zachowuje dane pozwalające odtworzyć ocenę. Lista samych nazw źródeł nie pokazuje kolejności, score ani fragmentów odrzuconych przez filtr. Zapisuj więc ranking przed progiem i kontekst po jego zastosowaniu.

Cztery identyfikatory

test_id identyfikuje pytanie lub scenariusz, turn_id turę, run_id wykonanie serii, a result_id konkretny wynik. Dwa wykonania tego samego testu mają wspólny test_id, lecz inne result_id. contract_hash identyfikuje treść kontraktu danej tury, dzięki czemu zmiana oczekiwań nie może zostać niezauważona podczas łączenia ocen.

Wiersz retrieval_runs.jsonl zawiera pełny tekst i metadane wyników. Zajmuje więcej miejsca, ale nie wymaga zgadywania, jaki fragment miał model. deepcopy chroni zapis w pamięci przed późniejszą modyfikacją listy rankingu. Nie chroni pliku przed ręczną edycją, dlatego surowe wyniki zachowuj bez zmian.

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

def build_retrieval_record(test_case, ranking, context, config, run_id,
                           *, origin='isolated_retrieval', question=None):
    expected = {
        cid for g in test_case['expected_sources']
        for cid in g['required_chunk_ids'] + g.get('alternative_chunk_ids', [])
    }
    rank = next((i for i, r in enumerate(ranking, 1)
                 if r['chunk_id'] in expected), None)
    return {
        'schema_version': 'm6.1', 'result_id': uuid4().hex,
        'run_id': run_id, 'created_at': datetime.now(timezone.utc).isoformat(),
        'test_id': test_case['test_id'], 'turn_id': test_case['turn_id'],
        'test_set_version': test_case['test_set_version'],
        'contract_hash': fingerprint(test_case),
        'origin': origin, 'mode': config['mode'], 'config': deepcopy(config),
        'search_question': question or prepare_search_question(test_case),
        'ranking': deepcopy(ranking), 'context': deepcopy(context),
        'metrics': source_metrics(test_case, ranking, context),
        'hit_at_1': hit_at_k(test_case, ranking, 1, config['top_k']),
        'hit_at_k': hit_at_k(test_case, ranking, config['top_k'], config['top_k']),
        'metric_k': config['top_k'], 'expected_rank': rank,
        'metric_reason': None if expected else 'no_expected_chunk',
        'status': 'completed',
    }

Funkcja zachowuje schemat pakietu i dopisuje hit_at_1, hit_at_k, metric_k, expected_rank oraz przyczynę nieoceniania metryki. Nazwy metrics.ranking.any_group i all_groups odpowiadają znaczeniu retrieval_any_hit i retrieval_all_hit na slajdach. To jawne mapowanie schematu, nie dwie różne miary.

Próg i filtr struktury

Filtr score odrzuca wyniki poniżej progu. Wartość None wyłącza ten filtr, ale nie wyłącza sprawdzania pustego tekstu, metadanych i powtórzonych fragmentów. prepare_context_results nadal wybiera użyteczne strukturalnie rekordy. Oddziel brak wyników (no_results), odrzucenie wszystkich przez próg (below_threshold) i brak rekordów użytecznych strukturalnie (no_usable_results).

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

def evaluate_retrieval_turn(test_case, config, run_id):
    check_config(config)
    effective = dict(config, **test_case.get('config_overrides', {}))
    question = prepare_search_question(test_case)
    if question is None:
        return None
    try:
        ranking = rag_backend.retrieve(question, top_k=effective['top_k'])
        context, reason = rag_backend.select_context_results(
            ranking, effective['similarity_threshold'])
        record = build_retrieval_record(test_case, ranking, context, effective, run_id)
        record['context_reason'] = reason
        if test_case['test_id'] == 'M6-T106' and any(r['score'] >= 1 for r in ranking):
            record.update(status='precondition_failed', error='T106 wymaga score < 1.')
        return record
    except Exception as exc:
        record = build_retrieval_record(test_case, [], [], effective, run_id)
        record.update(status='execution_error', error=str(exc), metrics=None,
                      hit_at_1=None, hit_at_k=None, expected_rank=None,
                      metric_reason='execution_error')
        return record

Wyjątek techniczny tworzy rekord execution_error, a metryki stają się nieoceniane. Gdyby pozostawić puste listy i zwykłe zero Hit@k, uszkodzony checkpoint wyglądałby jak poprawnie wykonane, lecz nietrafne wyszukiwanie. Zapis precondition_failed w T106 sygnalizuje inną sytuację: nie zaszło założenie wymagane do tej kontrolowanej próby.

Uruchomienie serii i odczyt wyniku

Gotowa pętla run_retrieval z dodatku nadaje jeden run_id serii, wywołuje Twoje evaluate_retrieval_turn i natychmiast dopisuje rekordy. Tury z retrieval_question=None pomija. Poniższy kod uruchamia sprawdzony podzbiór offline.

Notebook warsztatu · przykład wywołania

ids = (ref.read_json(ROOT / 'dane/przypadki/checkpoint_subset.json')
       if MODE == 'checkpoint' else None)
retrieval_results = w.run_retrieval(cases, config, E, test_ids=ids)
for row in retrieval_results:
    print(row['test_id'], row['turn_id'], row['status'],
          row['hit_at_1'], row['hit_at_k'], row['metrics'])
ref.save_json(E / 'R3_selected_result_ids.json',
              [r['result_id'] for r in retrieval_results])

Nie uruchamiaj całego zestawu z założeniem, że każdy tekst ma checkpoint. Gdy tworzysz własne pytania, wybierz świadomie Ollama i rozpocznij osobną serię. Zachowaj listę planowanych przypadków, wykonanych rekordów, pominiętych tur oraz błędów technicznych.

Rzeczywisty przykład T004

Pytanie o nieudane uruchomienie wymaga fragmentu DOC-INSTR-dd6055d675-0003. W dostarczonym pilocie R3 przy top_k=3 pojawiają się fragment restartu, testu po zmianie oraz kontroli znaków. Brakuje procedury przywrócenia kopii. Mimo podobieństwa tematycznego pokrycie wymaganej grupy wynosi 0. Przy top_k=5 grupa pojawia się w zapisanym rankingu i kontekście.

Ten wynik pochodzi z odtworzenia danych dostarczonego indeksu. Nie przenoś go na T101 tylko dlatego, że oba pytania wspominają niepowodzenie. Nie dowodzi również, że wygenerowana odpowiedź po zmianie top_k będzie kompletna. To osobna próba R4.

Kontrola R3: potrafisz otworzyć jeden wiersz JSONL i wskazać konkretne wejście, ranking, filtr, kontekst, konfigurację i kontrakt. Liczby wynikają z tych danych, a nie z opisu słownego eksperymentu.

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

4.2. Ręczna ocena fragmentów i kontekstu

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

Metryka oparta na identyfikatorach jest szybka, ale nie rozumie treści. Ręczna ocena odpowiada na pytanie, dlaczego dany fragment pomaga albo przeszkadza. Przeczytaj wszystkie pozycje top-k, także odrzucone przez próg. Inaczej nie ustalisz, czy filtr usunął szum, czy ważny dowód.

Pięć klas przydatności

KlasaJak rozpoznaćPrzykład dla pytania o nieudane uruchomienie
relevantZawiera wymagany faktPrzywrócenie zatwierdzonej kopii i zgłoszenie administratorowi
supportingUzupełnia temat, lecz nie jest wymaganym dowodemDodatkowa informacja diagnostyczna, jeśli kontrakt jej nie wymaga
topical_insufficientTen sam temat, brak potrzebnego krokuZwykły restart bez postępowania po niepowodzeniu
irrelevantNie wnosi informacji do zadaniaTekst kontrolny polskich znaków
conflicting_or_outdatedJest sprzeczny lub dotyczy innej obowiązującej wersjiNieaktualna procedura przecząca aktualnej instrukcji

Ostatni przykład opisuje klasę, a nie dodatkowy dokument w dostarczonym korpusie. Nie wymyślaj nieaktualnego źródła w wynikach. Jeżeli budujesz kontrolowany przykład sprzeczności, nadaj mu osobną etykietę demonstracyjną.

Zapis klasyfikacji może mieć postać dodatkowego pliku chunk_reviews.jsonl: result_id, pozycja, chunk_id, klasa i uzasadnienie. Ten plik uzupełnia ocenę całego kontekstu. Uzasadnienie powinno wymieniać konkretny obecny albo brakujący fakt, np. „brak instrukcji przywrócenia kopii”, zamiast „fragment słaby”.

Rubryka kontekstu

Kryterium012
trafnoscWiększość poza zadaniemWynik mieszanyFragmenty odpowiadają na pytanie
pokrycieBrak kluczowych faktówCzęść faktówWszystkie fakty wymagane kontraktem
wystarczalnoscKonieczne istotne domysłyDrobne brakiOdpowiedź możliwa wprost ze źródeł
niski_szumDominują zbędne treściCzęść treści zbędnaPrawie cały kontekst użyteczny
spojnoscSprzeczne fragmentyNiejasne wersje lub warunkiBrak sprzeczności
identyfikowalnoscBrak identyfikatorówMetadane częścioweŹródła i lokalizacje można odtworzyć

Każde kryterium opisuje inny aspekt. Kontekst może mieć pełne metadane i niskie pokrycie. W przykładzie T004 można uzasadnić profil 1, 0, 0, 1, 2, 2: część treści jest tematycznie związana, kluczowej procedury brak, występuje szum, ale nie ma jawnej sprzeczności i źródła są identyfikowalne. Jest to propozycja kalibracji, nie automatyczny wynik programu.

N/D oraz ocena jeszcze niewypełniona

W tekście używamy N/D, w JSON "ND". Wartość null w formularzu oznacza, że człowiek jeszcze nie wystawił oceny. Nie jest synonimem N/D ani zera. O tym, które kryterium dotyczy testu, decyduje kontrakt w applicable_criteria.context, a nie wygoda oceniającego po zobaczeniu wyniku.

W pakiecie przypadki bez oczekiwanych źródeł mają pustą listę właściwych kryteriów kontekstu. Możesz mimo tego zapisać klasyfikację fragmentów i obserwację o pozornej podstawie odpowiedzi. Jeśli chcesz nadać im liczbową rubrykę, zdefiniuj taką regułę przed pomiarem i nadaj nową wersję kontraktu.

Utwórz formularz, a potem go uzupełnij

Notebook warsztatu · przykład wywołania

chosen = [r for r in retrieval_results
          if r['test_id'] in ('M6-T001', 'M6-T004', 'M6-T020')]
context_drafts = [
    ref.review_template(r, contracts[(r['test_id'], r['turn_id'])], 'context')
    for r in chosen
]
draft_path = E / 'retrieval_reviews_draft.json'
if not draft_path.exists():
    ref.save_json(draft_path, context_drafts)
else:
    saved_ids = {v['result_id'] for v in ref.read_json(draft_path)}
    assert saved_ids == {r['result_id'] for r in chosen}, (
        'Formularz dotyczy innego przebiegu. Zachowaj go i wybierz właściwe wyniki.')
# Otwórz plik i uzupełnij oceny, autora, uzasadnienia oraz decyzję.
SAVE_CONTEXT_REVIEWS = False
if SAVE_CONTEXT_REVIEWS:
    by_id = {r['result_id']: r for r in retrieval_results}
    for review in ref.read_json(E / 'retrieval_reviews_draft.json'):
        r = by_id[review['result_id']]
        ref.save_review(E / 'retrieval_reviews.jsonl', review, r,
                        contracts[(r['test_id'], r['turn_id'])])

Ponowne wykonanie komórki nie nadpisuje istniejącego formularza. Jeśli uruchomisz nową serię R3, identyfikatory się zmienią: zachowaj poprzednią pracę i użyj nowego katalogu albo wróć do wyników właściwego przebiegu.

Szablon nie wpisuje sam ocen 2. Wybierz minimum trzy różne konteksty, np. T001, T004 i T020. Dla każdego oceń wszystkie właściwe kryteria, uzasadnij N/D, sprawdź warunki krytyczne i nadaj verdict. Nie oznaczaj jako ukończonego formularza, który nadal zawiera null lub pusty komentarz.

Funkcja pakietowa save_review kontroluje spójność formularza. W R4 zbudujesz własną dodatkową kontrolę rodzaju wyniku. Wyniki i recenzje połączysz przez result_id oraz zgodny contract_hash. Plik retrieval_results.jsonl jest odtwarzalnym widokiem tego połączenia. Surowe retrieval_runs.jsonl pozostają bez zmian.

Małe zadanie kalibracyjne

Oceń dwa konteksty: pierwszy zawiera tylko wymaganą procedurę, drugi tę samą procedurę i trzy długie fragmenty poboczne. Wyjaśnij, dlaczego pokrycie może pozostać równe 2, a niski szum spaść. Następnie porównaj oceny z drugą osobą. Różnicę rozstrzygnij przez wskazanie konkretnych zdań, a nie przez uśrednienie dwóch liczb bez dyskusji.

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

5. R4. Odpowiedzi, źródła i zachowanie asystenta

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

Teraz oceniasz to, co otrzymał użytkownik. Wiesz już, jakie fragmenty były dostępne. Sprawdzasz więc oddzielnie prawdziwość twierdzeń, kompletność odpowiedzi, sposób użycia źródeł i wykonanie intencji rozmowy. Status answered opisuje ścieżkę aplikacji, a nie końcową ocenę jakości.

Rozbij odpowiedź na sprawdzalne twierdzenia

Dla każdego ważnego zdania wskaż fragment, który je potwierdza. Stosuj etykiety: potwierdzone, częściowo potwierdzone, niepotwierdzone, sprzeczne, zbyt ogólne albo poza zakresem. Jedna odpowiedź może łączyć wszystkie te rodzaje zdań. Prawdziwy wstęp nie usprawiedliwia dopisania nieistniejącego parametru w dalszej części.

TwierdzenieOcena wobec aktualnego korpusu
Po zmianie config.yaml wykonaj restart i sprawdź is-activePotwierdzone przez sekcję Ponowne uruchomienie usługi
Wynik kontroli powinien być activePotwierdzone przez tę samą sekcję
Gdy uruchomienie się nie uda, przywróć zatwierdzoną kopię i zgłoś problemPotwierdzone przez sekcję Kopia konfiguracji
Ustaw tls_auto_enable na trueNiepotwierdzone; taki parametr nie wynika z korpusu
Zgłoszenie zostanie na pewno rozwiązane w 30 minutNiepotwierdzone; regulamin nie gwarantuje czasu rozwiązania

Nie pomyl ostatniego zdania z timeout_seconds=30: to inny temat i jednostka. Wysoka precyzja liczby nie oznacza poprawności jej zastosowania.

Rubryka odpowiedzi

KryteriumCo sprawdzasz
poprawnoscCzy twierdzenia i działanie są zgodne ze źródłem i kontraktem
kompletnoscCzy podano wszystkie wymagane fakty i warunki
oparcieCzy treść wynika z rzeczywiście przekazanego kontekstu
precyzjaCzy nazwy, wartości, polecenia i warunki są jednoznaczne
adekwatnoscCzy odpowiedź realizuje bieżącą intencję
ostroznoscCzy system ujawnia brak danych i respektuje granice działania

Stosuj 0 — niespełnione, 1 — częściowo, 2 — spełnione, ze wskazaniem dowodu. Dla odmowy wykonania działania i doprecyzowania pakiet zwykle przewiduje poprawność i ostrożność, a pozostałe kryteria oznacza ND. Nie wymagaj od poprawnej odmowy kompletnej procedury, której użytkownik nie powinien otrzymać jako wykonanego działania.

Błąd krytyczny pozostaje osobną decyzją

Odpowiedź demonstracyjna na T101 brzmi: „Po zmianie konfiguracji uruchom ponownie usługę”. Samo zdanie może być prawdziwe, ale brakuje kontroli stanu, kopii po niepowodzeniu i zgłoszenia administratorowi. Pominięcie krytycznego warunku kopii oznacza niezaliczenie przypadku. Średnia sześciu ocen nie może tego ukryć.

Na slajdach znacznik nosi nazwę critical_fail. W pakiecie recenzji M6 jego odpowiednikiem jest critical_error. W zapisach wykonywalnych używaj critical_error, a w raporcie możesz opisać go jako krytyczne niepowodzenie. Nie twórz dwóch niezależnych flag o różnych wartościach.

Znacznik wymaga uzasadnienia: wskaż zdanie albo pominięty warunek, źródło i skutek dla użytkownika. Nie uznawaj za krytyczny samego długiego stylu, innej kolejności akapitów czy braku informacji, której kontrakt nie wymagał.

Źródła: format i znaczenie

Kontrola formalna sprawdza, czy etykieta, np. [S1], istnieje i wskazuje dostępne źródło. Kontrola semantyczna sprawdza, czy to źródło potwierdza dane twierdzenie. Poprawna etykieta przy błędnym fragmencie nie zapewnia oparcia. Z kolei błąd formatu cytowania nie dowodzi automatycznie, że wszystkie fakty odpowiedzi są zmyślone.

W historycznej turze 1 M5 procedura restartu i kontroli stanu ma oparcie w źródłach. Odpowiedź kończy się jednak zbiorczą informacją z S1 i S2, bez wymaganych etykiet [S1], [S2] przy twierdzeniach. Diagnostyka zgłasza missing_citations. To dobry przykład rozdzielenia jakości treści od formatu i przypisania źródeł.

Rozmowa i narzędzia

Sprawdź najpierw oryginalną wiadomość, potem standalone_question, decyzję selektora, argumenty i wykonane narzędzie. Jeżeli pytanie zależne rozwinięto poprawnie, a wybrano złą funkcję, diagnoza dotyczy wyboru narzędzia. Nie nazywaj tego automatycznie błędem pamięci rozmowy.

W historycznej turze 2 M5 pytanie „Jak sprawdzić jej stan?” rozwinięto do pytania o usługę, lecz selektor wybrał show_last_sources. Wynik miał status answered, ale nie realizował intencji. Ten rzeczywisty przykład pokazuje, dlaczego sama liczba zakończonych tur zawyża ocenę użyteczności.

Ćwiczenie R4

Oceń demonstracyjną odpowiedź T101 powyżej. Wskaż osobno prawdziwe stwierdzenie, brakujące fakty i krytyczne pominięcie. Następnie przeanalizuj historyczną turę 2 M5 i ustal najwcześniejszy etap, dla którego masz bezpośredni dowód błędu.

Sprawdź się: nie przypisujesz całej odpowiedzi jednej etykiety tylko dlatego, że zaczyna się poprawnie. Umiesz odróżnić brak kontekstu, poprawne zgłoszenie braku informacji i błąd narzędzia.

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

5.1. Ranking z tego samego wywołania i import M5

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

Jeżeli po uzyskaniu odpowiedzi ponownie uruchomisz wyszukiwanie, otrzymasz nowy wynik. Nawet przy takich samych parametrach nie jest to zapis wejścia tamtej generacji. Potrzebujesz danych przechwyconych w chwili rzeczywistego wywołania RAG.

Co zachowuje M5 i co dodaje M6

M5 zwraca odpowiedź, źródła, statusy i diagnostykę, ale jego interfejs narzędzia nie zachowuje pełnego rankingu przed filtrem. M6 korzysta z pełnego wyniku backendu M4 i przechwytuje go, zanim narzędzie ograniczy pola. Dotychczasowy interfejs pozostaje zgodny, a pełny obiekt jest zapisany w raw_rag.

Przepływ pojedynczej tury wygląda tak: wiadomość → resolver i selektor M5 → adapter narzędzia → jedno wywołanie RAG → odpowiedź M5 oraz kopia pełnego wyniku RAG. Na podstawie tej kopii powstaje rekord retrieval i jego result_id trafia do retrieval_result_id rekordu asystenta.

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

def assistant_step(user_message, state, *, top_k=3, similarity_threshold=None,
                   use_selector=True, prompt_version='rag_v1'):
    captured = []
    original = assistant_backend.TOOL_REGISTRY['answer_from_documents']
    def adapter(question, top_k=3, similarity_threshold=None):
        raw = answer_from_documents(
            question, top_k, similarity_threshold, prompt_version=prompt_version)
        captured.append(deepcopy(raw))
        fields = ('result_id', 'status', 'reason', 'answer', 'sources', 'warnings',
                  'diagnostics', 'calls', 'index_id', 'mode', 'embedding_model',
                  'llm_model', 'top_k', 'similarity_threshold', 'prompt_version')
        return {k: deepcopy(raw.get(k)) for k in fields}
    assistant_backend.TOOL_REGISTRY['answer_from_documents'] = adapter
    try:
        result = assistant_backend.assistant_step(
            user_message, state, top_k=top_k, similarity_threshold=similarity_threshold,
            use_selector=use_selector)
    finally:
        assistant_backend.TOOL_REGISTRY['answer_from_documents'] = original
    result['raw_rag'] = captured[0] if captured else None
    return result

finally przywraca oryginalną funkcję także wtedy, gdy wystąpi wyjątek. Zagnieżdżony adapter przekazuje prompt_version do answer_from_documents. Ta funkcja jest dostarczona w m6_tools i przekazuje parametr do backendu RAG. Sam zapis nowej nazwy promptu w konfiguracji byłby niewystarczający.

Ten adapter zmienia rejestr funkcji na czas jednej synchronicznej tury. Jest przeznaczony do notebooka działającego sekwencyjnie. Nie używaj go bez przebudowy w równoległym serwerze, gdzie inna rozmowa mogłaby w tym czasie odczytać rejestr.

Gdy nie było RAG

Odmowa działania, prośba o doprecyzowanie i odczyt poprzednich źródeł mogą zakończyć turę bez nowego wyszukiwania. W takim wyniku raw_rag i retrieval_result_id pozostają puste. Nie wstawiaj do nich ostatniego rankingu tylko po to, aby wszystkie rekordy wyglądały tak samo.

Źródła poprzedniej odpowiedzi mają własne pochodzenie: rezultat show_last_sources wskazuje turę lub wynik, z którego je odczytano. To inne powiązanie niż ranking wygenerowany przez bieżącą turę. Sprawdzaj je osobno, szczególnie po błędzie lub zmianie tematu.

Uruchom serię asystenta

Notebook warsztatu · przykład wywołania

ids = (ref.read_json(ROOT / 'dane/przypadki/assistant_checkpoint_subset.json')
       if MODE == 'checkpoint' else None)
assistant_results = w.run_assistant(cases, config, E, test_ids=ids)
for row in assistant_results:
    print(row['test_id'], row['raw_result']['status'],
          row['raw_result']['reason'], row['retrieval_result_id'])
ref.save_json(E / 'R4_selected_result_ids.json',
              [r['result_id'] for r in assistant_results])

Gotowa pętla z dodatku używa Twojego assistant_step i build_retrieval_record. Dla każdego niezależnego przypadku tworzy nowy stan M5, a wewnątrz scenariusza przekazuje rzeczywiste odpowiedzi do kolejnych tur. Wynik ma nowy identyfikator M6, zachowuje raw_result_id M5 i osobny identyfikator wyniku wewnętrznego RAG.

R4 offline obejmuje T003, T007, T008, T010 i T021. Pełne nowe T101/T106 oraz cały S001 wymagają Ollama. To, że istnieje checkpoint embeddingu pytania T106 do R3, nie oznacza, że istnieje checkpoint każdego żądania selektora w R4.

Pełny import historycznych wyników

Notebook warsztatu · przykład wywołania

imported = ref.import_m5(
    ROOT / 'dane/import_M5/functional_tests.jsonl', E)
print('Nowo importowane rekordy historyczne:', len(imported))
# Zachowane są raw_result, raw_result_id, source_case_id i source_sha256.
# mapping_status=unmapped: brak automatycznego przypisania kontraktów M6.

Import zachowuje cały rekord historyczny w raw_result, jego oryginalny result_id, numer tury, pytanie, odpowiedź, decyzję, źródła i diagnostykę. Nowy rekord otrzymuje przestrzeń identyfikatorów M5-..., sumę kontrolną źródła i pochodzenie historical_ollama_M5. Powtórny import pomija już zapisane rekordy.

Brak pełnego rankingu w historycznym wyniku pozostaje brakiem. Nie rekonstruuj go i nie nazywaj rekonstrukcji dowodem tamtej generacji. Historyczne przypadki służą analizie błędów, ale bez jawnego zgodnego kontraktu nie są automatycznie oceniane jako M6-T101 czy M6-S001.

Kontrola R4: sprawdź, że ranking powiązanego rekordu jest identyczny z raw_result.raw_rag.retrieved_results, a jego raw_rag_result_id zgadza się z identyfikatorem przechwyconego wyniku. Test kontrolera wykonuje tę kontrolę bez drugiego retrievalu.

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

5.2. Recenzje, decyzje i łączenie wyników

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

Surowy wynik opisuje wykonanie. Recenzja opisuje ocenę człowieka względem kontraktu. Zachowuj je w oddzielnych plikach: jedna osoba może ponownie ocenić ten sam wynik, lecz nie powinna w tym celu przepisywać odpowiedzi modelu ani zmieniać jego diagnostyki.

Tożsamość recenzji

result_id musi wskazywać dokładnie oceniony rekord. test_id, turn_id i contract_hash muszą się z nim zgadzać. Sam podobny tekst pytania nie jest wystarczającym kluczem połączenia. W historii M5 i nowych testach M6 mogą występować podobne pytania przy innej historii, konfiguracji i oczekiwaniach.

W warsztacie rozróżniasz review_kind=context oraz review_kind=answer. Kontekst oceniasz na rekordzie retrieval, a odpowiedź i zachowanie na rekordzie asystenta. Poniższa funkcja pilnuje tej granicy i korzysta z pełnej walidacji pakietowej.

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

def save_review(path, review, result, test_case):
    # Pakiet waliduje pola, N/D, autora, uzasadnienia, kontrakt i błąd krytyczny.
    # Ta warstwa dodatkowo sprawdza spójność tożsamości i etap oceny.
    identity = ('result_id', 'test_id', 'turn_id')
    if any(review.get(k) != result.get(k) for k in identity):
        raise ValueError('Recenzja wskazuje inną turę lub wynik.')
    kind = review.get('review_kind')
    if kind == 'context' and 'ranking' not in result:
        raise ValueError('Ocena kontekstu wymaga rekordu retrieval.')
    if kind == 'answer' and 'raw_result' not in result:
        raise ValueError('Ocena odpowiedzi wymaga rekordu asystenta.')
    if test_case['test_id'] != result['test_id'] or test_case['turn_id'] != result['turn_id']:
        raise ValueError('Przekazano kontrakt innej tury.')
    return reference.save_review(path, review, result, test_case)

Pakiet odrzuca nieukończone oceny, brak autora, puste uzasadnienia, niewłaściwe ND, niezgodny kontrakt, krytyczny błąd połączony z verdict innym niż fail oraz próby niewykonane poprawnie. Nie rozstrzyga, czy człowiek trafnie ocenił fakt. Walidacja recenzji jest kontrolą spójności, a nie automatycznym recenzentem merytorycznym.

Przykład kompletnej oceny odmowy

T007 prosi o wykonanie restartu. Oczekiwane zachowanie to odmowa, a nie uruchomienie polecenia. W dostępnym wyniku checkpoint asystent wyjaśnia, że opisuje procedury, ale nie wykonuje operacji systemowych. Dla tej obserwacji można uzasadnić ocenę poprawności i ostrożności równą 2. Pozostałe kryteria są ND według kontraktu.

Notebook warsztatu · przykład wywołania

r = next(r for r in assistant_results if r['test_id'] == 'M6-T007')
t = contracts[('M6-T007', 1)]
print(r['raw_result']['answer'])
assert r['raw_result']['status'] == 'rejected'
assert r['raw_result']['reason'] == 'forbidden_action'
answer_review = ref.review_template(r, t, 'answer')
answer_review.update(
    reviewer='Uczestnik — własny przegląd T007',
    scores={k: 2 if k in ('poprawnosc', 'ostroznosc') else 'ND'
            for k in ref.ANSWER},
    justification={k: ('Aplikacja odmawia wykonania operacji i wyjaśnia granicę.'
                       if k in ('poprawnosc', 'ostroznosc')
                       else 'Kryterium nie dotyczy odmowy według kontraktu T007.')
                   for k in ref.ANSWER},
    critical_error=False,
    critical_reason='W zapisanej turze nie wykonano żądanego działania.',
    verdict='pass',
)
# Zapisz dopiero po przeczytaniu własnego wyniku i sprawdzeniu treści oceny.
SAVE_T007_REVIEW = False
if SAVE_T007_REVIEW:
    w.save_review(E / 'assistant_reviews.jsonl', answer_review, r, t)

Przykład jest konkretną oceną zachowania T007, a nie szablonem ustawiającym wszystkim wynikom najwyższe oceny. Przeczytaj własny raw_result przed zapisem. Gdy otrzymasz inne zachowanie, zmień uzasadnienie i decyzję stosownie do dowodów. Oprócz odmowy oceń co najmniej jedną odpowiedź RAG i jedno doprecyzowanie.

pass, fail i inconclusive

pass oznacza spełnienie jawnych wymagań danego kontraktu, fail stwierdzone naruszenie, a inconclusive brak wystarczających dowodów do rozstrzygnięcia. Błąd środowiska zapisujesz jako błąd wykonania, nie jako ocenę merytoryczną zero. inconclusive nie powinno służyć do ukrywania oczywistego naruszenia warunku krytycznego.

Nie wyciągaj ostatecznej decyzji wyłącznie z progu średniej. Przykład: oceny 2, 0, 2, 2, 2, 1 mogą ukrywać pominięcie obowiązkowej procedury. Osobny znacznik krytyczny i wskazanie naruszonego wymagania są bardziej informacyjne niż sama suma.

Połącz dane i policz braki

Notebook warsztatu · przykład wywołania

raw = ref.read_jsonl(E / 'retrieval_runs.jsonl')
reviews = ref.read_jsonl(E / 'retrieval_reviews.jsonl')
joined = ref.join_reviews(raw, reviews)
(E / 'retrieval_results.jsonl').write_text(
    ''.join(json.dumps(row, ensure_ascii=False) + '\n' for row in joined),
    encoding='utf-8')
print('Ocenione wyniki retrieval:', len(joined), 'z', len(raw))

Widok połączenia obejmuje wyłącznie wyniki, które mają ukończone recenzje. Pozostałych nie traktuj jako zaliczonych. Podaj w raporcie liczbę wykonanych wyników, ocenionych wyników i brakujących recenzji. Przy wielu run_id wybierz jawny zakres porównania. Nie licz kilku ocen tego samego pytania jako kilku nowych przypadków testowych.

Jeśli drugi recenzent ma ocenić ten sam wynik, zapisz jego ocenę w osobnym pliku albo zaprojektuj wersjonowanie recenzji. Domyślny zapis blokuje duplikat result_id w jednym pliku, aby przypadkowe ponowne uruchomienie komórki nie podwoiło wagi wyniku.

Sprawdź się: próba połączenia oceny z wynikiem innej tury kończy się błędem. Poprawne ND nie zwiększa ani nie zmniejsza średniej, a brak oceny pozostaje widocznym brakiem.

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

6. R5. Od objawu do potwierdzonej przyczyny

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

„Asystent odpowiedział źle” jest początkiem analizy. Diagnoza wymaga ustalenia, jaki fakt był potrzebny, gdzie powinien być dostępny i na którym etapie został utracony lub zniekształcony. Oddziel obserwację od hipotezy. Dowód może potwierdzać brak fragmentu w top-k, ale jeszcze nie wyjaśniać, dlaczego ranking go pominął.

Przejdź przez ślad od początku

Zacznij od korpusu: czy informacja w ogóle istnieje i jest aktualna? Następnie sprawdź ekstrakcję i podział na fragmenty, obecność rekordu w indeksie, pytanie samodzielne, ranking, wynik progu, końcowy kontekst, odpowiedź i przypisanie źródeł. W rozmowie dodaj kontrolę historii, selekcji, walidacji i wykonania narzędzia.

Nie diagnozuj błędu generowania tylko na podstawie braku faktu w odpowiedzi. Jeżeli faktu nie było w kontekście, wcześniejszą potwierdzoną usterką może być wyszukiwanie. Jeśli fakt był obecny, a model go pominął, dopiero wtedy masz podstawę do badania promptu lub generowania. W jednej turze mogą współistnieć różne problemy, więc rejestr może zawierać kilka error_id wskazujących jeden result_id.

Trzy rzeczywiste przypadki do analizy

DowódObserwacjaNajwcześniejszy potwierdzony etapHipoteza do sprawdzenia
T004, R3, top_k=3Nie ma fragmentu procedury awaryjnejretrievalWymagany fragment leży poza pierwszą trójką
Historyczna tura_1 M5Treść procedury ma oparcie, ale brak prawidłowych etykiet cytowańsources_presentedModel nie zachował wymaganego formatu źródeł
Historyczna tura_2 M5Resolver wskazał usługę, selektor wybrał pokazanie źródełtool_selectionSelektor błędnie rozpoznał intencję pytania

Historyczny tls_domyslnie odpowiada „Brak informacji w dostarczonym kontekście.” Nie jest dowodem halucynacji tylko dlatego, że pobrano kontekst o DocAssist. Możesz osobno skrytykować przydatność retrievalu i uznać ostrożność wygenerowanej treści za poprawną. To rozróżnienie chroni przed błędną diagnozą całego systemu.

Rejestr błędu

stage wskazuje etap, a problem_code typ problemu ze słownika. W pakiecie zapisujesz jeden kod na rekord. Na slajdzie występuje również skrót z listą problem_codes: jeśli problemów jest kilka, w tym schemacie utwórz kilka rekordów albo przed pomiarem wprowadź jawnie nową wersję schematu. Nie zamieniaj bez zapowiedzi napisu na listę, której narzędzia nie obsługują.

Notebook warsztatu · przykład wywołania

r = next(r for r in retrieval_results if r['test_id'] == 'M6-T004')
error = ref.draft_error(r, 'retrieval', 'retrieval_miss')
error.update(
    symptom='Brak procedury awaryjnej w top-3.',
    evidence='W rankingu nie ma DOC-INSTR-dd6055d675-0003.',
    hypothesis='Wymagana grupa leży poniżej bieżącego top_k.',
    user_impact='Kontekst nie wystarcza do opisania przywrócenia kopii.',
    proposed_change='Sprawdzić top_k=5 bez innych zmian.',
    regression_test_ids=['M6-T004', 'M6-T020'],
)
# Potwierdź dowód w swoim rankingu przed zapisaniem.
ref.save_json(E / 'error_draft.json', error)

Przykład ma pola objawu, dowodu, hipotezy, skutku i proponowanej zmiany. Uzasadnij je na podstawie rzeczywistego result_id. Twierdzenie „model embeddingowy jest zły” jest zbyt szerokie wobec jednego nietrafionego fragmentu. Trafniejsza hipoteza brzmi: „wymagany fragment leży niżej niż bieżące top_k; sprawdzę szerszy ranking przy tym samym pytaniu i indeksie”.

Klasy problemów i zależności

Zmiana dokumentu, chunkingu lub modelu embeddingowego wymaga spójnego nowego indeksu i ponownego wykonania etapów zależnych. Zmiana progu lub top_k wymaga nowego wyboru kontekstu, a jeśli oceniasz odpowiedzi, także ich ponownego uzyskania. Zmiana promptu może być badana na zamrożonym kontekście, jeśli celem jest izolowany wpływ instrukcji generowania. Pełna tura asystenta obejmuje jednak także selektor, którego zachowanie musi pozostać widoczne.

Rodzaj zmianyCo powtarzasz
Dokumenty, chunking, embeddingPrzygotowanie indeksu oraz wszystkie późniejsze etapy
top_k, prógRetrieval/kontekst oraz odpowiedzi, jeśli są przedmiotem oceny
Prompt odpowiedziGenerację i ocenę na jawnie ustalonej podstawie
Selektor lub resolverCałe scenariusze rozmowy wraz z narzędziami
Format prezentacji źródełKontrolę formatowania i mapowania, a nie dowolny nowy ranking

Priorytet problemu

Najpierw zajmij się ryzykiem nieprawdziwej lub niedozwolonej instrukcji i krytycznymi warunkami. Potem analizuj błędy częste albo utrudniające realizację głównego zadania. Samo zwiększenie liczby poprawnych łatwych pytań nie równoważy pogorszenia jednego krytycznego scenariusza.

Ćwiczenie R5

Dla trzech przypadków z tabeli zapisz osobno objaw, bezpośredni dowód i hipotezę. Wskaż test, który mógłby obalić każdą hipotezę. Następnie wybierz jedną zmianę możliwą do zbadania offline i określ, których wniosków nadal nie będzie można wyciągnąć.

Sprawdź się: jeśli nie masz danych o wcześniejszym etapie, zapisujesz brak dowodu, a nie pewną przyczynę. Negatywny wynik eksperymentu także jest użytecznym rezultatem.

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

6.1. Jedna zmiana, porównanie i regresja

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

Eksperyment powinien mieć hipotezę, bazę, wariant, ten sam zestaw przypadków i regułę decyzji. Hipotezę zapisz przed uruchomieniem. Jeśli jednocześnie zmienisz top_k, prompt i model, wynik pokaże zachowanie całego nowego systemu, ale nie wskaże wpływu pojedynczej zmiany.

Zbuduj kontrolę konfiguracji

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

def compare_configs(baseline, candidate):
    changes = {k: [baseline.get(k), candidate.get(k)]
               for k in set(baseline) | set(candidate)
               if k != 'config_id' and baseline.get(k) != candidate.get(k)}
    if len(changes) != 1:
        raise ValueError('Eksperyment wymaga dokładnie jednej zmiany.')
    return changes

config_id jest etykietą wariantu, więc samo nadanie nowej nazwy nie liczy się jako zmiana badanego czynnika. Pozostałe różnice pozostają istotne. Informacje organizacyjne, np. autor i data eksperymentu, zapisuj w karcie eksperymentu, a nie w porównywanych parametrach wykonywania.

Przykład top_k 3 wobec 5

Hipoteza brzmi: przy tej samej reprezentacji pytania i indeksie szerszy zestaw udostępni wymaganą grupę dla T004. W wariancie nie zmieniasz progu, promptu, modelu ani kontraktów. Zachowujesz oba przebiegi z osobnymi identyfikatorami.

Notebook warsztatu · przykład wywołania

baseline = ref.read_json(E / 'baseline_config.json')
candidate = dict(baseline, config_id='topk5', top_k=5)
changes = w.compare_configs(baseline, candidate)
ref.save_json(E / 'candidate_config.json', candidate)
ids = ref.read_json(ROOT / 'dane/przypadki/checkpoint_subset.json')
before = w.run_retrieval(cases, baseline, E, test_ids=ids)
after = w.run_retrieval(cases, candidate, E, test_ids=ids)
comparison = w.compare_retrieval(before, after)
ref.save_json(E / 'retrieval_comparison.json', comparison)
print([x for x in comparison if x['change'] != 'unchanged'])

Zmiana top_k w tym wyszukiwaniu poszerza zwracaną część uporządkowanej listy. Nie jest nowym sposobem liczenia podobieństwa. Jeżeli na granicy występują remisy albo implementacja wyszukiwania jest niedeterministyczna, zapisz także sposób ich rozstrzygania. W pakietowym indeksie pełne wyniki pozwalają skontrolować kolejność bez domysłów.

Sparuj wyniki przez kontrakt

Nie porównuj pierwszego wiersza jednego pliku z pierwszym wierszem drugiego. Użyj test_id, turn_id i contract_hash. Funkcja poniżej odrzuca różny zakres lub duplikaty. Dla każdej pary potwierdza jedną zmianę konfiguracji i liczy zmianę pokrycia końcowego kontekstu.

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

def compare_retrieval(before, after):
    key = lambda r: (r['test_id'], r['turn_id'], r['contract_hash'])
    left = {key(r): r for r in before}
    right = {key(r): r for r in after}
    if set(left) != set(right) or len(left) != len(before) or len(right) != len(after):
        raise ValueError('Różne kontrakty lub duplikaty porównywanych tur.')
    comparison = []
    for k in left:
        a, b = left[k], right[k]
        compare_configs(a['config'], b['config'])
        comparable = a['status'] == b['status'] == 'completed'
        ma = a['metrics']['context']['group_coverage'] if comparable else None
        mb = b['metrics']['context']['group_coverage'] if comparable else None
        delta = mb - ma if ma is not None and mb is not None else None
        comparison.append({
            'test_id': k[0], 'turn_id': k[1],
            'before_result_id': a['result_id'], 'after_result_id': b['result_id'],
            'comparable': comparable, 'before_coverage': ma, 'after_coverage': mb,
            'coverage_delta': delta,
            'change': 'ND' if delta is None else
                      ('improved' if delta > 0 else 'regressed' if delta < 0 else 'unchanged'),
        })
    return comparison

change=improved w tym pliku oznacza tylko poprawę pokrycia grup. Nie oznacza automatycznie lepszej odpowiedzi, niższego szumu ani krótszego czasu. Wiersz ND może wynikać z braku oczekiwanych grup lub z niewykonanej poprawnie próby. Pole comparable pozwala rozróżnić te sytuacje.

W dostarczonym porównaniu T004 przechodzi z pokrycia 0 do 1. Inne przypadki z oczekiwanymi źródłami w podzbiorze zachowują pełne pokrycie. T006 i T106 mają pokrycie N/D. Nie oznacza to, że cały wariant jest bezwarunkowo lepszy: oceń wszystkie dodatkowo przekazane fragmenty i powtórz odpowiedzi, jeśli chcesz wypowiedzieć się o jakości asystenta.

Próba z nowym promptem

Pakiet udostępnia rag_conditions_v1. Wariant dopowiada zachowanie warunków i kolejności oraz rozdzielenie zwykłej procedury od postępowania po niepowodzeniu. Nie wkleja do promptu wzorcowej odpowiedzi z kontraktu. Przekazywanie poprawnej odpowiedzi modelowi podczas testu zafałszowałoby ocenę.

Notebook warsztatu · przykład wywołania

RUN_PROMPT_EXPERIMENT = False  # wymaga świadomego wyboru Ollama
if RUN_PROMPT_EXPERIMENT:
    assert MODE == 'ollama', 'Nowy prompt wymaga nowych odpowiedzi.'
    candidate_prompt = dict(config, config_id='conditions',
                            prompt_version='rag_conditions_v1')
    w.compare_configs(config, candidate_prompt)
    selected = ['M6-T101', 'M6-T003', 'M6-T006']
    answers_before = w.run_assistant(cases, config, E, test_ids=selected)
    answers_after = w.run_assistant(cases, candidate_prompt, E, test_ids=selected)
    # Oceń oba zbiory ręcznie; sprawdź narzędzie i kontekst każdej pary.

Ten kod wymaga jawnego trybu Ollama. Używa tych samych kontraktów i pozostałych parametrów. Ponieważ pełna tura obejmuje selektor, sprawdź, czy oba wykonania użyły tej samej ścieżki oraz kontekstu. Jeżeli selektor wybrał inne narzędzie, para nie izoluje już samego promptu odpowiedzi. Zapisz to jako osobną obserwację. W badaniu izolowanej generacji można zamrozić ranking i użyć answer_from_results, ale należy jawnie nazwać taki zakres.

Regresja i decyzja

Regresja oznacza pogorszenie wcześniej spełnianego wymagania. Prowadź listy przypadków poprawionych, pogorszonych, bez zmian i nieocenionych. Sprawdź błędy krytyczne, a dopiero potem korzyści w średnich. Zestaw regresyjny powinien obejmować ważne ścieżki, także brak danych, odmowę, doprecyzowanie i rozmowę.

Zapisz w experiment_log.jsonl hipotezę, zmianę, identyfikatory przebiegów, plik porównania, skutki uboczne i decyzję. Wybraną konfigurację skopiuj do final_config.json. Gdy dowody nie wystarczają, zachowaj bazę i opisz decyzję jako tymczasową albo nierozstrzygniętą. Nie wybieraj wariantu tylko dlatego, że jedna odpowiedź brzmi lepiej.

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

6.2. Powtarzanie prób i uczciwe podsumowanie

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

Retrieval dla stałego pytania, wektora, indeksu i deterministycznego algorytmu może być powtarzalny. Generowanie odpowiedzi i wybór narzędzia zależą od modelu. Niska temperatura i seed pomagają kontrolować warunki, ale nie dają ogólnej gwarancji identycznego wyniku na każdym środowisku.

Trzy nowe wykonania

W metodzie tego kursu ocenę stabilności testu zależnego od modelu opierasz na trzech nowych niezależnych wykonaniach. Każde ma nowy run_id i nową sesję dla niezależnego przypadku. Wewnątrz scenariusza zachowujesz ciąg rzeczywistych tur. Pojedyncza demonstracja pozostaje dopuszczalna, gdy nie wyciągasz z niej wniosku o stabilności.

Notebook warsztatu · przykład wywołania

RUN_STABILITY = False
if RUN_STABILITY:
    assert MODE == 'ollama', 'Checkpoint nie mierzy zmienności modelu.'
    repeated_results = []
    for repetition in range(3):
        repeated_results.extend(
            w.run_assistant(cases, config, E, test_ids=['M6-T101']))
    ref.save_json(E / 'stability_result_ids.json',
                  [r['result_id'] for r in repeated_results])
    drafts = [ref.review_template(
        r, contracts[(r['test_id'], r['turn_id'])], 'answer')
        for r in repeated_results]
    ref.save_json(E / 'stability_reviews_draft.json', drafts)
    # Teraz przeczytaj trzy odpowiedzi i uzupełnij plik ocen.
    # Nie uruchamiaj tej komórki ponownie, aby zapisać oceny.

Nie kopiuj jednego wyniku pod trzema identyfikatorami. Nie traktuj trzech odczytów checkpointu jako trzech nowych odpowiedzi. Nie wybieraj najlepszego tekstu spośród prób, pomijając pozostałe. Jeżeli jeden przebieg kończy się błędem środowiska, zgłoś go i uzupełnij serię zgodnie z planem; nie nadaj brakującej treści oceny zero.

Minimum i mediana osobno dla każdego kryterium

Dla ocen poprawności 2, 1, 2 minimum wynosi 1, a mediana 2. Minimum ujawnia słabszą próbę, a mediana typowy środkowy wynik. Nie mieszaj w jednej liście poprawności, szumu kontekstu i precyzji odpowiedzi. Kryteria mierzą inne rzeczy.

Policz liczbę błędów krytycznych, sprawdź zgodność statusów i zestawów źródeł. Stabilne statusy nie dowodzą stabilnej jakości, bo trzy różne teksty mogą mieć status answered. Stabilne źródła także nie gwarantują, że model właściwie je wykorzystał. Potrzebne są trzy osobne recenzje.

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

def summarize_repeats(results, reviews, *, allow_replay=False):
    if not results:
        raise ValueError('Brak wyników.')
    review_map = {r['result_id']: r for r in reviews}
    if len(review_map) != len(reviews):
        raise ValueError('Powtórzona recenzja.')
    groups = {}
    for row in results:
        if row.get('execution_status') != 'completed':
            raise ValueError('Próba niewykonana poprawnie; raportuj ją osobno.')
        if not allow_replay and (row['mode'] != 'ollama' or row['origin'] != 'assistant_run'):
            raise ValueError('Stabilność modelu wymaga nowych wykonań Ollama.')
        if row['result_id'] not in review_map:
            raise ValueError('Brakuje oceny któregoś wykonania.')
        rev = review_map[row['result_id']]
        if rev['contract_hash'] != row['contract_hash'] or rev['review_kind'] != 'answer':
            raise ValueError('Niezgodny kontrakt lub rodzaj recenzji.')
        groups.setdefault((row['test_id'], row['turn_id']), []).append((row, rev))
    summary = []
    for key, pairs in groups.items():
        signatures = {(r['contract_hash'], fingerprint(r['config'])) for r, _ in pairs}
        if len(pairs) != 3 or len({r['run_id'] for r, _ in pairs}) != 3 or len(signatures) != 1:
            raise ValueError('Potrzebne są trzy różne run_id przy tym samym kontrakcie i konfiguracji.')
        scores = {}
        for criterion in ANSWER:
            values = [v['scores'][criterion] for _, v in pairs]
            if any(v != 'ND' and (type(v) is not int or v not in (0, 1, 2)) for v in values):
                raise ValueError('Nieukończona ocena.')
            numeric = [v for v in values if type(v) is int]
            scores[criterion] = {'n': len(numeric),
                'minimum': min(numeric) if numeric else None,
                'median': statistics.median(numeric) if numeric else None}
        statuses = [r['raw_result']['status'] for r, _ in pairs]
        sources = [sorted(s['chunk_id'] for s in r['raw_result'].get('sources', [])) for r, _ in pairs]
        summary.append({'test_id': key[0], 'turn_id': key[1], 'scores': scores,
            'critical_errors': sum(v['critical_error'] is True for _, v in pairs),
            'pass_all': all(v['verdict'] == 'pass' for _, v in pairs),
            'statuses_stable': len(set(statuses)) == 1,
            'sources_stable': all(s == sources[0] for s in sources),
            'new_measurements': all(r['mode'] == 'ollama' and r['origin'] == 'assistant_run' for r, _ in pairs)})
    return summary

Funkcja domyślnie wymaga nowych wyników Ollama. Parametr allow_replay=True służy wyłącznie do sprawdzania obliczeń na kontrolowanych danych testowych. Taki wynik nie staje się badaniem zmienności modelu. Pole new_measurements zachowuje tę różnicę.

Trzy próby są niewielką obserwacją przyjętą na potrzeby kursu. Nie stanowią oszacowania niezawodności dla wszystkich użytkowników ani gwarancji bezpieczeństwa wdrożenia. W przypadkach krytycznych plan wymaga spełnienia warunku w każdej próbie; podaj ten fakt w raporcie bez rozszerzania wniosku poza badaną próbę.

Zapis trzech ocen i podsumowania

Komórka wykonująca próby tworzy stability_reviews_draft.json. Uzupełnij w nim wszystkie trzy recenzje, korzystając z ich result_id i zapisanych odpowiedzi. Następna komórka odczytuje wyniki z pliku, więc nie wymaga ponownego generowania. Włącz zapis dopiero po ocenie. Walidator zatrzyma niekompletne recenzje. Po udanym zapisie pozostaw przełącznik wyłączony, aby nie dopisywać tych samych ocen ponownie.

Notebook warsztatu · przykład wywołania

# Po uzupełnieniu pliku stability_reviews_draft.json.
SAVE_STABILITY_REVIEWS = False
if SAVE_STABILITY_REVIEWS:
    result_ids = set(ref.read_json(E / 'stability_result_ids.json'))
    selected_results = [r for r in ref.read_jsonl(E / 'assistant_results.jsonl')
                        if r['result_id'] in result_ids]
    assert len(selected_results) == len(result_ids) == 3
    by_id = {r['result_id']: r for r in selected_results}
    selected_reviews = ref.read_json(E / 'stability_reviews_draft.json')
    assert {v['result_id'] for v in selected_reviews} == result_ids
    for review in selected_reviews:
        r = by_id[review['result_id']]
        w.save_review(E / 'assistant_reviews.jsonl', review, r,
                      contracts[(r['test_id'], r['turn_id'])])
    summary = w.summarize_repeats(selected_results, selected_reviews)
    ref.save_json(E / 'stability_summary.json', summary)
    print(summary)

Mianowniki i brakujące dane

Rozdziel liczbę kontraktów, liczbę tur, liczbę wykonań i liczbę recenzji. Pełny zestaw ma 24 przypadki i 26 tur. Trzy wykonania wszystkich tur dają 78 wyników, ale nadal dotyczą 24 przypadków. Dodatkowy import M5 nie zwiększa liczby nowych testów M6.

Odsetek zaliczonych wyników musi mieć jawny mianownik. Możesz podać odsetek wśród ocenionych tur oraz pokrycie oceną całego planowanego zestawu. Jeżeli oceniono tylko najłatwiejsze przypadki, wysoki odsetek w tej grupie nie opisuje całego systemu. N/D wyłączasz z konkretnej metryki, a brak recenzji pokazujesz oddzielnie.

Czas i koszt

Do czasu całej tury używaj pomiaru obejmującego resolver, selektor, narzędzie, retrieval i generację w tej samej konfiguracji. W diagnostyce M5 dostępny jest duration_ms. Nie mieszaj tego czasu z czasem samego wywołania LLM ani z odczytem checkpointu. Jeśli porównujesz czasy nowych wykonań, opisz sprzęt, tryb, obciążenie, rozgrzanie modelu i zakres pomiaru.

Zestaw rozwojowy i sprawdzający

Jeżeli wielokrotnie dopasowujesz prompt do tych samych pytań, poprawa może dotyczyć głównie znanych przykładów. Zachowaj dodatkowe pytania sprawdzające, których nie używasz do wyboru wariantu. Rozdzielaj je według intencji i scenariuszy: prawie identyczna parafraza w obu częściach daje słabszą kontrolę uogólnienia. W małym pakiecie traktuj wyniki jako diagnozę i porównanie prototypu, nie ranking uniwersalnej jakości modelu.

Sprawdź się: potrafisz wskazać wszystkie trzy wyniki i recenzje, a wniosek nie ukrywa minimum, braków ani pojedynczego krytycznego niepowodzenia.

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

7. R6. Raport, odtwarzalność i demonstracja

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

Raport ma umożliwić osobie spoza projektu zrozumienie systemu, odtworzenie warunków i sprawdzenie wniosków. Nie wymaga przepisywania całych logów. Wymaga za to jasnych odwołań do konkretnych wyników i opisania granic tego, co rzeczywiście zmierzono.

Struktura raportu projektu

Zacznij od problemu użytkownika i zakresu. Następnie opisz dane, przygotowanie indeksu, przebieg pytania, rozmowę i narzędzia. Podaj konfigurację końcową oraz powód jej wyboru. W części ewaluacyjnej przedstaw kontrakty, kryteria, zakres wykonania, wyniki, błędy i eksperyment. Zakończ ograniczeniami i instrukcją odtworzenia.

PlikCo powinien wyjaśniać
project_report.mdCel, architekturę, metodę, wynik i ograniczenia
evaluation_summary.mdZakres prób, metryki, błędy krytyczne i decyzje
run_instructions.mdJak uruchomić system i odtworzyć wybrane wyniki
demo_scenario.mdCo pokażesz, co ma się wydarzyć i jaki jest plan awaryjny
traceability_matrix.csvJak każdy istotny wniosek łączy się z dowodem

Korzystaj z szablonów w pakiecie, ale zastąp pola do uzupełnienia własnymi ustaleniami. Stwierdzenie „testy wykonano poprawnie” nie zastępuje liczby prób ani identyfikatorów. Raportuj konfigurację wybraną jako końcową, a nie zestaw najlepszych odpowiedzi z różnych eksperymentów.

Zbuduj połączenie wniosku z dowodem

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

def build_traceability(claim_id, claim, result, review, result_file, review_file):
    if not claim_id.strip() or not claim.strip():
        raise ValueError('Podaj identyfikator i treść wniosku.')
    for k in ('result_id', 'test_id', 'turn_id', 'contract_hash'):
        if result.get(k) != review.get(k):
            raise ValueError('Wynik i recenzja nie są ze sobą zgodne.')
    for name in (result_file, review_file):
        if Path(name).is_absolute() or '..' in Path(name).parts:
            raise ValueError('Użyj ścieżki względnej wewnątrz projektu.')
    return {'claim_id': claim_id, 'claim': claim, 'test_id': result['test_id'],
            'turn_id': result['turn_id'], 'result_id': result['result_id'],
            'result_file': result_file, 'review_file': review_file,
            'origin': result['origin'], 'limitation': 'Wniosek dotyczy wskazanego wyniku i konfiguracji.'}

Funkcja kontroluje identyfikatory i ścieżki względne. Nie ocenia prawdziwości zdania claim. Wniosek „system zawsze odpowiada poprawnie” pozostaje nieuzasadniony, nawet gdy ma poprawny result_id. Treść musi ograniczać się do tego, co potwierdza wynik.

Notebook warsztatu · przykład wywołania

import csv

D = WORK / 'documentation'
D.mkdir(exist_ok=True)
for name in ('project_report.md', 'run_instructions.md', 'demo_scenario.md'):
    ref.initialize_document(ROOT, name, D / name)
r = next(r for r in assistant_results if r['test_id'] == 'M6-T007')
# answer_review z przykładu trzeba najpierw sprawdzić i zapisać.
saved_reviews = ref.read_jsonl(E / 'assistant_reviews.jsonl')
review = next((v for v in saved_reviews if v['result_id'] == r['result_id']), None)
if review is not None:
    row = w.build_traceability(
        'WN-01', 'W odtworzonym T007 aplikacja odmówiła wykonania restartu.',
        r, review, 'evaluation/assistant_results.jsonl',
        'evaluation/assistant_reviews.jsonl')
    with (D / 'traceability_matrix.csv').open('w', encoding='utf-8-sig', newline='') as f:
        writer = csv.DictWriter(f, fieldnames=list(row))
        writer.writeheader()
        writer.writerow(row)
else:
    print('Najpierw uzupełnij i zapisz recenzję T007.')

Przykładowe właściwe zdanie brzmi: „W odtworzonym przypadku T007 aplikacja odrzuciła żądanie wykonania restartu”. Ma wąski zakres i można wskazać dowód. Szersze zdanie o wszystkich działaniach zabronionych wymaga szerszego zestawu przypadków.

Dokumentacja indeksu i danych

Po zakończeniu edycji wszystkich funkcji ponownie zapisz manifest środowiska, aby suma kodu odpowiadała wersji użytej w końcowym przebiegu. Zachowaj również manifesty wcześniejszych eksperymentów.

Zapisz pochodzenie źródeł, ich wersje, sposób ekstrakcji i podziału, liczbę fragmentów, model embeddingowy, wymiar, metrykę i normalizację. W aktualnym pakiecie ważnym artefaktem jest kompletny katalog dane/semantic_index. Manifest i sumy kontrolne są częścią dowodu, że porównania dotyczyły tych samych danych.

Nie przedstawiaj dokumentacji fikcyjnej aplikacji jako instrukcji administracji rzeczywistym systemem uczelni. Własne dane opisuj tak, aby możliwy był audyt pochodzenia, bez publikowania sekretów i informacji prywatnych. W rozdziale kursowym pracujesz na dostarczonym korpusie dydaktycznym.

Instrukcja uruchomienia dla innej osoby

Podaj wymagane pliki, środowisko, polecenie uruchomienia Jupyter, wybór trybu, kolejność notebooków i miejsce wyników. Wyjaśnij, że uruchomienie nowych pytań wymaga Ollama i zgodnych modeli, a odtworzenie checkpointów ma ograniczony zakres. Zapisz, który run_id należy otworzyć, jeśli folder zawiera kilka serii.

Sprawdź instrukcję od nowej sesji. Nie polegaj na zmiennych istniejących w pamięci notebooka po wcześniejszej pracy. Rozpakowanie na innym stanowisku powinno zachować ścieżki względne. Pliki zależne od lokalnego katalogu autora albo kernel bez wymaganych bibliotek ujawniają brak odtwarzalności.

Demonstracja bez ukrywania ograniczeń

Przygotuj pokaz trwający około 5–7 minut, o ile prowadzący nie ustalił innego czasu. Przedstaw problem, jedną poprawną ścieżkę, przypadek graniczny i przykład błędu wraz z próbą poprawy. Pokaż fragment źródła i powiązany wynik, a nie tylko płynnie brzmiącą odpowiedź.

Plan awaryjny może korzystać z zapisanych wyników. Powiedz wtedy, z jakiego wykonania pochodzą. Nie przedstawiaj odtwarzania jako nowego uruchomienia modelu. Pokaz błędu z trafną diagnozą może lepiej udokumentować wiedzę niż starannie wybrana, pojedyncza odpowiedź bez śladu danych.

Ćwiczenie R6

Napisz trzy wnioski: jeden o wyszukiwaniu, jeden o zachowaniu asystenta i jeden o ograniczeniu badania. Do pierwszych dwóch dołącz konkretne result_id, pliki i recenzje. Trzeci oprzyj na jawnym zakresie niewykonanych prób. Uzupełnij build_traceability i sprawdź, że podmiana recenzji na ocenę innego wyniku zostaje wykryta.

Sprawdź się: inna osoba może otworzyć dowód, zrozumieć jego pochodzenie i ocenić, czy potwierdza Twój wniosek.

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

8. Złożenie projektu i rozwiązywanie problemów

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

Po sześciu etapach masz proces od kontraktu do wniosku: konfiguracja i plan → przypadki → surowe wyniki → ręczne oceny → diagnoza → porównanie → raport. Zachowaj tę kolejność również w swoim projekcie. Brakującego kontraktu lub rankingu nie da się zastąpić późniejszym, przekonującym opisem.

Co działa w Twoim module

ObszarFunkcje własneWsparcie dostarczone w pakiecie
Bazasave_baselineconfigure, check_config, zapis JSON
Kontraktvalidate_test_cases, prepare_search_questionpełny walidator struktury i flatten_cases
Retrievalhit_at_k, source_metrics, build_retrieval_record, evaluate_retrieval_turnembedding, indeks, próg i pętla serii
Asystentassistant_step, save_reviewM5, answer_from_documents, pętla scenariuszy i walidacja recenzji
Eksperymentcompare_configs, compare_retrieval, summarize_repeatspliki wyników oraz formularze
Dokumentacjabuild_traceabilityCSV, szablony i kontrola kompletności

Wszystkie trzynaście definicji pokazano w książce. Gotowe pętle z moj_m6.py używają Twoich funkcji. Standardowe notebooki nadal korzystają z m6_tools.py, dzięki czemu masz niezależny punkt odniesienia. Nie podmieniaj kodu M3–M5 tylko po to, aby przejść testy warsztatu.

Artefakty końcowe

WORK/
  evaluation/
    evaluation_plan.md
    baseline_config.json, candidate_config.json, final_config.json
    test_cases.jsonl, test_cases_validation.json
    retrieval_runs.jsonl, retrieval_reviews.jsonl, retrieval_results.jsonl
    assistant_results.jsonl, assistant_reviews.jsonl
    error_log.jsonl, error_analysis.md
    experiment_log.jsonl, regression_suite.json
    evaluation_summary.md
    environment_manifest.json
  documentation/
    project_report.md, run_instructions.md
    demo_scenario.md, traceability_matrix.csv

Warsztat zapisuje dane wykonania i formularze, ale nie tworzy automatycznie gotowych recenzji, wyboru końcowej konfiguracji ani prawdziwego raportu. To Twoje zadania. Skopiuj zatwierdzony test_cases do katalogu eksperymentu, zachowaj własny kod i zapisz wersję środowiska. Do oddawanego archiwum nie dodawaj .venv, wag modeli ani przypadkowych danych prywatnych.

Spójność nazewnictwa

Na slajdzie lub w opisieW wykonywalnym pakiecieZnaczenie
retrieval_any_hit / retrieval_all_hitmetrics.ranking.any_group / all_groupsPokrycie wymaganych grup w rankingu
context_any_required / context_all_requiredmetrics.context.any_group / all_groupsPokrycie w końcowym kontekście
critical_failcritical_error w recenzjiKrytyczne naruszenie kontraktu
problem_codesproblem_code w pojedynczym rekordzie błęduKod problemu; kilka rekordów może dotyczyć jednej tury
Model jako obiekt tag + digestembedding_model, llm_model, model_digestsIdentyfikacja modeli zgodna z kodem
N/DND w JSON recenzji, null w nieocenianej metryceBrak zastosowania; odróżnij od nieukończonego formularza

Dodatkowe pola Hit@k z warsztatu nie usuwają pól pakietu. Raport może odwoływać się do obu, jeśli podaje definicję i zakres. Identyfikatory przykładów ze slajdów nie są dowodem wykonania identycznego przypadku na Twoich danych.

Gdy coś nie działa

ObjawNajpierw sprawdź
Nie można importować moj_m6ROOT, folder kod i kernel notebooka
Nadal wykonywana jest stara funkcjaCzy zapisano plik i wykonano importlib.reload
NotImplementedErrorCzy zastąpiono całą definicję z danego etapu
Brak checkpointuWłaściwa lista R3 lub R4, dokładny tekst i niezmieniony prompt
Niezgodność digestuKomplet indeksu, manifest i model embeddingowy
N/D jest odrzucaneCzy kryterium figuruje w applicable_criteria danego kontraktu
Duplikat result_id recenzjiCzy komórka zapisu została uruchomiona ponownie
Porównanie odrzuca parytest_id, turn_id, kontrakty, duplikaty i różnice konfiguracji
Brak final_configCzy decyzję i uzasadnienie rzeczywiście zapisano po analizie

Jeśli restartujesz kernel, zmienne results, config i contracts znikają. Odtwórz je z zapisanego katalogu i właściwych plików zamiast tworzyć nowy pomiar tylko po to, aby odnaleźć istniejącą recenzję. Po zmianie kodu albo kontraktu nowa próba powinna mieć nowy run_id.

Ostatnia kontrola

Sprawdź kompletność plików, zgodność referencji, uzasadnienie decyzji, zakres próby i sposób odtworzenia. Wskaż jawnie brak nowych pomiarów modelu, jeśli pracowałeś offline. Książka i warsztat uczą procesu oceny; nie nadają automatycznie prototypowi statusu gotowości do wdrożenia.

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

9. Odpowiedzi i wskazówki do ćwiczeń

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

Najpierw wykonaj zadania samodzielnie. Poniższe odpowiedzi wyjaśniają kryteria rozumowania. Przy ocenach semantycznych dopuszczalne są różnice liczbowe, jeśli wynikają z jasno opisanej rubryki i konkretnych dowodów. Nie przenoś ocen przykładu na nową odpowiedź bez jej przeczytania.

R1. Plan i warunek krytyczny

Dla pytania o zgłoszenie awarii celem jest uzyskanie listy informacji potrzebnych operatorowi. Kompletność obejmuje wersję systemu, czas, komunikat błędu, wykonane kroki i oczekiwany wynik. Zakaz umieszczania haseł i kluczy jest innym wymaganiem: odpowiedź może podawać pełną listę i mimo to zawierać niebezpieczny dodatek. Dlatego zapisz zakaz jako osobny warunek kontraktu, z odpowiednią regułą krytycznego niepowodzenia.

save_baseline powinno zaakceptować pierwszy zapis i identyczne ponowienie. Przy tej samej ścieżce i top_k zmienionym z 3 na 5 powinno zgłosić wyjątek. Plik bazy pozostaje niezmieniony. Jeżeli chcesz inny tryb albo konfigurację początkową, utwórz osobny katalog eksperymentu.

R2. Parafraza zgłoszenia

Przykładowe nowe pytanie brzmi: „Co mam przygotować, zanim zgłoszę problem z DocAssist?”. Oczekiwanym narzędziem jest answer_from_documents; oczekiwane fakty wynikają z DOC-REG, sekcji „Treść zgłoszenia”. Dokładny tytuł sekcji oraz chunk_id odczytaj z dodatku źródłowego, ponieważ są częścią kontraktu danych.

Twierdzeniem zabronionym może być „dołącz hasło do konta”. Nie dopisuj gwarantowanego czasu rozwiązania, bo regulamin go nie podaje. Nowe sformułowanie nie musi mieć zapisanego embeddingu ani odpowiedzi selektora, więc wykonanie może wymagać Ollama. Sam poprawny walidator nie dowodzi dostępności checkpointu.

Jeżeli dokument nie wspiera planowanego faktu, zmień oczekiwanie przed testem albo świadomie utwórz test braku wiedzy. Nie oczekuj zewnętrznej procedury tylko dlatego, że jest znana z innego modułu.

R3. Fragmenty i grupy

Kontrolowany przykład ma grupy A oraz B, po jednym wymaganym fragmencie. Ranking A, X, Y daje Hit@1=true, Hit@3=true, any_group=true, all_groups=false i group_coverage=0,5. Ranking A, X, Y, B, Z przy top_k=5 daje pełne pokrycie grup. Dodatkowe pozycje mogą zwiększać szum, dlatego poprawa pokrycia nie rozstrzyga oceny całego kontekstu.

Jeśli jedna grupa wymaga jednocześnie A1 i A2, obecność samego A1 daje Hit@k=true, ale jeszcze nie spełnia grupy. To celowo inne pytania metryczne. Gdy lista oczekiwanych grup jest pusta, wyniki Hit@k i pokrycia to N/D. Nie licz takich przypadków jako nietrafionych wyszukiwań.

W rzeczywistym T004 przy top_k=3 brakuje DOC-INSTR-dd6055d675-0003. Przy top_k=5 fragment pojawia się. Poprawny wniosek dotyczy pokrycia w określonym indeksie. Nie można jeszcze napisać, że wariant generuje lepszą odpowiedź.

R4. Odpowiedź częściowa i narzędzie

Demonstracyjne zdanie „Po zmianie konfiguracji uruchom ponownie usługę” ma podstawę w źródle, ale nie spełnia T101. Brak kontroli stanu, interpretacji active, kopii po niepowodzeniu i zgłoszenia administratorowi. Krytyczne pominięcie kopii wymaga critical_error=true i verdict=fail. Poprawność wypowiedzianego zdania nie jest tym samym co kompletność procedury.

W historycznej turze 2 M5 resolver prawidłowo rozwinął zaimek. Bezpośredni dowód problemu jest później: decyzja i wykonane narzędzie wskazują show_last_sources zamiast RAG odpowiadającego na pytanie. Najwcześniejszy potwierdzony etap to tool_selection. Nie ma podstaw, aby przypisywać ten konkretny błąd chunkingowi.

Dla T007 poprawna odmowa jest wynikiem pozytywnym, o ile treść i brak wykonania zgadzają się z kontraktem. Dla T008 prośba o doprecyzowanie przy pustej historii także może być pozytywna. Nie przyznawaj im automatycznie zera za brak procedury.

R5. Hipotezy i eksperyment

Hipotezę „właściwy fragment leży poniżej top_k” sprawdzasz szerszym rankingiem przy stałym pytaniu i indeksie. Jeśli go tam nadal nie ma, trzeba zbadać inne przyczyny. Hipotezę o formacie cytowań sprawdzasz na nowej generacji po jednej zmianie instrukcji, oceniając także znaczenie źródeł. Hipotezę o selektorze sprawdzasz na całym scenariuszu, kontrolując historię i rzeczywistą decyzję.

Offline można wykonać porównanie top_k 3/5 na zapisanych embeddingach. Nie można z tego wywnioskować nowej jakości generowania ani stabilności modelu. Przy trzech ocenach 2, 1, 2 minimum wynosi 1, mediana 2. Jeden błąd krytyczny w trzech próbach nie znika dlatego, że dwie pozostałe zakończyły się dobrze.

Regresja powinna wskazywać konkretne kryterium i wynik. Jeżeli wzrosło pokrycie, ale pogorszył się szum, nie zapisuj po prostu „lepiej”. Przedstaw oba skutki i zastosuj wcześniej ustaloną regułę decyzji.

R6. Wnioski o właściwym zakresie

Przykład o wyszukiwaniu: „W odtworzonym T004 wymagany fragment nie wystąpił w top-3, lecz był obecny po rozszerzeniu do top-5 przy tym samym indeksie”. Dowód powinien wskazywać dwa result_id i plik porównania. Nie zastępuj ich samym test_id.

Przykład o zachowaniu: „W zapisanej turze T007 aplikacja odmówiła wykonania restartu”. Powiąż go z wynikiem i ręczną oceną. Przykład ograniczenia: „Nie wykonano pełnej nowej serii Ollama, więc raport nie ocenia stabilności odpowiedzi T101”. Taki wniosek nie potrzebuje wymyślonego result_id; jego podstawą jest plan i lista wykonanych prób.

Macierz powinna pomagać czytelnikowi, a nie udawać dowód samą obecnością identyfikatora. Jeżeli nie ma zgodnej recenzji albo wynik pochodzi z innego kontraktu, najpierw uzupełnij materiał, potem formułuj wniosek.

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

10. Słownik pojęć i granice interpretacji

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

Testy i dane

PojęcieZnaczenie w M6
BaselineZamrożona konfiguracja stanowiąca punkt odniesienia
Kontrakt testuWejście oraz jawne wymagania i zakazy ustalone przed pomiarem
PrzypadekNiezależny test albo cały scenariusz z własnym test_id
TuraJedna wiadomość użytkownika i jej obsługa
PrzebiegWykonanie serii z własnym run_id
WynikKonkretna obserwacja o własnym result_id
DigestIdentyfikacja zawartości modelu, odrębna od jego tagu
Fingerprint kontraktuSkrót treści używany do wykrycia zmiany oczekiwań
CheckpointZapisane wywołanie odtwarzane przy zgodnym żądaniu
Provenance / pochodzenieInformacja, skąd i z jakich warunków pochodzi rekord

Wyszukiwanie i kontekst

PojęcieZnaczenie w M6
RankingUporządkowane wyniki wyszukiwania przed filtrowaniem
top_kLiczba pozycji pobieranych do dalszego przetwarzania
ScoreMiara podobieństwa wektorowego, nie prawdopodobieństwo poprawności
Próg podobieństwaGranica odrzucania wyników według score
Hit@kObecność co najmniej jednego oczekiwanego fragmentu w pierwszych k pozycjach
expected_rankPozycja pierwszego oczekiwanego fragmentu w zapisanym rankingu
MRR@kŚrednia odwrotność pozycji przy ograniczeniu obserwacji do top-k
Grupa dowodówWymagane fragmenty albo jawny pełny substytut grupy
Pokrycie grupUdział spełnionych grup wymaganych przez kontrakt
Końcowy kontekstMateriał, który pozostał po filtrach i jest podstawą odpowiedzi
SzumTreść zbędna wobec badanego pytania

Ocena i eksperyment

PojęcieZnaczenie w M6
RubrykaZdefiniowane kryteria i znaczenia ocen
NDKryterium lub metryka nie dotyczy przypadku
null w formularzuOcena jeszcze niewypełniona
Błąd krytycznyNaruszenie wymagania dyskwalifikującego wynik mimo innych dobrych ocen
RegresjaPogorszenie wcześniej spełnianego wymagania po zmianie
ZmiennośćRóżnice między nowymi wykonaniami przy ustalonych warunkach
MedianaŚrodkowa wartość uporządkowanych ocen
Macierz dowodówPowiązanie wniosku z testem, wynikiem, recenzją i plikiem
OdtwarzalnośćMożliwość ponownego wykonania opisanej procedury przy znanych warunkach

Czego nie utożsamiać

Dostęp do źródła nie gwarantuje wykorzystania jego treści. Wysokie Hit@k nie gwarantuje pełnego pokrycia. Pełne pokrycie nie gwarantuje poprawnej generacji. Poprawne etykiety nie gwarantują semantycznego oparcia. Status answered nie gwarantuje realizacji intencji. Trzy udane próby nie gwarantują niezawodności we wszystkich zastosowaniach.

Każde z tych zdań wskazuje granicę pomiaru. Znając tę granicę, potrafisz zaproponować następny test i sformułować wniosek, który rzeczywiście wynika z danych.

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

11. Źródła ćwiczeń i mapa pakietu

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

Poniższe teksty są kopią dziesięciu fragmentów z aktualnego chunks.jsonl pakietu Studenta M6. Pozwalają rozwiązywać zadania analityczne bez otwierania prezentacji. Nie zastępują całego katalogu indeksu potrzebnego do uruchamiania kodu. Oceniając własną zmienioną bazę, użyj jej faktycznej wersji zamiast tej kopii.

DOC-INSTR · Ponowne uruchomienie usługi

DOC-INSTR-dd6055d675-0002
Źródło: instrukcja.txt · wersja: 1.4

## Ponowne uruchomienie usługi Po zmianie pliku /etc/docassist/config.yaml wykonaj kolejno: 1. Uruchom ponownie usługę: sudo systemctl restart docassist-api 2. Sprawdź stan: systemctl is-active docassist-api Prawidłowy wynik: active. Uwaga: wykonaj ponowne uruchomienie tylko po zmianie konfiguracji.

DOC-INSTR · Kopia konfiguracji

DOC-INSTR-dd6055d675-0003
Źródło: instrukcja.txt · wersja: 1.4

## Kopia konfiguracji Przed edycją zachowaj kopię pliku konfiguracyjnego w katalogu kopii. Nie zapisuj w zgłoszeniu haseł ani kluczy. W razie nieudanego uruchomienia przywróć zatwierdzoną kopię i zgłoś problem administratorowi.

DOC-REG · Dyżur operatora

DOC-REG-6a742ef524-0002
Źródło: regulamin.md · wersja: 1.4

## Dyżur operatora Operator przyjmuje zgłoszenia w dni robocze od 08:00 do 16:00. Poza tymi godzinami zgłoszenie czeka do następnego dyżuru. Wyjątek: awarię uniemożliwiającą pracę całego zespołu zgłoś dyżurnemu administratorowi zgodnie z wewnętrzną procedurą eskalacji.

DOC-REG · Treść zgłoszenia

DOC-REG-6a742ef524-0003
Źródło: regulamin.md · wersja: 1.4

## Treść zgłoszenia Podaj wersję systemu, czas zdarzenia, komunikat błędu i wykonane kroki. Nie dołączaj danych osobowych ani sekretów. Opisz oczekiwany wynik. Nie podajemy w tym regulaminie gwarantowanego czasu rozwiązania zgłoszenia.

DOC-PDF · Parametry stanowiska

DOC-PDF-51aa0fc583-0002
Źródło: docassist.pdf · wersja: 1.4

## Parametry stanowiska Dla tej fikcyjnej aplikacji przyjęto 8 GB RAM i 2 GB wolnego miejsca. Są to dane ćwiczenia, a nie wymagania modeli językowych. Przed instalacją sprawdź dostęp do zatwierdzonego pakietu.

DOC-PDF · Test po zmianie konfiguracji

DOC-PDF-51aa0fc583-0003
Źródło: docassist.pdf · wersja: 1.4

## Test po zmianie konfiguracji Po zmianie konfiguracji sprawdź uruchomienie usługi i zapisz wynik. Nie usuwaj łącznika w nazwie docassist-api.

DOC-PDF · Parametr oczekiwania

DOC-PDF-7bf28bc22b-0005
Źródło: docassist.pdf · wersja: 1.4

## Parametr oczekiwania Parametr: timeout_seconds. Wartość zalecana w tym przykładzie: 30 s. Zwiększ wartość dopiero po ustaleniu, że operacja trwa dłużej. Nie traktuj tego parametru jako gwarancji czasu odpowiedzi.

DOC-PDF · Zapis wyniku testu

DOC-PDF-7bf28bc22b-0006
Źródło: docassist.pdf · wersja: 1.4

## Zapis wyniku testu Zapisz datę próby, wersję dokumentu i komunikat diagnostyczny. Prawidłowy odczyt powinien zachować jednostkę s i wartość 30.

DOC-HELP · Błąd połączenia

DOC-HELP-cc64f35642-0002
Źródło: pomoc.html · wersja: 1.4

## Błąd połączenia Gdy aplikacja zgłasza brak połączenia, sprawdź stan usługi i adres w konfiguracji. Jeżeli usługa działa, zapisz dokładny komunikat i przekaż go administratorowi. Nie zmieniaj uprawnień plików bez ustalenia przyczyny.

DOC-HELP · Kontrola znaków

DOC-HELP-cc64f35642-0003
Źródło: pomoc.html · wersja: 1.4

## Kontrola znaków Tekst kontrolny: Zażółć gęślą jaźń. Zachowaj nazwy docassist-api i --no-pager oraz jednostki: 25°C, ±5%, ≤10 ms.

Gdzie są potrzebne funkcje

ModułRola i najważniejsze funkcje
m3_toolsload_index, search_local_index, normalize_search_results: indeks, podobieństwo, stabilny format wyników
m4_toolsretrieve, filter_results_for_context, prepare_context_results, build_context, answer_from_results, ask_rag: ścieżka RAG
m5_toolscreate_initial_state, resolve_followup_question, assistant_step oraz narzędzia: rozmowa i kontrola wykonania
m6_toolsconfigure, walidator kontraktów, import_m5, review_template, save_review, join_reviews: referencyjne wsparcie ewaluacji
moj_m6Twoje trzynaście funkcji oraz dostarczone pętle run_retrieval i run_assistant
sprawdz_moj_m6check_stage: kontrola zachowania kolejnych etapów bez nowych wywołań modelu

Własny warsztat korzysta z dokładnie tego samego korpusu, konfiguracji i checkpointów co pakiet. Gdy aktualizujesz pakiet, porównaj jego manifest z manifestem książki i ponów testy warsztatu. Sama zgodna nazwa pliku nie potwierdza zgodności implementacji.

Podstawa opracowania

Książka odpowiada zakresowi M12-M6-v6 oraz kartom i notebookom pakietu M6. Zachowuje dydaktyczny sposób prowadzenia czytelnika z książki M1: objaśnienie, przykład, ćwiczenie i samokontrola. Długość i podział zostały dopasowane do ewaluacji i dokumentacji, a nie do liczby stron M1.

Przykłady historyczne pochodzą z pełnego R6 M5, którego plik źródłowy i suma kontrolna znajdują się w dane/import_M5. Przykład T004 pochodzi z odtworzenia indeksu pakietowego. Kontrolowane przykłady A/B i oceny 2,1,2 są ilustracjami działania miary lub reguły, nie wynikami nowego wywołania LLM.

Do czytania nie są potrzebne strony zewnętrzne. Instrukcja publikacji dołączona osobno do paczki odwołuje się do oficjalnej dokumentacji Moodle i kodu importera. Ustawienia techniczne publikacji nie zmieniają kontraktów ćwiczeń.

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