Strona główna / Artykuły

Audyt procesu wyszukiwania w RAG pod kątem przestarzałych fragmentów danych, odchyleń oraz zanieczyszczonego kontekstu.

Audyt w Pythonie dla wyszukiwania w RAG: złote zapytania, bramy powiązania kosinowego, sprawdzanie odchyłek Kolmogorowa-Smirnowa oraz efektywność kontekstu, plus jak czytać raport.

Wydawca: REACTAPP.TOP12 min czytania2728 słów

Źródła: I Built a RAG System That Audits Itself. Here’s How (With Code).

Cichy rodzaj awarii RAG

Wyobraź sobie bota obsługi klienta, który informuje klienta, że okno zwrotu wynosi 30 dni, podczas gdy w rzeczywistości jest to 14 dni. Dokument z zasadami został zmodyfikowany w styczniu, a indeks embedding został ostatnio edytowany w październiku. Nikt nie ucierpiał i nie doszło do żadnego pozwu, ale niepokojące jest to, że nikt nie wie, od jak dawna wysyłane są błędne odpowiedzi. Tydzień? Trzy miesiące? Bez możliwości sprawdzenia nie da się tego stwierdzić.

To właśnie ta awaria zasługuje na uwagę – i to nie jest ta dramatyczna, w której model tworzy halucynacje lub mówi bez sensu. To ta cicha awaria. Wykresy pokazują pozytywny wynik, a niewielka grupa użytkowników jest wprowadzana w błąd od miesięcy, bez żadnego ostrzeżenia.

Większość zespołów tworzących systemy RAG mierzy niewłaściwe rzeczy. Pytają, czy ostateczna odpowiedź brzmi dobrze, pomijając przy tym kwestię jakości materiałów, z których została ona utworzona. Głównym powodem jest brak prostego narzędzia do tego celu. Poniżej przedstawiam takie narzędzie: skrypt w Pythonie, który automatycznie sprawdza warstwę pozyskiwania danych, zbudowany z bibliotek otwartego oprogramowania oraz pliku JSON z zapytaniami testowymi; jego konfiguracja zajmuje około 30 minut i nie wymaga żadnej platformy do monitorowania ani specjalistycznej wiedzy statystycznej.

Zwracaj uwagę na proces pozyskiwania danych, a nie na model

Gdy system RAG zachowuje się niewłaściwie, pierwszą reakcją jest obwinianie modelu: błędny prompt, niewłaściwa temperatura, zła wersja. Po dostosowaniu promptu lub przejściu na nowszy model sytuacja poprawia się na kilka tygodni, po czym problem powraca, ponieważ prawdziwy winowajca znajduje się gdzie indziej. Łańcuch operacji w systemach produkcyjnych zazwyczaj przebiega w ten sposób:

Dokument się zmienia, ale indeks embeddingu nie jest odświeżany. Łączna sekcja danych starych informacji zostaje pobraana, model LLM odpowiada z pewnością siebie na podstawie przestarzałego kontekstu, odpowiedź jest błędna, a nikt nie potrafi wytłumaczyć dlaczego.

Model nie kłamie. Robi dokładnie to, co mu kazano, czyli generuje odpowiedź na podstawie dostarczonych sekcji danych. Jeśli podasz mu dane sprzed sześciu miesięcy, odpowiedź również będzie sprzed sześciu miesięcy, podana płynnie i z bezbłędną gramatyką.

Kluczowy problem leży w standardowej metodzie oceny, która jest przeprowadzana od początku do końca: wprowadza się zapytanie, a oceniana jest odpowiedź, która pojawia się w wyniku. RAGAS, będący jednym z najpowszechniej stosowanych frameworki oceny, został opracowany w oparciu o cały proces i ocenia precyzję kontekstu, wierność oraz istotność odpowiedzi (Es i in., 2023, EACL 2024). Jest to solidny framework. Mimo to ocenia on cały system jako całość, a nie sam warstwę wyszukiwania, więc dobra ocena całego procesu może maskować powolne pogorszenie efektywności wyszukiwania, gdy model czasami ma szczęście i może to skompensować.

Pod tym problemem kryje się jeszcze jeden. Liu i współpracownicy poinformowali w 2024 roku, że modele językowe radzą sobie znacznie gorzej, gdy istotne informacje znajdują się w środku długiego kontekstu, nawet w przypadku modeli wyszkolonych specjalnie do obsługi długich danych wejściowych. Dlatego pobieranie pięciu fragmentów, z których trzy są jedynie częściowo istotne, negatywnie wpływa na funkcjonowanie systemu, a zasoby poświęcone im obniżają jakość odpowiedzi. Żaden z tych problemów nie występuje, chyba że bezpośrednio sprawdzi się warstwę pobierania danych, co jest właśnie planem.

