Asosiy qismga o'tish

Advanced RAG Patterns

14-bob 51 kod bloki 10372 so'z

Oddiy RAG pipeline — retrieve, generate, javob — ko'p holatlarda yetarli. Lekin production da sifatni oshirish uchun maxsus pattern lar kerak. Bu bobda eng samarali advanced pattern larni batafsil ko'rib chiqamiz: har birining konsepti, qachon ishlatish, diagramma va kod misoli bilan.


1. Corrective RAG (CRAG)

Kodlar: CRAG — retrieval yomon bo'lsa o'zini tuzatadi 14-bob · grade → rewrite → fallback · ~90 qator

1. Oddiy RAG hech qachon "bilmadim" demaydi

Vector search doim 5 ta chunk qaytaradi. Ular mutlaqo aloqasiz bo'lsa ham qaytaradi. LLM esa shu axlat bilan javob yozadi.

CRAG oraga nazoratchi qo'yadi: kontekstni LLM ga bermasdan oldin uni baholaydi, va yomon bo'lsa — tuzatadi.

2. Uchta yo'l

  • CORRECT (ball > 0.7) — kontekst yaxshi, to'g'ridan-to'g'ri javob yoz
  • AMBIGUOUS (0.3–0.7) — kontekst qisman to'g'ri, savolni qayta yozib yana qidir
  • INCORRECT (< 0.3) — kontekst yaroqsiz, tashqi manbaga murojaat qil yoki rad et
python25 qator
GRADE = (
    "Quyidagi hujjat bo'lagi savolga javob berish uchun foydalimi? "
    "0 dan 1 gacha faqat bitta son yoz (masalan 0.8). Boshqa hech narsa yozma.\n\n"
    "Savol: {question}\n\nBo'lak: {chunk}"
)

def grade_chunk(question: str, chunk: str) -> float:
    out = _ollama_chat(
        [{"role": "user", "content": GRADE.format(question=question, chunk=chunk[:900])}],
        temperature=0.0, max_tokens=8,
    )
    try:
        return max(0.0, min(1.0, float((out or "0").strip().split()[0])))
    except (ValueError, IndexError):
        return 0.0


def grade_context(question: str, chunks: list[dict]) -> tuple[float, list[dict]]:
    graded = []
    for c in chunks:
        row = dict(c)
        row["grade"] = grade_chunk(question, c["content"])
        graded.append(row)
    best = max((g["grade"] for g in graded), default=0.0)
    return best, sorted(graded, key=lambda x: x["grade"], reverse=True)

3. Bilimni tozalash — "knowledge refinement"

Yaxshi chunk ichida ham keraksiz matn bo'ladi. CRAG uni kesib tashlaydi: faqat savolga tegishli gaplarni qoldiradi.

Bu token tejaydi va "lost in the middle" muammosini kamaytiradi.

python17 qator
REFINE = (
    "Quyidagi matndan FAQAT savolga tegishli gaplarni ko'chir. "
    "Hech narsa qo'shma, o'zgartirma. Tegishli gap bo'lmasa bo'sh qaytar.\n\n"
    "Savol: {question}\n\nMatn: {chunk}"
)

def refine_knowledge(question: str, chunks: list[dict]) -> str:
    kept = []
    for c in chunks:
        out = _ollama_chat(
            [{"role": "user", "content": REFINE.format(question=question,
                                                        chunk=c["content"])}],
            temperature=0.0, max_tokens=300)
        cleaned = (out or "").strip()
        if len(cleaned) > 40:
            kept.append(cleaned)
    return "\n\n".join(kept)

4. To'liq CRAG sikli

Diqqat: max_rounds cheklovi majburiy. Busiz tizim yomon savolda cheksiz aylanadi va pulingizni yeydi.

python35 qator
def crag_answer(session, question: str, max_rounds: int = 2) -> dict:
    current = question
    trail = []

    for round_no in range(1, max_rounds + 1):
        emb, _ = generate_query_embedding(current)
        chunks = retrieve_chunks(session, emb, top_k=5)
        best, graded = grade_context(current, chunks)
        trail.append({"round": round_no, "query": current, "best_grade": round(best, 2)})

        if best > 0.7:
            good = [g for g in graded if g["grade"] > 0.5]
            context = refine_knowledge(current, good)
            answer = _ollama_chat([
                {"role": "system", "content": SYSTEM},
                {"role": "user", "content": f"Kontekst:\n{context}\n\nSavol: {question}"},
            ], temperature=0.2)
            return {"answer": answer, "action": "CORRECT", "trail": trail}

        if best < 0.3:
            break

        current = step_back_query(current)

    return {
        "answer": "Bazada bu savolga yetarli ma'lumot topilmadi.",
        "action": "INCORRECT",
        "trail": trail,
    }


result = crag_answer(session, "Ucell uchun qanday to'lov shlyuzi ishlatilgan?")
for step in result["trail"]:
    print(f"{step['round']}-urinish  ball={step['best_grade']}  savol: {step['query'][:55]}")
print(f"\nQaror: {result['action']}\n{result['answer'][:200]}")
Natija
1-urinish  ball=0.45  savol: Ucell uchun qanday to'lov shlyuzi ishlatilgan?
2-urinish  ball=0.85  savol: Ucell billing tizimi qanday komponentlardan iborat?

Qaror: CORRECT
Ucell billing tizimida Click va Payme to'lov shlyuzlari integratsiya qilingan.
Integratsiya REST API orqali amalga oshirilgan...

5. Nima bo'ldi

Birinchi urinishda savol juda tor edi — "to'lov shlyuzi" so'zi hujjatda boshqacha yozilgan. Ball 0.45, ya'ni AMBIGUOUS.

Step-back savolni kengaytirdi, ikkinchi urinish 0.85 ball oldi va javob topildi. Oddiy RAG bu savolda yolg'on javob yozardi.

Narxi jiddiy: har bir chunk uchun 1 ta baholash chaqiruvi + refinement chaqiruvlari. 2 raund = ~15 LLM chaqiruvi. Oddiy RAG da 1 ta.

Shuning uchun: CRAG ni hamma savolga emas, faqat vector search ball past bo'lganda ishga tushiring. Yuqori ballda oddiy pipeline bilan javob bering.

Konsept

CRAG — retrieval natijasini baholash va agar sifatsiz bo'lsa, muqobil manba dan qidirish. Oddiy RAG topilgan hujjatlarni ko'r-ko'rona ishlatadi. CRAG esa "bu hujjatlar savolga javob berish uchun yetarlimi?" deb so'raydi.

Uchta natija: - Correct — hujjatlar mos. Ulardan generate qil. - Incorrect — hujjatlar umuman mos emas. Web search ga o'tib, undan generate qil. - Ambiguous — qisman mos. Bazadan topilgan + web search natijalarini birlashtirib generate qil.

Diagram

matn13 qator
   Savol
     |
     v
  [Retrieve from DB]
     |
     v
  [Evaluate Relevance]
     |
     +----- Correct -------> [Generate from DB docs]
     |
     +----- Incorrect -----> [Web Search] --> [Generate from web docs]
     |
     +----- Ambiguous ------> [Web Search] --> [Merge DB + Web] --> [Generate]

Kod

python83 qator
from langgraph.graph import StateGraph, END
from typing import TypedDict, List, Literal

class CRAGState(TypedDict):
    question: str
    db_documents: List[str]
    web_documents: List[str]
    combined_documents: List[str]
    retrieval_grade: str  # "correct", "incorrect", "ambiguous"
    generation: str

def retrieve_from_db(state: CRAGState) -> dict:
    docs = pgvector_search(embed(state["question"]), top_k=5)
    return {"db_documents": [d.page_content for d in docs]}

def evaluate_retrieval(state: CRAGState) -> dict:
    """Lightweight classifier — retrieval sifatini baholaydi."""
    prompt = f"""Quyidagi hujjatlar savolga javob berish uchun yetarlimi?

Savol: {state['question']}
Hujjatlar: {state['db_documents'][:3]}

Javob variantlari:
- "correct" — hujjatlar to'liq javob beradi
- "incorrect" — hujjatlar umuman mos emas
- "ambiguous" — qisman mos, qo'shimcha ma'lumot kerak

Faqat bitta so'z qaytaring:"""

    grade = llm.invoke(prompt).strip().lower()
    return {"retrieval_grade": grade}

def route_by_grade(state: CRAGState) -> str:
    grade = state["retrieval_grade"]
    if grade == "correct":
        return "generate_from_db"
    if grade == "incorrect":
        return "web_search"
    return "web_search_and_merge"

def web_search(state: CRAGState) -> dict:
    results = tavily_search(state["question"], max_results=5)
    web_docs = [r["content"] for r in results]
    return {"web_documents": web_docs, "combined_documents": web_docs}

def web_search_and_merge(state: CRAGState) -> dict:
    results = tavily_search(state["question"], max_results=3)
    web_docs = [r["content"] for r in results]
    merged = state["db_documents"] + web_docs
    return {"web_documents": web_docs, "combined_documents": merged}

def generate_from_db(state: CRAGState) -> dict:
    context = "\n\n".join(state["db_documents"])
    answer = llm_generate(context, state["question"])
    return {"generation": answer}

def generate_from_combined(state: CRAGState) -> dict:
    context = "\n\n".join(state["combined_documents"])
    answer = llm_generate(context, state["question"])
    return {"generation": answer}

# Graph
crag = StateGraph(CRAGState)
crag.add_node("retrieve", retrieve_from_db)
crag.add_node("evaluate", evaluate_retrieval)
crag.add_node("web_search", web_search)
crag.add_node("web_search_and_merge", web_search_and_merge)
crag.add_node("generate_from_db", generate_from_db)
crag.add_node("generate_from_combined", generate_from_combined)

crag.set_entry_point("retrieve")
crag.add_edge("retrieve", "evaluate")
crag.add_conditional_edges("evaluate", route_by_grade, {
    "generate_from_db": "generate_from_db",
    "web_search": "web_search",
    "web_search_and_merge": "web_search_and_merge"
})
crag.add_edge("web_search", "generate_from_combined")
crag.add_edge("web_search_and_merge", "generate_from_combined")
crag.add_edge("generate_from_db", END)
crag.add_edge("generate_from_combined", END)

crag_app = crag.compile()

Qachon ishlatish

  • Ichki baza to'liq emas va web search orqali to'ldirish mumkin
  • Foydalanuvchi savollari ba'zan bazadagi mavzulardan tashqariga chiqadi
  • Sifat muhim — yomon retrieval dan ko'ra web dan qidirish yaxshiroq

2. Self-RAG

Kodlar: Self-RAG — modelning o'zi qaror qabul qiladi 14-bob · 4 ta nazorat nuqtasi · ~90 qator

1. Har savolga retrieval kerak emas

"Salom", "rahmat", "2+2 nechchi" — bularga baza kerak emas. Oddiy RAG baribir qidiradi, baribir 5 ta chunk topadi va ularni kontekstga tiqadi.

Self-RAG to'rt joyda to'xtab so'raydi:

  • Retrieval kerakmi?
  • Topilgan chunk tegishlimi?
  • Javob kontekst bilan tasdiqlanganmi?
  • Javob foydalimi?
python13 qator
def needs_retrieval(question: str) -> bool:
    prompt = (
        "Bu savolga javob berish uchun tashqi ma'lumot bazasi kerakmi? "
        "Salomlashish, matematika, umumiy bilim uchun KERAK EMAS. "
        "Faqat HA yoki YOQ deb yoz.\n\nSavol: " + question
    )
    out = _ollama_chat([{"role": "user", "content": prompt}],
                       temperature=0.0, max_tokens=5)
    return "HA" in (out or "").upper()


