Naprawianie cichych błędów RAG: pobieranie danych, dzielenie na fragmenty, filtry i oceny
Odpowiedzi RAG mogą brzmieć poprawnie, a mimo to być błędne. Przewodnik inspirowany Pythonem do analizy procesów pobierania danych, dzielenia ich na fragmenty, filtrowania metadanych, oceny wyników, wyszukiwania hybrydowego oraz uzasadniania odpowiedzi.
Źródła: RAG Is Failing Quietly: A Debugging Playbook for Python Teams
Błędy, których nikt nie widzi
Najbardziej irytujące błędy w systemach RAG to nie te głośne. Błędy głośne są proste: bot przyznaje, że nic nie znalazł, baza wektorowa jest niedostępna, zakończyła się niepowodzeniem operacja generowania embeddingów lub odpowiedź przychodzi bez źródeł.
Błędy ciche są droższe w naprawie. Dokumenty są odzyskiwane, odpowiedź jest czytelna, źródła wyglądają wiarygodnie, a panel sterowania pokazuje pomyślne odzyskanie danych. Wtedy ekspert z danej dziedziny czyta odpowiedź i stwierdza, że jest błędna. Zazwyczaj wtedy zespół uświadamia sobie, że naprawiał niewłaściwy poziom systemu.
RAG to nie jedna funkcja. To seria decyzji, które nakładają się na siebie. Ktoś decyduje, jak pytanie zostanie odczytane i przepisane, gdzie podzielić dokumenty, który mechanizm wyszukiwania zostanie użyty, jakie metadane ograniczą listę kandydatów, jak ponownie uporządkować krótką listę, co ostatecznie trafi do promptu, jak model będzie się zachowywał, gdy dowody są skąpe, oraz czy napisana odpowiedź rzeczywiście wynika z tego, co zostało dostarczone. Patrzenie tylko na końcowy prompt oznacza przybycie po tym, jak szkoda już została wyrządzona.
Proces debugowania
Rzeczywisty system przypomina bardziej to, co opisano poniżej, niż uporządkowany schemat na slajdzie:
flowchart LR
A[User question] --> B[Query rewrite]
B --> C[Retriever]
C --> D[Reranker]
D --> E[Evidence pack]
E --> F[Answer generator]
F --> G[Verifier]
G --> H[Final answer]
C --> I[Trace log]
D --> I
E --> I
F --> I
G --> I
Każda część systemu może zawieść. Generator bierze na siebie winę, ponieważ jest najbardziej widocznym etapem, ale wiele awarii zaczyna się wcześniej w łańcuchu przetwarzania. Mechanizm wyszukiwania może pobrać niewłaściwy okres czasowy. Narzędzie do ponownego rankowania może promować nieistotny fragment tekstu podobnego do szukanego. Pakiet dowodów może pominąć jedyne przydatne akapit ze względu na zbyt ograniczony budżet tokenów. Skuteczne debugowanie polega na upewnieniu się, że każdy etap jest widoczny.
Awaria 1: podobny tekst nie jest przydatnym dowodem
Wyszukiwanie wektorowe wykrywa podobieństwo semantyczne, które nie zawsze odpowiada istotności zadania. Weźmy przykład użytkownika pytającego, czy klienci korporacyjni mogą otrzymać zwrot pieniędzy po aktualizacji polityki w drugim kwartale. Mechanizm wyszukiwania może zwrócić fragment dotyczący polityki zwrotów tylko dlatego, że zawiera słowa „korporacja”, „zwrot” i „polityka”. Taki fragment może być przestarzały, niekompletny lub niepowiązany z drugim kwartałem.
Przydatny zapis pokazuje ID pobranego dokumentu, jego wersję, datę wejścia w życie, wynik podobieństwa, wynik ponownego rankingu, zastosowane filtry metadanych oraz to, czy ostateczna odpowiedź na niego powołała się. Jeśli logi podają jedynie wynik taki jak 0,83, błąd nie może zostać zdiagnozowany.
Błąd 2: dzielenie na fragmenty zniszczyło znaczenie
Dzielenie na fragmenty to nie kwestia przechowywania danych – to ono decyduje o jakości odpowiedzi. Polityka zwrotów pieniędzy może być zorganizowana według kryteriów uprawniających do zwrotu, wyjątków, dodatkowych zapisów dla przedsiębiorstw, dat wejścia w życie oraz różnic regionalnych. Jeśli narzędzie do dzielenia na fragmenty oddzieli datę wejścia w życie od wyjątków, odpowiedź może cytować akapit dotyczący kryteriów uprawniających, pomijając warunek, który zmienia wynik.
Lepsze fragmenty zawierają stabilny identyfikator dokumentu, nagłówek sekcji, wersję, datę, wystarczająco dużo otaczającego tekstu, aby zachować znaczenie, oraz odnośnik do oryginału. Nic z tego nie uzasadnia stosowania ogromnych fragmentów, które jedynie dodają szumu. Celem są fragmenty, które zachowują moment, w którym następuje decyzja.
Błąd 3: brak filtrów metadanych
Wiele systemów RAG zaczyna jako czyste wyszukiwanie semantyczne. Działa to dobrze, dopóki ten sam temat nie pojawia się w różnych wersjach, produktach, użytkownikach, regionach lub grupach uprawnień. Pytanie dotyczące obecnej polityki zwrotów w firmie nie powinno zmuszać systemu do przeszukiwania każdego dokumentu zwrotu, jaki kiedykolwiek został napisany. Najpierw powinien zostać zastosowany filtr.
from dataclasses import dataclass
from datetime import date
@dataclass(frozen=True)
class SearchFilters:
product: str | None
customer_tier: str | None
region: str | None
as_of: date
permission_group: str
def build_filters(user_context: dict) -> SearchFilters:
return SearchFilters(
product=user_context.get("product"),
customer_tier=user_context.get("tier"),
region=user_context.get("region"),
as_of=date.today(),
permission_group=user_context["permission_group"],
)
Kolejność ma znaczenie. Zastosuj filtry dotyczące uprawnień i aktualności przed poproszeniem modelu o rozumowanie. Pobieranie prywatnego lub przestarzałego kontekstu w nadziei, że prompt to naprawi, to zły pomysł.
Błąd 4: zbiór oceny dla scenariuszy awarii
Naj szybszym sposobem na ulepszenie systemu RAG jest mały, celowo dobrany zbiór do oceny. Pomiń tysiące przykładów. Zacznij od około trzydziestu, które odzwierciedlają rzeczywiste sposoby awarii systemu:
- pytania, których odpowiedź nie znajduje się w korpusie
- pytania wymagające dwóch źródeł
- pytania, w których stara i nowa polityka się kolidują
- pytania o podobnej sformułowaniu dotyczące różnych produktów
- pytania zależne od tabeli
- pytania, w których właściwą odpowiedzią jest przekazanie zadania człowiekowi
Niewielki mechanizm wyszukiwania mógłby wyglądać w ten sposób:
from dataclasses import dataclass
@dataclass(frozen=True)
class RagCase:
question: str
required_doc_ids: set[str]
forbidden_doc_ids: set[str]
def evaluate_retrieval(cases: list[RagCase], retrieve) -> dict:
total = len(cases)
hit = 0
leaked_forbidden = 0
for case in cases:
results = retrieve(case.question)
retrieved_ids = {item["doc_id"] for item in results}
if case.required_doc_ids & retrieved_ids:
hit += 1
if case.forbidden_doc_ids & retrieved_ids:
leaked_forbidden += 1
return {
"cases": total,
"required_hit_rate": hit / total,
"forbidden_leak_rate": leaked_forbidden / total,
}
Jest celowo prosty. Nie określa jakości odpowiedzi, ale wykrywa wiele błędów przy wyszukiwaniu, zanim model zobaczy jakikolwiek kontekst.
Błąd 5: ocenianie odpowiedzi bez dowodów
Odpowiedź typu RAG powinna być oceniana na podstawie faktycznie dostarczonych dowodów, a nie całego korpusu. Ta różnica ma znaczenie z trzech powodów. Jeśli prawidłowa odpowiedź istnieje w bazie wiedzy, ale nie została odnaleziona, nie można oczekiwać, że generator ją zna – to jest błąd przy wyszukiwaniu. Jeśli prawidłowe dowody zostały odnalezione, a odpowiedź je zignorowała, to jest błąd przy generowaniu. Jeśli odpowiedź powołuje się na źródło, które nie potwierdza jej twierdzenia, to jest błąd w uzasadnieniu. Do wszystkich trzech przypadków potrzebne są etykiety.
@dataclass(frozen=True)
class AnswerEval:
question: str
answer: str
evidence_doc_ids: set[str]
expected_claims: set[str]
def simple_claim_check(eval_case: AnswerEval) -> dict:
answer_lower = eval_case.answer.lower()
missing = [
claim
for claim in eval_case.expected_claims
if claim.lower() not in answer_lower
]
return {
"passed": len(missing) == 0,
"missing_claims": missing,
"evidence_count": len(eval_case.evidence_doc_ids),
}
Ustawienia produkcyjne mogą wprowadzać ludzką weryfikację lub ocenę za pomocą modeli językowych w przypadku bardziej subtelnych twierdzeń, ale nawet szybka weryfikacja twierdzeń jest lepsza od niejasnego przyzwolenia.
Lepszy ślad
Każda odpowiedź powinna pozostawić po sobie uporządkowany ślad:
import time
import uuid
from dataclasses import dataclass, field
@dataclass
class RagTrace:
run_id: str = field(default_factory=lambda: str(uuid.uuid4()))
started_at: float = field(default_factory=time.time)
query: str = ""
rewritten_query: str | None = None
filters: dict = field(default_factory=dict)
retrieved: list[dict] = field(default_factory=list)
evidence_doc_ids: list[str] = field(default_factory=list)
prompt_tokens: int = 0
completion_tokens: int = 0
verifier_result: str | None = None
latency_ms: int | None = None
def finish_trace(trace: RagTrace) -> RagTrace:
trace.latency_ms = int((time.time() - trace.started_at) * 1000)
return trace
Dzięki niemu na praktyczne pytania znajdują się odpowiedzi. Czy wyszukano właściwą kolekcję? Czy filtry usunęły odpowiednie dokumenty? Czy system odnajdywania dostarczył oczekiwanego źródła? Czy zestaw dowodów zawierał zbyt dużo kontekstu? Czy sprawdzający zauważył brakcytowane źródła? Czy opóźnienie wynikało z procesu odnajdywania, generowania czy weryfikacji? Bez odpowiedzi na te pytania nie ma jeszcze systemu RAG – jest tylko prompt z przypisaną polami wyszukiwania.
Hybrydowe wyszukiwanie jest często najsłabszym rozwiązaniem
Zespoły często zbyt szybko przechodzą od zwykłego wyszukiwania wektorowego do systemu RAG opartego na agencjach. Najpierw warto spróbować hybrydowego podejścia do odnajdywania danych. Wyszukiwanie semantyczne sprawdza się, gdy użytkownik i dokument używają różnych słów. Wyszukiwanie według słów kluczowych jest skuteczne tam, gdzie ważna jest dokładność dosłownych ciągów znaków, takich jak identyfikatory, kody błędów, nazwy pól lub klauzule w tekście prawniczym. Prawdziwe systemy często wymagają jednocześnie obu tych metod.
def hybrid_rank(vector_results: list[dict], keyword_results: list[dict]) -> list[dict]:
scores: dict[str, float] = {}
items: dict[str, dict] = {}
for rank, item in enumerate(vector_results, start=1):
doc_id = item["doc_id"]
scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (rank + 10)
items[doc_id] = item
for rank, item in enumerate(keyword_results, start=1):
doc_id = item["doc_id"]
scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (rank + 10)
items[doc_id] = item
return sorted(
items.values(),
key=lambda item: scores[item["doc_id"]],
reverse=True,
)
Ten styl łączenia oparty na wzajemnym rankingu nie jest skomplikowany, ale przydatny. Nagradza dokumenty, które pojawiają się w obu listach wyników, jednocześnie pozwalając każdemu z narzędzi wyszukiwania na ujawnienie czegoś ważnego.
Kiedy agentic retrieval się opłaca
Agentic RAG nadaje się do zadań wymagających dokładniejszej analizy. Dobrymi przykładami są przypadki, gdy pierwsza próba często nie dostarcza wystarczających dowodów, pytania obejmujące kilka dokumentów, procesy decyzyjne między SQL, wyszukiwaniem wektorowym a wywołaniami API, modele, które muszą sprawdzić, czy mają wystarczające dane, oraz procesy, które korzystają z ponownych prób z innymi zapytaniami.
Nieodpowiednie przypadki to proste odpowiedzi z FAQ, szybkie autodopisywanie, stabilne wyszukiwanie dokumentacji oraz wszystko, gdzie każde dodatkowe wywołanie modelu jest zbyt kosztowne. Problem musi uzasadniać użycie agentic retrieval. Jeśli filtr metadanych może go rozwiązać, należy go użyć.
Lista kontrolna dla produkcji
Zanim zaczniesz obwiniać model, sprawdź najpierw te kwestie:
- Czy dokumenty mają wersje?
- Czy uprawnienia są sprawdzane przed pobieraniem?
- Czy przestarzałe dokumenty są domyślnie wykluczane?
- Czy fragmenty tekstu są dobierane pod kątem znaczenia, a nie tylko liczby znaków?
- Czy dokładne terminy są uwzględniane w wyszukiwaniu według słów kluczowych?
- Czy tabele i obrazy są obsługiwane oddzielnie?
- Czy oceny obejmują przypadki braku odpowiedzi?
- Czy ID pobranych dokumentów są rejestrowane?
- Czy źródła są weryfikowane?
- Czy jakość odpowiedzi jest mierzona oddzielnie od jakości wyszukiwania?
Większość problemów z RAG nie jest tajemnicza – po prostu nie są zauważane.
Gdzie pęka łańcuch dowodów
RAG sprawia, że systemy LLM wydają się bardziej stabilne, ale to stabilizacja nie następuje automatycznie. Model może korzystać wyłącznie z dowodów, które otrzymuje. Narzędzie do wyszukiwania może znaleźć tylko to, co zostało prawidłowo zindeksowane. Indeks może dostarczać jedynie te metadane, które zostały zachowane. Oceniający może wykryć tylko te błędy, które ktoś zadał sobie trud opisać. Ta praca jest mniej efektowna niż demo, ale to właśnie w niej buduje się zaufanie.
Dlatego gdy system RAG działa nieskutecznie w tle, prompt nie jest właściwym punktem wyjścia. Lepsze pytanie brzmi: gdzie pękła łańcuch dowodów?