aidoc.uz

2-qism · Agentlar va bilim izlash·05-bob

RAG — modelga tashqi xotira berish

Gallyutsinatsiyani kamaytirish va bilimni yangilash. BM25 dan vektor qidiruvgacha, bo'lak strategiyalarini o'lchash, baholash metrikalari va to'liq ishlaydigan hujjat savol-javob tizimi.

O'tgan bobda agent qurdik. U internetdan qidira oladi — lekin topgan narsasini saqlay olmaydi.

Va yana bir muammo bor, undan ham jiddiyroq.

Muammoni ko'rib chiqamiz#

Modeldan o'z hujjatingiz haqida so'rang:

javob = client.messages.create(
    model="claude-opus-5",
    max_tokens=512,
    messages=[{
        "role": "user",
        "content": "Bizning kompaniyaning ta'til siyosati qanday?",
    }],
)

Model ishonch bilan javob beradi. Va bu javob to'liq to'qib chiqarilgan bo'ladi — chunki u sizning ichki hujjatingizni hech qachon ko'rmagan.

2020-yilda Meta yechim taklif qildi: RAGRetrieval-Augmented Generation.

G'oyasi juda oddiy:

Modeldan javob so'rashdan oldin, unga kerakli hujjatlarni topib berasiz.


Avval: an'anaviy qidiruv#

RAG'ni tushunish uchun avval oddiy qidiruv qanday ishlashini ko'raylik. Bu tarixiy ekskursiya emas — BM25 bugun ham RAG tizimlarining yarmida ishlatiladi.

Teskari indeks#

Million hujjatda so'z qidirish uchun hammasini o'qib chiqish kerakmi? Yo'q:

from collections import defaultdict
 
 
def teskari_indeks_qur(hujjatlar):
    """so'z → qaysi hujjatlarda uchraydi"""
    indeks = defaultdict(set)
    for i, hujjat in enumerate(hujjatlar):
        for soz in hujjat.lower().split():
            indeks[soz].add(i)
    return indeks
 
 
hujjatlar = [
    "mushuk uyda uxlaydi",
    "it bog'da yuguradi",
    "mushuk va it do'st",
]
 
indeks = teskari_indeks_qur(hujjatlar)
print(indeks["mushuk"])      # {0, 2}
print(indeks["it"])          # {1, 2}

Bitta lug'at qidiruvi — va sizda mos hujjatlar ro'yxati. Bu 1960-yillardan beri ishlatiladi va hamon eng tez usul.

BM25 — noldan yozamiz#

TF-IDF ni birinchi bobda ko'rgan edik. BM25 — uning kuchli varianti va bugungi sanoat standarti.

Ikkita qo'shimcha sozlagichi bor:

k
Chastota to'yinishi
So'z 100 marta uchrasa, u 1 marta uchraganidan 100 barobar muhimroq EMAS. k parametri bu 'so'nayotgan foyda'ni boshqaradi. Odatda 1.2–2.0.
b
Uzunlik normallashtirish
Uzun hujjatda har qanday so'z ko'proq uchraydi. b=1 bo'lsa uzunlik to'liq hisobga olinadi, b=0 bo'lsa umuman olinmaydi. Odatda 0.75.
import math
from collections import Counter
 
 
class BM25:
    def __init__(self, hujjatlar, k=1.5, b=0.75):
        self.k = k
        self.b = b
        self.hujjatlar = [h.lower().split() for h in hujjatlar]
        self.N = len(self.hujjatlar)
        self.ortacha_uzunlik = sum(len(h) for h in self.hujjatlar) / self.N
 
        # Har bir so'z nechta hujjatda uchraydi
        self.df = Counter()
        for h in self.hujjatlar:
            for soz in set(h):
                self.df[soz] += 1
 
        # Har bir hujjatdagi so'z chastotalari
        self.tf = [Counter(h) for h in self.hujjatlar]
 
    def idf(self, soz):
        """Noyoblik: kam hujjatda uchrasa — yuqori ball."""
        n = self.df.get(soz, 0)
        return math.log((self.N - n + 0.5) / (n + 0.5) + 1)
 
    def ball(self, sorov, hujjat_id):
        h = self.hujjatlar[hujjat_id]
        tf = self.tf[hujjat_id]
        uzunlik = len(h)
 
        jami = 0.0
        for soz in sorov.lower().split():
            f = tf.get(soz, 0)
            if f == 0:
                continue
 
            surat = f * (self.k + 1)
            maxraj = f + self.k * (1 - self.b + self.b * uzunlik / self.ortacha_uzunlik)
            jami += self.idf(soz) * surat / maxraj
 
        return jami
 
    def qidir(self, sorov, top_k=3):
        ballar = [(i, self.ball(sorov, i)) for i in range(self.N)]
        ballar.sort(key=lambda x: x[1], reverse=True)
        return [(i, b) for i, b in ballar[:top_k] if b > 0]
 
 