for q in ["Salom, qalaysiz?", "Ucell loyihasi haqida ayting", "15 ni 3 ga bo'ling"]:
    print(f"{needs_retrieval(q)!s:<6} {q}")
Natija
False  Salom, qalaysiz?
True   Ucell loyihasi haqida ayting
False  15 ni 3 ga bo'ling
python21 qator
def is_relevant(question: str, chunk: str) -> bool:
    prompt = (
        "Bu hujjat bo'lagi savolga javob berishga yordam beradimi? "
        "Faqat HA yoki YOQ.\n\n"
        f"Savol: {question}\n\nBo'lak: {chunk[:800]}"
    )
    out = _ollama_chat([{"role": "user", "content": prompt}],
                       temperature=0.0, max_tokens=5)
    return "HA" in (out or "").upper()


def is_supported(answer: str, context: str) -> bool:
    prompt = (
        "Bu javobdagi barcha faktlar kontekstda bormi? "
        "Bitta fakt ham kontekstdan tashqarida bo'lsa YOQ deb yoz. "
        "Faqat HA yoki YOQ.\n\n"
        f"Kontekst:\n{context}\n\nJavob: {answer}"
    )
    out = _ollama_chat([{"role": "user", "content": prompt}],
                       temperature=0.0, max_tokens=5)
    return "HA" in (out or "").upper()

2. To'liq oqim

Eng qimmatli qism — oxirgi tekshiruv. Javob yozilgandan keyin uni kontekstga solishtiramiz. Agar tasdiqlanmasa — javobni foydalanuvchiga ko'rsatmaymiz.

Bu hallucination ga qarshi eng kuchli himoya: yolg'on javob yozilishi mumkin, lekin u chiqmaydi.

python33 qator
def self_rag(session, question: str, top_k: int = 5) -> dict:
    if not needs_retrieval(question):
        answer = _ollama_chat([{"role": "user", "content": question}], temperature=0.5)
        return {"answer": answer, "path": "retrieval-siz", "supported": None}

    emb, _ = generate_query_embedding(question)
    chunks = retrieve_chunks(session, emb, top_k=top_k)
    relevant = [c for c in chunks if is_relevant(question, c["content"])]

    if not relevant:
        return {"answer": "Bazada tegishli ma'lumot topilmadi.",
                "path": "tegishli chunk yo'q", "supported": None}

    context = "\n\n".join(c["content"] for c in relevant)
    answer = _ollama_chat([
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": f"Kontekst:\n{context}\n\nSavol: {question}"},
    ], temperature=0.2)

    supported = is_supported(answer, context)
    if not supported:
        return {"answer": "Ishonchli javob shakllantira olmadim.",
                "path": f"{len(relevant)}/{len(chunks)} chunk, tasdiqlanmadi",
                "supported": False}

    return {"answer": answer,
            "path": f"{len(relevant)}/{len(chunks)} chunk tegishli",
            "supported": True}


for q in ["Salom!", "Ucell uchun nima qilingan?", "Nihol Marsda ofis ochganmi?"]:
    r = self_rag(session, q)
    print(f"[{r['path']}]  {r['answer'][:70]}")
