Wybór i dostosowywanie modeli embeddingowych dla systemów produkcyjnych RAG
Dowiedz się, w jaki sposób modele embeddingowe przekształcają tekst w wektory nadające się do wyszukiwania, dlaczego słownictwo danej dziedziny zakłóca wyszukiwanie semantyczne oraz jak wybierać, kompresować i dopasowywać modele do zastosowań produkcyjnych RAG.
To jest piąty odcinek z serii poświęconej budowie systemów generowania wzmocnionego wyszukiwaniem o poziomie produkcyjnym, pokazującej drogę od surowych dokumentów do systemu, który potrafi odpowiadać na rzeczywiste pytania. W wcześniejszych etapach tej serii omówiono czyszczenie i normalizację wydobytego treści, a następnie jej podział na jednostki wyszukiwania poprzez dzielenie na fragmenty. Gdy mamy już te fragmenty, kolejnym krokiem jest przekształcenie ich w coś, co indeks wyszukiwania może faktycznie porównać.
Załóżmy, że chodzi o tego samego menedżera relacji przedstawionego wcześniej, który wciąż boryka się z kwestią linii kredytowej w wysokości 12 milionów euro dla klienta korporacyjnego o wysokim ryzyku. Aby spełnić to żądanie, trzeba znaleźć wymóg pogłębionej weryfikacji ukryty w klauzuli polisy, próg zatwierdzenia znajdujący się w wierszu tabeli oraz sekwencję kontrolną opisaną na diagramie procesu. Etap dzielenia na fragmenty już wyodrębnił te elementy jako odrębne, możliwe do śledzenia części treści.
Mimo to żadna z tych informacji nie może być jeszcze wyszukiwana za pomocą indeksu wektorowego.
Model embedding przekształca każdy fragment tekstu w wektor liczbowy o stałej długości. Ten sam model następnie przekształca wprowadzone zapytanie w wektor, a indeks wybiera te fragmenty, które znajdują się najbliżej niego w tym przestrzeni wektorowej. Nie ma gwarancji, że frazy takie jak „Group Credit Committee approval” w dokumencie polityki i „GCC sign-off threshold” w pytaniu użytkownika faktycznie będą blisko siebie – zależy to wyłącznie od wybranego modelu oraz tego, czego nauczył się podczas treningu.
O tej zależności właśnie jest ta część serii.
Wybór modelu embeddingowego oznacza stawianie na to, jak dużo słownictwa i sformułowań dokumenty te mają wspólnego z zapytaniami użytkowników. Jeśli wybierzesz niewłaściwy model dla swojej dziedziny, stracisz tygodnie na próby rozwiązania problemu, który wygląda jak błąd wyszukiwania, ale w rzeczywistości jest problemem reprezentacji – same wektory są umieszczone w niewłaściwy sposób, więc żadna optymalizacja indeksu tego nie naprawi.
Co tak naprawdę oblicza model embeddingowy
W istocie model embeddingowy przyjmuje sekwencję tokenów i wytwarza jeden gęsty wektor, zazwyczaj o rozmiarze od 384 do 3072 wymiarów, w zależności od konkretnego modelu. Ten wektor ma stanowić skompresowaną reprezentację znaczenia danych wejściowych.
Założeniem leżącym u podstaw tej metody jest to, że dane o podobnym znaczeniu mają wektorów znajdujące się blisko siebie w tym przestrzeni. Bliskość ta najczęściej mierzy się za pomocą podobieństwa kosinowego, który bierze pod uwagę kąt dzielący dwa wektory, a nie ich bezpośrednią odległość — co sprawia, że metoda ta jest niewrażliwa na różnice w długości tekstu.
Rozważmy zasadę zgodności stanowiącą, że klienci korporacyjni sklasyfikowani jako wysokiego ryzyka muszą przejść dodatkowe procedury weryfikacyjne przed tym, jak można złożyć propozycję kredytową do rozpatrzenia. Sprawny model uniwersalny prawdopodobnie umieściłby swój wektor w pobliżu podobnych sformułowań dotyczących zgodności z obowiązującymi regulacjami pochodzących od innych banków, w pobliżu materiałów prawniczych dotyczących obowiązków w zakresie weryfikacji, oraz w pobliżu wytycznych regulacyjnych dotyczących obsługi klientów o wysokim ryzyku.
Menedżer relacji może jednak sformułować tę samą podstawową potrzebę zupełnie inaczej, pytając na przykład o kroki, które muszą zostać podjęte przed złożeniem wniosku o kredyt. To, czy takie potoczne sformułowanie jest bliskie formalnemu językowi polityki, zależy od tego, w jakim stopniu dane treningowe łączyły nieformalne pytania operacyjne z formalnym językiem przestrzegania regulacji. Modele trenowane głównie na szerokim, uniwersalnym tekście internetowym często nie przyswajają takich specyficznych połączeń, gdy dziedzina jest wyspecjalizowana.
Ta luka pomiędzy potocznym sformułowaniem zapytań a językiem specjalistycznych dokumentów nazywana jest niezgodnością słownictwa i stanowi główną przyczynę problemów z jakością wyszukiwania w systemach RAG korporacyjnych.
Tokowizacja i okno kontekstowe
Zanim model embeddingowy cokolwiek obliczy, najpierw dzieli dane wejściowe na tokeny przy użyciu własnego wewnętrznego słownika. Liczba tokenów nie odpowiada wprost liczbie słów ani znaków. Fragment składający się z 500 tokenów w języku angielskim może odpowiadać mniej więcej 350–400 słowom, podczas gdy taka sama liczba tokenów w języku niemieckim – gdzie słowa są często złożone – może reprezentować mniej odrębnych idei.
Każdy model embeddingowy określa maksymalną długość kontekstu, a wszystko, co jest dłuższe, albo zostaje skrócone, albo wymaga specjalnego potraktowania. Modele Sentence-Transformers zazwyczaj mają limit w przedziale od 256 do 512 tokenów. Model text-embedding-3-large firmy OpenAI obsługuje do 8,191 tokenów. BGE-M3 może pracować z aż 8,192 tokenami. Model Jina embeddings v3 również obsługuje maksymalnie 8,192 tokenów.
Praktyczną zasadą przy tworzeniu systemów RAG w produkcji jest to, że granice fragmentów tekstu ustalone wcześniej w procesie muszą mieścić się w limicie kontekstowym narzuconym przez wybrany model embeddingu. Każdy fragment przekraczający ten limit jest cicho odcinany, a powstały wektor odzwierciedla jedynie część oryginalnego tekstu — co stanowi problem, który nie pojawi się w żadnych logach procesu.
Przestrzeń semantyczna i moment jej awarii
Większość obecnych modeli embeddingu to kodery typu transformer, szkolenie których odbywa się przy użyciu celu kontrastywnego: pary tekstów o podobnym znaczeniu są zbliżane w przestrzeni wektorowej, natomiast pary o różnym znaczeniu są oddalane. Po odpowiednim czasie szkolenia model uzyskuje taką strukturę geometryczną, w której bliskość służy jako wskaźnik powinowactwa semantycznego.
Taki układ funkcjonuje dobrze, dopóki zapytania i dokumenty używają tej samej terminologii, stylu pisania oraz koncepcji co to, na czym szkolono model. W przypadku RAG dla przedsiębiorstw zazwyczaj zawodzi na kilka przewidywalnych sposobów:
- Terminologia specyficzna dla danej dziedziny powoduje błędy, które łatwo przeoczyć. Analityk pytający o „progi składania raportów SAR w celu strukturyzacji” może nie otrzymać żadnego wyniku przy porównaniu z fragmentem polityki sformułowanym jako „Kryteria składania raportów o podejrzanej działalności w celu strukturyzacji transakcji”, jeśli model nigdy nie nauczył się, że te dwa sformułowania oznaczają to samo.
Rozpoznanie dokładnego momentu, w którym modele ogólne przestają być skuteczne, ma takie samo znaczenie jak znajomość modelu, który zajmuje czoło listy rankingowej publicznych testów.
Wybór modelu embedding w 2025 roku
Ogólna liczba modeli embedding znacznie się zmniejszyła. Poniższe porównanie obejmuje modele najważniejsze dla systemów RAG w bankowości i usługach finansowych na początku 2025 roku.
Nie istnieje uniwersalny zwycięzca we wszystkich scenariuszach biznesowych. Twoja decyzja zależy od zestawu języków, które musisz obsłużyć, budżetu opóźnień oraz dostępnej infrastruktury, od tego, czy lokalna inferencja w ogóle jest dla ciebie możliwa, oraz od wielkości luki terminologicznej pomiędzy twoją dziedziną a danymi, na których zostały wytrenowane modele ogólnego zastosowania — to właśnie ta luka decyduje o tym, czy dopracowanie modelu jest warte wysiłku.
Asymetryczne odzyskiwanie danych i informowanie modelu o rodzaju wprowadzanego wejścia
Jedną z różnic, której często nie dostrzegają zespoły, jest asymetryczne odzyskiwanie danych. Podczas odzyskiwania fragmentów tekstowych zapytanie i fragment, z którym jest ono porównywane, są strukturalnie bardzo różne. Zapytania zazwyczaj są krótkie, sformułowane jako pytania i często pozbawione dużej części słownictwa występującego we właściwej odpowiedzi. Fragmenty tekstowe natomiast są dłuższe, przedstawiane jako fakty i bogate w terminy specjalistyczne.
Pewne modele są zaprojektowane tak, aby bezpośrednio rozpoznawać tę asymetrię. Modele E5 dodają przedrostek „query:” lub „passage:” do tekstu wejściowego, dzięki czemu model wie, jaką rolę ma pełnić. Narzędzie Embed v3 od Cohere umożliwia to za pomocą parametru input_type, którego dopuszczalne wartości to „search_query”, „search_document”, „classification” oraz „clustering”.
Użycie niewłaściwego typu danych wejściowych podczas indeksowania lub wyszukiwania w tajemnicy pogarsza wyniki pomiaru podobieństwa w sposób trudny do wyśledzenia aż do źródła problemu. Jeśli umieścisz dokument jako zapytanie, otrzymasz wektor dostosowany do geometrii zapytania, a nie tekstu. Proces wydobywania informacji nie kończy się całkowitą porażką – po prostu traci precyzję, co łatwo przeoczyć podczas rutynowych testów.
import cohere
from typing import List
co = cohere.Client(api_key="your_api_key")
def embed_documents(chunks: List[str]) -> List[List[float]]:
"""Embed document chunks for indexing with explicit document input type."""
response = co.embed(
texts=chunks,
model="embed-english-v3.0",
input_type="search_document",
embedding_types=["float"]
)
return response.embeddings.float
def embed_query(query: str) -> List[float]:
"""Embed a search query with explicit query input type."""
response = co.embed(
texts=[query],
model="embed-english-v3.0",
input_type="search_query",
embedding_types=["float"]
)
return response.embeddings.float[0]
Wektory rzadkie: Gdzie dopasowanie słów kluczowych przewyższa wyszukiwanie semantyczne
Embeddingi gęste oddają znaczenie. Z kolei reprezentacje rzadkie pokazują, które terminy są obecne oraz jak silnie powinny być uwzględniane. W przypadku znacznej części typów zapytań w systemach RAG korporacyjnych, wyszukiwanie rzadkie przewyższa wyraźnie wyszukiwanie gęste, a w większości systemów produkcyjnych połączenie obu metod daje lepsze wyniki niż użycie tylko jednej z nich.
BM25 jako niezawodna baza odniesienia
BM25 pozostaje standardowym podejściem do wyszukiwania opartego na słowach kluczowych. Ocenia trafność używając częstotliwości występowania terminu w dokumencie, rzadkości tego terminu we całym korpusie oraz współczynnika normalizacji uwzględniającego długość dokumentu. Nie wymaga trenowania modelu, nie ma potrzeby GPU ani żadnych wywołań API do embeddingów.
Rozważmy zapytanie typu „CRD-EU-047 approval authority threshold”. BM25 wysoko sklasyfikuje każdy fragment zawierający te dokładne terminy. Z kolei model gęsty może nie pokazać takiego fragmentu, chyba że jego korpus treningowy przypadkowo nawiązał silną związek pomiędzy tym konkretnym kodem polityki a pojęciem uprawnień do zatwierdzania.
from rank_bm25 import BM25Okapi
import re
from typing import List, Tuple
def tokenise(text: str) -> List[str]:
"""Simple whitespace and punctuation tokeniser for BM25."""
return re.findall(r'\b\w+\b', text.lower())
class BM25Index:
def __init__(self, documents: List[str]):
self.documents = documents
tokenised = [tokenise(doc) for doc in documents]
self.bm25 = BM25Okapi(tokenised)
def search(self, query: str, top_k: int = 10) -> List[Tuple[int, float]]:
"""Return (doc_index, score) pairs for the top_k results."""
tokens = tokenise(query)
scores = self.bm25.get_scores(tokens)
ranked = sorted(enumerate(scores), key=lambda x: x[1], reverse=True)
return ranked[:top_k]
Dense retrieval dobrze radzi sobie z wykrywaniem podobieństwa koncepcyjnego, natomiast sparse retrieval skutecznie znajduje dokładne dopasowania terminów i identyfikatorów. Połączenie obu metod — hybrydowy retrieval — przynosi szczególne korzyści w przypadku korpusów bankowych, gdzie język regulacyjny jest precyzyjny i bogaty w identyfikatory.
W korpusach opartych na stabilnym, dokładnie zdefiniowanym języku regulacji, sam BM25 często osiąga podobny poziom dokładności do dense retrieval przy wąskich zapytaniach faktograficznych, przy jednoczesnym znacznie mniejszym obciążeniu infrastruktury. Jego główną słabością jest problem synonimii: zapytanie zawierające „EDD requirements” nie odnajdzie fragmentu tekstu, w którym występuje tylko „Enhanced Due Diligence requirements”, chyba że dokładna fraza się w nim powtórzy.
SPLADE: Rzadkie wektory, które uczą się rozszerzania słownictwa
SPLADE (Sparse Lexical and Expansion Model) stanowi kompromis pomiędzy prostym dopasowywaniem słów kluczowych a pełnym, gęstym wyszukiwaniem. Podczas indeksowania wykorzystywany jest zasłonięty model językowy w celu wzbogacenia zarówno dokumentów, jak i zapytań o semantycznie powiązany słownictwo, które niekoniecznie występuje w oryginalnej formułacji. Rezultatem jest wektor rzadki, którego wymiary odpowiadają poszczególnym tokenom słownictwa, przypisane im wagi zależą od tego, jak ważny jest dany token dla wprowadzonego tekstu.
Zatem fragment zakodowany metodą SPLADE, omawiający wymagania EDD, może mieć większą wagę dla takich terminów jak „weryfikacja klienta”, „ocena ryzyka” i „potwierdzenie tożsamości”, nawet jeśli żadna z tych dokładnych fraz nie występuje w tekście źródłowym. Takie rozszerzenie poprawia wyniki wyszukiwania w zapytaniach opartych na synonimach, zachowując jednocześnie efektywność i zrozumiałość charakterystyczne dla reprezentacji rzadkich, przyjaznych indeksom odwróconym.
Kompromisem jest zwiększony koszt inferencji podczas tworzenia indeksu oraz większe zapotrzebowanie na przestrzeń przechowywania w porównaniu z zwykłym algorytmem BM25. Jednak w korpusach usług finansowych, gdzie ten sam pojęcie polityki jest opisywane inaczej w różnych jurysdykcjach i przy różnych wersjach dokumentów, takie rozszerzenie może znacząco poszerzyć zakres dostępnych wyników wyszukiwania.
Matryoshka Embeddings: regulowana wielkość wektorów dla kontroli kosztów
Nauka reprezentacji typu Matryoshka (MRL) tworzy wektory embeddingowe, w których pierwsze N wymiarów już stanowi kompletową, samodzielną reprezentację danych wejściowych — dodatkowe wymiary dodają coraz bardziej szczegółowe informacje, zamiast zastępować to, co było wcześniej.
Ta technika zawdzięcza swoją nazwę rosyjskim lalekom-matrioszkom: wektor Matryoshki o 1536 wymiarach zawiera w swoich pierwszych 256 pozycjach w pełni funkcjonalną reprezentację o 256 wymiarach, w pierwszych 512 pozycjach — reprezentację o 512 wymiarach i tak dalej.
Familia modeli text-embedding-3 firmy OpenAI obsługuje to bezpośrednio za pomocą parametru wymiarów.
from openai import OpenAI
from typing import List
client = OpenAI()
def embed_with_matryoshka(
texts: List[str],
dimensions: int = 256,
model: str = "text-embedding-3-large"
) -> List[List[float]]:
"""
Embed texts at a specified sub-dimension.
Lower dimensions reduce storage and index cost.
Measure retrieval quality drop before committing to a dimension.
"""
response = client.embeddings.create(
input=texts,
model=model,
dimensions=dimensions
)
return [item.embedding for item in response.data]
Wektory embeddingowe typu Matryoshka zawierają w sobie coraz bardziej szczegółowe reprezentacje: pierwsze 256 wymiarów już dostarcza funkcjonalnej reprezentacji przydatnej do wyszukiwania, a każdy kolejny poziom zwiększa precyzję semantyczną przy proporcjonalnym wzroście zużycia pamięci.
Prawdziwą korzyścią w produkcji jest możliwość dostosowywania kompromisu pomiędzy przestrzenią przechowywania a jakością według potrzeb, bez konieczności ponownego szkolenia modelu czy budowania indeksu od zera. W przypadku korpusu polityk bankowych można przeprowadzić testy wydajności przy 256, 512, 1024 i 3072 wymiarach i stwierdzić, że 512 wymiarów zapewnia 97% pełnego odzyskiwania danych, przy użyciu jedynie 17% przestrzeni przechowywania potrzebnej do przechowywania pełnych wektorów.
W praktyce ten kompromis rzadko jest tak klarowny, jak wygląda na wykresach porównawczych. Subtelne różnice w danej dziedzinie – szczególnie pomiędzy blisko spokrewnionymi koncepcjami regulacyjnymi – często znajdują się właśnie w wyżcym wymiarze wektora. Przed zatwierdzeniem zmniejszonej liczby wymiarów do użycia w produkcji przeprowadź testy na własnym korpusie i rzeczywistych wzorcach zapytań.
Kompresowanie wektorów bez znacznego straty dokładności
Standardowe embeddingi przechowują każdą wymiar jako liczbę zmiennoprzecinkową 32-bitową. Gdy zwiększymy to do miliona fragmentów dokumentów, z po 1536 wymiarami każdy, otrzymamy około 6 GB surowych wektorów, zanim zostaną dodane koszty indeksowania. Na poziomie przedsiębiorstw taka ilość pamięci i związane z nią koszty przestają być błędem zaokrąglenia.
Kwantyzacja rozwiązuje ten problem poprzez zmniejszenie liczby bitów używanych do reprezentacji każdej wymiaru. W praktyce dominują trzy techniki: kwantyzacja skalarna (przekształcanie float32 na int8), kwantyzacja binarna (przekształcanie float32 na jeden bit) oraz kwantyzacja iloczynowa (skompresowanie każdego wektora do krótszego kodu).
Kwantyzacja skalarna: int8
Kwantyzacja skalarna mapuje ciągły zakres float32 na 256 dyskretnych wartości całkowitych. Każda wymiar zmniejsza się z 4 bajtów do 1, co redukuje zużycie pamięci o 75%. Ponieważ modele embeddingu wysokowymiarowe rozpraszają informacje równomiernie między wieloma wymiarami, żaden pojedynczy wymiar sam w sobie nie ma dużego znaczenia, więc dokładność utracona przez to zaokrąglenie jest zazwyczaj niewielka.
import numpy as np
from typing import Tuple
def quantise_to_int8(
embeddings: np.ndarray
) -> Tuple[np.ndarray, float, float]:
"""
Scalar quantisation to int8.
Returns quantised array plus the scale and zero_point needed for dequantisation.
"""
min_val = embeddings.min()
max_val = embeddings.max()
scale = (max_val - min_val) / 255.0
zero_point = -round(min_val / scale)
quantised = np.clip(
np.round(embeddings / scale) + zero_point,
0, 255
).astype(np.uint8)
return quantised, scale, zero_point
def dequantise_from_int8(
quantised: np.ndarray,
scale: float,
zero_point: float
) -> np.ndarray:
"""Reconstruct approximate float32 embeddings from int8."""
return ((quantised.astype(np.float32) - zero_point) * scale)
Kwantyzacja binarna
Kwantyzacja binarna idzie jeszcze dalej, sprowadzając każdy wymiar do pojedynczego bitu, który po prostu rejestruje, czy oryginalna wartość float była dodatnia, czy ujemna. Dzięki temu zużycie pamięci zmniejsza się o około 97% w porównaniu z float32. Ponieważ reprezentacja nie jest już ciągła, podobieństwo mierzy się odległością Hamminga zamiast podobieństwem kosinowym.
Ta technika działa najlepiej w modelach, których rozkłady wyników są naturalnie dobrze zrównoważone, tak że dla danego wejścia mniej więcej połowa wymiarów znajduje się po obu stronach zera. Jeśli wymiary modelu są nierównoważone, a nie zrównoważone, kwantyzacja binarna powoduje znacznie większą utratę jakości. Cohere stworzyło Embed v3, mając tę ograniczającą się pod uwagę, a opublikowana ocena tego modelu przez Anthropic wskazuje na spadek jakości wyszukiwania poniżej 1%, przy jednoczesnym zmniejszeniu potrzebnych zasobów przechowywania o 97% w ich zestawach testowych. Traktuj tę wartość jako punkt wyjścia, a nie gwarancję, i zweryfikuj ją na własnym korpusie przed poleganiem na niej.
import numpy as np
def quantise_to_binary(embeddings: np.ndarray) -> np.ndarray:
"""
Binary quantisation: positive dimensions become 1, negative become 0.
Packs 8 dimensions per byte using numpy packbits.
"""
binary_matrix = (embeddings > 0).astype(np.uint8)
return np.packbits(binary_matrix, axis=1)
def hamming_similarity(
query_binary: np.ndarray,
corpus_binary: np.ndarray
) -> np.ndarray:
"""Compute normalised Hamming similarity for binary embeddings."""
n_bits = corpus_binary.shape[1] * 8
xor = np.bitwise_xor(
query_binary,
corpus_binary
)
hamming_distances = np.unpackbits(xor, axis=1).sum(axis=1)
return 1.0 - (hamming_distances / n_bits)
Dopasowywanie modelu do specyfiki domeny RAG
Dopasowywanie modelu jest właściwym rozwiązaniem, gdy potwierdzisz, że embeddery uniwersalne rzeczywiście nie radzą sobie z twoimi danymi. Celem jest nauczenie modelu, że słownictwo, skróty oraz powiązania konceptualne specyficzne dla twojej dziedziny znajdują się blisko siebie w przestrzeni semantycznej.
Dopracowywanie modelu nie zawsze jest konieczne i nie zawsze stanowi właściwe rozwiązanie. Jeśli problemy z wyszukiwaniem wynikają z niewłaściwego dzielenia tekstu na fragmenty, jak omówiono wcześniej w tej serii, dostosowanie modelu embedding nie pomoże. Jeśli przyczyną są ustawienia procesu ponownego sortowania wyników lub sposób tworzenia zapytań, dopracowywanie modelu skupia się zupełnie na niewłaściwej warstwie systemu. Zanim zainwestujesz w to zasoby, przeanalizuj błędy wyszukiwania według typu zapytania, aby ustalić dokładne miejsce problemu.
Kiedy ogólne modele embedding zawodzą
W szczególności w bankowości, w kontekście RAG, kilka powtarzających się wzorców błędów uzasadnia inwestycję w dopracowywanie modelu:
Skróty specyficzne dla danej dziedziny są błędnie interpretowane. Model uniwersalny może łączyć „NPA” z National Parks Association zamiast z pojęciem aktywów niewydających dochodu, a „KYC” może być tylko słabo powiązane z koncepcjami zgodności i procesu onboardingu, które faktycznie dominują w zapytaniach bankowych.
Znaki powiązań pomiędzy powiązanymi koncepcjami w różnych dokumentach giną. Poszukiwanie „facility restructuring provisions” powinno ujawnić tekst polityki dotyczący „frameworki modyfikacji kredytów”, ale model szkoleny głównie na ogólnym treściach internetowych może nigdy nie napotkać tych sformułowań w wystarczająco bliskim kontekście, by móc nawiązać takie połączenie.
Kody regulacyjne i identyfikatory nie są wystarczająco doceniane. Numery wersji polityk, kody regulacji oraz oznaczenia jurysdykcji powinny w znaczący sposób wpływać na ranking, jednak modele embeddingowe zazwyczaj traktują je jako tokeny o niskiej wartości, które nie przekazują większych informacji semantycznych.
Progi liczbowe tracą swój kontekst regulacyjny. Wyrażenie takie jak „10 milionów EUR” umieszczone samodzielnie nie powinno automatycznie odpowiadać zapytaniu dotyczącemu „uprawnień do zatwierdzania dużych ryzyk” – taka relacja powstaje tylko wtedy, gdy model został wyszkolony na danych specyficznych dla danego obszaru, które łączą tę liczbę z jej znaczeniem regulacyjnym.
Budowanie par treningowych na podstawie danych specyficznych dla danego obszaru
Dokładna kalibracja modeli sentence-transformers przy użyciu celu kontrastywnego zależy od par pozytywnych: przykładów łączących zapytanie z fragmentem tekstu, który model powinien nauczyć się traktować jako powiązany. Przykłady negatywne mogą być wybrane ręcznie lub automatycznie pobrane z otaczającego korpusu.
W kontekście RAG w bankowości te pary pozytywne można zebrać z kilku praktycznych źródeł:
Istniejące zestawy pytań i odpowiedzi już przygotowane przez zespoły ds. zgodności i kredytowania, w których każde pytanie jest powiązane z odpowiadającym mu fragmentem tekstu.
Prymatyczna struktura dokumentów politycznych, w której nagłówek połączony z paragrafem poniżej tworzy gotowy przykład pozytywny.
Zapisane zapytania analityków w połączeniu z fragmentami tekstu, które faktycznie zostały pobraane, gdy odpowiedź była poprawna.
Zapytania wygenerowane maszynowo przez model językowy sztuczny dla każdego fragmentu tekstu, przy czym sam ten fragment służy jako pasaż pozytywny do porównania.
Wśród tych metod generowanie syntetycznych zapytań jest zazwyczaj najbardziej praktycznym rozwiązaniem, gdy istnieje niewiele już oznaczonych danych.
from openai import OpenAI
import json
from typing import List, Dict
client = OpenAI()
def generate_training_queries(
chunk: str,
chunk_metadata: Dict,
n_queries: int = 3
) -> List[Dict]:
"""
Generate synthetic query-passage pairs for fine-tuning.
The chunk itself is the positive passage for each generated query.
"""
prompt = f"""You are generating training data for a banking RAG system.
Given the following policy passage, generate {n_queries} realistic questions
that a credit analyst, compliance officer, or relationship manager might ask
that this passage directly answers. Each question should use natural language
and may use different terminology than the passage itself.
Passage:
{chunk}
Return a JSON array of objects with keys "query" and "difficulty".
Difficulty should be "narrow" (single fact) or "synthesis" (multiple facts).
Return only the JSON array, no other text."""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}
)
try:
result = json.loads(response.choices[0].message.content)
queries = result.get("queries", result) if isinstance(result, dict) else result
return [
{
"query": q["query"],
"passage": chunk,
"document_id": chunk_metadata.get("document_id"),
"chunk_id": chunk_metadata.get("chunk_id"),
"difficulty": q.get("difficulty", "narrow")
}
for q in queries
]
except (json.JSONDecodeError, KeyError):
return []
Szkolenie kontrastywne z stratą typu triplet
Najskuteczniejszym celem szkolenia modeli embeddingowych skierowanych na wyszukiwanie jest uczenie kontrastywne, wykorzystujące albo negatywy w obrębie partii danych, albo celowo wybrane trudne negatywy. Sentence-transformers obsługuje ten model poprzez MultipleNegativesRankingLoss, który wykorzystuje każdy inny przykład w partii treningowej jako ukryty negatyw dla danej pary anchor-pozytywny.
from sentence_transformers import SentenceTransformer, InputExample
from sentence_transformers.losses import MultipleNegativesRankingLoss
from torch.utils.data import DataLoader
from typing import List, Dict
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def build_training_examples(
pairs: List[Dict]
) -> List[InputExample]:
"""
Convert query-passage pairs into InputExample objects.
MultipleNegativesRankingLoss expects (anchor, positive) pairs.
Negatives are sampled automatically from other items in the batch.
"""
return [
InputExample(texts=[pair["query"], pair["passage"]])
for pair in pairs
if pair.get("query") and pair.get("passage")
]
def fine_tune_embedding_model(
base_model_name: str,
training_pairs: List[Dict],
output_path: str,
epochs: int = 3,
batch_size: int = 16,
warmup_steps: int = 100
) -> SentenceTransformer:
"""
Fine-tune a sentence-transformers model on domain-specific query-passage pairs.
base_model_name: HuggingFace model identifier or local path.
training_pairs: List of dicts with "query" and "passage" keys.
output_path: Directory to save the fine-tuned model.
"""
model = SentenceTransformer(base_model_name)
logger.info(f"Loaded base model: {base_model_name}")
logger.info(f"Training on {len(training_pairs)} query-passage pairs")
examples = build_training_examples(training_pairs)
loader = DataLoader(examples, shuffle=True, batch_size=batch_size)
loss = MultipleNegativesRankingLoss(model)
total_steps = len(loader) * epochs
logger.info(f"Training for {epochs} epochs, {total_steps} total steps")
model.fit(
train_objectives=[(loader, loss)],
epochs=epochs,
warmup_steps=warmup_steps,
output_path=output_path,
show_progress_bar=True,
checkpoint_path=output_path,
checkpoint_save_steps=len(loader)
)
logger.info(f"Fine-tuned model saved to: {output_path}")
return model
Wyniki benchmarkingu: korpus bankowy przed i po drobnej optymalizacji
Rozważmy test wydajności przeprowadzony na korpusie polityki kredytowej przedsiębiorstw składającym się z 847 fragmentów pochodzących z dokumentów polityki, matryc zatwierdzeń, udoskonalonych procedur weryfikacji oraz wytycznych dotyczących zwalczania prania pieniędzy. Zbiór do oceny zawiera 120 zapytań należących do czterech kategorii: wąskie wyszukiwania faktograficzne, pytania z progiem dopuszczalnym, pytania wymagające wielu dowodów oraz pytania syntezy.
Korzyści z doprecyzowania modelu na podstawie specyfiki danej dziedziny są najbardziej widoczne w zapytaniach typu „próg”, takich jak pytania o próg zatwierdzenia obowiązujący wobec klientów korporacyjnych o wysokim ryzyku w UE. To właśnie tutaj skróty bankowe oraz terminologia regulacyjna najbardziej odbiegają od tego, z czym model uniwersalny miał do czynienia podczas szkolenia wstępnego, dlatego uzupełnienie tej luki w słownictwie przynosi największy wzrost dokładności wyników. Zapytania oparte na wielu źródłach informacji oraz zapytania syntezy poprawiają się mniej pod wpływem samego doprecyzowania modelu, ale znacznie bardziej, gdy dodano do nich hybrydowy mechanizm wyszukiwania.
Traktuj te liczby jako przykłady tego, co może osiągnąć dobrze przeprowadzony projekt dopasowywania na odpowiednio oznaczonym zbiorze danych bankowych, a nie jako obietnicę. Twoja własna struktura korpusu, mieszanka zapytań oraz dokładność oznaczania wpłyną na wyniki. Najważniejsze jest rozbicie jakości wyszukiwania według typu zapytania, zamiast podawania jednego łącznego wyniku, ponieważ każdy typ zapytania ma tendencję do awarii z różnych powodów.
Kwestie wydajności przy masowym używaniu embeddingów
Przesyłanie 100 000 fragmentów przez API do generowania embeddingów, po jednym zapytaniu na raz, jest zarówno wolne, jak i kosztowne. Pipeline’y do generowania embeddingów klasy produkcyjnej zamiast tego przetwarzają dane grupowo, dzięki czemu mogą zwiększyć wydajność, przestrzegać ograniczeń szybkości, skutecznie odbudowywać się po awariach i zapewnić deterministyczne generowanie wyników.
Przetwarzanie grupowe za pomocą API dostawców
Interfejs embeddingów OpenAI umożliwia przetwarzanie do 2 048 danych w jednej prośbie. Interfejs Embed firmy Cohere obsługuje maksymalnie 96 tekstów na zapytanie, chyba że użyje się ich dedykowanej API do przetwarzania partii większej liczby danych. Uruchamianie procesów inferencji lokalnie za pomocą sentence-transformers zapewnia możliwość konfiguracji wielkości partii, ograniczonej jedynie dostępną pamięcią GPU.
import time
import logging
from typing import List, Optional
from openai import OpenAI, RateLimitError, APIError
logger = logging.getLogger(__name__)
client = OpenAI()
def embed_in_batches(
texts: List[str],
model: str = "text-embedding-3-large",
batch_size: int = 512,
max_retries: int = 3,
retry_delay: float = 2.0,
dimensions: Optional[int] = None
) -> List[List[float]]:
"""
Embed a large list of texts using batched API calls with retry logic.
texts: Pre-chunked text strings. Caller is responsible for ensuring
no text exceeds the model's token limit.
batch_size: Number of texts per API call. Stay well below the API limit
to avoid hitting per-request token limits.
dimensions: Optional Matryoshka dimension reduction for supported models.
"""
all_embeddings: List[List[float]] = []
total_batches = (len(texts) + batch_size - 1) // batch_size
for batch_idx in range(0, len(texts), batch_size):
batch = texts[batch_idx: batch_idx + batch_size]
current_batch = batch_idx // batch_size + 1
logger.info(f"Embedding batch {current_batch}/{total_batches} "
f"({len(batch)} texts)")
kwargs = {
"input": batch,
"model": model
}
if dimensions is not None:
kwargs["dimensions"] = dimensions
attempt = 0
while attempt < max_retries:
try:
response = client.embeddings.create(**kwargs)
# Preserve input order: API returns items sorted by index
sorted_items = sorted(response.data, key=lambda x: x.index)
all_embeddings.extend([item.embedding for item in sorted_items])
break
except RateLimitError:
attempt += 1
wait = retry_delay * (2 ** attempt)
logger.warning(f"Rate limit hit on batch {current_batch}. "
f"Waiting {wait:.1f}s before retry {attempt}/{max_retries}")
time.sleep(wait)
except APIError as e:
attempt += 1
logger.error(f"API error on batch {current_batch}: {e}. "
f"Retry {attempt}/{max_retries}")
if attempt >= max_retries:
raise
time.sleep(retry_delay)
logger.info(f"Embedding complete. Total vectors: {len(all_embeddings)}")
return all_embeddings
Uruchamianie procesów inferencji lokalnie za pomocą sentence-transformers
Część organizacji napotyka ograniczenia związane z lokalizacją danych, które uniemożliwiają wysyłanie dokumentów politycznych do zewnętrznej API. W takich przypadkach rozwiązaniem jest lokalna inferencja przy użyciu sentence-transformers.
from sentence_transformers import SentenceTransformer
import numpy as np
from typing import List, Optional
import logging
logger = logging.getLogger(__name__)
class LocalEmbeddingPipeline:
"""
Production-ready local embedding pipeline using sentence-transformers.
Suitable for data-residency-constrained banking environments.
"""
def __init__(
self,
model_name_or_path: str,
device: str = "cpu",
batch_size: int = 64,
normalise: bool = True
):
self.model = SentenceTransformer(model_name_or_path, device=device)
self.batch_size = batch_size
self.normalise = normalise
self.device = device
logger.info(f"Loaded model: {model_name_or_path} on {device}")
def embed(
self,
texts: List[str],
show_progress: bool = True
) -> np.ndarray:
"""
Embed a list of texts. Returns an (N, D) numpy array.
Normalises to unit length if normalise=True (required for cosine similarity).
"""
embeddings = self.model.encode(
texts,
batch_size=self.batch_size,
show_progress_bar=show_progress,
normalize_embeddings=self.normalise,
convert_to_numpy=True
)
logger.info(f"Embedded {len(texts)} texts. "
f"Output shape: {embeddings.shape}")
return embeddings
def embed_query(self, query: str) -> np.ndarray:
"""Embed a single query. Returns a 1D array."""
return self.embed([query], show_progress=False)[0]
Sprawdzanie długości tokenów przed ich embedowaniem
Jeśli fragment tekstowy przekracza limit tokenów modelu, jest on skracany bez żadnego ostrzeżenia. W korpusie zasad takie ciche skracanie może usunąć dokładnie tę klauzulę lub wartość numeryczną, która sprawiła, że fragment ten w ogóle zasługiwał na pobranie. Walidacja liczby tokenów przed embedowaniem pozwala wykryć ten problem, zanim dotrze on do indeksu.
from transformers import AutoTokenizer
from typing import List, Tuple
import logging
logger = logging.getLogger(__name__)
def validate_chunk_lengths(
chunks: List[str],
model_name: str,
max_tokens: int,
truncation_strategy: str = "warn"
) -> Tuple[List[str], List[int]]:
"""
Validate that all chunks are within the model's token limit.
truncation_strategy:
"warn" - Log a warning for oversized chunks and include them (will be truncated by model).
"skip" - Remove oversized chunks and return only valid ones.
"raise" - Raise ValueError on the first oversized chunk.
Returns (validated_chunks, oversized_indices).
"""
tokeniser = AutoTokenizer.from_pretrained(model_name)
oversized = []
for idx, chunk in enumerate(chunks):
token_count = len(tokeniser.encode(chunk, add_special_tokens=True))
if token_count > max_tokens:
oversized.append(idx)
msg = (f"Chunk {idx} has {token_count} tokens, "
f"exceeds model limit of {max_tokens}. "
f"First 80 chars: {chunk[:80]!r}")
if truncation_strategy == "raise":
raise ValueError(msg)
else:
logger.warning(msg)
if truncation_strategy == "skip" and oversized:
valid = [c for i, c in enumerate(chunks) if i not in set(oversized)]
logger.info(f"Removed {len(oversized)} oversized chunks. "
f"{len(valid)} chunks remain.")
return valid, oversized
return chunks, oversized
Dołączanie metadanych i informacji o pochodzeniu do embedów
Sam wektor embedingu nie wystarcza sam w sobie, aby regulowany system RAG funkcjonował prawidłowo. Każdy wektor wymaga dołączonych strukturalnych metadanych, dzięki którym etapy wyszukiwania, ponownego sortowania i generowania mogą potwierdzić źródło treści, egzekwować uprawnienia dostępu, ograniczać wyniki według jurysdykcji oraz odsyłać do autorytatywnego dokumentu źródłowego.
Wracając do scenariusza propozycji kredytu w wysokości 12 milionów euro, każdy wpleciony fragment powinien zawierać co najmniej pola pokazane tutaj:
from dataclasses import dataclass, field
from typing import Optional, List
import uuid
@dataclass
class EmbeddedChunk:
"""
Production embedding record for a banking policy RAG system.
The vector enables retrieval. The metadata enables everything else.
"""
# Vector
vector: List[float]
vector_dimensions: int
embedding_model: str
embedding_model_version: str
# Content
text: str
content_type: str # "narrative", "table_row", "proposition", "image_description"
# Provenance
document_id: str
document_version: str # e.g. "7.2"
policy_id: Optional[str] # e.g. "CRD-EU-047"
jurisdiction: Optional[str] # e.g. "EU"
effective_date: Optional[str]
# Chunk structure
chunk_id: str = field(default_factory=lambda: str(uuid.uuid4()))
parent_id: Optional[str] = None
section: Optional[str] = None
page_number: Optional[int] = None
source_artifact_path: Optional[str] = None # path to original image/table
# Access control
classification: str = "INTERNAL" # "PUBLIC", "INTERNAL", "CONFIDENTIAL"
permitted_roles: List[str] = field(default_factory=list)
# Indexing
indexed_at: Optional[str] = None
indexing_pipeline_version: Optional[str] = None
Spójne przechowywanie tych metadanych na każdym etapie procesu, od dzielenia na fragmenty, przez wplecanie, aż po indeksowanie wektorowe, to nie tylko dobra praktyka. W regulowanym środowisku bankowym uzyskanie technicznie poprawnej odpowiedzi opartej na przestarzałej wersji polityki stanowi naruszenie wymogów regulacyjnych. Zadaniem wektora jest znalezienie fragmentu; zadaniem metadanych jest potwierdzenie, że fragment pochodzi z właściwej, aktualnej wersji źródła.
Ocena jakości wplecania
Tabela rankingów publicznych, takie jak MTEB, przedstawiają wyniki wyszukiwania o zastosowaniu ogólnym w szerokim zakresie zbiorów danych akademickich. Te liczby pomagają wyeliminować modele, które wyraźnie słabo radzą sobie z zadaniem. Są jednak niewystarczające, gdy chodzi o wybór najlepszego modelu do specjalistycznego korpusu, takiego jak wewnętrzna baza polityk bankowych.
Jedynym miernikiem, który naprawdę ma znaczenie, jest sposób, w jaki model radzi sobie z twoimi własnymi dokumentami przy użyciu twoich zapytań, oceniany na podstawie Twoich własnych kryteriów istotności.
Budowanie zestawu do oceny wyszukiwania
Zestaw testowy do wyszukiwania stworzony dla procesu embeddingów w bankowości RAG musi obejmować kilka typów zapytań:
Szczegółowe wyszukiwania faktograficzne, które odnoszą się do jednego autorytatywnego fragmentu informacji, np. pytanie o to, jak często klienci korporacyjni o wysokim ryzyku muszą przechodzić coroczną ocenę.
Pytania oparte na progu, które łączą określony warunek liczbowy z przypisaną do niego zasadą zarządzania, na przykład pytanie o to, która władza zatwierdzająca jest wymagana, gdy wartość przedsiębiorstwa w UE przekracza 10 milionów euro.
Pytania wymagające wielu źródeł informacji, przy których pełna odpowiedź zależy od połączenia więcej niż jednego fragmentu danych, na przykład pytanie o to, jakie kontrole muszą zostać przeprowadzone, zanim można w ogóle złożyć wniosek o kredyt korporacyjny o wysokim ryzyku.
Pytania syntetyczne, które czerpią treść z kilku sekcji jednocześnie, na przykład pytanie o pełny opis ram kontroli AML regulujących udzielanie kredytów korporacyjnym o wysokim ryzyku.
Pytania międzydokumentowe, istotne tam, gdzie polityki odnoszą się do siebie w różnych dokumentach.
import numpy as np
from typing import List, Dict, Set
def recall_at_k(
retrieved_ids: List[str],
relevant_ids: Set[str],
k: int
) -> float:
"""
Compute Recall@k for a single query.
relevant_ids is the ground truth set of chunk identifiers.
retrieved_ids is the ordered list of retrieved chunk identifiers.
"""
if not relevant_ids:
return 0.0
top_k_retrieved = set(retrieved_ids[:k])
return len(top_k_retrieved & relevant_ids) / len(relevant_ids)
def mean_reciprocal_rank(
retrieved_ids: List[str],
relevant_ids: Set[str]
) -> float:
"""Compute MRR for a single query."""
for rank, chunk_id in enumerate(retrieved_ids, start=1):
if chunk_id in relevant_ids:
return 1.0 / rank
return 0.0
def evaluate_embedding_model(
model_name: str,
evaluation_queries: List[Dict],
corpus_chunks: List[Dict],
k_values: List[int] = [1, 5, 10, 20]
) -> Dict:
"""
Evaluate an embedding model on a labelled retrieval dataset.
evaluation_queries: List of dicts with "query" and "relevant_chunk_ids" keys.
corpus_chunks: List of dicts with "chunk_id" and "text" keys.
Returns per-query-type and aggregate retrieval metrics.
"""
from sentence_transformers import SentenceTransformer
model = SentenceTransformer(model_name)
corpus_texts = [c["text"] for c in corpus_chunks]
corpus_ids = [c["chunk_id"] for c in corpus_chunks]
corpus_embeddings = model.encode(corpus_texts, normalize_embeddings=True)
results_by_type: Dict[str, List] = {}
all_recall: Dict[int, List[float]] = {k: [] for k in k_values}
all_mrr: List[float] = []
for query_item in evaluation_queries:
query = query_item["query"]
relevant = set(query_item["relevant_chunk_ids"])
query_type = query_item.get("query_type", "unspecified")
query_embedding = model.encode(query, normalize_embeddings=True)
scores = corpus_embeddings @ query_embedding
ranked_indices = np.argsort(scores)[::-1]
retrieved = [corpus_ids[i] for i in ranked_indices]
mrr = mean_reciprocal_rank(retrieved, relevant)
all_mrr.append(mrr)
for k in k_values:
r = recall_at_k(retrieved, relevant, k)
all_recall[k].append(r)
if query_type not in results_by_type:
results_by_type[query_type] = {"mrr": [], "recall": {k: [] for k in k_values}}
results_by_type[query_type]["mrr"].append(mrr)
for k in k_values:
results_by_type[query_type]["recall"][k].append(
recall_at_k(retrieved, relevant, k)
)
aggregate = {
"model": model_name,
"n_queries": len(evaluation_queries),
"mrr": float(np.mean(all_mrr)),
"recall": {k: float(np.mean(all_recall[k])) for k in k_values}
}
per_type = {
qt: {
"mrr": float(np.mean(data["mrr"])),
"recall": {k: float(np.mean(data["recall"][k])) for k in k_values},
"n_queries": len(data["mrr"])
}
for qt, data in results_by_type.items()
}
return {"aggregate": aggregate, "by_query_type": per_type}
Metryki pobierania danych nie powinny być oceniane w oderwaniu od tego, jak wpływają na jakość ostatecznej odpowiedzi. Załóżmy, że wskaźnik Recall@5 poprawia się o trzy punkty, ale liczba niemal identycznych fragmentów pobieranych rośnie o 30 procent — taka kompromis może w rzeczywistości nie pomóc modelowi językowemu, ponieważ dostarczenie mu trzech niemal identycznych tekstów zamiast jednego przydatnego nie dodaje żadnych rzeczywistych dowodów.
Prawidłowym podejściem jest ocena całego procesu, od początkowej zapytania aż po ostateczną odpowiedź przedstawianą użytkownikowi. Rola modelu embeddingowego ogranicza się do umieszczenia odpowiednich dowodów w oknie kontekstowym. To, czy te dowody doprowadzą do odpowiedzi zarówno dokładnej, jak i zgodnej z wymogami, zależy od każdego kolejnego etapu.
Kanał embeddingowy jako infrastruktura
Gdy fragment skończy przetwarzanie w pipeline embeddingu, musi wyjść po drugiej stronie z dołączonym wektorem, w pełni uzupełnionymi metadanymi oraz stabilnym identyfikatorem, który umożliwi późniejsze odzyskanie, aktualizację lub usunięcie tego fragmentu bez uszkodzenia rekordów znajdujących się obok niego w indeksie.
To element infrastruktury, a nie jednorazowy skrypt. Pipeline embeddingu klasy produkcyjnej przeznaczone do zastosowań bankowych podlegających regulacjom wymagają następujących elementów:
- Idempotencja. Jeśli fragment zostanie ponownie włączony do procesu embeddingu z powodu zmiany wersji modelu, powinien nadpisać istniejący rekord, a nie stworzyć duplikat.
Żadne z tych elementów nie jest opcjonalne w środowisku produkcyjnym. To właśnie one odróżniają pipeline, który funkcjonuje tylko w demonstracji, od takiego, który można obsługiwać, audytować i utrzymywać w czasie w regulowanym środowisku.
Powrót do propozycji kredytu w wysokości 12 milionów euro
Pierwotne pytanie menedżera relacji nie uległo zmianie: jaką władzę zatwierdzającą wymaga ta transakcja i które kontrole muszą zostać przeprowadzone, zanim będzie można ją złożyć?
- Część 4 omawiała, jak projektować jednostki wyszukiwania, które zachowują te odpowiedzi w ich oryginalnej formie: klauzulę polityki EDD, wiersz macierzy zatwierdzeń określający uprawnienia GCC oraz diagram przepływu pracy przedstawiający kontrole przed wysłaniem.
- W tej części przekształciliśmy te jednostki wyszukiwania w wektory nadające się do wyszukiwania, wykorzystując model sprawdzony w kontekście terminologii specyficznej dla bankowości, dostosowany tak, by rozumieć związek skrótów regulacyjnych z ich kontekstem przestrzegania przepisów, oraz wzbogacony o pełne metadane pochodzenia, które umożliwiają późniejsze potwierdzenie wersji polityki i jurysdykcji podczas wyszukiwania.
Gdy przychodzi zapytanie, indeks wektorowy znajduje wiersz macierzy zatwierdzeń obejmujący ryzykowne pozycje o wartości powyżej 10 milionów euro, klauzulę EDD oraz diagram przedstawiający sekwencję kontroli — i zwraca je wraz z metadanymi potwierdzającymi, że wszystkie trzy odnoszą się do polisy CRD-EU-047, wersja 7.2, jurysdykcja UE, obowiązująca od 15 stycznia 2026 roku.
To, co trafia do warstwy generowania, to dane dokładne, kompletne i śledzone.
To jest różnica pomiędzy pipeline’em do embeddingów, który po prostu ładowa fragmenty do bazy danych wektorowej, a tym, który zachowuje wszystko, co jest potrzebne do uzyskiwania wiarygodnych i sprawdzalnych odpowiedzi.
Zanim przejdziesz do indeksowania wektorowego: lista kontrolna
Zanim umieścisz fragmenty w indeksie wektorowym, upewnij się co do następujących kwestii:
- Czy parametr typu danych został poprawnie ustawiony w modelu embeddingowym zarówno przy wywołaniach do indeksowania, jak i przy wyszukiwaniu? Taka niezgodność powoli obniża dokładność odzyskiwania informacji.
- Czy długość tokenów każdego fragmentu tekstu została sprawdzona przed jego embedowaniem? Ukryte skracanie zmienia to, co faktycznie reprezentuje tekst embeddedowany, przy czym w całym procesie nie pojawia się żaden błąd.
- Czy każda rekordowa wartość wektorowa zawiera pełne informacje o pochodzeniu – wersję dokumentu, identyfikator polityki, jurysdykcję oraz datę wejścia w życie?
- Czy model embeddingowy został rzeczywiście przetestowany na własnym korpusie danych i wzorcach zapytań, a nie wybrany wyłącznie na podstawie wyników publicznych testów?
- Jeśli dostosowaliście model, czy macie dane dotyczące efektywności odzyskiwania informacji przed i po modyfikacjach, uzyskane na oddzielonym zbiorze zapytań?
Jeśli czegoś z tego brakuje, indeks wektorowy nie pomoże ci to wykryć — chętnie przechowuje wszystko, co mu podasz. Nic w bazie danych nie wskaże na wektor utworzony z skróconego fragmentu, zapytanie z niewłaściwym typem danych lub fragment włączony przy użyciu wersji modelu niezgodnej z resztą indeksu. Te braki same się nie ujawniają; pojawiają się później jako problemy z jakością wyszukiwania, które z zewnątrz wyglądają jak problemy z LLM.
We wcześniejszej części tej serii, w punkcie 4, pokazano, jak przekształcić czyste dokumenty w jednostki wyszukiwania, zachowując przy tym ich strukturę. W tej części omówiono, jak przekształcić te jednostki wyszukiwania w wektory nadające się do wyszukiwania, przy jednoczesnym zachowaniu informacji o ich pochodzeniu.
Następnie omówiono, w jaki sposób te wektory faktycznie łączą się z indeksem: intensywne wyszukiwanie wektorowe, rzadkie wyszukiwanie, podejścia hybrydowe, algorytmy przybliżonego znajdowania najbliższego sąsiada oraz filtrowanie metadanych, które decyduje, które wektory w ogóle mogą być brane pod uwagę podczas wyszukiwania.
Literatura pokrewna
- Architektura odniesieniowa dla systemów AI-agencyjnych na poziomie przemysłowym — Poznaj podstawowe warstwy, strategie pamięci, metody pozyskiwania danych oraz zasady bezpieczeństwa niezbędne do przeniesienia systemów AI-agencyjnych z fazy prototypu na niezawodne systemy produkcyjne w przedsiębiorstwach.
- Wyjaśnienie baz danych wektorowych: silnik stojący za RAG i wyszukiwaniem AI — Dowiedz się, jak bazy danych wektorowych przekształcają tekst w wektory, umożliwiają semantyczne wyszukiwanie i procesy typu RAG oraz napędzają praktyczne zastosowania AI, takie jak rekomendacje.