korpus = [
    "Python dasturlash tili sodda sintaksisga ega",
    "Python'da ro'yxatlarni saralash uchun sort metodi ishlatiladi",
    "JavaScript brauzerda ishlaydigan dasturlash tili",
    "Ma'lumotlar bazasi so'rovlari uchun SQL tili ishlatiladi",
]
 
bm25 = BM25(korpus)
for i, ball in bm25.qidir("Python saralash"):
    print(f"{ball:.3f}  {korpus[i]}")
2.147  Python'da ro'yxatlarni saralash uchun sort metodi ishlatiladi
0.412  Python dasturlash tili sodda sintaksisga ega

Ishlaydi. Va juda tez — million hujjatda ham millisekundlarda.


Yechim: kontekstli vektorlar#

Muammoni transformer hal qiladi. U so'zning kontekstdagi ma'nosini vektorga aylantiradi.

from sentence_transformers import SentenceTransformer, util
 
model = SentenceTransformer("all-MiniLM-L6-v2")
 
sorov = "ro'yxatni tartiblash"
vektorlar = model.encode(korpus, convert_to_tensor=True)
sorov_vektori = model.encode(sorov, convert_to_tensor=True)
 
ballar = util.cos_sim(sorov_vektori, vektorlar)[0]
 
for i in ballar.argsort(descending=True)[:2]:
    print(f"{ballar[i]:.3f}  {korpus[i]}")
0.617  Python'da ro'yxatlarni saralash uchun sort metodi ishlatiladi
0.284  Python dasturlash tili sodda sintaksisga ega

"tartiblash" so'zi hujjatda umuman yo'q — lekin model ma'noni tushundi.

Bi-encoder va cross-encoder#

Vektorni ikki xil olish mumkin va farq juda muhim.

CROSS-ENCODER
so'rov va hujjat BIRGA modelga beriladi
aniqroq — so'zlar bir-biriga e'tibor beradi
lekin har so'rovda BUTUN korpusni qayta hisoblash kerak
BI-ENCODER
so'rov va hujjatlar ALOHIDA kodlanadi
hujjat vektorlari oldindan hisoblanadi
so'rov kelganda faqat 1 ta yangi vektor
Ikki yondashuv: qaysi biri qachon

Farqni o'lchaymiz:

import time
from sentence_transformers import CrossEncoder
 
hujjatlar = korpus * 250          # 1000 ta hujjat
 
# Bi-encoder: hujjatlar OLDINDAN kodlanadi
bi = SentenceTransformer("all-MiniLM-L6-v2")
baza = bi.encode(hujjatlar, convert_to_tensor=True)      # bir marta
 
boshlandi = time.time()
q = bi.encode("Python saralash", convert_to_tensor=True)
util.cos_sim(q, baza)
print(f"bi-encoder:    {time.time() - boshlandi:.3f} s")
 
# Cross-encoder: har juftlik alohida
cross = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
boshlandi = time.time()
cross.predict([("Python saralash", h) for h in hujjatlar])
print(f"cross-encoder: {time.time() - boshlandi:.3f} s")
bi-encoder:    0.021 s
cross-encoder: 3.847 s

180 barobar farq — va bu atigi 1000 ta hujjatda.