Trzy sposoby awarii, których nie dostrzegają standardowe metryki

Zatruwanie fragmentów

Nazwa „chunk poisoning” nie pochodzi z żadnego artykułu naukowego. Opisuje fragmenty tekstu, które wydają się istotne, ponieważ ich sformułowania pokrywają się z zapytaniem, dzięki czemu uzyskują wysokie pozycje w wyszukiwaniu wektorowym, mimo że nie zawierają odpowiedzi. Częstą przyczyną jest podział tekstu w taki sposób, że słowa kluczowe trafiają do jednego fragmentu, a rzeczywista treść do drugiego. Fragment bogaty w słowa kluczowe uzyskuje dobre wyniki w pomiarze podobieństwa kosinowego, przydatny fragment nie trafia do czołowych wyników, a model tworzy wiarygodną, lecz błędną odpowiedź.

Przestarzała treść

Załóżmy, że FAQ zaktualizowało swoją politykę zwrotów pieniędzy w styczniu, a indeks został ostatni raz zaktualizowany w październiku. Stary fragment z polityką nadal pojawia się w wynikach zapytań dotyczących zwrotów i nadal jest uznawany przez model za „istotny”. Użytkownicy w rezultacie spotykają się z sprzecznościami ze strony zespołu obsługi, podczas gdy wskaźniki nic nie wskazują.

Drift embeddingów

Postępuje powoli i kumulatywnie, co czyni go najniebezpieczniejszym z tych trzech. Model embeddingowy rejestruje korpus w stanie, w jakim znajdował się w momencie tworzenia indeksu. Każda edycja dokumentu, każdy nowy termin pojawiający się w dziedzinie oraz każda zmiana w sposobie formułowania zapytań przez użytkowników nieco obniżają dokładność tego zapisu. Średnia podobieństwo kosinowe może spaść z 0,82 do 0,74 w ciągu sześciu miesięcy. Nic się nie psuje, nie wydaje się żadne ostrzeżenie, a degradacja jest widoczna dopiero wtedy, gdy odpowiedzi wyraźnie się pogarszają – w tym momencie system jest już o kilka miesięcy w tyle.

Szum w kontekście również ma swoje konsekwencje. Przy pięciu fragmentach w oknie, z których trzy są nieistotne, marnuje się tokeny, co negatywnie wpływa na jakość, ponieważ treść znajdująca się pośrodku długiego kontekstu otrzymuje mniej uwagi niż treść na jego brzegach, jak udokumentowali Liu i współpracownicy.

Co obejmuje audyt

