Advanced RAG Patterns
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
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
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.
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.
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]}")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.
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
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
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
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?
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}")False Salom, qalaysiz? True Ucell loyihasi haqida ayting False 15 ni 3 ga bo'ling
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.
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]}")[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.
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
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
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
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.
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}")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
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)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]}")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.
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
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
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
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.
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.
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.
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)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.
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}")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.
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
[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
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
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
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
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).
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.
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])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.
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
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
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
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.
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)}")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.
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])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.
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]}")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.
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
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
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?
- Birinchi: HyDE yoki Step-back — eng oson qo'shiladi, retrieval sifatini darhol oshiradi
- Ikkinchi: CRAG — web search fallback qo'shish production uchun muhim
- Uchinchi: Adaptive RAG — turli savollar uchun turli strategiya
- 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
1. Tushuntirish funksiyasi
Prompt qisqa bo'lishi kerak — bizga insho emas, bir jumla kerak. max_tokens=80 buni majburlaydi.
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()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")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.
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]}")5 -> 3 chunk Ucell billing tizimi modernizatsiyasi Ucell SMS gateway integratsiyasi Kompaniya haqida umumiy
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?".
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
1. Obyekt va munosabatlarni ajratish
Graph RAG dagi kabi, lekin natija grafga emas, jadvalga yoziladi. NetworkX kerak emas.
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".
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])){'entities': 178, 'relations': 241}3. Ikki darajali kalit so'zlar
Savoldan ikki xil kalit so'z ajratamiz: aniq obyektlar (lokal) va umumiy mavzular (global).
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?")){'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.
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")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.
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.
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
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.
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 pairsMEM_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]}")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.
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)- 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.
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]}")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 loyihalariXarajat: 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?"
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
1. To'liq lokal setup
Tashqi API yo'q. Embedding — sentence-transformers, LLM — Ollama. Internet faqat birinchi yuklashda kerak.
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.
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 rels3. 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.
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)){'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.
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]}\"")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.
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.
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.