RAG'ni bosqichma-bosqich quramiz#

Endi to'liq tizim. Har bir qismni alohida yozamiz.

1. INDEKSLASH — bir marta
hujjatlar → bo'laklar → vektorlar → baza
2. QIDIRISH — har so'rovda
so'rov → vektor → eng yaqin K ta bo'lak
3. GENERATSIYA — har so'rovda
prompt + bo'laklar + so'rov → LLM → javob
RAG'ning uch bosqichi
  1. O'rnatish

    terminal
    pip install sentence-transformers chromadb pypdf anthropic
    Successfully installed chromadb-0.6.3 sentence-transformers-3.4.1 pypdf-5.2.0 ...
  2. Hujjatlarni o'qish

    from pathlib import Path
    from pypdf import PdfReader
     
     
    def pdf_oqi(yol: Path) -> str:
        """PDF dan matnni chiqaradi."""
        reader = PdfReader(yol)
        sahifalar = [(sahifa.extract_text() or "") for sahifa in reader.pages]
        return "\n\n".join(sahifalar)
     
     
    def hujjatlarni_yukla(papka: str) -> list[dict]:
        """Papkadagi barcha PDF va matn fayllarni o'qiydi."""
        hujjatlar = []
        for yol in Path(papka).rglob("*"):
            if yol.suffix.lower() == ".pdf":
                matn = pdf_oqi(yol)
            elif yol.suffix.lower() in (".txt", ".md"):
                matn = yol.read_text(encoding="utf-8")
            else:
                continue
     
            if matn.strip():
                hujjatlar.append({"manba": str(yol), "matn": matn})
     
        return hujjatlar
  3. Bo'laklarga bo'lish

    Bu butun tizim sifatini belgilaydigan qaror.

    def bolaklarga_bol(matn: str, olcham: int = 500, ustma_ust: int = 100) -> list[str]:
        """
        Matnni bo'laklarga bo'ladi, jumla chegaralarini hurmat qilib.
     
        olcham     — taxminiy bo'lak uzunligi (so'zda)
        ustma_ust  — qo'shni bo'laklar necha so'z bilan qoplashadi
        """
        sozlar = matn.split()
        bolaklar = []
        boshi = 0
     
        while boshi < len(sozlar):
            oxiri = boshi + olcham
            bolak = " ".join(sozlar[boshi:oxiri])
     
            # Oxirgi to'liq jumlada tugatishga harakat qilamiz
            if oxiri < len(sozlar):
                oxirgi_nuqta = bolak.rfind(". ")
                if oxirgi_nuqta > len(bolak) * 0.5:      # nuqta juda erta bo'lmasa
                    bolak = bolak[: oxirgi_nuqta + 1]
     
            bolaklar.append(bolak.strip())
            boshi += olcham - ustma_ust                  # ustma-ust siljish
     
        return [b for b in bolaklar if len(b) > 50]      # juda qisqalarini tashlaymiz

    Nima uchun ustma-ust tushish kerak:

    Ustma-ustsiz:
      bo'lak 1: "... ta'til yiliga 24 ish kuni."
      bo'lak 2: "Bu muddat 5 yildan ortiq ishlaganlarga 30 kunga oshadi. ..."
                    ↑ "Bu muddat" nimaga ishora qilyapti? Kontekst yo'qoldi.
    
    Ustma-ust bilan:
      bo'lak 1: "... ta'til yiliga 24 ish kuni. Bu muddat 5 yildan ortiq..."
      bo'lak 2: "...ta'til yiliga 24 ish kuni. Bu muddat 5 yildan ortiq ishlaganlarga 30 kunga oshadi."
                    ↑ ikkala bo'lakda ham to'liq ma'no bor
    
  4. Vektor bazasiga solish

    import chromadb
    from sentence_transformers import SentenceTransformer
     
     
    class RAG:
        def __init__(self, embedder="all-MiniLM-L6-v2", kolleksiya="hujjatlar"):
            self.embedder = SentenceTransformer(embedder)
            self.client = chromadb.PersistentClient(path="./chroma_baza")
            self.kolleksiya = self.client.get_or_create_collection(
                name=kolleksiya,
                metadata={"hnsw:space": "cosine"},        # kosinus masofa
            )
     
        def indeksla(self, hujjatlar: list[dict], olcham=500, ustma_ust=100):
            """Hujjatlarni bo'laklab bazaga yozadi."""
            idlar, matnlar, meta = [], [], []
     
            for h_id, hujjat in enumerate(hujjatlar):
                bolaklar = bolaklarga_bol(hujjat["matn"], olcham, ustma_ust)
     
                for b_id, bolak in enumerate(bolaklar):
                    idlar.append(f"{h_id}-{b_id}")
                    matnlar.append(bolak)
                    meta.append({
                        "manba": hujjat["manba"],
                        "bolak_raqami": b_id,
                        "jami_bolak": len(bolaklar),
                    })
     
            # Paket-paket kodlaymiz — xotira tejaladi
            vektorlar = self.embedder.encode(
                matnlar, batch_size=32, show_progress_bar=True
            ).tolist()
     
            self.kolleksiya.add(
                ids=idlar, documents=matnlar,
                embeddings=vektorlar, metadatas=meta,
            )
            print(f"{len(matnlar)} ta bo'lak indekslandi")
  5. Qidirish

        def qidir(self, sorov: str, top_k: int = 5, filtr: dict | None = None):
            """So'rovga eng mos bo'laklarni topadi."""
            sorov_vektori = self.embedder.encode(sorov).tolist()
     
            natija = self.kolleksiya.query(
                query_embeddings=[sorov_vektori],
                n_results=top_k,
                where=filtr,                       # masalan: {"manba": "siyosat.pdf"}
            )
     
            return [
                {
                    "matn": matn,
                    "manba": meta["manba"],
                    "masofa": masofa,
                    "oxshashlik": 1 - masofa,      # kosinus masofadan o'xshashlikka
                }
                for matn, meta, masofa in zip(
                    natija["documents"][0],
                    natija["metadatas"][0],
                    natija["distances"][0],
                )
            ]

    Muhim: so'rov uchun aynan o'sha embedder ishlatiladi. Boshqa model — boshqa vektor fazosi — natija ma'nosiz bo'ladi.

  6. Javob generatsiyasi

    import anthropic
     
        def sora(self, savol: str, top_k: int = 5) -> dict:
            """Qidiradi va javob yozadi."""
            bolaklar = self.qidir(savol, top_k)
     
            if not bolaklar:
                return {"javob": "Bazada hech narsa topilmadi.", "manbalar": []}
     
            # Kontekstni raqamlab tayyorlaymiz — model ularga havola qila olsin
            kontekst = "\n\n".join(
                f"[{i + 1}] (manba: {b['manba']})\n{b['matn']}"
                for i, b in enumerate(bolaklar)
            )
     
            prompt = f"""Quyidagi kontekstga TAYANIB savolga javob bering.
     
    Qoidalar:
    - Faqat kontekstdagi ma'lumotdan foydalaning
    - Har bir da'vodan keyin manba raqamini qavsda ko'rsating, masalan [1]
    - Agar javob kontekstda BO'LMASA, aniq ayting: "Berilgan hujjatlarda bu haqda ma'lumot yo'q"
    - Taxmin qilmang va o'z bilimingizdan qo'shmang
     
    KONTEKST:
    {kontekst}
     
    SAVOL: {savol}
     
    JAVOB:"""
     
            javob = anthropic.Anthropic().messages.create(
                model="claude-opus-5",
                max_tokens=2048,
                messages=[{"role": "user", "content": prompt}],
            )
     
            matn = "".join(b.text for b in javob.content if b.type == "text")
     
            return {
                "javob": matn,
                "manbalar": [{"manba": b["manba"], "oxshashlik": round(b["oxshashlik"], 3)}
                             for b in bolaklar],
                "tokenlar": javob.usage.input_tokens + javob.usage.output_tokens,
            }
  7. Ishga tushirish

    rag = RAG()
     
    hujjatlar = hujjatlarni_yukla("./hujjatlar")
    print(f"{len(hujjatlar)} ta hujjat topildi")
     
    rag.indeksla(hujjatlar)
     
    natija = rag.sora("Ta'til siyosati qanday?")
    print(natija["javob"])
    print("\nManbalar:")
    for m in natija["manbalar"]:
        print(f"  {m['oxshashlik']:.3f}  {m['manba']}")