Trzy sprawdzenia odpowiadają tym trzem modelem awarii.

  1. Relevancja i liczba trafień. Dla każdej zapytania w zestawie testowym skrypt pobiera najważniejsze K fragmentów i oblicza podobieństwo kosinowe pomiędzy wektorem reprezentującym zapytanie a wektorem każdego z fragmentów. Sprawdza również, czy fragmenty, które według danych powinny zawierać odpowiedź, faktycznie się pojawiły. Jeśli tak nie jest, zapytanie jest oznaczone jako nieudane.
  2. Odchylenie od linii bazowej. Każdy przebieg testu przechowuje swoją rozkład liczb ocen podobieństwa, a późniejsze przebiegi porównują ten nowy rozkład z zapisaną linią bazową za pomocą testu Kolmogorowa-Smirnowa. Jeśli oba rozkłady znacznie się różnią, wyświetla się alert o odchyleniu. Nie trzeba analizować matematyki: wartość p poniżej 0,05 oznacza jedynie, że coś się zmieniło i wymaga dalszej analizy.
  • Efektywność kontekstu. Skrypt mierzy, jaki ułamek pobranych tokenów pojawia się w odpowiedzi modelu. Wysoki ułamek oznacza precyzyjne i istotne wybieranie informacji, natomiast niski oznacza, że szum wypełnia kontekst. Wartość poniżej 40% jest powodem do dalszych badań.
  • Audyt generuje ustrukturyzowany raport w formacie JSON. Wystarczy cotygodniowy zlecenie typu cron, z uwagą poświęconą wskaźnikowi flag oraz analizie odchyleń. Większość systemów RAG nigdy nie miała takiego poziomu monitoringu.

    Złota lista zapytań

    Zanim zostanie napisany jakikolwiek kod, potrzebna jest złota lista zapytań: plik JSON zawierający reprezentatywne zapytania, z których każde jest sparowane z identyfikatorami fragmentów, które powinny pojawić się w ich wynikach.

    [
      {
        "query": "What is the refund policy for digital products?",
        "expected_chunk_ids": ["faq_doc_chunk_12", "faq_doc_chunk_13"],
        "notes": "Customer FAQ, policy updated 2024-Q1"
      },
      {
        "query": "How do I reset my API key?",
        "expected_chunk_ids": ["api_docs_chunk_07"],
        "notes": "API documentation, stable"
      }
    ]
    

    Rozmiar ma znaczenie. Na początku wydawało się, że dwadzieścia zapytań to wystarczy, ale tak nie jest, ponieważ test odchylenia wymaga większej liczby punktów danych, a 20 daje rozkład zbyt rzadki, by można mu było ufać. Uznaj 50 za minimalną wartość, a 100 lub więcej za punkt, w którym sygnały odchylenia stają się istotne.

    Zacznij od wybrania najczęściej występujących zapytań użytkowników z dzienników aplikacji. Ręcznie przeprowadź wyszukiwanie dla każdego z nich, sprawdź wyniki i zapisz ID fragmentów, które znajdują się na najwyższych pozycjach. To zadanie jest monotonne i zajmuje kilka godzin, jednak jest to jedyna część, której nie można zautomatyzować, a na niej opiera się cała reszta. Niektóre nawyki pomagają:

    • Rozmieszczaj zapytania w różnych kategoriach tematycznych zamiast grupować je wokół jednego dokumentu.
    • Dodaj kilka zapytań, które w ogóle nie powinny zwracać żadnych wyników, ponieważ przykłady negatywne pomagają wykryć fałszywie pozytywne wyniki wyszukiwania.
    • Odśwież zbiór złoty, gdy korpus ulegnie znacznym zmianom, a nie w sposób ciągły. Jest to stabilny punkt odniesienia, a nie aktualny zbiór danych.

    Struktura projektu i główny skrypt

    Repozytorium jest zorganizowane w następujący sposób:

    rag-retrieval-audit/
    ├── audit/
    │   ├── __init__.py           # Package exports
    │   ├── rag_audit.py          # Main audit logic — all three checks
    │   └── config.py             # All thresholds and model settings
    ├── examples/
    │   ├── golden_queries.json   # Sample golden set (10 queries)
    │   └── run_audit.py          # End-to-end demo — runs without an existing collection
    ├── tests/
    │   └── test_audit.py         # Unit tests for all scoring functions
    ├── requirements.txt          # sentence-transformers, chromadb, scipy, numpy
    └── README.md                 # Setup, usage, how to read the report
    

    Oto struktura pliku rag_audit.py, który zawiera wszystkie trzy sprawdzenia. Jest to zarys służący do orientacji, a nie pełna implementacja.

    # rag_audit.py — structure overview
    # Full implementation: https://github.com/satyam671/rag-retrieval-audit
    
    # ── CONFIGURATION (tune to your pipeline)
    EMBEDDING_MODEL      = "all-MiniLM-L6-v2"  # must match your index
    TOP_K                = 5
    RELEVANCE_THRESHOLD  = 0.70
    DRIFT_P_THRESHOLD    = 0.05
    EFFICIENCY_THRESHOLD = 0.40
    
    # ── CHECK 1: Are the right chunks coming back?
    def score_relevance(query_embedding, chunk_embeddings, expected_ids, retrieved_ids):
        """Cosine similarity per chunk + expected chunk hit/miss against golden set."""
        ...
    # ── CHECK 2: Has retrieval quality shifted over time?
    def detect_drift(current_sims, baseline_path=None):
        """Two-sample KS test comparing current similarity distribution to baseline."""
        ...
    # ── CHECK 3: How much of the context window is signal?
    def score_efficiency(retrieved_docs, answer):
        """Token overlap between retrieved chunks and the LLM answer."""
        ...
    # ── RUNNER
    def run_audit(golden_set_path, collection_name, chroma_persist_dir,
                  baseline_path=None, answers_path=None, output_path="rag_audit_report.json"):
        """Runs all three checks, writes a structured JSON report, saves the baseline."""
        ...
    

    W dokumencie README skryptu wyjaśniono, jak go sklonować, skierować na kolekcję ChromaDB i uruchomić, w tym jak zastąpić go innym magazynem wektorowym.

    Przebieg każdej weryfikacji

    Ocena istotności fragmentów

    Weryfikacja istotności wykonuje dwie czynności, których ludzie często nie zauważają podczas samodzielnego jej budowania.

    Najpierw oblicza podobieństwo kosinowe bezpośrednio na podstawie embeddingów, nie używając wartości odległości zwracanych przez ChromaDB. Domyślną miarą w ChromaDB jest L2, a nie podobieństwo kosinowe, i te dwa parametry nie są ze sobą zamienialne. Ich mieszanie daje wyniki, które wydają się sensowne, ale mierzą coś innego.

    Po drugie, potwierdza to, że oczekiwane ID fragmentów złotego zestawu pojawiły się w grupie top-K, a to właśnie ujawnia problem zatruwania fragmentów. Średnia podobieństwo na poziomie 0,75 wydaje się w porządku, dopóki nie dowiadujemy się, że fragment przeznaczony do odpowiedzi na zapytanie uzyskał wynik 0,62 i zajął szóste miejsce, czyli jedno miejsce poza progiem. Model nigdy nie uzyskał prawidłowej odpowiedzi, mimo że wartości podobieństwa wciąż wyglądały na akceptowalne.

    Praktyczna różnica między stosowaniem mechanizmu gating a jego brakiem jest wyraźna w kodzie.

    # WITHOUT the relevance gate — standard approach most teams use
    def build_context_naive(query, collection, top_k=5):
        results = collection.query(
            query_embeddings=[embed(query)],
            n_results=top_k,
            include=["documents"]
        )
        # Pass everything back, no quality check
        return "\n\n".join(results["documents"][0])
    
    # WITH the relevance gate - what the audit tells you to build
    def build_context_gated(query, collection, model, top_k=5, threshold=0.70):
        q_emb   = model.encode([query], normalize_embeddings=True)[0]
        results = collection.query(
            query_embeddings=[q_emb.tolist()],
            n_results=top_k,
            include=["documents", "embeddings", "ids"]
        )
        passed_chunks = []
        for i, chunk_emb in enumerate(results["embeddings"][0]):
            sim = cosine_sim(q_emb, np.array(chunk_emb))
            if sim >= threshold:
                passed_chunks.append(results["documents"][0][i])
        # Empty context is better than wrong context
        return "\n\n".join(passed_chunks)
    

    W wersji naiwnej najwyżej sklasyfikowane fragmenty są uwzględniane bez względu na to, jak dobre są. W wersji z filtrem każdy fragment, który nie zasłużył na swoje miejsce, zostaje odrzucony. Odpowiedź „Nie mam wystarczających informacji, by na to odpowiedzieć” w pustym kontekście jest lepsza od pewnej siebie, błędnej odpowiedzi opartej na szumie.

    Wykrywanie odchyłek w embeddingach

    Sprawdzenie odchyłek opiera się na teście Kolmogorowa-Smirnowa. Porównuje on kształt obecnej rozkładu podobieństwa z zapisaną bazą i zaznacza istotne odchylenia. Dlaczego rozkłady, a nie średnie wartości? Ponieważ jedna liczba ukrywa zbyt wiele informacji. Średnia może pozostać na poziomie 0,78, podczas gdy rozkład pod nią staje się płaski i rozprzestrzenia się, co oznacza, że niektóre zapytania stają się znacznie gorsze, podczas gdy inne to kompensują. Test KS wykrywa zmiany w rozkładzie, których nie dostrzegają średnie wartości.

    Wykres przykładowy pokazuje zmiany w sposobie embedowania w produkcyjnym systemie RAG sześć miesięcy po aktualizacji korpusu bez ponownego indeksowania: statystyka KS wynosi 0,21, a wartość p wynosi 0,003.

    Jedna uwaga: test wymaga wystarczającej ilości danych, aby być wiarygodny. Przy 15 zapytaniach wartości p są niestabilne i mogą prowadzić do fałszywego poczucia pewności, natomiast 50 lub więcej zapytań daje stabilny sygnał. To kolejny powód, dla którego rozmiar zbiórki danych ma większe znaczenie, niż się wydaje.

    Efektywność okna kontekstowego

    To najtrudniejsza z trzech weryfikacji, ponieważ istnieją rzeczywiste ograniczenia. Przekrycie na poziomie słów pomiędzy pobranymi fragmentami a odpowiedzią modelu jest jedynie wskaźnikiem. Gdy model mocno parafrazuje, jak to robią modele typu GPT-4, takie przekrycie niedoszacowuje faktycznego wykorzystania kontekstu.

    Mimo to wykrywa systematyczne problemy. Jeśli wyniki efektywności stale są poniżej 40%, coś w wcześniejszych etapach jest nie tak: fragmenty są zbyt duże, wartość top-K jest zbyt wysoka, lub proces wyszukiwania przywołuje teksty powiązane tematycznie, które nie odpowiadają na zapytanie. Ta weryfikacja nie jest precyzyjna, ale podaje właściwy kierunek, a to wystarcza, by rozpocząć dochodzenie.

    Czytanie raportu

    Wyniki znajdują się w pliku JSON, a na następujące pola należy zwrócić uwagę:

    • flag_rate to ułamek zapytań, które spowodowały błąd. Wartość powyżej 20% oznacza, że proces wyszukiwania wymaga uwagi, a gwałtowny wzrost z 8% w jednym tygodniu na 22% w następnym wskazuje na zmianę.
    • drift_analysis.drift_detected to wartość logiczna, na którą należy zwrócić uwagę. Jeśli ma wartość true, należy sprawdzić mean_delta, aby określić kierunek zmian. Ujemna wartość delta oznacza spadek podobieństwa, co jest sygnałem do ponownego indeksowania.
    • drift_analysis.ks_statistic rośnie wraz z wielkością przesunięcia rozkładu. Wartość poniżej 0,10 to prawdopodobnie szum. Wartość powyżej 0,20 wskazuje na rzeczywisty problem, niezależnie od wartości p.
    • query_results[i].relevance.expected_chunk_hit to prawdziwa wartość dla każdej zapytania. Wartość false oznacza, że fragment, który powinien był odpowiedzieć na zapytanie, nie znalazł się w gronie top-K. Jeśli tak się dzieje w przypadku ponad 15% elementów z zestawu referencyjnego, należy ponownie utworzyć indeks.

    Przykład uruchamia się za pomocą jednego polecenia, pokazanego tutaj.

    python -m examples.run_audit
    

    Arkusz pomocny streszcza audyt w jednym obrazie.

    Uruchamiaj go co tydzień i przechowuj wszystkie wartości wyjściowe. Trend pomiędzy poszczególnymi wykonyaniami mówi więcej niż pojedyncze wykonywanie.

    Lekcja o wielkości próby

    Przeprowadzenie detekcji odchylenia na zestawie „złotym” składającym się z 15 zapytań dało wartości p, które nie miały sensu. Test technicznie się odbywał, ale w niewiarygodny sposób, i łatwo było stracić dwa dni, podejrzewając błędy w implementacji statystycznej, zanim zdało się sprawę, że dane są po prostu zbyt skąpe.

    Gdy liczba zapytań jest mniejsza niż 50, wyniki dotyczące odchylenia nie są wiarygodne, dlatego najpierw należy stworzyć zestaw, a dopiero potem włączyć kontrolę odchylenia. Korpus obejmujący kilka oddzielnych dziedzin tematycznych wymaga jednego zestawu „złotego” na każdą dziedzinę, który jest audytowany osobno, ponieważ rozkład danych w złożonym korpusie może ukrywać spadek w danej dziedzinie, który byłby widoczny, gdyby rozpatrywać go osobno.

    Ograniczenia tego podejścia

    Skrypt sprawdza, czy pobrano właściwe fragmenty. Nie ocenia on, czy granice fragmentów są dobrze umieszczone, czy rozmiar fragmentów odpowiada typom zapytań, ani czy semantyczne dzielenie na fragmenty powinno zastąpić dzielenie o stałym rozmiarze.

    To pytania mają znaczenie. Testy NVIDIA na pięciu zestawach danych, podsumowane w analizie FloTorch, wykazały, że najlepszy rozmiar fragmentów zmienia się w zależności od typu zapytania: zapytania faktograficzne osiągają maksymalnie 256 do 512 tokenów, natomiast zapytania analityczne wymagają 1024 lub więcej. Każdy pojedynczy rozmiar fragmentów stanowi kompromis, a idealny zestaw łączący oba typy zapytań skutkuje średnią, która uwzględnia podstawowe niezgodności. Bezpośrednie testowanie strategii dzielenia na fragmenty oraz porównywanie podejścia semantycznego z dzieleniem o stałym rozmiarze przy użyciu rzeczywistych danych to odrębne zadania.

    Odnośniki

    • Artykuł RAGAS na temat automatycznej oceny generacji wzbogaconej o wyszukiwanie informacji (arXiv 2309.15217, 2023; EACL 2024).
    • Badanie „zagubione pośrodku” dotyczące zachowania modeli językowych w długich kontekstach, opublikowane w tomie 12 TACL w 2024 roku.
    • Badania aplikacyjne NVIDIA nad strategiami dzielenia tekstu na fragmenty w różnych typach dokumentów, z 2024 roku.