Osadzania: teoria i praktyka
Każdy system wyszukiwania semantycznego, każdy potok RAG i każda aplikacja AI współpracująca z językiem naturalny ma podstawowy wspólny element: osadzania. Jestem tłumaczeniem znaczenia w liczbach, pomost pomiędzy światem tekstu a światem matematyki. Bez osadzania, baza danych nie jest w stanie odróżnić „psa” od „samochodu” – dzięki osadzeniu wie, że „pies” i bliżej do „kota” niż do „tostera”.
W pierwszym artykule z tej serii skonfigurowaliśmy pgwektor i nauczyłem się oszczędzać i wektory zapytań w PostgreSQL. Ale skąd pochodzą te wektory? Jak wygenerować osadzenie jakość? A przede wszystkim, jaki model wybrać spośród kilkudziesięciu dostępnych? W tym artykule odpowiadamy na wszystkie te pytania, od teorii matematycznej po praktykę z Pythonem i PostgreSQL.
Przegląd serii
| # | Przedmiot | Centrum |
|---|---|---|
| 1 | pgwektor | Instalacja, operatorzy, indeksowanie |
| 2 | Jesteś tutaj - Osadzania | Modele, odległości, generacja |
| 3 | RAG z PostgreSQL | Rurociąg RAG od końca do końca |
| 4 | Zaawansowane wyszukiwanie podobieństw | Wyszukiwanie hybrydowe, filtrowanie |
| 5 | Indeksowanie i wydajność | HNSW, IVFFlat, strojenie |
| 6 | RAG w produkcji | Monitoring, skalowanie, CI/CD |
Czego się nauczysz
- Co to jest osadzanie i dlaczego jest podstawą współczesnej sztucznej inteligencji
- Ewolucja historyczna: od kodowania one-hot do Word2Vec, GloVe, BERT i Sentence Transformers
- Matematyczne właściwości osadzania: analogie wektorowe i grupowanie semantyczne
- Cztery metryki odległości z formułami i przypadkami użycia
- Jak wygenerować osadzanie w Pythonie: lokalnie i poprzez API
- Jak zapisywać i wysyłać zapytania do osadzania w PostgreSQL za pomocą pgvector
- Osadzanie multimodalne: tekst, obrazy, dźwięk i kod
- Jak ocenić jakość modelu osadzania (MTEB)
- Koszty i strategie skalowania dla milionów dokumentów
1. Czym są osady
Un osadzanie oraz gęstą reprezentację wektorową obiektu (słowo, zdanie, dokument, obraz) w ciągłej przestrzeni o zmniejszonej wymiarowości. W praktyce jest to tablica liczb zmiennoprzecinkowych, która oddaje „znaczenie” tego obiektu.
# L'embedding della frase "Il gatto dorme sul divano"
# generato con text-embedding-3-small di OpenAI (1536 dimensioni)
embedding = [
0.0231, -0.0456, 0.0891, -0.0123, 0.0567, -0.0234,
0.0789, -0.0345, 0.0123, -0.0678, 0.0456, -0.0891,
# ... altri 1524 valori ...
]
print(f"Tipo: {type(embedding)}") # <class 'list'>
print(f"Dimensioni: {len(embedding)}") # 1536
Kluczowy spostrzeżenie jest następujące: w dobrze wyszkolonej przestrzeni wektorowej, odległość geometryczna pomiędzy dwoma wektorami odzwierciedla podobieństwo semantyczne wśród koncepcji, które reprezentują. Zwroty o podobnym znaczeniu będą miały bliskie wektory, zdania o różnych znaczeniach będą daleko.
Charakterystyka osadzania
| Nieruchomość | Opis | Przykład |
|---|---|---|
| Gęsty | Każdy wymiar ma wartość różną od zera | [0,023, -0,045, 0,089, ...] |
| Kontynuować | Wartości rzeczywiste, a nie dyskretne | Każdy komponent to float32/float16 |
| Stała wymiarowość | Ten sam model zawsze tworzy wektory o tej samej długości | Wymiary 384, 768, 1536 lub 3072 |
| Znaczące semantycznie | Odległości między wektorami odzwierciedlają relacje znaczeniowe | sim("kot", "koci") > sim("kot", "samochód") |
Jeśli pomyślimy o przestrzeni osadzania jak o mapie, podobne koncepcje tworzą „dzielnice”: zwierzęta w jednym obszarze, pojazdy w innym, emocje w jeszcze innym. Ale piękno polega na tym, że te relacje wyłaniają się automatycznie z treningu, a nie przychodzą programowany ręcznie.
2. Od słów do wektorów: ewolucja historyczna
Historia osadzania to ciąg coraz bardziej wyrafinowanych pomysłów, każdy z nich co eliminuje ograniczenia poprzedniego. Zrozumienie tej ewolucji pomaga zrozumieć, dlaczego nowoczesne modele sprawdzają się bardzo dobrze.
2.1 Kodowanie One-Hot (lata 90.)
Najprostsze podejście: każde słowo jest reprezentowane przez wektor zawierający tylko jedną jedynkę i wszystkie pozostałe 0. Jeśli słownik ma V słów, każdy wektor ma V wymiarów.
# Vocabolario: ["gatto", "cane", "pesce", "auto", "moto"]
# Dimensione vettore = dimensione vocabolario = 5
gatto = [1, 0, 0, 0, 0]
cane = [0, 1, 0, 0, 0]
pesce = [0, 0, 1, 0, 0]
auto = [0, 0, 0, 1, 0]
moto = [0, 0, 0, 0, 1]
# Problema 1: la distanza tra "gatto" e "cane" e uguale
# alla distanza tra "gatto" e "auto"
import numpy as np
dist_gatto_cane = np.linalg.norm(
np.array(gatto) - np.array(cane)
) # sqrt(2) = 1.414
dist_gatto_auto = np.linalg.norm(
np.array(gatto) - np.array(auto)
) # sqrt(2) = 1.414 -- identica!
# Problema 2: con un vocabolario di 100.000 parole,
# ogni vettore ha 100.000 dimensioni (sparso, inefficiente)
Ograniczenia kodowania One-Hot
Wybuchowa wymiarowość: dla słownika zawierającego 100 tys. słów każdy wektor ma 100 tys. wymiarów, prawie wszystko na zero. Brak informacji semantycznych: wszystkie wektory są jednakowo odległe między nimi. „Kot” jest tak samo daleki od „kociaka”, jak od „trzęsienia ziemi”. To podejście nie ujmuje żadnych relacji znaczeniowych między słowami.
2.2 TF-IDF (częstotliwość terminów – odwrotna częstotliwość dokumentów)
Krok dalej: zamiast 0/1 składowe wektora wskazują, jak ważne jest słowo w dokumencie w porównaniu z całym korpusem. Ale każdy dokument staje się wektorem rozproszonym w wymiarowość słownictwa.
from sklearn.feature_extraction.text import TfidfVectorizer
documenti = [
"il gatto dorme sul divano",
"il cane gioca nel giardino",
"l'automobile corre sulla strada",
"il felino riposa sulla poltrona",
]
vectorizer = TfidfVectorizer()
tfidf_matrix = vectorizer.fit_transform(documenti)
# Risultato: matrice sparsa (4 documenti x N termini)
print(f"Shape: {tfidf_matrix.shape}") # (4, 14)
print(f"Termini: {vectorizer.get_feature_names_out()}")
# Problema: "gatto dorme" e "felino riposa" sono lontani
# perchè usano parole diverse, anche se il significato e simile
from sklearn.metrics.pairwise import cosine_similarity
sim = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[3:4])
print(f"Similarità gatto-felino: {sim[0][0]:.3f}") # ~0.07 (bassa!)
TF-IDF poprawia kodowanie typu one-hot, ważąc słowa według ważności, ale cierpi z tego powodu podstawowy problem: nie rozumie, że „kot” i „koci” to synonimy, bo myśli tylko na dokładnych dopasowaniach leksykalnych.
2.3 Word2Vec: Rewolucja (2013)
W 2013 roku Tomas Mikolov i jego zespół w Google wydali Word2Vec, który wszystko zmienił. Genialny pomysł: słowo jest definiowane przez kontekst, w którym się pojawia. Słowa które pojawiają się w podobnych kontekstach, będą miały podobne reprezentacje.
Word2Vec wykorzystuje płytkie sieci neuronowe do uczenia się gęstych wektorów (zwykle 100-300 wymiarów) z dużych korpusów tekstowych. Dwie architektury:
Architektury Word2Vec
| Architektura | Wejście | Wyjścia | Opis |
|---|---|---|---|
| CBOW | Słowa kontekstowe | Docelowe słowo | Przewiduje słowo centralne, biorąc pod uwagę otaczający kontekst |
| Pomiń gram | Docelowe słowo | Słowa kontekstowe | Przewiduje otaczające słowa, biorąc pod uwagę słowo centralne |
from gensim.models import Word2Vec
# Corpus di esempio (in produzione: milioni di frasi)
frasi = [
["il", "gatto", "dorme", "sul", "divano"],
["il", "cane", "gioca", "nel", "giardino"],
["il", "felino", "riposa", "sulla", "poltrona"],
["il", "cane", "corre", "nel", "parco"],
]
# Addestramento Word2Vec (Skip-gram)
model = Word2Vec(
sentences=frasi,
vector_size=100, # dimensionalità embedding
window=5, # contesto: 5 parole prima e dopo
min_count=1, # includi parole con almeno 1 occorrenza
sg=1, # 1 = Skip-gram, 0 = CBOW
epochs=100
)
# Ora "gatto" e "felino" sono vicini!
print(model.wv.most_similar("gatto", topn=3))
# [('felino', 0.92), ('cane', 0.85), ('dorme', 0.71)]
# Accesso al vettore
vettore_gatto = model.wv["gatto"]
print(f"Dimensioni: {vettore_gatto.shape}") # (100,)
print(f"Primi 5: {vettore_gatto[:5]}")
2.4 GloVe: Globalne wektory (2014)
Stanford opracował GloVe (Global Vectors for Word Representation) z innym podejściem: zamiast sieci neuronowej, GloVe rozkłada na czynniki macierz współwystępowań globalny korpus. Łączy w sobie zalety globalnych metod statystycznych (takich jak LSA) z nimi lokalnego kontekstu Word2Vec.
GloVe minimalizuje funkcję kosztu, która zapewnia iloczyn skalarny pomiędzy dwoma wektorami słów jest proporcjonalna do logarytmu prawdopodobieństwa ich współwystąpienia:
2.5 FastText: Osadzanie podsłów (2016)
Facebook AI Research (FAIR) rozszerzył Word2Vec o Szybki Tekst, które reprezentuje każde słowo jako zbiór n-gramów znaków. Rozwiązuje to dwa krytyczne problemy:
- Rzadkie lub poza słownictwem (OOV): słowa: FastText może generować osadzanie wcześniej niewidocznych słów, tworząc wektory podsegmentów
- Morfologia: słowa powiązane morfologicznie (np. „bieg”, „bieg”, „biegacz”) mają wspólne n-gramy i dlatego mają podobne wektory
Ewolucja: od rzadkich do gęstych reprezentacji
| Metoda | Rok | Typ | Typowe wymiary | Semantyka |
|---|---|---|---|---|
| Jeden gorący | - | Rozsiany | V (słownictwo) | Nic |
| TF-IDF | 1972 | Rozsiany | V (słownictwo) | Statystyka |
| Word2Stary | 2013 | Gęsty | 100-300 | Lokalny kontekst |
| Rękawica | 2014 | Gęsty | 50-300 | Globalny + lokalny |
| Szybki Tekst | 2016 | Gęsty | 100-300 | Podsłowo + kontekst |
| BERT | 2018 | Gęsty | 768 | Dynamika kontekstowa |
| Transformatory zdań | 2019 | Gęsty | 384-1024 | Całe zdania |
3. Matematyczne właściwości osadzania
Jednym z najbardziej fascynujących odkryć Word2Vec jest to, że przestrzeń wektorowa się uczy relacje algebraiczne pomiędzy pojęciami. Działania arytmetyczne na wektorach dają semantycznie spójne wyniki.
3.1 Analogie wektorowe
Słynna analogia: król - mężczyzna + kobieta = królowa. W ujęciu wektorowym, różnica między „królem” a „mężczyzną” oddaje pojęcie „królewstwa” i dodanie go do „kobiety” dostajesz „królową”. Formalnie:
import gensim.downloader as api
# Carica embeddings GloVe pre-addestrati
model = api.load("glove-wiki-gigaword-100")
# king - man + woman = ?
result = model.most_similar(
positive=["king", "woman"],
negative=["man"],
topn=3
)
print(result)
# [('queen', 0.7698), ('princess', 0.6450), ('monarch', 0.6345)]
# Altre analogie che funzionano:
# Parigi - Francia + Italia = Roma
result2 = model.most_similar(
positive=["paris", "italy"],
negative=["france"],
topn=1
)
print(result2) # [('rome', 0.8722)]
# buono - cattivo + triste = ?
result3 = model.most_similar(
positive=["good", "sad"],
negative=["bad"],
topn=1
)
print(result3) # [('happy', 0.6891)]
3.2 Klastrowanie semantyczne
Osadzenia naturalnie tworzą klastry w przestrzeni wektorowej. Jeśli projektujemy wektory w 2D (używając t-SNE lub UMAP), obserwujemy, że słowa należą do tej samej kategorii grupują razem: zwierzęta blisko zwierząt, kraje blisko krajów, zawody blisko do zawodów.
Ta właściwość ma fundamentalne znaczenie dla zastosowań praktycznych: wyszukiwanie podobieństw działa właśnie dlatego, że dokumenty na podobne tematy są ściśle osadzone w przestrzeni wektorowej.
4. Nowoczesne osadzanie: osadzanie kontekstowe
Word2Vec i GloVe generują pojedynczy wektor dla słowa, niezależny od kontekst. Ale „szkoła” ma różne znaczenia w „łacinie szkolnej” i „szkółce ryb”. TO osadzania kontekstowe, wprowadzone wraz z BERT w 2018 r., rozwiązują ten problem: to samo słowo ma różne wektory w zależności od kontekstu.
4.1 Osadzenia BERT
BERT (Dwukierunkowe reprezentacje enkodera z transformatorów) przetwarza całe zdanie i wyprowadza wektor dla każdego żetonu. Aby osadzić całe zdanie, zwykle używasz:
- Tokeny CLS: pierwszy specjalny token [CLS] zawiera zbiorczą reprezentację zdania
- Średnie łączenie: średnia wszystkich wektorów tokenów — generalnie daje lepsze wyniki wyszukiwania podobieństw
BERT nie jest optymalny do wyszukiwania podobieństw
Oryginalny BERT nie był przeszkolony do tworzenia wysokiej jakości osadzonych zdań. Token CLS i zoptymalizowane pod kątem klasyfikacji, a nie podobieństwa semantycznego. Aby wyszukać podobieństwo, potrzebne są wyspecjalizowane modele, takie jak Transformatory Zdań.
4.2 Transformatory zdań (SBERT)
W 2019 roku Reimers i Gurevych wprowadzili Sentence-BERT, dostrajając BERT za pomocą Syjamski, aby tworzyć sensowne osadzania zdań. To zrewolucjonizowało podobieństwo wyszukiwanie: po raz pierwszy możliwe było porównywanie zdań o prostej odległości cosinus, uzyskanie wysokiej jakości wyników.
4.3 Osadzanie modeli: kompleksowe porównanie
Porównanie modeli osadzania (2026)
| Model | Dostawcy | Wymiary | Wynik MTEB | Koszt / 1 milion tokenów | Notatki |
|---|---|---|---|---|---|
| osadzanie tekstu-3-małe | OpenAI | 1536 | 62,3 | 0,02 USD | Dobry stosunek jakości do ceny |
| osadzanie tekstu-3-duże | OpenAI | 3072 | 64,6 | 0,13 USD | Maksymalna jakość OpenAI |
| osadź-v3 | Przystać do siebie | 1024 | 64,5 | 0,10 USD | Obsługuje ponad 100 języków |
| podróż-3 | AI podróży | 1024 | 67.1 | 0,06 USD | Góra do odzyskania |
| all-MiniLM-L6-v2 | Przytulana twarz | 384 | 56.3 | Bezpłatny | Szybki, lokalny, kompaktowy |
| all-mpnet-base-v2 | Przytulana twarz | 768 | 57,8 | Bezpłatny | Najlepszy podstawowy model open source |
| gte-large-en-v1.5 | Alibaba (HF) | 1024 | 65,4 | Bezpłatny | Konkurencja z modelami komercyjnymi |
| bge-large-en-v1.5 | BAAI (HF) | 1024 | 64.2 | Bezpłatny | Świetne dla RAG-a |
Jak wybrać model
- Prototyp / ograniczony budżet: all-MiniLM-L6-v2 (bezpłatny, szybki, 384 dim)
- Produkcja, niskie koszty: osadzanie tekstu-3-small (OpenAI, token 0,02 USD/1 mln)
- Najwyższa jakość pobierania: voyage-3 lub gte-large-en-v1.5
- Wielojęzyczny: Cohere embed-v3 (ponad 100 języków)
- Własny hosting / prywatność: bge-large-en-v1.5 lub gte-large-en-v1.5
5. Pomiary odległości pomiędzy wektorami
Wybór metryki odległości bezpośrednio wpływa na jakość poszukiwania podobieństwa. Przyjrzyjmy się czterem głównym metrykom, ich wzorom matematycznym i mocnym stronom i kiedy ich używać.
5.1 Podobieństwo cosinusowe
Najczęściej używana metryka do osadzania tekstu. Mierzy kąt między dwoma wektorami, pomijając ich wielkość (długość). Dwa wektory skierowane w tym samym kierunku mają cosinus podobieństwa 1, ortogonalny 0, przeciwieństwa -1.
W pgvector operator <=> obliczyć odległość cosinus
(= 1 - cosinus podobieństwa), gdzie 0 oznacza identyczne, a 2 oznacza przeciwieństwa.
5.2 Odległość euklidesowa (L2)
Odległość „w linii prostej” pomiędzy dwoma punktami w przestrzeni. Uwzględnia zarówno kierunku niż wielkość wektorów.
W pgvector operator <-> oblicz odległość L2.
Produkt punktowy 5.3 (produkt punktowy)
Iloczyn skalarny mierzy zarówno kierunek, jak i wielkość. Dla wektorów znormalizowanych (norma = 1), iloczyn skalarny jest równoważny podobieństwu cosinusowi.
W pgvector operator <#> obliczyć ujemny iloczyn wewnętrzny (dla zgodności
z ZAMÓWIENIEM PRZEZ ASC).
5.4 Odległość do Manhattanu (L1)
Suma różnic bezwzględnych składnik po składniku. Mniej wrażliwy na wartości odstające w odniesieniu do odległości euklidesowej.
import numpy as np
from scipy.spatial.distance import cosine, euclidean, cityblock
# Due vettori di esempio (normalizzati)
a = np.array([0.5, 0.3, 0.8, 0.1, 0.6])
b = np.array([0.4, 0.35, 0.75, 0.15, 0.55])
# Normalizzazione L2
a_norm = a / np.linalg.norm(a)
b_norm = b / np.linalg.norm(b)
# 1. Cosine Similarity (1 - cosine distance)
cos_sim = 1 - cosine(a_norm, b_norm)
print(f"Cosine similarity: {cos_sim:.6f}") # ~0.999
# 2. Distanza Euclidea (L2)
l2_dist = euclidean(a_norm, b_norm)
print(f"Distanza L2: {l2_dist:.6f}") # ~0.042
# 3. Dot Product (per vettori normalizzati = cosine similarity)
dot = np.dot(a_norm, b_norm)
print(f"Dot product: {dot:.6f}") # ~0.999
# 4. Distanza Manhattan (L1)
l1_dist = cityblock(a_norm, b_norm)
print(f"Distanza L1: {l1_dist:.6f}") # ~0.072
# Relazione L2-Cosine per vettori normalizzati:
# d_L2^2 = 2 * (1 - cos_sim)
print(f"\nVerifica: L2^2 = {l2_dist**2:.6f}")
print(f"2*(1-cos) = {2*(1-cos_sim):.6f}") # uguale!
Kiedy używać jakiego metryki
| Metryczny | operator pgvector | Użyj Kiedy | Unikaj Kiedy |
|---|---|---|---|
| Małe rzeczy | <=> |
Osadzanie tekstu, gdy wielkość nie ma znaczenia | Dane przestrzenne, których wielkość jest znacząca |
| L2 (euklidesowy) | <-> |
Obrazy, dane liczbowe, gdy liczy się wielkość | Wektory o różnych skalach pomiędzy składnikami |
| Produkt kropkowy | <#> |
Wektory już znormalizowane (nieco lepsza wydajność) | Wektory nieznormalizowane (wyniki zniekształcone wielkością) |
| Manhattan (L1) | Nienatywny w pgvector | Rzadkie dane, odporność na wartości odstające | Ogólne zastosowanie przy gęstych osadach |
Praktyczna zasada
W 95% przypadków z osadzeniem tekstu użyj odległość cosinus
(<=> w pgvectorze). Nowoczesne modele osadzania tworzą wektory
już znormalizowany, co sprawia, że iloczyny cosinusowe i kropkowe są praktycznie równoważne. Odległość
Euklidesowy ma sens w przypadku danych przestrzennych lub gdy wielkość wektora niesie informację.
6. Wygeneruj osadzanie w Pythonie
Zobaczmy teraz, jak wygenerować osadzenie przy użyciu trzech różnych podejść: modele lokalne z Transformatory zdań, API OpenAI i API wnioskowania HuggingFace. Każde podejście ma konkretne korzyści i kompromisy.
6.1 Transformatory zdań (lokalne)
Najbardziej elastyczne i prywatne podejście: model działa na Twoim komputerze, bez danych wychodzi z sieci, brak kosztów wywołania API.
# pip install sentence-transformers
from sentence_transformers import SentenceTransformer
import numpy as np
# Carica il modello (scaricato automaticamente al primo uso)
model = SentenceTransformer("all-MiniLM-L6-v2")
# Embedding di una singola frase
frase = "PostgreSQL e un database relazionale open-source"
embedding = model.encode(frase)
print(f"Tipo: {type(embedding)}") # numpy.ndarray
print(f"Dimensioni: {embedding.shape}") # (384,)
# Embedding di più frasi (batch - molto più efficiente)
frasi = [
"PostgreSQL e un database relazionale open-source",
"pgvector aggiunge il supporto per vettori a PostgreSQL",
"Il machine learning richiede grandi quantità di dati",
"La pizza margherita e un piatto tipico napoletano",
]
embeddings = model.encode(
frasi,
batch_size=32, # processa 32 frasi alla volta
show_progress_bar=True, # mostra progresso per batch grandi
normalize_embeddings=True # normalizza a norma L2 = 1
)
print(f"Shape: {embeddings.shape}") # (4, 384)
# Calcola similarità tra tutte le coppie
from sentence_transformers.util import cos_sim
similarities = cos_sim(embeddings, embeddings)
print(f"\nMatrice di similarità:\n{similarities}")
# Le prime 2 frasi (su PostgreSQL) avranno alta similarità
# La frase sulla pizza sarà distante dalle altre
6.2 API osadzania OpenAI
Interfejs API OpenAI oferuje wysokiej jakości modele bez zarządzania infrastrukturą. Idealny do produkcji średnionakładowej.
# pip install openai
from openai import OpenAI
import os
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def get_embeddings(
texts: list[str],
model: str = "text-embedding-3-small"
) -> list[list[float]]:
"""Genera embeddings per una lista di testi."""
response = client.embeddings.create(
input=texts,
model=model,
)
return [item.embedding for item in response.data]
# Singolo embedding
testo = "PostgreSQL come vector database per AI"
embedding = get_embeddings([testo])[0]
print(f"Dimensioni: {len(embedding)}") # 1536
# Batch di embeddings (fino a 2048 testi per chiamata)
testi = [
"Come installare pgvector su Docker",
"Tutorial per similarity search in PostgreSQL",
"Guida alla cottura della pasta al forno",
]
embeddings = get_embeddings(testi)
print(f"Embeddings generati: {len(embeddings)}") # 3
# Dimensione ridotta con text-embedding-3-small
# Puoi specificare dimensioni inferiori per risparmiare spazio
response = client.embeddings.create(
input=["Testo di esempio"],
model="text-embedding-3-small",
dimensions=512 # ridotto da 1536 a 512
)
emb_ridotto = response.data[0].embedding
print(f"Dimensioni ridotte: {len(emb_ridotto)}") # 512
6.3 Interfejs API wnioskowania HuggingFace
Kompromis pomiędzy modelami lokalnymi i komercyjnymi API: dostęp do tysięcy modeli open source za pośrednictwem API, z dużą darmową warstwą.
# pip install huggingface_hub
from huggingface_hub import InferenceClient
import os
client = InferenceClient(
token=os.getenv("HF_TOKEN")
)
def get_hf_embeddings(
texts: list[str],
model: str = "BAAI/bge-large-en-v1.5"
) -> list[list[float]]:
"""Genera embeddings usando HuggingFace Inference API."""
result = client.feature_extraction(
text=texts,
model=model,
)
return result
# Genera embeddings
testi = [
"Vector search con PostgreSQL e pgvector",
"Come creare indici HNSW per ricerca veloce",
]
embeddings = get_hf_embeddings(testi)
print(f"Embeddings: {len(embeddings)}") # 2
print(f"Dimensioni: {len(embeddings[0])}") # 1024 (bge-large)
6.4 Efektywne przetwarzanie wsadowe
Gdy potrzebujesz wygenerować elementy osadzone dla tysięcy lub milionów dokumentów, wydajność przetwarzania wsadowego staje się krytyczne.
import time
from typing import Generator
from sentence_transformers import SentenceTransformer
import numpy as np
def chunk_list(
lst: list, chunk_size: int
) -> Generator[list, None, None]:
"""Divide una lista in chunk di dimensione fissa."""
for i in range(0, len(lst), chunk_size):
yield lst[i:i + chunk_size]
def generate_embeddings_batch(
texts: list[str],
model_name: str = "all-MiniLM-L6-v2",
batch_size: int = 256,
device: str = "cpu" # "cuda" per GPU
) -> np.ndarray:
"""Genera embeddings in batch con progress tracking."""
model = SentenceTransformer(model_name, device=device)
all_embeddings = []
total_batches = (len(texts) + batch_size - 1) // batch_size
start = time.time()
for i, batch in enumerate(chunk_list(texts, batch_size)):
batch_emb = model.encode(
batch,
batch_size=batch_size,
normalize_embeddings=True,
show_progress_bar=False
)
all_embeddings.append(batch_emb)
elapsed = time.time() - start
rate = (i + 1) * batch_size / elapsed
print(
f"Batch {i+1}/{total_batches} - "
f"{rate:.0f} testi/sec"
)
return np.vstack(all_embeddings)
# Utilizzo
texts = [f"Documento numero {i}" for i in range(10_000)]
embeddings = generate_embeddings_batch(
texts,
batch_size=256,
device="cuda" # usa GPU se disponibile
)
print(f"Shape finale: {embeddings.shape}") # (10000, 384)
7. Historyzuj osadzanie w PostgreSQL
Teraz, gdy wiemy, jak generować osadzania, zobaczmy, jak zapisać je w PostgreSQL za pomocą pgvector i wykonywać zapytania wyszukiwania podobieństw. To jest praktyczny link do artykułu 1 z serii.
7.1 Układ tabeli
-- Abilita pgvector
CREATE EXTENSION IF NOT EXISTS vector;
-- Tabella documenti con embedding
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(500) NOT NULL,
content TEXT NOT NULL,
source VARCHAR(255),
category VARCHAR(100),
embedding vector(384), -- dimensione del modello scelto
created_at TIMESTAMPTZ DEFAULT NOW(),
metadata JSONB DEFAULT '{}'::jsonb
);
-- Indice HNSW per ricerca veloce (cosine distance)
CREATE INDEX idx_documents_embedding
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);
-- Indice su categoria per filtri combinati
CREATE INDEX idx_documents_category
ON documents (category);
7.2 Wstawianie z Pythona
import psycopg2
from psycopg2.extras import execute_values
from sentence_transformers import SentenceTransformer
import numpy as np
# Configurazione
DB_CONFIG = {
"host": "localhost",
"port": 5432,
"dbname": "vectordb",
"user": "admin",
"password": "secret_password",
}
# 1. Genera embeddings
model = SentenceTransformer("all-MiniLM-L6-v2")
documenti = [
{
"title": "Introduzione a pgvector",
"content": "pgvector e un'estensione PostgreSQL per vettori...",
"source": "blog",
"category": "database"
},
{
"title": "RAG con LangChain",
"content": "Retrieval Augmented Generation combina retrieval...",
"source": "tutorial",
"category": "ai"
},
{
"title": "Python per Data Science",
"content": "Python e il linguaggio più usato per data science...",
"source": "guide",
"category": "programming"
},
]
# Genera embeddings per i contenuti
testi = [d["content"] for d in documenti]
embeddings = model.encode(testi, normalize_embeddings=True)
# 2. Salva in PostgreSQL
conn = psycopg2.connect(**DB_CONFIG)
cur = conn.cursor()
# Prepara i dati per batch insert
values = []
for doc, emb in zip(documenti, embeddings):
values.append((
doc["title"],
doc["content"],
doc["source"],
doc["category"],
emb.tolist() # converti numpy array in lista Python
))
# Inserimento batch efficiente
execute_values(
cur,
"""INSERT INTO documents
(title, content, source, category, embedding)
VALUES %s""",
values,
template="(%s, %s, %s, %s, %s::vector)"
)
conn.commit()
print(f"Inseriti {len(values)} documenti con embeddings")
cur.close()
conn.close()
7.3 Wyszukiwanie podobieństw w Pythonie
def similarity_search(
query: str,
top_k: int = 5,
category: str = None,
threshold: float = 0.3
) -> list[dict]:
"""Cerca documenti simili alla query."""
# Genera embedding della query
query_embedding = model.encode(
query, normalize_embeddings=True
).tolist()
conn = psycopg2.connect(**DB_CONFIG)
cur = conn.cursor()
# Query con filtro opzionale
if category:
cur.execute("""
SELECT id, title, content, category,
1 - (embedding <=> %s::vector) AS similarity
FROM documents
WHERE category = %s
AND 1 - (embedding <=> %s::vector) > %s
ORDER BY embedding <=> %s::vector
LIMIT %s
""", (
query_embedding, category,
query_embedding, threshold,
query_embedding, top_k
))
else:
cur.execute("""
SELECT id, title, content, category,
1 - (embedding <=> %s::vector) AS similarity
FROM documents
WHERE 1 - (embedding <=> %s::vector) > %s
ORDER BY embedding <=> %s::vector
LIMIT %s
""", (
query_embedding,
query_embedding, threshold,
query_embedding, top_k
))
results = []
for row in cur.fetchall():
results.append({
"id": row[0],
"title": row[1],
"content": row[2][:200], # troncato
"category": row[3],
"similarity": round(row[4], 4),
})
cur.close()
conn.close()
return results
# Esempio d'uso
risultati = similarity_search(
"come usare i vettori in un database",
top_k=3,
category="database"
)
for r in risultati:
print(f"[{r['similarity']}] {r['title']}")
7.4 Indeksowanie: HNSW vs IVFFlat
W przypadku zbiorów danych zawierających więcej niż kilka tysięcy dokumentów indeks jest niezbędny akceptowalna wydajność. pgvector oferuje dwa typy indeksów:
HNSW vs IVFFlat
| Charakterystyczny | HNSW | IVFFlat |
|---|---|---|
| Szybkość zapytania | Bardzo szybko | Szybko |
| Przypomnienie sobie czegoś | 95-99% | 85-95% |
| Czas budowania | Wolno (minuty) | Szybko (sekundy) |
| Pamięć | Wysoka (wykres w pamięci RAM) | Niski (centrroidy) |
| Wstaw/Aktualizuj | Dobry (aktualizacja przyrostowa) | Wymaga okresowej odbudowy |
| Polecane dla | Produkcja, wysoka jakość | Prototypowanie, statyczne zbiory danych |
-- HNSW (raccomandato per produzione)
-- m: connessioni per nodo (16-64, default 16)
-- ef_construction: qualità costruzione (64-512, default 64)
CREATE INDEX idx_hnsw_cosine
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);
-- Per L2 distance
CREATE INDEX idx_hnsw_l2
ON documents
USING hnsw (embedding vector_l2_ops)
WITH (m = 16, ef_construction = 200);
-- IVFFlat (più veloce da costruire)
-- lists: numero di cluster (sqrt(N) come regola base)
CREATE INDEX idx_ivfflat_cosine
ON documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100); -- per ~10K documenti
-- Parametri di query per controllare recall vs velocità
SET hnsw.ef_search = 100; -- default 40, alza per più recall
SET ivfflat.probes = 10; -- default 1, alza per più recall
8. Osadzania dla różnych typów danych
Osadzanie nie ogranicza się do tekstu. Nowoczesne modele mogą generować reprezentacje grafikę wektorową dla obrazów, dźwięku, kodu źródłowego, a nawet danych multimodalnych.
Osadzanie multimodalne: modele typów danych
| Typ danych | Model | Wymiary | Przypadek użycia |
|---|---|---|---|
| Tekst | all-MiniLM-L6-v2, osadzanie tekstu-3-małe | 384-3072 | Wyszukiwanie semantyczne, RAG, klasyfikacja |
| Obrazy | CLIP (OpenAI), SigLIP (Google) | 512-768 | Wyszukiwanie obrazów, klasyfikacja wizualna |
| Audio | Szepnij, KLIK | 512-1280 | Wyszukiwanie audio, klasyfikacja muzyki |
| Kod | CodeBERT, osadzanie StarCoder | 768 | Wyszukiwanie kodu, wykrywanie duplikatów |
| Multimodalny | KLIP, ImageBind (Meta) | 512-1024 | Wyszukiwanie międzymodalne (tekst według obrazu) |
# pip install transformers pillow
from transformers import CLIPProcessor, CLIPModel
from PIL import Image
import torch
import numpy as np
# Carica CLIP
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
# Embedding di un'immagine
image = Image.open("foto_gatto.jpg")
inputs = processor(images=image, return_tensors="pt")
with torch.no_grad():
image_embedding = model.get_image_features(**inputs)
image_emb = image_embedding[0].numpy()
print(f"Image embedding: {image_emb.shape}") # (512,)
# Embedding di testo (nello STESSO spazio!)
text_inputs = processor(
text=["un gatto che dorme", "un cane che gioca"],
return_tensors="pt",
padding=True
)
with torch.no_grad():
text_embeddings = model.get_text_features(**text_inputs)
text_embs = text_embeddings.numpy()
# Calcola similarità cross-modale
from numpy.linalg import norm
for i, text in enumerate(["un gatto che dorme", "un cane che gioca"]):
sim = np.dot(image_emb, text_embs[i]) / (
norm(image_emb) * norm(text_embs[i])
)
print(f"Similarità '{text}': {sim:.4f}")
# "un gatto che dorme" avra similarità più alta con foto_gatto.jpg
Siła CLIP polega na tym, że tekst i obrazy żyją w nim tę samą przestrzeń wektorową. Możesz wyszukiwać obrazy za pomocą zapytania tekstowego lub znajdować tekst powiązany z obrazem. To otwiera scenariusze takie jak wyszukiwanie multimodalne w PostgreSQL: zapisz w nim osadzenia CLIP tabela i okręgi pgvector z zapytaniami tekstowymi.
9. Oceń jakość Osadzania
Skąd wiesz, czy model osadzania jest „dobry”? Odpowiedź zależy od zadania specyficzne, ale istnieją ustandaryzowane punkty odniesienia i obiektywne wskaźniki.
9.1 MTEB: test porównawczy osadzania ogromnego tekstu
MTEB jest punktem odniesienia do oceny modeli osadzania. Zmierz wydajność na 58+ zadaniach pogrupowanych w 8 kategoriach:
- Wyszukiwanie: znaleźć odpowiednie dokumenty na podstawie zapytania
- Semantyczne podobieństwo tekstowe (STS): jak podobne są dwa zdania
- Klasyfikacja: klasyfikuj teksty na kategorie
- Klastrowanie: grupuj podobne teksty
- Klasyfikacja par: określić, czy dwa teksty są ze sobą powiązane
- Zmiana rankingu: zmień kolejność wyników według trafności
- Streszczenie: jakość podsumowań
- Wydobywanie bittekstu: znajdź tłumaczenia równoległe
from sentence_transformers import SentenceTransformer
from sentence_transformers.evaluation import (
InformationRetrievalEvaluator
)
# Prepara dataset di valutazione
queries = {
"q1": "come installare pgvector",
"q2": "cos'è la similarity search",
"q3": "embedding di immagini con CLIP",
}
corpus = {
"d1": "Guida installazione pgvector su Ubuntu",
"d2": "pgvector per PostgreSQL: setup Docker",
"d3": "Ricerca per similarità vettoriale",
"d4": "CLIP: modello multimodale per immagini e testo",
"d5": "Ricetta pasta alla carbonara",
}
# Mappatura query -> documenti rilevanti
relevant = {
"q1": {"d1": 1, "d2": 1}, # d1 e d2 rilevanti per q1
"q2": {"d3": 1},
"q3": {"d4": 1},
}
# Valuta il modello
model = SentenceTransformer("all-MiniLM-L6-v2")
evaluator = InformationRetrievalEvaluator(
queries=queries,
corpus=corpus,
relevant_docs=relevant,
name="custom-eval"
)
results = evaluator(model)
print(f"NDCG@10: {results['custom-eval_ndcg@10']:.4f}")
print(f"MAP@10: {results['custom-eval_map@10']:.4f}")
9.2 Ocena wewnętrzna i zewnętrzna
Dwa podejścia do ewaluacji
| Typ | Co mierzy | Przykład | Kiedy stosować |
|---|---|---|---|
| Wewnętrzny | Właściwości samych wektorów | Analogie, grupowanie, STS | Szybkie porównanie modeli |
| Zewnętrzny | Wykonanie ostatniego zadania | Jakość RAG, precyzja badań | Ostateczna decyzja w produkcji |
Praktyczne porady
Nie polegaj tylko na wyniku MTEB. Model może mieć wysoki wynik MTEB, ale słabo działają w Twojej konkretnej domenie. Zawsze oceniaj na swoim zestawie danych: utwórz mały zestaw odpowiednich zapytań i dokumentów ze swojej domeny i dokonaj pomiaru nDCG i MAP. Dzięki temu uzyskasz znacznie bardziej wiarygodne oszacowanie rzeczywistej wydajności.
10. Redukcja wymiarowości
Wektory wielowymiarowe są trudne do wizualizacji i mogą być kosztowne pod względem przechowywania i obliczeń. Techniki redukcji wymiarowości pomagają zarówno w wizualizacji, jak i optymalizacji.
10.1 Techniki wizualizacji
Techniki redukcji wymiarowej
| Technika | Utrzymywać | Prędkość | Typowe zastosowanie |
|---|---|---|---|
| PCA | Globalna wariancja | Bardzo szybko | Rozdrabnianie w celu przechowywania, wstępnego przetwarzania |
| t-SN | Struktura lokalna | Powolny | Wizualizacja 2D klastrów |
| UMAP | Struktura lokalna + globalna | Przeciętny | Wizualizacja 2D, również do redukcji przed indeksowaniem |
# pip install umap-learn matplotlib
import umap
import matplotlib.pyplot as plt
import numpy as np
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("all-MiniLM-L6-v2")
# Testi con categorie diverse
testi = [
# Database
"PostgreSQL e un database relazionale",
"MongoDB e un database NoSQL",
"Redis e un database in-memory",
# AI/ML
"Il deep learning usa reti neurali profonde",
"GPT e un modello di linguaggio",
"La regressione lineare e un algoritmo semplice",
# Cucina
"La pizza si cuoce nel forno a legna",
"Il tiramisu e un dolce italiano",
"La pasta alla carbonara usa uova e guanciale",
]
categorie = ["DB"]*3 + ["AI"]*3 + ["Cucina"]*3
colori = ["blue"]*3 + ["red"]*3 + ["green"]*3
# Genera embeddings (384 dimensioni)
embeddings = model.encode(testi, normalize_embeddings=True)
# Riduci a 2D con UMAP
reducer = umap.UMAP(n_components=2, random_state=42)
emb_2d = reducer.fit_transform(embeddings)
# Visualizza
plt.figure(figsize=(10, 8))
for i, (x, y) in enumerate(emb_2d):
plt.scatter(x, y, c=colori[i], s=100, zorder=5)
plt.annotate(testi[i][:30], (x, y),
fontsize=8, ha='left')
plt.title("Embeddings in 2D (UMAP)")
plt.savefig("embeddings_umap.png", dpi=150)
plt.show()
10.2 Osadzenia Matrioszki
Najnowsza i innowacyjna technika: m.in Nauka reprezentacji Matrioszki (MRL) osadzania są trenowane w taki sposób, że pierwszych N składników wektora jest już osadzeniem ważne. Możesz obciąć wektor z 1536 do 512 lub 256 wymiarów przy zachowaniu dobrej jakości.
OpenAI text-embedding-3-small e text-embedding-3-large wsparcie
ta technika: możesz określić parametr dimensions w celu uzyskania wektorów
bardziej zwarty bez ponownego obliczania osadzania.
from openai import OpenAI
import numpy as np
import os
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
testo = "PostgreSQL come vector database per applicazioni AI"
# Genera lo stesso embedding a dimensioni diverse
for dim in [256, 512, 1024, 1536]:
response = client.embeddings.create(
input=[testo],
model="text-embedding-3-small",
dimensions=dim
)
emb = response.data[0].embedding
print(f"Dimensioni: {dim}, norma: {np.linalg.norm(emb):.4f}")
# In PostgreSQL: usa colonne con dimensione appropriata
# CREATE TABLE docs_compact (
# id BIGSERIAL PRIMARY KEY,
# content TEXT,
# embedding vector(256) -- più compatto, 6x meno storage
# );
11. Skalowanie kosztów i strategii
W przypadku przejścia od prototypu do produkcji koszty wytworzenia i przechowywania osadzanie staje się czynnikiem krytycznym. Zobaczmy szczegółową analizę.
11.1 Koszty na 1 milion dokumentów
Kosztorys: 1 milion dokumentów (średnio 500 tokenów/dokument)
| Model | Koszt generacji | Rozmiar wektorowy | Pamięć (float32) | Początkowa suma |
|---|---|---|---|---|
| all-MiniLM-L6-v2 | 0 USD (lokalnie) | 384 | ~1,4 GB | Tylko czas GPU/CPU |
| osadzanie tekstu-3-małe | ~10 dolarów | 1536 | ~5,7 GB | ~10 USD + miejsce |
| osadzanie tekstu-3-duże | ~65 dolarów | 3072 | ~11,4 GB | ~65 USD + miejsce |
| podróż-3 | ~30 dolarów | 1024 | ~3,8 GB | ~30 USD + miejsce |
Formuła przechowywania: N dokumentów * rozmiar * 4 bajty (float32). Przykład: 1M * 1536 * 4 = 5,7 GB tylko dla operatorów.
11.2 Hosting własny a API: kompromis
Porównanie hostowanych samodzielnie i API
| Czekam | Hostowane samodzielnie | API (OpenAI, Cohere) |
|---|---|---|
| Koszt początkowy | Wysoka (GPU ~1-3 USD/godz.) | Niski (płatność za użycie) |
| Koszt w przeliczeniu na objętość | Najtańszy dokument o wartości ponad ~10 mln | Liniowy, skaluje się wraz z objętością |
| Utajenie | Niski (brak sieci) | 50-200 ms na połączenie |
| Prywatność | Dane pozostają lokalne | Dane przesyłane podmiotom trzecim |
| Konserwacja | Zarządzanie GPU, aktualizacje, monitorowanie | Zero |
| jakość | To zależy od wybranego modelu | Generalnie wysoki i stały |
11.3 Strategie buforowania
Osadzanie pamięci podręcznej jest niezbędne, aby zmniejszyć koszty i opóźnienia. Jeśli to samo tekst jest żądany kilka razy, ponowne generowanie osadzania nie ma sensu.
import hashlib
import json
import psycopg2
from typing import Optional
import numpy as np
class EmbeddingCache:
"""Cache embeddings in PostgreSQL per evitare ricalcoli."""
def __init__(self, db_config: dict):
self.db_config = db_config
self._init_table()
def _init_table(self):
conn = psycopg2.connect(**self.db_config)
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS embedding_cache (
content_hash VARCHAR(64) PRIMARY KEY,
model_name VARCHAR(100) NOT NULL,
embedding vector(1536),
created_at TIMESTAMPTZ DEFAULT NOW()
)
""")
conn.commit()
cur.close()
conn.close()
def _hash(self, text: str, model: str) -> str:
"""Hash deterministico del contenuto + modello."""
key = f"{model}::{text}"
return hashlib.sha256(key.encode()).hexdigest()
def get(
self, text: str, model: str
) -> Optional[list[float]]:
"""Cerca embedding in cache."""
h = self._hash(text, model)
conn = psycopg2.connect(**self.db_config)
cur = conn.cursor()
cur.execute(
"SELECT embedding FROM embedding_cache "
"WHERE content_hash = %s",
(h,)
)
row = cur.fetchone()
cur.close()
conn.close()
return row[0] if row else None
def put(
self, text: str, model: str, embedding: list[float]
):
"""Salva embedding in cache."""
h = self._hash(text, model)
conn = psycopg2.connect(**self.db_config)
cur = conn.cursor()
cur.execute(
"INSERT INTO embedding_cache "
"(content_hash, model_name, embedding) "
"VALUES (%s, %s, %s::vector) "
"ON CONFLICT (content_hash) DO NOTHING",
(h, model, embedding)
)
conn.commit()
cur.close()
conn.close()
12. Połączenie z serią PostgreSQL AI
Podsumujmy, jak osadzania integrują się z budowanym przez nas ekosystemem w tej serii:
Pełen przebieg: od artykułu 1 do artykułu 3
| Krok | Przedmiot | Działanie |
|---|---|---|
| 1 | pgvector (art. 1) | Skonfiguruj PostgreSQL za pomocą pgvector, utwórz tabele z kolumnami wektorowymi |
| 2 | Osadzenia (art. 2 – to) | Wybierz szablon, wygeneruj osadzenia, zapisz je w pgvector |
| 3 | RAG z PostgreSQL (art. 3) | Połącz pobieranie za pośrednictwem pgvector z LLM, aby odpowiadać na pytania |
W artykule 1 przygotowaliśmy infrastrukturę: PostgreSQL z zainstalowanym pgvectorem, tabele ze skonfigurowanymi kolumnami wektorowymi i indeksami HNSW. W tym artykule wypełniliśmy tę lukę podstawowe: skąd pochodzą te wektory, jak wybrać odpowiedni model i jak je wygenerować skutecznie. W następnym artykule zbudujemy kompletny rurociąg RAG, który wykorzysta wszystko ten stos: dokumenty indeksowane w pgvector, osadzania generowane na bieżąco dla zapytań, oraz LLM, który generuje odpowiedzi na podstawie pobranego kontekstu.
13. Wnioski i lista kontrolna
Osadzenia są podstawowym elementem łączącym język naturalny z matematyka wektorowych baz danych. Wybór odpowiedniego modelu, metryka odległości odpowiednia i skuteczna strategia skalowania to decyzje, które mają bezpośredni wpływ jakość i koszty Twojego systemu AI.
Lista kontrolna: Wybór odpowiedniego modelu osadzania
- Zdefiniuj zadanie: wyszukiwanie, klasyfikacja, grupowanie, STS?
- Zidentyfikuj język: Tylko w języku angielskim, wielojęzyczny czy specyficzny dla domeny?
- Oceń ograniczenia: budżet, prywatność, opóźnienia, dostępna infrastruktura
- Wybierz 2-3 kandydatów z tabeli modeli (rozdział 4.3)
- Utwórz zestaw ewaluacyjny z Twojej domeny (50-100 zapytań z odpowiednimi dokumentami)
- Mierzy nDCG i MAP w zbiorze danych każdego kandydata
- Oblicz koszty w pełni sprawny dla oczekiwanego wolumenu (sekcja 11)
- Przetestuj mały rozmiar: 512 przyciemnień często wystarcza do wielu zastosowań
- Zaimplementuj buforowanie w celu obniżenia kosztów regeneracji
- Monitoruj jakość w miarę upływu czasu z zestawem ewaluacyjnym
Następny artykuł: RAG z PostgreSQL
W następnym artykule z tej serii zbudujemy taki Rurociąg RAG gotowy (Generacja rozszerzona pobierania) przy użyciu PostgreSQL + pgvector jako bazy wiedzy. Zobaczymy, jak połączyć wyszukiwanie podobieństw z inteligentnym fragmentowaniem, jak integrować LLM, takie jak GPT-4 i Claude, oraz sposoby pomiaru jakości wygenerowanych odpowiedzi.