Bo'lak strategiyasini o'lchash#

Ko'pchilik bo'lak o'lchamini "his-tuyg'u bilan" tanlaydi. To'g'ri yo'l — o'lchash.

def strategiyalarni_solishtir(hujjatlar, test_savollar, variantlar):
    """
    test_savollar — [(savol, kutilgan_matn_parchasi), ...]
    variantlar    — [(olcham, ustma_ust), ...]
    """
    natijalar = []
 
    for olcham, ustma_ust in variantlar:
        rag = RAG(kolleksiya=f"test_{olcham}_{ustma_ust}")
        rag.indeksla(hujjatlar, olcham=olcham, ustma_ust=ustma_ust)
 
        topildi = 0
        for savol, kutilgan in test_savollar:
            bolaklar = rag.qidir(savol, top_k=5)
            if any(kutilgan.lower() in b["matn"].lower() for b in bolaklar):
                topildi += 1
 
        natijalar.append({
            "olcham": olcham,
            "ustma_ust": ustma_ust,
            "recall@5": topildi / len(test_savollar),
        })
 
    return natijalar
 
 
variantlar = [(200, 0), (200, 50), (500, 0), (500, 100), (1000, 200)]
for n in strategiyalarni_solishtir(hujjatlar, test_savollar, variantlar):
    print(f"o'lcham={n['olcham']:>4}  ustma-ust={n['ustma_ust']:>3}  "
          f"recall@5={n['recall@5']:.2%}")