Natija
[retrieval-siz]  Salom! Sizga qanday yordam bera olaman?
[4/5 chunk tegishli]  Nihol Ucell uchun billing tizimini modernizatsiya qilgan...
[tegishli chunk yo'q]  Bazada tegishli ma'lumot topilmadi.

3. Uchinchi qator eng muhimi

"Nihol Marsda ofis ochganmi?" — oddiy RAG bu savolda 5 ta chunk topardi (ofis, kompaniya, joylashuv haqida) va LLM ulardan biror narsa to'qib chiqarardi.

Self-RAG relevance filtridan hech biri o'tmadi va tizim halol javob berdi.

Narxi: 1 + N + 1 ta qo'shimcha LLM chaqiruvi (N = chunk soni). 5 chunk da bu ~7 chaqiruv.

Tejash yo'li: is_relevant ni har bir chunk uchun alohida emas, hammasini bitta promptda raqamlab yuboring — 5 chaqiruv o'rniga 1 ta. Sifat biroz tushadi, tezlik 4 barobar oshadi.

Konsept

Self-RAG — eng akademik jihatdan puxta yondashuv. Model har bir bosqichda o'zini baholaydi va qaror qabul qiladi. Oddiy RAG da retrieval va generation alohida qadamlar. Self-RAG da esa model maxsus token lar orqali o'z-o'zini boshqaradi.

Maxsus token lar: - [Retrieve]: "Bu savolga javob berish uchun qidirish kerakmi?" (yes/no) - [ISREL]: "Bu hujjat savolga mosmi?" (relevant/irrelevant) - [ISSUP]: "Mening javobim hujjatlarga asoslangan-mi?" (fully supported / partially supported / no support) - [ISUSE]: "Bu javob foydalanuvchiga foydalimi?" (useful grade 1-5)

Diagram

python28 qator
  Savol
    |
    v
  [Retrieve?] ---no---> [Generate directly]
    |                         |
   yes                        v
    |                    [ISUSE check]
    v                         |
  [Retrieve docs]             v
    |                      OUTPUT
    v
  [ISREL check] ----irrelevant----> [Drop doc, try next]
    |
  relevant
    |
    v
  [Generate with doc]
    |
    v
  [ISSUP check] ----not supported---> [Regenerate]
    |
  supported
    |
    v
  [ISUSE check]
    |
    v
  OUTPUT

Kod konsepti

python61 qator
class SelfRAGState(TypedDict):
    question: str
    needs_retrieval: bool
    documents: List[str]
    filtered_documents: List[str]
    generation: str
    is_supported: bool
    usefulness_score: int

def decide_retrieval(state: SelfRAGState) -> dict:
    """[Retrieve] token — qidirish kerakmi?"""
    prompt = f"""Bu savolga javob berish uchun tashqi ma'lumot kerakmi?
Savol: {state['question']}

"yes" yoki "no" deb javob bering:"""

    needs = llm.invoke(prompt).strip().lower()
    return {"needs_retrieval": needs == "yes"}

def route_retrieval(state: SelfRAGState) -> str:
    if state["needs_retrieval"]:
        return "retrieve"
    return "generate_direct"

def filter_relevant_docs(state: SelfRAGState) -> dict:
    """[ISREL] token — har bir hujjatni alohida baholaydi."""
    filtered = []
    for doc in state["documents"]:
        prompt = f"""Bu hujjat savolga mosmi?
Savol: {state['question']}
Hujjat: {doc[:500]}

"relevant" yoki "irrelevant":"""

        result = llm.invoke(prompt).strip().lower()
        if "relevant" in result:
            filtered.append(doc)

    return {"filtered_documents": filtered if filtered else state["documents"][:2]}

def check_support(state: SelfRAGState) -> dict:
    """[ISSUP] token — javob hujjatlarga asoslangan-mi?"""
    prompt = f"""Javob hujjatlarga asoslangan-mi?
Hujjatlar: {state['filtered_documents'][:3]}
Javob: {state['generation']}

"fully_supported", "partially_supported" yoki "no_support":"""

    result = llm.invoke(prompt).strip().lower()
    return {"is_supported": "no_support" not in result}

def check_usefulness(state: SelfRAGState) -> dict:
    """[ISUSE] token — javob foydalimi?"""
    prompt = f"""1-5 ball: javob savolga qanchalik foydali?
Savol: {state['question']}
Javob: {state['generation']}

Faqat raqam:"""

    score = int(llm.invoke(prompt).strip())
    return {"usefulness_score": score}

Qachon ishlatish

  • Eng yuqori sifat talab qilinadigan tizimlar (tibbiyot, huquq)
  • Retrieval har doim kerak emas — ba'zi savollarga model o'zi javob bera oladi
  • Har bir bosqichda sifat nazorati kerak

3. Adaptive RAG

Kodlar: Adaptive Retrieval — savol turiga qarab strategiya 14-bob · classifier + 4 strategiya · ~100 qator

1. Bitta strategiya hamma savolga to'g'ri kelmaydi

"Kompaniya qachon tashkil topgan?" — bitta aniq fakt kerak, 1–2 chunk yetarli.

"Kompaniyaning kuchli tomonlari nima?" — ko'p manbadan umumlashtirish kerak, 10+ chunk kerak.

Ikkalasiga bir xil top_k=5 berish — ikkalasiga ham noto'g'ri.

python25 qator
CLASSIFY = (
    "Savolni bitta toifaga ajrat. Faqat toifa nomini yoz:\n"
    "FACTUAL — aniq bitta fakt so'ralgan (sana, raqam, nom)\n"
    "ANALYTICAL — tahlil, taqqoslash, umumlashtirish kerak\n"
    "OPINION — fikr, baho, tavsiya so'ralgan\n"
    "CONTEXTUAL — oldingi suhbatga bog'liq\n\n"
    "Savol: {question}"
)

def classify_query(question: str) -> str:
    out = _ollama_chat(
        [{"role": "user", "content": CLASSIFY.format(question=question)}],
        temperature=0.0, max_tokens=10,
    )
    text_out = (out or "").upper()
    for label in ("FACTUAL", "ANALYTICAL", "OPINION", "CONTEXTUAL"):
        if label in text_out:
            return label
    return "FACTUAL"


for q in ["Kompaniya qachon tashkil topgan?",
          "Bank va telekom loyihalarini solishtiring",
          "Qaysi yondashuvni tavsiya qilasiz?"]:
    print(f"{classify_query(q):<12} {q}")
Natija
FACTUAL      Kompaniya qachon tashkil topgan?
ANALYTICAL   Bank va telekom loyihalarini solishtiring
OPINION      Qaysi yondashuvni tavsiya qilasiz?

2. Har toifa uchun alohida strategiya

  • FACTUAL — kam chunk, yuqori threshold, rerank majburiy
  • ANALYTICAL — savolni bo'laklarga ajratib, ko'p chunk yig'ish
  • OPINION — turli nuqtai nazarlarni qamrash uchun diversity
  • CONTEXTUAL — avval suhbat tarixi bilan qayta yozish
python30 qator
def retrieve_factual(session, question: str) -> list[dict]:
    emb, _ = generate_query_embedding(question)
    chunks = retrieve_chunks(session, emb, top_k=15)
    chunks = [c for c in chunks if c["score"] >= 0.45]
    return search_and_rerank(session, question, candidates=15, top_n=3) if chunks else []


def retrieve_analytical(session, question: str) -> list[dict]:
    seen, out = set(), []
    for sub in decompose_query(question):
        emb, err = generate_query_embedding(sub)
        if err or emb is None:
            continue
        for c in retrieve_chunks(session, emb, top_k=5):
            if c["id"] not in seen:
                seen.add(c["id"])
                out.append(c)
    return out[:12]


def retrieve_opinion(session, question: str) -> list[dict]:
    emb, _ = generate_query_embedding(question)
    chunks = retrieve_chunks(session, emb, top_k=25)
    return filter_by_diversity(chunks, max_similarity=0.85, top_k=8)


def retrieve_contextual(session, question: str, history: list[dict]) -> list[dict]:
    rewritten = rewrite_query(question, history)
    emb, _ = generate_query_embedding(rewritten)
    return retrieve_chunks(session, emb, top_k=6)
python22 qator
STRATEGIES = {
    "FACTUAL":    retrieve_factual,
    "ANALYTICAL": retrieve_analytical,
    "OPINION":    retrieve_opinion,
}

def adaptive_retrieve(session, question: str, history: list[dict] | None = None) -> dict:
    category = classify_query(question)

    if category == "CONTEXTUAL":
        chunks = retrieve_contextual(session, question, history or [])
    else:
        chunks = STRATEGIES[category](session, question)

    return {"category": category, "chunks": chunks, "count": len(chunks)}


for q in ["Kompaniya qachon tashkil topgan?",
          "Bank va telekom loyihalarini solishtiring",
          "Qaysi texnologiyani tavsiya qilasiz?"]:
    r = adaptive_retrieve(session, q)
    print(f"{r['category']:<12} {r['count']:>2} chunk   {q[:45]}")
Natija
FACTUAL       3 chunk   Kompaniya qachon tashkil topgan?
ANALYTICAL   11 chunk   Bank va telekom loyihalarini solishtiring
OPINION       8 chunk   Qaysi texnologiyani tavsiya qilasiz?

3. Natija

Uchta savol — uchta butunlay boshqa retrieval. Faktik savolga 3 ta aniq chunk, tahliliy savolga 11 ta keng chunk.

Qo'shimcha xarajat — bitta klassifikatsiya chaqiruvi (~200 ms). Bu retrieval sifatidagi yutuqqa nisbatan arzon.

Klassifikator xato qilsa nima bo'ladi? Hech qanday falokat emas — shunchaki suboptimal strategiya ishlatiladi. Shuning uchun classify_query da fallback FACTUAL — u eng xavfsiz, eng tor strategiya.

Kengaytirish: o'z domeningizga qarab toifa qo'shing. Masalan huquqiy tizimda "PROTSEDURA" toifasi — qadamma-qadam ko'rsatma talab qiladigan savollar uchun.

Konsept

Adaptive RAG — savol murakkabligiga qarab strategiya tanlash. Barcha savollar bir xil pipeline dan o'tishi shart emas. Oddiy savol uchun oddiy pipeline, murakkab savol uchun murakkab pipeline ishlatiladi.

Uchta yo'nalish: - Simple fact — to'g'ridan-to'g'ri retrieval + generate - Complex analysis — savol decompose + multi-step retrieval + reasoning - Off-topic — iltifot bilan rad etish

Diagram

python20 qator
        Savol
          |
          v
  [Query Classifier]
     /    |     \
    /     |      \
   v      v       v
Simple  Complex  Off-topic
   |      |         |
   v      v         v
[Retrieve] [Decompose] "Bu mavzu bo'yicha
   |      |         javob bera olmayman"
   v      v
[Generate] [Multi-step retrieve]
   |      |
   v      v
 Javob  [Synthesize]
              |
              v
            Javob

Kod

python75 qator
class AdaptiveState(TypedDict):
    question: str
    query_type: str  # "simple", "complex", "off_topic"
    documents: List[str]
    sub_questions: List[str]
    sub_results: List[dict]
    generation: str

def classify_query(state: AdaptiveState) -> dict:
    """Savol murakkabligini aniqlaydi."""
    prompt = f"""Savolni tasniflang:
- "simple" — bitta fakt, bitta retrieval yetarli
- "complex" — taqqoslash, tahlil, ko'p qadamli fikrlash kerak
- "off_topic" — bizning baza mavzusiga aloqasi yo'q

Savol: {state['question']}

Faqat bitta so'z:"""

    query_type = llm.invoke(prompt).strip().lower()
    return {"query_type": query_type}

def route_by_complexity(state: AdaptiveState) -> str:
    return state["query_type"]

def simple_retrieve_and_generate(state: AdaptiveState) -> dict:
    """Oddiy savol — bitta retrieval va generate."""
    docs = pgvector_search(embed(state["question"]), top_k=5)
    context = "\n".join([d.page_content for d in docs])
    answer = llm_generate(context, state["question"])
    return {"generation": answer}

def decompose_and_retrieve(state: AdaptiveState) -> dict:
    """Murakkab savol — bo'lib, qidirib, birlashtirish."""
    # Decompose
    sub_qs = json.loads(llm.invoke(
        f"Savolni 2-4 kichik savolga bo'ling, JSON list: {state['question']}"
    ))

    # Retrieve for each
    results = []
    for sq in sub_qs:
        docs = pgvector_search(embed(sq), top_k=3)
        results.append({"question": sq, "docs": [d.page_content for d in docs]})

    # Synthesize
    parts = [f"Q: {r['question']}\nA: {r['docs']}" for r in results]
    context = "\n---\n".join(parts)
    answer = llm.invoke(
        f"Quyidagi ma'lumotlar asosida to'liq javob bering:\n{context}\n\nOriginal: {state['question']}"
    )
    return {"generation": answer, "sub_questions": sub_qs, "sub_results": results}

def refuse_off_topic(state: AdaptiveState) -> dict:
    return {"generation": "Kechirasiz, bu savol bizning ma'lumot bazamiz doirasidan tashqarida. "
                          "Men faqat Nihol kompaniyasi loyihalari haqida javob bera olaman."}

# Graph
adaptive = StateGraph(AdaptiveState)
adaptive.add_node("classify", classify_query)
adaptive.add_node("simple", simple_retrieve_and_generate)
adaptive.add_node("complex", decompose_and_retrieve)
adaptive.add_node("off_topic", refuse_off_topic)

adaptive.set_entry_point("classify")
adaptive.add_conditional_edges("classify", route_by_complexity, {
    "simple": "simple",
    "complex": "complex",
    "off_topic": "off_topic"
})
adaptive.add_edge("simple", END)
adaptive.add_edge("complex", END)
adaptive.add_edge("off_topic", END)

adaptive_app = adaptive.compile()

Qachon ishlatish

  • Foydalanuvchi savollari juda turli xil — oddiydan murakkabgacha
  • Oddiy savollarga tezroq javob berish, murakkab savollarga chuqurroq javob berish kerak
  • Bazadan tashqari savollarni iltifot bilan rad etish kerak

4. RAPTOR

Kodlar: RAPTOR — xulosalar daraxti 14-bob · clustering + rekursiv xulosa · ~110 qator

1. Chunk lar butunni ko'rsatmaydi

"Kompaniyaning umumiy yo'nalishi nima?" — bu savolga javob bitta chunk da ham yo'q. U 50 ta chunk ichiga tarqalgan.

RAPTOR yechimi: indekslash paytida xulosalar yozib qo'yish. Chunk larni guruhlash → har guruhga xulosa → xulosalarni yana guruhlash → yuqoriroq xulosa. Daraxt hosil bo'ladi.

python19 qator
import numpy as np
from sklearn.mixture import GaussianMixture

def embed_all(texts: list[str]) -> np.ndarray:
    vectors = []
    for t in texts:
        emb, err = generate_query_embedding(t)
        vectors.append(emb if not err and emb else [0.0] * 768)
    return np.array(vectors)


def cluster_texts(vectors: np.ndarray, max_clusters: int = 10) -> np.ndarray:
    n = len(vectors)
    if n <= 2:
        return np.zeros(n, dtype=int)

    k = min(max_clusters, max(2, n // 5))
    gmm = GaussianMixture(n_components=k, random_state=42, covariance_type="diag")
    return gmm.fit_predict(vectors)

2. Nega GMM, k-means emas?

GMM ehtimollik beradi — bitta chunk bir nechta klasterga tegishli bo'lishi mumkin. Matnda bu tabiiy: "Ucell billing loyihasi" ham telekom, ham billing mavzusiga kiradi.

covariance_type="diag" — yuqori o'lchamli embedding uchun majburiy, aks holda model o'qilmaydi.

python13 qator
SUMMARIZE = (
    "Quyidagi matnlarning umumiy mazmunini 3-4 gapda yoz. "
    "Asosiy mavzular va faktlarni saqlab qol. "
    "Faqat xulosani yoz.\n\n{texts}"
)

def summarize_cluster(texts: list[str]) -> str:
    joined = "\n\n---\n\n".join(t[:900] for t in texts[:8])
    out = _ollama_chat(
        [{"role": "user", "content": SUMMARIZE.format(texts=joined)}],
        temperature=0.3, max_tokens=250,
    )
    return (out or "").strip()

3. Daraxtni qurish

Har bir daraja: klasterlash → xulosa → xulosalar keyingi darajaning kirishi. Klaster soni 1 ga tushguncha yoki max_levels ga yetguncha davom etadi.

python30 qator
def build_raptor_tree(chunks: list[str], max_levels: int = 3) -> dict[int, list[str]]:
    tree = {0: chunks}
    current = chunks

    for level in range(1, max_levels + 1):
        if len(current) <= 3:
            break

        vectors = embed_all(current)
        labels = cluster_texts(vectors)

        summaries = []
        for label in sorted(set(labels)):
            members = [current[i] for i in range(len(current)) if labels[i] == label]
            summary = summarize_cluster(members)
            if summary:
                summaries.append(summary)

        if not summaries or len(summaries) >= len(current):
            break

        tree[level] = summaries
        current = summaries
        print(f"{level}-daraja: {len(summaries)} ta xulosa")

    return tree


chunks = chunk_text(open("nihol_corpus.txt", encoding="utf-8").read(), size=800)
tree = build_raptor_tree(chunks)
Natija
1-daraja: 9 ta xulosa
2-daraja: 3 ta xulosa
3-daraja: 1 ta xulosa

4. Hamma darajani bitta indeksga

Muhim qaror: daraxtni bo'ylab yurmaymiz. Barcha darajalarni bitta vector store ga solamiz va oddiy qidiruv qilamiz.

Tor savol tabiiy ravishda 0-darajadagi aniq chunk ni topadi. Keng savol 2–3 darajadagi xulosani topadi. Hech qanday maxsus mantiq kerak emas.

python31 qator
TREE_SQL = (
    "INSERT INTO chunks (title, content, level, embedding) "
    "VALUES (:t, :c, :l, :e)"
)

def index_tree(session, tree: dict[int, list[str]]) -> int:
    total = 0
    for level, texts in tree.items():
        for t in texts:
            emb, err = generate_query_embedding(t)
            if err or emb is None:
                continue
            session.execute(text(TREE_SQL), {
                "t": f"level-{level}",
                "c": t,
                "l": level,
                "e": str(emb),
            })
            total += 1
    session.commit()
    return total


print(f"Indekslandi: {index_tree(session, tree)} ta yozuv")

for q in ["Ucell billing moduli qachon yangilandi?",
          "Kompaniyaning umumiy yo'nalishi nima?"]:
    emb, _ = generate_query_embedding(q)
    top = retrieve_chunks(session, emb, top_k=3)
    levels = [c.get("level") for c in top]
    print(f"\n{q}\n  topilgan darajalar: {levels}")
Natija
Indekslandi: 464 ta yozuv

Ucell billing moduli qachon yangilandi?
  topilgan darajalar: [0, 0, 1]

Kompaniyaning umumiy yo'nalishi nima?
  topilgan darajalar: [3, 2, 2]

5. Mana isbot

Tor savol 0-darajani (asl chunk) topdi. Keng savol 3 va 2-darajani (xulosalar) topdi. Hech qanday klassifikator ishlatilmadi — embedding o'zi hal qildi.

Xarajat jiddiy: 451 chunk uchun ~13 ta xulosa LLM chaqiruvi + 464 ta embedding. Indekslash bir necha barobar sekinlashadi.

Qachon arziydi: korpus katta va savollar ham tor, ham keng bo'lsa. Faqat faktik savollar bo'lsa — RAPTOR ortiqcha, oddiy chunking yetarli.

Konsept

RAPTOR — Recursive Abstractive Processing for Tree-Organized Retrieval. Oddiy RAG da hujjatlar tekis (flat) chunk larga bo'linadi. RAPTOR esa daraxt (tree) strukturasi quradi:

  • Leaf node lar — original chunk lar (batafsil ma'lumot)
  • Parent node lar — child chunk larning summary lari
  • Root node — butun hujjatning umumiy xulosa si

Foydalanuvchi "batafsil aytib bering" desa — leaf node lardan qidiriladi. "Umumiy ko'rinish bering" desa — yuqori daraja node lardan qidiriladi.

Diagram

python13 qator
                    [Root Summary]
                   "Nihol — IT kompaniya,
                    bank va telekom loyihalari"
                   /                    \
                  /                      \
    [Branch: Bank loyihalari]    [Branch: Telekom loyihalari]
    "Nihol 5 ta bank loyiha      "Nihol Ucell, Beeline uchun 
     amalga oshirgan..."          loyihalar qilgan..."
      /        \                    /          \
     /          \                  /            \
  [Leaf 1]   [Leaf 2]         [Leaf 3]      [Leaf 4]
  "2020: NBU   "2023: Kapital  "Ucell IBM    "Beeline 
   loyihasi..." bank..."        Power CPU..." monitoring..."

Kod

python61 qator
from typing import List, Dict

def build_raptor_tree(chunks: List[str], max_depth: int = 3) -> Dict:
    """Chunk lardan RAPTOR daraxtini quradi."""

    tree = {"leaves": chunks, "levels": []}
    current_level = chunks

    for depth in range(max_depth):
        if len(current_level) <= 1:
            break

        # Har 3-5 chunk ni guruhlash va summary qilish
        summaries = []
        group_size = min(5, max(2, len(current_level) // 3))

        for i in range(0, len(current_level), group_size):
            group = current_level[i:i + group_size]
            combined = "\n\n".join(group)

            summary = llm.invoke(
                f"Quyidagi matnlarning qisqacha xulosasini yozing:\n{combined}"
            )
            summaries.append(summary)

        tree["levels"].append(summaries)
        current_level = summaries

    return tree

def raptor_search(tree: Dict, question: str, detail_level: str = "auto") -> List[str]:
    """Daraxtning to'g'ri darajasidan qidiradi."""

    if detail_level == "detailed" or detail_level == "auto":
        # Leaf node lardan qidirish
        leaf_results = vector_search(tree["leaves"], question, top_k=5)
        if detail_level == "detailed":
            return leaf_results

    if detail_level == "overview":
        # Yuqori daraja summary lardan qidirish
        if tree["levels"]:
            top_level = tree["levels"][-1]
            return vector_search(top_level, question, top_k=3)

    # Auto: leaf + summary larni aralashtirish
    leaf_results = vector_search(tree["leaves"], question, top_k=3)
    if tree["levels"]:
        summary_results = vector_search(tree["levels"][0], question, top_k=2)
        return leaf_results + summary_results

    return leaf_results

# Ishlatish
tree = build_raptor_tree(all_chunks)

# Batafsil savol
detailed = raptor_search(tree, "Nihol Ucell uchun qanday CPU modullarini yetkazgan?", "detailed")

# Umumiy savol
overview = raptor_search(tree, "Nihol kompaniyasi qanday sohalarda ishlaydi?", "overview")

Qachon ishlatish

  • Hujjatlar hajmi katta va turli darajadagi savollar keladigan tizimlar
  • "Umumiy ko'rinish bering" va "batafsil aytib bering" savollarini farqlash kerak
  • Hierarchik tuzilgan ma'lumotlar: korporativ hisobotlar, texnik hujjatlar

5. HyDE (Hypothetical Document Embeddings)

Konsept

Muammo: foydalanuvchi savoli qisqa ("Nihol bank loyihalari"), hujjat esa uzun va batafsil. Qisqa savol vectori va uzun hujjat vectori vector space da bir-biridan uzoqda bo'lishi mumkin.

HyDE yechimi: LLM dan gipotetik javob yozishni so'rayman. Bu gipotetik javob haqiqiy hujjatlarga yaqinroq bo'ladi vector space da. Keyin gipotetik javob ni embed qilib, u bilan qidiraman.

Diagram

python14 qator
  Oddiy RAG:
  "Nihol bank loyihalari" --embed--> [0.2, 0.8, ...] --search--> (past natija)

  HyDE:
  "Nihol bank loyihalari" 
       |
       v
  [LLM: gipotetik javob yozadi]
  "Nihol kompaniyasi bank sohasida bir nechta 
   loyihalarni amalga oshirgan. Jumladan, 
   Milliy bank uchun modernizatsiya loyihasi..."
       |
       v
  --embed--> [0.5, 0.7, ...] --search--> (yaxshi natija!)

Gipotetik javob noto'g'ri bo'lishi mumkin (hallucination). Bu muhim emas! Bizga faqat uning vector representation kerak — u haqiqiy hujjatlar bilan yaqinroq bo'ladi.

Kod

python27 qator
def hyde_search(question: str, top_k: int = 5) -> List[str]:
    """HyDE: gipotetik javob orqali qidirish."""

    # 1. Gipotetik javob generatsiya qilish
    prompt = f"""Quyidagi savolga taxminiy javob yozing. 
Aniq bo'lmasa ham yozing — bu faqat qidirish uchun.
To'g'ri bo'lishi shart emas.

Savol: {question}

Taxminiy javob:"""

    hypothetical_answer = llm.invoke(prompt)

    # 2. Gipotetik javobni embed qilish
    hyde_embedding = embed_model.encode(hypothetical_answer)

    # 3. Bu embedding bilan bazadan qidirish
    results = pgvector_search(hyde_embedding, top_k=top_k)

    return [doc.page_content for doc in results]

# Oddiy search bilan solishtirish
regular_results = regular_search("Nihol bank", top_k=5)
hyde_results = hyde_search("Nihol bank loyihalari", top_k=5)

# HyDE ko'pincha yaxshiroq natija beradi qisqa/noaniq savollarda

Qachon yordam beradi

  • Qisqa, noaniq savollar: "bank loyihalari", "Ucell narsa"
  • Foydalanuvchi savol berish usulini bilmaydigan tizimlar
  • Domain-specific terminologiya farqi: foydalanuvchi boshqa so'z ishlatadi

Qachon zarar qiladi

  • Aniq, uzun savollar: "Nihol 2023 yilda Kapitalbank uchun Dell PowerEdge serverlari bilan qanday loyiha qilgan?" — bu savolda HyDE kerak emas, to'g'ridan-to'g'ri search yaxshiroq
  • Real-time tizimlar — qo'shimcha LLM call = qo'shimcha latency

6. Step-back Prompting

Kodlar: Step-back Prompting — avval umumiy, keyin aniq 14-bob · ikki bosqichli kontekst · ~60 qator

1. Tor savol — tor kontekst

"Ucell billing modulida qaysi versiyada webhook qo'shilgan?" — retrieval faqat "webhook" so'zi bor chunk ni topadi. Lekin javob uchun butun modul tarixi kerak edi.

Step-back ikki marta qidiradi: bir marta tor savol bilan (aniq fakt), bir marta keng savol bilan (fon ma'lumot).

python15 qator
STEPBACK = (
    "Quyidagi aniq savoldan bir pog'ona umumiyroq savol yoz. "
    "Maqsad — javob uchun kerak bo'ladigan fon ma'lumotini topish.\n"
    "Misol:\n"
    "  Aniq: 'X modulida qaysi versiyada webhook qo'shilgan?'\n"
    "  Umumiy: 'X moduli qanday rivojlangan?'\n\n"
    "Faqat savolni yoz.\n\nAniq savol: {question}"
)

def step_back_query(question: str) -> str:
    out = _ollama_chat(
        [{"role": "user", "content": STEPBACK.format(question=question)}],
        temperature=0.2, max_tokens=60,
    )
    return (out or question).strip()

2. Ikkala kontekstni birlashtirish

Tartib muhim: promptda avval umumiy kontekst, keyin aniq kontekst. LLM avval mavzuni tushunadi, keyin tafsilotni o'qiydi — bu javob sifatini oshiradi.

python35 qator
def step_back_answer(session, question: str) -> dict:
    broad_question = step_back_query(question)

    narrow_emb, _ = generate_query_embedding(question)
    broad_emb, _ = generate_query_embedding(broad_question)

    narrow = retrieve_chunks(session, narrow_emb, top_k=4)
    broad = retrieve_chunks(session, broad_emb, top_k=4)

    seen = {c["id"] for c in narrow}
    broad = [c for c in broad if c["id"] not in seen]

    context = (
        "UMUMIY KONTEKST:\n"
        + "\n\n".join(c["content"] for c in broad)
        + "\n\nANIQ KONTEKST:\n"
        + "\n\n".join(c["content"] for c in narrow)
    )

    answer = _ollama_chat([
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": f"{context}\n\nSavol: {question}"},
    ], temperature=0.2)

    return {
        "answer": answer,
        "broad_question": broad_question,
        "chunks": len(narrow) + len(broad),
    }


r = step_back_answer(session, "Ucell billing modulida to'lov shlyuzi qachon ulangan?")
print("Keng savol :", r["broad_question"])
print("Chunk soni :", r["chunks"])
print("\nJavob:\n" + r["answer"][:300])
Natija
Keng savol : Ucell billing tizimi loyihasi qanday bosqichlarda amalga oshirilgan?
Chunk soni : 7

Javob:
Ucell billing tizimi modernizatsiyasi 2023-yil boshida boshlangan. Loyihaning
ikkinchi bosqichida, 2023-yil iyun oyida Click va Payme to'lov shlyuzlari
REST API orqali ulangan. Uchinchi bosqichda hisobot moduli qo'shilgan.

3. Nima bo'ldi

Tor qidiruv "to'lov shlyuzi" gapi bor chunk ni topdi — lekin unda sana yo'q edi. Keng qidiruv loyiha bosqichlari haqidagi chunk ni topdi — sana o'sha yerda edi.

Ikkalasi birga bo'lgandagina to'liq javob chiqdi.

Query Transformations bilan farqi: u yerda step-back savolni almashtiradi. Bu yerda esa qo'shadi — ikkala kontekst ham ishlatiladi. Ikkinchisi kuchliroq, lekin 2 barobar ko'proq token yeydi.

Qachon kerak emas: savol allaqachon keng bo'lsa — step-back undan ham kengroq savol yasaydi va foydasiz umumiy matn keladi.

Konsept

Ba'zan savol juda tor (specific) bo'ladi va retrieval yaxshi natija bermaydi. Step-back prompting savolni kengaytiradi (abstract qiladi) va kengaytirilgan savol bilan qidiradi.

Misol: - Original: "Nihol Ucell uchun qanday server yetkazgan?" (tor) - Step-back: "Nihol va Ucell o'rtasidagi hamkorlik qanday?" (keng)

Keng savol ko'proq tegishli hujjatlarni topadi. Keyin original savolga aniq javob berish uchun topilgan kontekstdan foydalaniladi.

Diagram

python16 qator
  Original savol: "Qanday server yetkazgan?"
       |
       v
  [Step-back: savolni kengaytirish]
       |
  "Nihol va Ucell hamkorligi qanday?"
       |
       v
  [Retrieve with broad query]
       |
  [Nihol-Ucell shartnomasi, server yetkazish, texnik xizmat...]
       |
       v
  [Generate: keng kontekstdan aniq javob]
       |
  "Nihol Ucell ga IBM Power CPU VRM modullarini yetkazgan"

Kod

python35 qator
def step_back_search(question: str, top_k: int = 5) -> dict:
    """Step-back: savolni kengaytirish va qidirish."""

    # 1. Savolni kengaytirish
    prompt = f"""Quyidagi savolni kengroq, abstrakt shaklda qayta yozing.
Maqsad: ko'proq tegishli hujjatlarni topish.

Original: {question}

Kengaytirilgan savol:"""

    broad_question = llm.invoke(prompt).strip()

    # 2. Ikkala savol bilan qidirish
    original_docs = pgvector_search(embed(question), top_k=top_k)
    broad_docs = pgvector_search(embed(broad_question), top_k=top_k)

    # 3. Natijalarni birlashtirish (duplikat larni olib tashlash)
    all_docs = []
    seen = set()
    for doc in original_docs + broad_docs:
        if doc.page_content not in seen:
            all_docs.append(doc)
            seen.add(doc.page_content)

    return {
        "original_question": question,
        "step_back_question": broad_question,
        "documents": [d.page_content for d in all_docs[:top_k]]
    }

# Misol
result = step_back_search("Nihol Ucell uchun qanday server yetkazgan?")
# step_back_question: "Nihol kompaniyasining Ucell bilan hamkorligi qanday?"
# documents: [shartnoma, server yetkazish, texnik xizmat...]

Qachon ishlatish

  • Tor, aniq savollar yaxshi natija bermayapti
  • Foydalanuvchilar domain terminologiyasini bilmaydi
  • Retrieval recall ni oshirish kerak (ko'proq tegishli hujjat topish)

7. Multi-modal RAG

Kodlar: Multi-modal RAG — rasm va jadvallarni qidirish 14-bob · vision model + caption · ~90 qator

1. PDF dagi ma'lumotning yarmi matn emas

Texnik hujjatlarda arxitektura diagrammalari, jadvallar, grafiklar bor. Oddiy PDF parser ulardan bo'sh joy chiqaradi — ma'lumot butunlay yo'qoladi.

Yechim: rasmni matnga aylantirish — vision model bilan tavsif yozdirish va o'sha tavsifni indekslash.

python24 qator
import fitz
import base64

def extract_images(pdf_path: str, min_size: int = 120) -> list[dict]:
    doc = fitz.open(pdf_path)
    images = []
    for page_no in range(len(doc)):
        for img in doc[page_no].get_images(full=True):
            pix = fitz.Pixmap(doc, img[0])
            if pix.width < min_size or pix.height < min_size:
                continue
            if pix.n - pix.alpha >= 4:
                pix = fitz.Pixmap(fitz.csRGB, pix)
            images.append({
                "page": page_no + 1,
                "bytes": pix.tobytes("png"),
                "size": f"{pix.width}x{pix.height}",
            })
    doc.close()
    return images


images = extract_images("nihol_arxitektura.pdf")
print(f"Topilgan rasmlar: {len(images)}")
Natija
Topilgan rasmlar: 12

2. Kichik rasmlarni tashlash shart

min_size = 120 — logotiplar, ikonkalar, bezak chiziqlari chiqib ketadi. Ularni tavsiflash pul va vaqt isrofi, natijada esa indeksda "kompaniya logotipi" degan foydasiz chunk paydo bo'ladi.

3. Vision model bilan tavsif

Prompt aniq bo'lishi kerak. "Rasmni tasvirla" desangiz — "bu diagramma" degan javob olasiz. Bizga qidiriladigan matn kerak: ichidagi yozuvlar, raqamlar, bog'lanishlar.

python21 qator
CAPTION = (
    "Bu texnik rasmni batafsil tasvirla. Quyidagilarni albatta yoz:\n"
    "1. Rasm turi (diagramma, jadval, grafik, skrinshot)\n"
    "2. Rasmdagi BARCHA matn va raqamlar\n"
    "3. Elementlar orasidagi bog'lanishlar\n"
    "4. Asosiy xulosa\n"
    "O'zbek tilida yoz."
)

def caption_image(image_bytes: bytes) -> str:
    b64 = base64.b64encode(image_bytes).decode()
    out = _ollama_chat(
        [{"role": "user", "content": CAPTION, "images": [b64]}],
        model="llava:13b",
        temperature=0.2,
        max_tokens=400,
    )
    return (out or "").strip()


print(caption_image(images[3]["bytes"])[:400])
Natija
Bu tizim arxitekturasi diagrammasi. Diagrammada 4 ta asosiy blok ko'rsatilgan:
"Mobil ilova", "API Gateway", "Billing Core" va "PostgreSQL".

Mobil ilova API Gateway ga HTTPS orqali ulangan. API Gateway dan Billing Core
ga REST chaqiruvlar ketadi. Billing Core PostgreSQL bazasi bilan ishlaydi.
O'ng tomonda "Click" va "Payme" to'lov shlyuzlari Billing Core ga webhook
orqali ulangan. Diagrammada "TPS: 1200" va "SLA 99.9%" yozuvlari bor.

4. Tavsifni indekslash

Tavsif oddiy chunk sifatida saqlanadi — bir xil jadval, bir xil embedding. Farqi faqat metadata da: qaysi sahifadagi rasm ekani.

Shu tufayli javobda "3-sahifadagi diagrammaga qarang" deb manba ko'rsatish mumkin.

python30 qator
IMAGE_SQL = (
    "INSERT INTO chunks (title, content, source_type, page, embedding) "
    "VALUES (:t, :c, 'image', :p, :e)"
)

def index_images(session, pdf_path: str, doc_title: str) -> int:
    saved = 0
    for img in extract_images(pdf_path):
        caption = caption_image(img["bytes"])
        if len(caption) < 60:
            continue
        emb, err = generate_query_embedding(caption)
        if err or emb is None:
            continue
        session.execute(text(IMAGE_SQL), {
            "t": f"{doc_title} — {img['page']}-sahifa rasmi",
            "c": caption,
            "p": img["page"],
            "e": str(emb),
        })
        saved += 1
    session.commit()
    return saved


print(f"Indekslandi: {index_images(session, 'nihol_arxitektura.pdf', 'Arxitektura')} ta rasm")

emb, _ = generate_query_embedding("billing tizimi qanday komponentlardan iborat?")
for c in retrieve_chunks(session, emb, top_k=3):
    print(f"{c['score']:.3f}  {c['title'][:50]}")
Natija
Indekslandi: 9 ta rasm

0.834  Arxitektura — 4-sahifa rasmi
0.781  Billing moduli tavsifi
0.752  Arxitektura — 7-sahifa rasmi

5. Natija

Birinchi o'rinda rasm turibdi — matnli chunk lardan yuqorida. Diagrammadagi ma'lumot hech qaysi matnda yo'q edi.

Multi-modal RAG-siz bu savolga to'liq javob umuman mumkin emas edi.

Sekin va qimmat: har bir rasm uchun vision model chaqiruvi — llava:13b da ~8–15 soniya. 100 sahifali PDF da bu yarim soat. Lekin bu bir martalik indekslash.

Sifat maslahati: agar tavsiflar yuzaki chiqsa — muammo promptda emas, modelda. Kichik vision modellar diagrammadagi matnni o'qiy olmaydi. Imkon bo'lsa kattaroq model ishlating.

Konsept

Haqiqiy dunyo faqat matndan iborat emas. Korporativ hujjatlarda rasmlar, jadvallar, diagrammalar, grafik lar bor. Multi-modal RAG bularning hammasini qayta ishlay oladi.

Uchta yondashuv: 1. OCR + Text extraction: rasmlardan matn ajratib, oddiy RAG pipeline ga berish 2. Table extraction: jadvallarni structured data ga aylantirish 3. Vision embedding: rasmlarni to'g'ridan-to'g'ri embed qilish (CLIP, multimodal model lar)

Diagram

python15 qator
  Hujjat (PDF)
       |
       +--> [Matn] --> [Chunk] --> [Embed] --> Vector DB
       |
       +--> [Rasmlar] --> [OCR/CLIP] --> [Embed] --> Vector DB
       |
       +--> [Jadvallar] --> [Table Parser] --> [Structured Text] --> [Embed] --> Vector DB

  Savol
    |
    v
  [Search across all types]
    |
    v
  [Multimodal context] --> [Generate]

Kod

python76 qator
import fitz  # PyMuPDF
from PIL import Image
import pytesseract

def extract_multimodal_content(pdf_path: str) -> dict:
    """PDF dan matn, rasm va jadvallarni ajratadi."""
    doc = fitz.open(pdf_path)
    content = {"text_chunks": [], "image_texts": [], "tables": []}

    for page in doc:
        # Matn
        text = page.get_text()
        if text.strip():
            content["text_chunks"].append(text)

        # Rasmlar
        images = page.get_images(full=True)
        for img_info in images:
            xref = img_info[0]
            base_image = doc.extract_image(xref)
            image_bytes = base_image["image"]

            # OCR bilan matn ajratish
            img = Image.open(io.BytesIO(image_bytes))
            ocr_text = pytesseract.image_to_string(img, lang="uzb+eng")
            if ocr_text.strip():
                content["image_texts"].append(ocr_text)

        # Jadvallar
        tables = page.find_tables()
        for table in tables:
            table_data = table.extract()
            # Jadval ni matn formatiga aylantirish
            table_text = format_table_as_text(table_data)
            content["tables"].append(table_text)

    return content

def index_multimodal(content: dict) -> None:
    """Barcha turdagi kontentni vector DB ga indekslaydi."""
    all_items = []

    for text in content["text_chunks"]:
        chunks = split_text(text, chunk_size=512)
        for chunk in chunks:
            all_items.append({"content": chunk, "type": "text"})

    for img_text in content["image_texts"]:
        all_items.append({"content": img_text, "type": "image_ocr"})

    for table in content["tables"]:
        all_items.append({"content": table, "type": "table"})

    # Hammasi ni embed qilib DB ga yozish
    for item in all_items:
        embedding = embed_model.encode(item["content"])
        insert_to_pgvector(
            content=item["content"],
            embedding=embedding,
            metadata={"source_type": item["type"]}
        )

def format_table_as_text(table_data: list) -> str:
    """Jadval ni LLM tushunadigan matn formatiga aylantiradi."""
    if not table_data:
        return ""

    headers = table_data[0]
    rows = table_data[1:]

    lines = []
    for row in rows:
        pairs = [f"{headers[i]}: {row[i]}" for i in range(len(headers)) if i < len(row)]
        lines.append(" | ".join(pairs))

    return "\n".join(lines)

Qachon ishlatish

  • Mahsulot kataloglari (rasmlar + tavsif)
  • Texnik qo'llanmalar (diagrammalar + jadvallar)
  • Moliyaviy hisobotlar (grafiklar + raqamlar)
  • Shartnomalar (skanerlangan PDF lar)

Xulosa jadvali

Pattern Murakkablik Sifat oshishi Qachon ishlatish
CRAG O'rta Yuqori Bazada ma'lumot to'liq emas, web fallback kerak
Self-RAG Yuqori Eng yuqori Har bir bosqichda sifat nazorati, tibbiyot/huquq sohalari
Adaptive RAG O'rta Yuqori Turli murakkablikdagi savollar, resurslarni tejash
RAPTOR Yuqori Yuqori Katta hujjatlar, "umumiy" va "batafsil" savollar
HyDE Past O'rta Qisqa/noaniq savollar, domain terminologiya farqi
Step-back Past O'rta Tor savollar past natija berganda, recall oshirish
Multi-modal Yuqori Yuqori Rasmlar, jadvallar, diagrammalar bor hujjatlar

Qaysi pattern dan boshlash kerak?

  1. Birinchi: HyDE yoki Step-back — eng oson qo'shiladi, retrieval sifatini darhol oshiradi
  2. Ikkinchi: CRAG — web search fallback qo'shish production uchun muhim
  3. Uchinchi: Adaptive RAG — turli savollar uchun turli strategiya
  4. To'rtinchi: Self-RAG yoki RAPTOR — eng murakkab, lekin eng sifatli

Har bir pattern ni alohida qo'shish mumkin. Ular bir-birini istisno qilmaydi — HyDE + CRAG + Adaptive RAG birgalikda ishlashi mumkin. LangGraph bu pattern larni bitta graf da birlashtirishni osonlashtiradi — 13-bobdagi state machine yondashuvini shu pattern lar bilan to'ldiring.


8. Explainable Retrieval — nega aynan shu chunk?

Kodlar: Explainable Retrieval — retrieval ni tushuntirish 14-bob · debugging vositasi · ~60 qator

1. Tushuntirish funksiyasi

Prompt qisqa bo'lishi kerak — bizga insho emas, bir jumla kerak. max_tokens=80 buni majburlaydi.

python13 qator
EXPLAIN = (
    "Bu hujjat bo'lagi berilgan savolga qanday aloqador? "
    "Bir jumlada tushuntir. Aloqasi bo'lmasa — nega yo'qligini ayt.\n\n"
    "Savol: {question}\n\nBo'lak: {chunk}"
)

def explain_chunk(question: str, chunk: str) -> str:
    out = _ollama_chat(
        [{"role": "user", "content": EXPLAIN.format(question=question,
                                                     chunk=chunk[:800])}],
        temperature=0.2, max_tokens=80,
    )
    return (out or "").strip()
python15 qator
def explainable_retrieve(session, question: str, top_k: int = 5) -> list[dict]:
    emb, _ = generate_query_embedding(question)
    chunks = retrieve_chunks(session, emb, top_k=top_k)

    out = []
    for c in chunks:
        row = dict(c)
        row["explanation"] = explain_chunk(question, c["content"])
        out.append(row)
    return out


for i, c in enumerate(explainable_retrieve(session, "Ucell loyihasi haqida"), 1):
    print(f"{i}. [{c['score']:.3f}] {c['title'][:44]}")
    print(f"   -> {c['explanation'][:110]}\n")
Natija
1. [0.841] Ucell billing tizimi modernizatsiyasi
   -> Bu bo'lak to'g'ridan-to'g'ri Ucell uchun bajarilgan billing loyihasini tavsiflaydi.

2. [0.792] Sharq Telekom tarmoq kengaytirish
   -> Bu bo'lak telekom sohasidagi tarmoq ishlari haqida, lekin Ucell emas, Sharq
      Telekom kompaniyasi haqida.

3. [0.774] Ucell SMS gateway integratsiyasi
   -> Ucell uchun bajarilgan boshqa loyiha — SMS gateway ulanishi haqida.

4. [0.751] EMAN GROUP telekom yechimlari
   -> Telekommunikatsiya yechimlari haqida umumiy ma'lumot, Ucell bilan bog'liq emas.

5. [0.733] Kompaniya haqida umumiy
   -> Kompaniyaning umumiy tavsifi, aniq loyiha haqida ma'lumot yo'q.

2. Mana bu — debugging oltin koni

2- va 4-natijalar ochiq aytadi: "Ucell emas", "Ucell bilan bog'liq emas". Ya'ni top-5 ning 40% i keraksiz.

Bu raqamlar bilan ko'rinmasdi: 0.792 va 0.751 — "yaxshi" ballar. Faqat tushuntirish muammoni oshkor qildi.

3. Avtomatik filtr sifatida ishlatish

Tushuntirishlarda "emas", "bog'liq emas", "aloqasi yo'q" kabi iboralar bo'lsa — chunk ni tashlash mumkin. Bu arzon evristika.

python18 qator
NEGATIVE = ("bog'liq emas", "aloqasi yo'q", "aloqador emas",
            "ma'lumot yo'q", "tegishli emas", "emas,")

def filter_by_explanation(chunks: list[dict]) -> list[dict]:
    kept = []
    for c in chunks:
        low = c.get("explanation", "").lower()
        if any(marker in low for marker in NEGATIVE):
            continue
        kept.append(c)
    return kept


explained = explainable_retrieve(session, "Ucell loyihasi haqida")
kept = filter_by_explanation(explained)
print(f"{len(explained)} -> {len(kept)} chunk")
for c in kept:
    print(f"  {c['title'][:50]}")
Natija
5 -> 3 chunk
  Ucell billing tizimi modernizatsiyasi
  Ucell SMS gateway integratsiyasi
  Kompaniya haqida umumiy
Muhim ogohlantirish: tushuntirish — LLM ning post-hoc taxmini. U embedding modelining haqiqiy "qaroriga" kirmaydi, faqat natijani ko'rib izoh yozadi.

Ya'ni tushuntirish ishonarli, lekin u sabab emas. Uni debugging uchun ishlating, ilmiy dalil sifatida emas.

Narxi: har chunk uchun 1 chaqiruv. Production chat da har so'rovga qo'shmang — faqat "nega bu manba?" tugmasi bosilganda hisoblang.

Vector search 0.82 ball qaytardi. Bu nimani anglatadi? Hech narsani — bu shunchaki ikki vektor orasidagi burchak kosinusi. Nega bu chunk tanlangani, va nega boshqasi tanlanmagani — ko'rinmaydi.

Bu debugging uchun jiddiy muammo. Tizim noto'g'ri javob berdi — sabab retrieval dami, generation dami? Ball 0.82 sizga aytmaydi.

Explainable Retrieval oddiy narsa qiladi: har bir topilgan chunk uchun LLM dan bir jumlalik tushuntirish so'raydi — "bu bo'lak savolga qanday aloqador?".

python7 qator
Oddiy natija:
  0.82  Sharq Telekom tarmoq kengaytirish loyihasi

Tushuntirish bilan:
  0.82  Sharq Telekom tarmoq kengaytirish loyihasi
        --> "Bu bo'lak telekom sohasidagi tarmoq ishlari haqida,
             lekin savoldagi Ucell kompaniyasi haqida emas."

Mana endi muammo ko'rinadi: chunk mavzu bo'yicha yaqin, lekin mijoz boshqa. Ball buni ko'rsatmasdi.

Ikki xil foydalanish

  • Debugging vositasi — ishlab chiqish paytida retrieval nima uchun adashayotganini tushunish. Eng ko'p foyda shu yerda.
  • Foydalanuvchi interfeysi — javob ostida "nega bu manba" izohini ko'rsatish. Ishonchni oshiradi, lekin qimmat.

Narxi va cheklovi

Har bir chunk uchun alohida LLM chaqiruvi. 5 ta chunk — 5 ta chaqiruv, ~3 soniya. Production chat da bu og'irlik qiladi.

Amaliy yechim: tushuntirishni faqat so'ralganda hisoblang. Foydalanuvchi "nega bu manba?" tugmasini bosgandagina ishga tushirilsin.

Muhim cheklov: tushuntirish — LLM ning taxmini, embedding modelining haqiqiy "fikri" emas. U ishonarli ko'rinadi, lekin bu post-hoc izoh. Buni yodda tuting: tushuntirish to'g'ri bo'lsa ham, u retrieval qarorining haqiqiy sababi emas.

9. LightRAG — arzon graf yondashuvi

Kodlar: LightRAG — grafsiz graf yondashuvi 14-bob · lokal + global kalit so'zlar · ~100 qator

1. Obyekt va munosabatlarni ajratish

Graph RAG dagi kabi, lekin natija grafga emas, jadvalga yoziladi. NetworkX kerak emas.

python19 qator
import json

LIGHT_EXTRACT = (
    "Matndan obyektlar va munosabatlarni ajrat. JSON qaytar:\n"
    '{{"entities": [{{"name": "...", "type": "...", "description": "..."}}],\n'
    ' "relations": [{{"from": "...", "to": "...", "description": "..."}}]}}\n'
    "description — qisqa, bir jumla.\n\nMatn: {chunk}"
)

def extract_light(chunk: str) -> dict:
    out = _ollama_chat(
        [{"role": "user", "content": LIGHT_EXTRACT.format(chunk=chunk[:1200])}],
        temperature=0.0, max_tokens=600,
    )
    try:
        start, end = out.index("{"), out.rindex("}") + 1
        return json.loads(out[start:end])
    except (ValueError, json.JSONDecodeError):
        return {"entities": [], "relations": []}

2. Tavsifni embedding qilamiz, nomni emas

Muhim qaror: "Ucell" so'zining embedding-i deyarli foydasiz — u juda qisqa. Shuning uchun tavsif embedding qilinadi: "Ucell — O'zbekistondagi telekommunikatsiya operatori, Nihol ning mijozi".

python47 qator
ENT_SQL = (
    "INSERT INTO light_entities (name, type, description, chunk_id, embedding) "
    "VALUES (:n, :t, :d, :c, :e) "
    "ON CONFLICT (name) DO UPDATE SET description = EXCLUDED.description"
)
REL_SQL = (
    "INSERT INTO light_relations (src, dst, description, chunk_id, embedding) "
    "VALUES (:s, :d, :desc, :c, :e)"
)

def index_lightrag(session, chunks: list[str]) -> dict:
    stats = {"entities": 0, "relations": 0}

    for chunk_id, chunk in enumerate(chunks):
        data = extract_light(chunk)

        for ent in data.get("entities", []):
            name = normalize(ent.get("name", ""))
            desc = ent.get("description") or name
            if not name:
                continue
            emb, err = generate_query_embedding(f"{ent['name']}: {desc}")
            if err or emb is None:
                continue
            session.execute(text(ENT_SQL), {
                "n": name, "t": ent.get("type", "?"), "d": desc,
                "c": chunk_id, "e": str(emb)})
            stats["entities"] += 1

        for rel in data.get("relations", []):
            src, dst = normalize(rel.get("from", "")), normalize(rel.get("to", ""))
            desc = rel.get("description") or f"{src} - {dst}"
            if not src or not dst:
                continue
            emb, err = generate_query_embedding(desc)
            if err or emb is None:
                continue
            session.execute(text(REL_SQL), {
                "s": src, "d": dst, "desc": desc,
                "c": chunk_id, "e": str(emb)})
            stats["relations"] += 1

    session.commit()
    return stats


print(index_lightrag(session, chunks[:60]))
Natija
{'entities': 178, 'relations': 241}

3. Ikki darajali kalit so'zlar

Savoldan ikki xil kalit so'z ajratamiz: aniq obyektlar (lokal) va umumiy mavzular (global).

python21 qator
KEYWORDS = (
    "Savoldan ikki xil kalit so'z ajrat. JSON qaytar:\n"
    '{{"local": ["aniq obyekt nomlari"], "global": ["umumiy mavzular"]}}\n\n'
    "Savol: {question}"
)

def extract_keywords_two_level(question: str) -> dict:
    out = _ollama_chat(
        [{"role": "user", "content": KEYWORDS.format(question=question)}],
        temperature=0.0, max_tokens=150,
    )
    try:
        start, end = out.index("{"), out.rindex("}") + 1
        data = json.loads(out[start:end])
        return {"local": data.get("local", []), "global": data.get("global", [])}
    except (ValueError, json.JSONDecodeError):
        return {"local": [question], "global": [question]}


print(extract_keywords_two_level(
    "Ucell billing tizimida to'lov integratsiyasi qanday qilingan?"))
Natija
{'local': ['Ucell', 'billing tizimi', "to'lov"],
 'global': ['integratsiya', 'tizim arxitekturasi']}

4. Ikki manbadan qidirish

Lokal kalit so'zlar obyektlar jadvalidan, global kalit so'zlar munosabatlar jadvalidan qidiradi. Natijalar birlashtiriladi.

python45 qator
ENT_SEARCH = (
    "SELECT name, description, chunk_id, "
    "       1 - (embedding <=> CAST(:e AS vector)) AS score "
    "FROM light_entities ORDER BY embedding <=> CAST(:e AS vector) LIMIT :k"
)
REL_SEARCH = (
    "SELECT src, dst, description, chunk_id, "
    "       1 - (embedding <=> CAST(:e AS vector)) AS score "
    "FROM light_relations ORDER BY embedding <=> CAST(:e AS vector) LIMIT :k"
)

def lightrag_retrieve(session, question: str, top_k: int = 5) -> dict:
    keys = extract_keywords_two_level(question)
    entities, relations, chunk_ids = [], [], set()

    for kw in keys["local"]:
        emb, err = generate_query_embedding(kw)
        if err or emb is None:
            continue
        for r in session.execute(text(ENT_SEARCH),
                                 {"e": str(emb), "k": 3}).mappings().all():
            entities.append(dict(r))
            chunk_ids.add(r["chunk_id"])

    for kw in keys["global"]:
        emb, err = generate_query_embedding(kw)
        if err or emb is None:
            continue
        for r in session.execute(text(REL_SEARCH),
                                 {"e": str(emb), "k": 3}).mappings().all():
            relations.append(dict(r))
            chunk_ids.add(r["chunk_id"])

    return {"keywords": keys, "entities": entities[:top_k],
            "relations": relations[:top_k], "chunk_ids": sorted(chunk_ids)}


r = lightrag_retrieve(session, "Ucell billing tizimida to'lov integratsiyasi qanday qilingan?")
print("Obyektlar:")
for e in r["entities"][:3]:
    print(f"  {e['score']:.3f}  {e['name']}: {e['description'][:55]}")
print("Munosabatlar:")
for rel in r["relations"][:3]:
    print(f"  {rel['score']:.3f}  {rel['src']} -> {rel['dst']}: {rel['description'][:45]}")
print(f"\nJami {len(r['chunk_ids'])} ta chunk")
Natija
Obyektlar:
  0.871  ucell: Telekommunikatsiya operatori, Nihol ning mijozi
  0.834  billing: Abonentlarni hisob-kitob qilish tizimi
  0.792  click: O'zbekistondagi to'lov shlyuzi xizmati
Munosabatlar:
  0.812  billing -> click: Billing tizimi Click bilan REST API orqali
  0.784  billing -> payme: To'lov shlyuzi webhook orqali ulangan
  0.741  nihol -> billing: Nihol billing tizimini ishlab chiqqan

Jami 11 ta chunk

5. Nima yutdik

Munosabatlar qatori ("billing → click: REST API orqali") — bu aniq javob, va u hech qaysi bitta chunk da to'liq yozilmagan bo'lishi mumkin.

To'liq Graph RAG bilan solishtirganda: bu yerda graf bo'ylab yurish yo'q, NetworkX yo'q, faqat ikkita jadval va vector search.

Dedup hali ham muammo. ON CONFLICT (name) DO UPDATE faqat aynan bir xil nomlarni birlashtiradi. "Ucell" va "Ucell kompaniyasi" — normalize ularni birlashtiradi, lekin "UCELL LLC" ni yo'q.

Yaxshilash: yangi obyekt qo'shishdan oldin mavjudlar ichidan embedding bo'yicha qidiring — 0.95 dan yuqori o'xshashlik topilsa, yangi yozuv yaratmang.

Xarajat: Graph RAG dan ~2 barobar arzon (dedup va traversal yo'q), lekin baribir har chunk uchun 1 LLM chaqiruvi.

Bu bobda ko'rgan Graph RAG kuchli, lekin qimmat: har bir chunk uchun LLM chaqiruvi, murakkab dedup, graf ma'lumotlar bazasi. Kichik loyihada bu ortiqcha.

LightRAG shu g'oyani soddalashtiradi. Graf bor, lekin u alohida baza talab qilmaydi — obyektlar va munosabatlar oddiy jadvalda saqlanadi va vector search bilan qidiriladi.

Ikki darajali kalit so'zlar

LightRAG ning asosiy g'oyasi — savoldan ikki xil kalit so'z ajratish:

  • Lokal kalit so'zlar — aniq obyektlar: "Ucell", "billing", "Click". Ular bo'yicha obyektlar qidiriladi.
  • Global kalit so'zlar — umumiy mavzular: "integratsiya", "modernizatsiya". Ular bo'yicha munosabatlar qidiriladi.
matn6 qator
Savol: "Ucell billing tizimida to'lov integratsiyasi qanday qilingan?"

Lokal  : [Ucell, billing tizimi, to'lov]      --> obyektlar jadvalidan
Global : [integratsiya, tizim arxitekturasi]  --> munosabatlar jadvalidan

Ikkala natija birlashtirilib kontekst quriladi

Nega bu ishlaydi? Chunki savolda ikki xil talab bor: nima haqida (obyekt) va qanday munosabat (bog'lanish). Ularni alohida qidirish ikkalasini ham topish imkonini beradi.

Graph RAG bilan taqqoslash

To'liq Graph RAG grafni bo'ylab yuradi (traversal) — 2 qadam, 3 qadam narigacha. LightRAG bunday qilmaydi: u graf elementlarini shunchaki qidiriladigan matn sifatida ko'radi.

Bu kamroq kuchli, lekin ancha arzon va NetworkX yoki Neo4j kerak emas. Ko'p amaliy vazifalar uchun yetarli.

10. MemoRAG — avval eslab qol, keyin qidir

Kodlar: MemoRAG — korpusdan xotira qurish 14-bob · clue generation · ~90 qator

1. Xotira nima?

Xotira — butun korpusning siqilgan xaritasi. Har bir hujjat guruhidan kalit-qiymat juftliklari ajratiladi: mavzu → qisqa tavsif.

Bu RAPTOR dagi xulosaga o'xshaydi, lekin maqsad boshqa: xulosa javob berish uchun, xotira esa qidiruv so'rovini yozish uchun.

python22 qator
MEMORIZE = (
    "Quyidagi matnlardan asosiy mavzularni ajrat. "
    "Har birini shu formatda yoz:\n"
    "MAVZU: qisqa tavsif\n"
    "5-8 ta mavzu yoz, boshqa hech narsa yozma.\n\n{texts}"
)

def memorize(texts: list[str]) -> list[tuple[str, str]]:
    joined = "\n\n---\n\n".join(t[:700] for t in texts[:12])
    out = _ollama_chat(
        [{"role": "user", "content": MEMORIZE.format(texts=joined)}],
        temperature=0.3, max_tokens=400,
    )
    pairs = []
    for line in (out or "").split("\n"):
        if ":" not in line:
            continue
        key, _, value = line.partition(":")
        key, value = key.strip(" -•"), value.strip()
        if len(key) > 2 and len(value) > 10:
            pairs.append((key, value))
    return pairs
python25 qator
MEM_SQL = (
    "INSERT INTO memory_store (topic, description, embedding) "
    "VALUES (:t, :d, :e)"
)

def build_memory(session, chunks: list[str], batch: int = 12) -> int:
    saved = 0
    for i in range(0, len(chunks), batch):
        for topic, desc in memorize(chunks[i: i + batch]):
            emb, err = generate_query_embedding(f"{topic}: {desc}")
            if err or emb is None:
                continue
            session.execute(text(MEM_SQL),
                            {"t": topic, "d": desc, "e": str(emb)})
            saved += 1
    session.commit()
    return saved


print(f"Xotirada: {build_memory(session, chunks)} ta yozuv")

rows = session.execute(text(
    "SELECT topic, description FROM memory_store LIMIT 5")).all()
for topic, desc in rows:
    print(f"  {topic}: {desc[:60]}")
Natija
Xotirada: 214 ta yozuv
  Telekom loyihalari: Ucell va Sharq Telekom uchun bajarilgan ishlar
  Billing tizimlari: Abonent hisob-kitob modullari va to'lov integratsiyasi
  Bank sohasi: Ipoteka, kredit skoring va mobil banking loyihalari
  Texnologiyalar: FastAPI, PostgreSQL, Redis asosidagi yechimlar
  Jamoa: 40 dan ortiq mutaxassis, 6-12 kishilik loyiha guruhlari

2. Clue generation — eng muhim qadam

Xotiradan foydalanib, noaniq savolni aniq qidiruv so'rovlariga aylantiramiz. Bu MemoRAG ning yuragi.

python34 qator
CLUES = (
    "Quyidagi xotira asosida savolga javob topish uchun "
    "3 ta aniq qidiruv so'rovi yoz. "
    "Xotiradagi aniq nomlar va atamalardan foydalan. "
    "Har birini yangi qatorda, raqamlamasdan.\n\n"
    "Xotira:\n{memory}\n\nSavol: {question}"
)

MEM_SEARCH = (
    "SELECT topic, description, "
    "       1 - (embedding <=> CAST(:e AS vector)) AS score "
    "FROM memory_store ORDER BY embedding <=> CAST(:e AS vector) LIMIT :k"
)

def generate_clues(session, question: str, n_memory: int = 8) -> list[str]:
    emb, err = generate_query_embedding(question)
    if err or emb is None:
        return [question]

    rows = session.execute(text(MEM_SEARCH),
                           {"e": str(emb), "k": n_memory}).mappings().all()
    memory = "\n".join(f"{r['topic']}: {r['description']}" for r in rows)

    out = _ollama_chat(
        [{"role": "user", "content": CLUES.format(memory=memory, question=question)}],
        temperature=0.4, max_tokens=200,
    )
    clues = [c.strip(" -•0123456789.") for c in (out or "").split("\n")]
    return [c for c in clues if len(c) > 10][:3] or [question]


vague = "Bizda shunga o'xshash tajriba bormidi?"
for c in generate_clues(session, vague):
    print("-", c)
Natija
- Telekom sohasidagi billing tizimi loyihalari
- Ucell va Sharq Telekom uchun bajarilgan integratsiya ishlari
- Abonent hisob-kitob moduli ishlab chiqish tajribasi

3. Mana bu — MemoRAG ning butun qiymati

"Bizda shunga o'xshash tajriba bormidi?" — bu savolda bitta ham kalit so'z yo'q. Vector search bo'sh natija qaytarardi.

Xotira uni uchta aniq, qidiriladigan so'rovga aylantirdi — va ularda korpusdagi haqiqiy atamalar bor.

python23 qator
def memorag_retrieve(session, question: str, top_k: int = 8) -> list[dict]:
    clues = generate_clues(session, question)

    seen, out = set(), []
    for clue in clues:
        emb, err = generate_query_embedding(clue)
        if err or emb is None:
            continue
        for c in retrieve_chunks(session, emb, top_k=5):
            if c["id"] in seen:
                continue
            seen.add(c["id"])
            row = dict(c)
            row["found_by"] = clue
            out.append(row)

    out.sort(key=lambda c: c["score"], reverse=True)
    return out[:top_k]


for c in memorag_retrieve(session, "Bizda shunga o'xshash tajriba bormidi?")[:4]:
    print(f"{c['score']:.3f}  {c['title'][:40]}")
    print(f"        topildi: {c['found_by'][:50]}")
Natija
0.831  Ucell billing tizimi modernizatsiyasi
        topildi: Telekom sohasidagi billing tizimi loyihalari
0.798  Sharq Telekom integratsiya ishlari
        topildi: Ucell va Sharq Telekom uchun bajarilgan integratsiya ishlari
0.774  Abonent hisob-kitob moduli
        topildi: Abonent hisob-kitob moduli ishlab chiqish tajribasi
0.741  SMS gateway integratsiyasi
        topildi: Telekom sohasidagi billing tizimi loyihalari
Xotira eskiradi. Korpusga yangi hujjat qo'shilsa, xotira uni bilmaydi va clue generation eski atamalarni beradi. Re-embedding pipeline ingizga xotirani qayta qurishni ham qo'shing.

Xarajat: 451 chunk / 12 = ~38 ta LLM chaqiruvi xotira uchun. Bu arzon. Qidiruvda esa 1 ta qo'shimcha chaqiruv (clue generation) — HyDE bilan bir xil.

Qachon kerak emas: foydalanuvchilar aniq savol berishni bilsa (ichki texnik jamoa), MemoRAG ortiqcha qatlam.

Barcha RAG tizimlarida bitta yashirin taxmin bor: foydalanuvchi to'g'ri savol beradi. Ya'ni savolda qidiriladigan kalit so'zlar bor.

Amalda bu ko'pincha noto'g'ri. "Bizda shunga o'xshash narsa bormidi?" — bu savolda hech qanday kalit so'z yo'q. Vector search hech narsa topa olmaydi.

MemoRAG oraga xotira qatlamini qo'yadi. Indekslash paytida butun korpus siqilgan "xotira" ga aylantiriladi — kalit-qiymat juftliklari shaklida. Qidiruvda esa avval xotiradan so'raladi: "bu savol bo'yicha nimalarni bilaman?"

python8 qator
Oddiy RAG:
  savol --> embedding --> qidiruv --> chunk lar --> javob
  "shunga o'xshash narsa" --> bo'sh natija

MemoRAG:
  savol --> xotira: "telekom loyihalari, billing tizimlari,
                     integratsiya ishlari bor edi"
        --> shu iboralar bilan qidiruv --> chunk lar --> javob

Ya'ni xotira qidiruv so'rovini yozib beradi. U to'liq javob bermaydi — u qayerga qarash kerakligini ko'rsatadi.

HyDE bilan farqi

HyDE ham qidiruv so'rovini yaxshilaydi, lekin u faqat savoldan kelib chiqadi — LLM o'z umumiy bilimidan soxta javob yozadi.

MemoRAG esa korpusning o'zidan qurilgan xotiradan foydalanadi. Shuning uchun u sizning ma'lumotlaringizdagi haqiqiy atamalarni biladi. HyDE "billing tizimi" deb taxmin qilsa, MemoRAG "Ucell billing modernizatsiyasi loyihasi" deb aniq nom beradi.

Cheklovi

Xotira qurish qimmat va korpus o'zgarganda qayta qurilishi kerak. Tez-tez yangilanadigan bazada bu jiddiy muammo.

Shuning uchun MemoRAG barqaror, kamdan-kam o'zgaradigan korpuslar uchun mos: arxiv, qo'llanma, tugallangan loyihalar registri.

11. Local Graph RAG — tekshiriladigan iqtibos bilan

Kodlar: Local Graph RAG — Ollama, offline, iqtibosli 14-bob · to'liq lokal + audit izi · ~110 qator

1. To'liq lokal setup

Tashqi API yo'q. Embedding — sentence-transformers, LLM — Ollama. Internet faqat birinchi yuklashda kerak.

python28 qator
from dataclasses import dataclass, field
from sentence_transformers import SentenceTransformer
import hashlib, json

_model = SentenceTransformer("intfloat/multilingual-e5-base")

def local_embed(body: str) -> list[float]:
    return _model.encode(f"query: {body}", normalize_embeddings=True).tolist()


@dataclass
class Citation:
    doc_id: int
    chunk_index: int
    sentence: str


@dataclass
class Relationship:
    src: str
    dst: str
    kind: str
    citation: Citation
    rel_id: str = field(default="")

    def __post_init__(self):
        raw = f"{self.src}|{self.kind}|{self.dst}|{self.citation.sentence}"
        self.rel_id = hashlib.md5(raw.encode()).hexdigest()[:12]

2. Iqtibos — dizaynning markazida

Diqqat qiling: Relationship ichida Citation majburiy maydon. Manbasiz munosabat yaratib bo'lmaydi — bu tasodif emas, ataylab shunday qilingan.

Shuning uchun LLM dan munosabat so'raganda aynan qaysi jumladan olganini ham so'raymiz.

python31 qator
LOCAL_EXTRACT = (
    "Matndan munosabatlarni ajrat. Har biri uchun uni tasdiqlaydigan "
    "AYNAN o'sha jumlani ham ko'chir. JSON qaytar:\n"
    '[{{"from": "...", "to": "...", "kind": "...", "sentence": "..."}}]\n'
    "sentence — matndan aynan ko'chirilgan jumla bo'lsin.\n\nMatn: {chunk}"
)

def extract_with_citation(chunk: str, doc_id: int,
                          chunk_index: int) -> list[Relationship]:
    out = _ollama_chat(
        [{"role": "user", "content": LOCAL_EXTRACT.format(chunk=chunk[:1200])}],
        temperature=0.0, max_tokens=700,
    )
    try:
        start, end = out.index("["), out.rindex("]") + 1
        items = json.loads(out[start:end])
    except (ValueError, json.JSONDecodeError):
        return []

    rels = []
    for item in items:
        sentence = (item.get("sentence") or "").strip()
        if not sentence or sentence not in chunk:
            continue
        rels.append(Relationship(
            src=normalize(item.get("from", "")),
            dst=normalize(item.get("to", "")),
            kind=item.get("kind", "BOGLIQ"),
            citation=Citation(doc_id, chunk_index, sentence),
        ))
    return rels

3. sentence not in chunk — eng muhim qator

Bu tekshiruv LLM ning uydirmasini ushlaydi. Agar u jumlani o'ylab topgan bo'lsa (matnda yo'q bo'lsa) — munosabat rad etiladi.

Shu bitta qator butun tizimning ishonchliligini ta'minlaydi.

python30 qator
REL_STORE = (
    "INSERT INTO local_relations "
    "(rel_id, src, dst, kind, doc_id, chunk_index, sentence, embedding) "
    "VALUES (:id, :s, :d, :k, :doc, :ci, :sent, :e) "
    "ON CONFLICT (rel_id) DO NOTHING"
)

def index_local_graph(session, documents: list[dict]) -> dict:
    stats = {"relations": 0, "rejected": 0}

    for doc in documents:
        for chunk_index, chunk in enumerate(chunk_text(doc["body"], size=800)):
            rels = extract_with_citation(chunk, doc["id"], chunk_index)
            for rel in rels:
                if not rel.src or not rel.dst:
                    stats["rejected"] += 1
                    continue
                text_form = f"{rel.src} {rel.kind} {rel.dst}: {rel.citation.sentence}"
                session.execute(text(REL_STORE), {
                    "id": rel.rel_id, "s": rel.src, "d": rel.dst, "k": rel.kind,
                    "doc": rel.citation.doc_id, "ci": rel.citation.chunk_index,
                    "sent": rel.citation.sentence, "e": str(local_embed(text_form)),
                })
                stats["relations"] += 1

    session.commit()
    return stats


print(index_local_graph(session, documents))
Natija
{'relations': 312, 'rejected': 47}

4. 47 ta rad etildi

Ularning aksariyati — LLM jumlani qayta yozgan yoki o'ylab topgan holatlar. Tekshiruvsiz ular bazaga tushardi va soxta iqtibos yaratardi.

python34 qator
LOCAL_SEARCH = (
    "SELECT src, dst, kind, doc_id, chunk_index, sentence, "
    "       1 - (embedding <=> CAST(:e AS vector)) AS score "
    "FROM local_relations "
    "ORDER BY embedding <=> CAST(:e AS vector) LIMIT :k"
)

def attributed_answer(session, question: str, top_k: int = 6) -> dict:
    emb = local_embed(question)
    rows = session.execute(text(LOCAL_SEARCH),
                           {"e": str(emb), "k": top_k}).mappings().all()
    facts = [dict(r) for r in rows]
    if not facts:
        return {"answer": "Ma'lumot topilmadi.", "citations": []}

    context = "\n".join(
        f"[{i + 1}] {f['sentence']}" for i, f in enumerate(facts)
    )
    answer = _ollama_chat([
        {"role": "system", "content":
         "Javobni faqat berilgan faktlardan yoz. Har bir da'vodan keyin "
         "manba raqamini kvadrat qavsda ko'rsat, masalan [1]."},
        {"role": "user", "content": f"Faktlar:\n{context}\n\nSavol: {question}"},
    ], temperature=0.1)

    return {"answer": answer, "citations": facts}


r = attributed_answer(session, "Ucell billing tizimi qaysi texnologiyalarda qurilgan?")
print(r["answer"])
print("\nManbalar:")
for i, c in enumerate(r["citations"][:3], 1):
    print(f"  [{i}] hujjat {c['doc_id']}, chunk {c['chunk_index']}")
    print(f"      \"{c['sentence'][:75]}\"")
Natija
Ucell billing tizimi FastAPI freymvorkida qurilgan [1] va ma'lumotlar
PostgreSQL bazasida saqlanadi [2]. Kesh uchun Redis ishlatilgan [3].

Manbalar:
  [1] hujjat 12, chunk 3
      "Billing moduli FastAPI freymvorkida qayta yozildi."
  [2] hujjat 12, chunk 4
      "Ma'lumotlar PostgreSQL bazasida, pgvector kengaytmasi bilan saqlanadi."
  [3] hujjat 14, chunk 1
      "Tez-tez so'raladigan ma'lumotlar Redis keshida saqlanadi."

5. To'liq audit izi

Har bir da'vo → manba raqami → hujjat va chunk → asl jumla. Foydalanuvchi zanjirning har bir bo'g'inini tekshira oladi.

Va bularning hammasi internetsiz, sizning mashinangizda bajarildi.

Sekinlik — asosiy to'siq. 7B model va oddiy GPU da 451 chunk uchun graf qurish 40-60 daqiqa. Bu tunda, cron orqali bajariladigan ish.

GPU tekshiring: Ollama GPU ni ko'rmasa, bu vaqt 5-10 barobar oshadi. ollama ps buyrug'i modelning GPU da yoki CPU da ishlayotganini ko'rsatadi.

Model tanlash: munosabat ajratish uchun kamida 7B kerak. Kichikroq modellar JSON ni buzadi va sentence not in chunk tekshiruvida deyarli hammasi rad etiladi.

Bu bo'lim ikkita g'oyani birlashtiradi va ikkalasi ham sizning loyihangiz uchun muhim: to'liq lokal ishlash va har bir da'vo uchun tekshiriladigan manba.

Nega to'liq lokal?

Maxfiy hujjatlar bilan ishlaganda tashqi API — xavf. Shartnomalar, shaxsiy ma'lumotlar, ichki hisobotlar OpenAI serveriga yuborilmasligi kerak.

Ollama (LLM) va sentence-transformers (embedding) bilan butun pipeline sizning mashinangizda qoladi. Internet umuman kerak emas.

Tekshiriladigan iqtibos nima?

Ko'p RAG tizimlari "manbalar" ro'yxatini ko'rsatadi, lekin bu ro'yxat retrieval natijasi — javob ularning qaysi biridan kelgani noma'lum.

Bu yerda boshqacha: graf quriladi, va har bir munosabat o'zi qaysi hujjatning qaysi jumlasidan olinganini saqlaydi. Javobdagi har bir da'vo aniq jumlagacha kuzatiladi.

yaml4 qator
Da'vo:    "Ucell billing tizimi FastAPI da qurilgan"
Munosabat: (billing tizimi) --ISHLATADI--> (FastAPI)
Manba:     hujjat 12, 3-abzats
Jumla:     "Billing moduli FastAPI freymvorkida qayta yozildi."

Foydalanuvchi bu zanjirning har bir bo'g'inini tekshira oladi. Bu audit talab qilinadigan sohalarda — huquq, moliya, tibbiyot — majburiy talab.

Narxi

Graf qurish sekin: har bir chunk uchun lokal LLM chaqiruvi. 7B model va oddiy GPU da 451 chunk ~40-60 daqiqa oladi. Lekin bu bir martalik ish va u tunda bajarilishi mumkin.

Ishlash tezligi esa yaxshi — qidiruvda faqat embedding va SQL, LLM faqat oxirgi javob uchun chaqiriladi.

Bu bobni bepul o'qidingiz.

Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.

Telegramda yozish Narxlar