Tipik natija:

o'lcham= 200  ustma-ust=  0  recall@5=62.00%
o'lcham= 200  ustma-ust= 50  recall@5=74.00%
o'lcham= 500  ustma-ust=  0  recall@5=71.00%
o'lcham= 500  ustma-ust=100  recall@5=86.00%     ← eng yaxshi
o'lcham=1000  ustma-ust=200  recall@5=79.00%

Boshqa strategiyalar#

UsulQanday ishlaydiQachon
Qat'iy o'lchamN so'z yoki belgiBoshlash uchun; bir xil turdagi matn
RekursivAvval paragraf, keyin jumla bo'yichaUniversal tanlov — LangChain standarti
StrukturaviyMarkdown sarlavhalari, HTML teglari bo'yichaHujjat tuzilmasi aniq bo'lsa
SemantikJumlalar embeddingi orasidagi farq chegaradan oshsa ajratadiMavzu o'zgarishini avtomatik topadi; qimmat
PropozitsiyalarLLM matnni atomar faktlarga bo'ladiEng sifatli, eng qimmat
# Rekursiv bo'luvchi — amalda eng ko'p ishlatiladigan
from langchain_text_splitters import RecursiveCharacterTextSplitter
 
bolgich = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    separators=["\n\n", "\n", ". ", " ", ""],    # tartib bo'yicha sinaydi
)
bolaklar = bolgich.split_text(matn)

separators tartibi muhim: avval paragraf bo'yicha bo'lishga urinadi, sig'masa jumla bo'yicha, keyin so'z bo'yicha va oxirida belgi bo'yicha.


Embedding modelini tanlash#

Bu eng muhim qaror. Yomon embedder → noto'g'ri kontekst → va paradoksal ravishda gallyutsinatsiya ko'payadi, chunki model noto'g'ri hujjatga tayanib javob yozadi.

O'z ma'lumotingizda sinang#

from sentence_transformers import SentenceTransformer, util
import time
 
NOMZODLAR = [
    "all-MiniLM-L6-v2",                                    # 22M — tez
    "paraphrase-multilingual-MiniLM-L12-v2",               # 118M — ko'p tilli
    "intfloat/multilingual-e5-base",                       # 278M — sifatli
    "BAAI/bge-m3",                                         # 568M — eng yaxshilaridan
]
 
 
def embedderlarni_sina(nomzodlar, test_juftlar):
    """test_juftlar — [(savol, to'g'ri_javob_matni, [noto'g'ri_variantlar]), ...]"""
    for nom in nomzodlar:
        model = SentenceTransformer(nom)
        boshlandi = time.time()
        togri = 0
 
        for savol, javob, notogrilar in test_juftlar:
            variantlar = [javob] + notogrilar
            s = model.encode(savol, convert_to_tensor=True)
            v = model.encode(variantlar, convert_to_tensor=True)
            if util.cos_sim(s, v)[0].argmax().item() == 0:
                togri += 1
 
        vaqt = time.time() - boshlandi
        print(f"{nom:<45} aniqlik={togri/len(test_juftlar):.2%}  vaqt={vaqt:.1f}s")

Xotira va narxni hisoblash#

def baza_hajmi(bolaklar_soni, olcham, bayt=4):
    """Vektor bazasi qancha joy egallaydi."""
    return bolaklar_soni * olcham * bayt / 1024**3
 
 
for olcham in [384, 768, 1024]:
    for n in [10_000, 1_000_000]:
        print(f"{n:>9,} bo'lak × {olcham:>4} o'lcham = "
              f"{baza_hajmi(n, olcham):>6.2f} GB")
   10,000 bo'lak ×  384 o'lcham =   0.01 GB
1,000,000 bo'lak ×  384 o'lcham =   1.43 GB
   10,000 bo'lak ×  768 o'lcham =   0.03 GB
1,000,000 bo'lak ×  768 o'lcham =   2.86 GB
1,000,000 bo'lak × 1024 o'lcham =   3.81 GB

Kvantlash bilan bu 4 barobar kamayadi (float32 → int8), sifat esa deyarli o'zgarmaydi.


Vektor bazasini tanlash#

TuriMisolQachonCheklovi
KutubxonaFAISSBaza o'zgarmaydi, faqat o'qishCRUD yo'q
SQL kengaytmasipgvectorMavjud Postgres bazangiz borHajm katta bo'lsa sekinlashadi
Maxsus bazaChroma, Qdrant, WeaviateProduction, kengayishAlohida servis
BulutPineconeInfratuzilma bilan shug'ullanmaslikQimmat, ma'lumot tashqarida

Boshlash uchun Chroma — o'rnatish oson, kod sodda, mahalliy ishlaydi. Hajm oshganda Qdrant yoki pgvector'ga o'tasiz.


Tizimni baholash#

Bu bosqichni tashlab ketish — eng ko'p uchraydigan xato. "Ishlayotgandek ko'rinadi" — bu o'lchov emas.

Qidiruvni baholash#

def baholash(rag, test_toplami, top_k=5):
    """
    test_toplami — [(savol, [to'g'ri_bo'lak_id_lari]), ...]
    """
    precision_lar, recall_lar, rr_lar = [], [], []
 
    for savol, togri_idlar in test_toplami:
        topilgan = rag.qidir(savol, top_k)
        topilgan_idlar = [t["id"] for t in topilgan]
 
        mos = set(topilgan_idlar) & set(togri_idlar)
 
        precision_lar.append(len(mos) / len(topilgan_idlar) if topilgan_idlar else 0)
        recall_lar.append(len(mos) / len(togri_idlar) if togri_idlar else 0)
 
        # MRR — birinchi to'g'ri javob qaysi o'rinda
        rr = 0.0
        for orin, tid in enumerate(topilgan_idlar, 1):
            if tid in togri_idlar:
                rr = 1 / orin
                break
        rr_lar.append(rr)
 
    n = len(test_toplami)
    return {
        f"precision@{top_k}": sum(precision_lar) / n,
        f"recall@{top_k}": sum(recall_lar) / n,
        "MRR": sum(rr_lar) / n,
    }

Metrikalarni tushunish:

MetrikaSavoliQachon muhim
Precision@kTopilganlarning nechtasi mos?Kontekst toza bo'lishi kerak
Recall@kMos hujjatlarning nechtasi topildi?Hech narsa qolib ketmasligi kerak
MRRTo'g'ri javob qaysi o'rinda?Savol-javobda — eng muhimi

MRR ni raqamda ko'ramiz:

1-o'rinda topildi  →  1/1 = 1.00
2-o'rinda topildi  →  1/2 = 0.50
5-o'rinda topildi  →  1/5 = 0.20
umuman topilmadi   →  0.00

LLM'ni hakam qilish#

Qidiruvni o'lchash oson. Javob sifatini o'lchash qiyinroq — buning uchun boshqa LLM ishlatiladi.

def faithfulness(savol, javob, kontekst, client):
    """Javobdagi da'volar kontekstdan kelib chiqadimi? 0 dan 1 gacha."""
    prompt = f"""Quyidagi javobni tekshiring.
 
KONTEKST:
{kontekst}
 
JAVOB:
{javob}
 
Vazifa:
1. Javobdagi har bir alohida da'voni ajratib yozing
2. Har biri uchun: kontekstdan kelib chiqadimi? (HA/YO'Q)
3. Oxirida: "NATIJA: X/Y" formatida yozing, X — tasdiqlangan da'volar soni
 
Faqat shu formatda javob bering."""
 
    j = client.messages.create(
        model="claude-opus-5", max_tokens=1024,
        messages=[{"role": "user", "content": prompt}],
    )
    matn = "".join(b.text for b in j.content if b.type == "text")
 
    import re
    mos = re.search(r"NATIJA:\s*(\d+)\s*/\s*(\d+)", matn)
    if mos:
        x, y = int(mos.group(1)), int(mos.group(2))
        return x / y if y else 0.0
    return None

Asosiy metrikalar:

MetrikaNima o'lchaydi
FaithfulnessJavob kontekstga sodiqmi? ← gallyutsinatsiyani o'lchaydi
Answer relevanceJavob savolga tegishlimi, yoki suvmi?
Context precisionMos bo'laklar yuqoriroq turibdimi?
Answer correctnessJavob faktik jihatdan to'g'rimi?

ragas kutubxonasi bularning hammasini tayyor beradi:

from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_precision
 
natija = evaluate(
    dataset,
    metrics=[faithfulness, answer_relevancy, context_precision],
)
print(natija)

RAG yoki fine-tuning?#

RAGFine-tuning
Bilim yangilashBazani yangilaysiz — daqiqalarQayta o'qitish — kunlar
NarxHar so'rovda qidiruvBir marta qimmat, keyin arzon
Manba ko'rsatishHaYo'q
Uslub o'rgatishYomonYaxshi
Yangi qobiliyatYo'qHa
Ma'lumot hajmiCheksizCheklangan

Sodda qoida:

  • Modelga nimani bilishi kerak bo'lsa → RAG
  • Modelga qanday gapirishi kerak bo'lsa → fine-tuning
  • Ikkalasi ham kerak bo'lsa → birga ishlating, ular raqib emas

Uchinchi variant: embedder'ni fine-tuning qilish. Soha juda tor bo'lsa (tibbiyot, huquq), umumiy embedder yomon ishlaydi. sentence-transformers bilan uni o'z ma'lumotingizga moslash noldan o'qitishdan ancha arzon.


Nima noto'g'ri ketishi mumkin#

BelgiSababYechim
Javob "ma'lumot yo'q" deydi, lekin hujjatda borBo'lak topilmayaptitop_k ni oshiring, bo'lak o'lchamini o'zgartiring
Javob noto'g'ri hujjatga tayanadiEmbedder sohaga mos emasBoshqa model sinang, gibrid qidiruv qo'shing
Model kontekstni e'tiborsiz qoldiradiPrompt kuchsiz"Faqat kontekstdan" qoidasini kuchaytiring
Aniq atama topilmayaptiVektor qidiruvi so'zni emas, ma'noni topadiBM25 bilan gibrid qidiruv (keyingi bob)
Javob to'liq emasKerakli ma'lumot bir necha bo'lakdatop_k ni oshiring yoki ota hujjatni torting
Juda sekinHar so'rovda embeddingSo'rov embeddinglarini keshlang
PDF dan matn chiqmayaptiSkanerlangan hujjatOCR kerak
Amaliy topshiriq

O'z hujjatlaringiz ustida RAG quring va o'lchang

O'zingizga tegishli 10-20 ta hujjat toping (universitet materiallari, ish hujjatlari, qonun matnlari — nima bo'lsa ham) va to'liq RAG tizimini quring.

Keyin o'lchang:

  1. LLM yordamida 30 ta test savoli generatsiya qiling
  2. recall@5 ni o'lchang — to'g'ri bo'lak topilyaptimi?
  3. faithfulness ni o'lchang — javob kontekstga sodiqmi?
  4. Kamida uchta o'zgarish sinang: bo'lak o'lchami, embedder, top_k

Natijalarni jadvalga yozing. Qaysi o'zgarish eng ko'p foyda berdi?

Bonus: top_k ni oshirganda recall va faithfulness qanday o'zgardi? Nima uchun?

Maslahatni ko'rsatish

Test to'plamini avtomatik tuzish: savollar = [] for bolak in bolaklar[:50]: j = client.messages.create( model='claude-haiku-4-5', max_tokens=256, messages=[{'role':'user','content': f'Quyidagi matn asosida BITTA aniq savol yozing. ' f'Faqat savolni yozing:\n\n{bolak}'}], ) savol = ''.join(b.text for b in j.content if b.type=='text') savollar.append((savol.strip(), bolak)) Keyin recall@5 ni o'lchang: savol berilganda o'sha bo'lak topilyaptimi? Tajriba jadvalini yuriting: variant recall@5 faithfulness -------------------------------------------------------- asosiy (500/100, MiniLM) 0.86 0.91 + top_k=10 0.94 0.88 + bge-m3 embedder 0.91 0.93 ... Diqqat: top_k ni oshirsangiz recall oshadi, lekin faithfulness TUSHISHI mumkin — chunki kontekstga shovqin qo'shiladi. Bu klassik murosa.


Xulosa#

hujjatlar → bo'laklar
500 so'z, 20% ustma-ust
bo'laklar → vektorlar
embedding modeli
vektorlar → baza
Chroma, HNSW indeks bilan
so'rov → vektor → top-K
kosinus o'xshashligi
prompt + kontekst → LLM
«faqat kontekstdan» qoidasi bilan
javob + manbalar
RAG quvurining to'liq ko'rinishi
Yodda saqlang
  • 01RAG — modelga tashqi xotira. Bilim yangilanadi, manba ko'rsatiladi, gallyutsinatsiya kamayadi. Bugungi eng keng tarqalgan naqsh.
  • 02Bo'lak o'lchami va embedder tanlovi — sifatni belgilaydigan ikki qaror. Ular texnik mayda-chuydadek ko'rinadi, aslida butun tizim shularga tayanadi.
  • 03Ustma-ust tushish deyarli har doim yordam beradi. 10-20% qo'shing — muhim jumla chegarada bo'lib qolmasin.
  • 04Promptdagi «faqat kontekstdan foydalaning» va «bilmasangiz ayting» qoidalarini tashlab ketmang. Ular gallyutsinatsiyani sezilarli kamaytiradi.
  • 05Bi-encoder tez, cross-encoder aniq. Amalda ikkalasi birga: keng qidiruv + tor saralash.
  • 06«Ishlayotgandek ko'rinadi» — bu o'lchov emas. recall@k va faithfulness'ni o'lchang, keyin o'zgartiring.

Bu naïve RAG — eng sodda ko'rinishi. U ko'p holatlarda ishlaydi, lekin muammolari ham bor: noto'g'ri bo'lak topilishi, aniq atamalarni topa olmaslik, kontekst yetarli bo'lmasligi.

Keyingi bobda ilg'or RAG — bu muammolarning har biriga aniq yechim.