27 β Kapston I: RAG chatbot¶
β¬ οΈ Oldingi: 26 β Deploy: FastAPI bilan LLM xizmati Β· π Kitob boshi Β· Keyingi: 28 β Kapston II: agent-asoslangan avtomatlashtirish β‘οΈ
Bu bobda: kitobning butun yo'lini bitta ishlaydigan loyihada birlashtiramiz β o'z hujjatlaringiz ustida savol-javob qiladigan, suhbatni eslaydigan va har javobda manba ko'rsatadigan RAG chatbot. Loyihani professional tarzda fayllarga ajratamiz:
config.py(sozlama va model),ingest.py(hujjat -> chunk -> embed -> Chroma, 15-bob),rag.py(retrieve + generate + citations, 16-bob),chat.py(suhbat xotirasi, 8-bob, + CLI interfeys). So'ng bir savolni tarix bilan standalone (mustaqil) qilib qayta yozish orqali ko'p-navbatli suhbatni; xarajat (22-bob) va xato (23-bob) boshqaruvini qisqa ulaymiz; va nihoyat FastAPI/chat(26-bob) variantini eslatib o'tamiz. Oxirida βpip installdanpython chat.pygacha to'liq foydalanish ko'rsatmasi va "keyingi qadamlar".
Muammodan boshlaymiz: alohida bilim β yetarli emas¶
Shu paytgacha har bobda bittadan ko'nikma o'rgandik: birinchi so'rov (2-bob), suhbat xotirasi (8-bob), embeddings va vektor baza (13-14-bob), RAG indekslash va so'rov (15-16-bob), xarajat (22-bob), ishonchlilik (23-bob), deploy (26-bob). Har biri o'zicha kichik misol edi.
Lekin haqiqiy ilova β bu alohida misollar yig'indisi emas, balki ularning birlashgan, yaxlit tizimi. Foydalanuvchi sizning chatbotingizdan "kechagi savolimga qaytsak..." deb so'raganda, u bir vaqtning o'zida: tarixni eslashni (8-bob), savolga mos hujjatni topishni (16-bob), manbani ko'rsatishni (16-bob) va bularning hammasi arzon hamda xatosiz ishlashini (22-23-bob) kutadi.
Bu bobda biz aynan shunday tizim quramiz: o'z hujjatlaringiz bo'yicha gaplashadigan, manbali, esi bor chatbot. Bu β kitobning birinchi kapston (yakuniy loyiha) bobi.
Hayotiy o'xshatish. Shu paytgacha biz alohida-alohida g'isht, sement, sim, quvur tayyorlashni o'rgandik. Kapston β shu materiallardan uy qurish. Materiallarni bilish β bir narsa; ulardan turadigan, foydalaniladigan bino yasash β boshqa narsa. Aynan shu "yig'ish" mahorati sizni dasturchidan ilova quruvchiga aylantiradi.
Bu bob avvalgi boblarga tayanadi
Bu yerda 15, 16 va 8-boblardagi kodni qaytadan noldan tushuntirmaymiz β ularni o'qigan deb hisoblaymiz. O'rniga ularni toza, fayllarga ajratilgan loyiha ko'rinishida birlashtiramiz va yangi qism β ko'p-navbatli suhbatni RAG bilan bog'lashni β qo'shamiz. Agar biror qism notanish tuyulsa, tegishli bobga qaytib o'qing.
To'liq tizim: arxitektura¶
Avval butun tizimning "katta rasmini" ko'raylik. RAG chatbot β ikki fazadan iborat (15-bobdan tanish): indekslash (oldindan, bir marta) va chat (har savolda). Chat fazasi esa endi yangi qism β suhbat xotirasini ham o'z ichiga oladi.
Tizim to'rt fayldan iborat. Har biri bitta vazifani bajaradi (bir mas'uliyat β toza kod belgisi):
| Fayl | Vazifa | Qaysi bobdan |
|---|---|---|
config.py |
Sozlama: provayder, model nomlari, yo'llar (.envdan o'qiydi) |
2, 4-bob |
ingest.py |
Indekslash: hujjat -> chunk -> embed -> Chroma | 15-bob |
rag.py |
So'rov: retrieve + prompt + generate + citations | 16-bob |
chat.py |
Suhbat xotirasi + savolni qayta yozish + CLI | 8-bob |
Hayotiy o'xshatish. Bu to'rt fayl β restoranning to'rt xonasi:
config.pyβ menyu va qoidalar (nima ishlatamiz),ingest.pyβ ombor (mahsulotni oldindan tayyorlab qo'yish),rag.pyβ oshxona (buyurtmaga taom pishirish),chat.pyβ zal (mijoz bilan muloqot). Hammasi alohida, lekin birga ishlaydi.
Endi har bir faylni bosqichma-bosqich quramiz.
1-bosqich: config.py β bitta joyda sozlama¶
Yaxshi loyihada barcha sozlama bitta joyda turadi: model nomlari, yo'llar, provayder. Shunda provayderni almashtirish yoki modelni yangilash uchun butun kodni titkilamaysiz β faqat config.pyni o'zgartirasiz.
# config.py β barcha sozlama bir joyda
import os
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv() # .env faylidagi kalitlarni muhitga yuklaydi
# --- Provayder va mijoz ---
# OpenAI-mos: faqat base_url + kalit + model almashtirib boshqasiga o'tasiz (4-bob).
# Standart: OpenAI. Groq/Gemini-mos uchun base_url'ni izohdan oching.
client = OpenAI()
# client = OpenAI(base_url="https://api.groq.com/openai/v1",
# api_key=os.environ["GROQ_API_KEY"]) # bepul, tez
# --- Model nomlari (eslatma: nomlar o'zgaradi β provayder ro'yxatini tekshiring) ---
CHAT_MODEL = "gpt-5.4-mini" # arzon/tez chat modeli
EMBED_MODEL = "text-embedding-3-small" # embedding modeli (indeks bilan bir xil bo'lishi SHART)
# --- Yo'llar va sozlamalar ---
DB_YOL = "./chroma_db" # Chroma doimiy bazasi
KOLLEKSIYA = "bilim_bazasi" # kolleksiya nomi
HUJJATLAR_YOL = "./hujjatlar" # indekslanadigan hujjatlar papkasi
CHUNK_OLCHAM = 800 # chunk hajmi (belgi) β 15-bob
CHUNK_OVERLAP = 100 # ustma-ustlik (belgi) β 15-bob
TOP_K = 4 # retrieve'da nechta chunk β 16-bob
Sozlamani markazlashtirish β production odati
config.py β kitobdagi "ko'p-provayderli" g'oyaning amaliy tayanchi. Bugun OpenAI'da ishlab, ertaga arzonroq Groq'ga yoki lokal Ollama'ga (21-bob) o'tmoqchi bo'lsangiz β faqat client va ikki model nomini o'zgartirasiz. Qolgan uchta fayl configdan import qiladi va umuman o'zgarmaydi. Embedding modelini esa o'zgartirsangiz β bazani qayta indekslashingiz kerak (16-bob: savol va chunk bir xil model bilan embed bo'lishi shart).
2-bosqich: ingest.py β indekslash quvuri (15-bob)¶
Birinchi faza β indekslash. hujjatlar/ papkasidagi har bir faylni yuklab, bo'laklab (chunk), embed qilib, metadata bilan Chroma'ga yozamiz. Bu bir marta (yoki hujjat o'zgarganda) ishlaydi. Kod 15-bobdan tanish β endi configdan sozlamani oladi.
# ingest.py β hujjat -> chunk -> embed -> Chroma (15-bob)
from pathlib import Path
from pypdf import PdfReader
import chromadb
from config import client, EMBED_MODEL, DB_YOL, KOLLEKSIYA
from config import HUJJATLAR_YOL, CHUNK_OLCHAM, CHUNK_OVERLAP
def hujjat_yukla(yol: Path) -> str:
"""Formatga qarab matn ajratadi: .pdf uchun pypdf, qolganlar to'g'ridan-to'g'ri."""
if yol.suffix.lower() == ".pdf":
reader = PdfReader(str(yol))
return "\n".join((s.extract_text() or "") for s in reader.pages)
return yol.read_text(encoding="utf-8") # .txt, .md
def chunk_qil(matn: str, olcham=CHUNK_OLCHAM, overlap=CHUNK_OVERLAP) -> list[str]:
"""Matnni overlap bilan belgi-bo'laklarga ajratadi (15-bob)."""
boklar, boshi = [], 0
while boshi < len(matn):
bolak = matn[boshi:boshi + olcham].strip()
if bolak:
boklar.append(bolak)
boshi += olcham - overlap # keyingi bo'lak overlap qadar orqaga suriladi
return boklar
def embed_qil(matnlar: list[str]) -> list[list[float]]:
"""Matnlar ro'yxatini vektorlarga aylantiradi (bitta so'rovda)."""
javob = client.embeddings.create(model=EMBED_MODEL, input=matnlar)
return [d.embedding for d in javob.data]
def indeksla():
"""hujjatlar/ papkasini to'liq indekslab, Chroma'ga doimiy saqlaydi."""
db = chromadb.PersistentClient(path=DB_YOL)
try:
db.delete_collection(KOLLEKSIYA) # toza boshlash
except Exception:
pass
kol = db.create_collection(KOLLEKSIYA)
papka = Path(HUJJATLAR_YOL)
fayllar = [p for p in papka.iterdir()
if p.suffix.lower() in {".txt", ".md", ".pdf"}]
if not fayllar:
print(f"Diqqat: {HUJJATLAR_YOL} papkasida hujjat topilmadi.")
return
jami = 0
for fayl in fayllar:
boklar = chunk_qil(hujjat_yukla(fayl))
if not boklar:
continue
vektorlar = embed_qil(boklar)
kol.add(
ids=[f"{fayl.name}-{i}" for i in range(len(boklar))],
embeddings=vektorlar,
documents=boklar, # asl matn β modelga beriladi
metadatas=[{"manba": fayl.name, "chunk_raqami": i} # citation uchun
for i in range(len(boklar))],
)
jami += len(boklar)
print(f" {fayl.name}: {len(boklar)} ta chunk")
print(f"\nTayyor: {len(fayllar)} ta fayl, {jami} ta chunk indekslandi -> {DB_YOL}")
if __name__ == "__main__":
indeksla()
Endi python ingest.py ishga tushirsangiz, hujjatlar/ papkasidagi hamma fayl ./chroma_db ichiga doimiy saqlanadi. Bu β chatbotning "bilim bazasi".
Indekslash β pul/token sarflaydi
Har chunkni embed qilish embedding modeliga so'rov yuboradi β bu token, demak (bulutli provayderda) pul (22-bob). Shuning uchun indekslashni PersistentClient bilan bir marta qildik va diskka saqladik β har chatda qaytadan hisoblamaymiz. Katta hujjat to'plamida embed so'rovlarini paketlarga (har ~100 chunk) bo'lish kerak (15-bobdagi maslahat).
3-bosqich: rag.py β retrieve + generate + citations (16-bob)¶
Ikkinchi faza β so'rov. Bu fayl 16-bobning yuragi: savolni olib, bazadan top-k chunkni topadi (retrieve), promptni yig'adi (augment), modeldan faqat kontekstga tayanib javob oladi (generate) va manbalarni qaytaradi (citations).
# rag.py β retrieve + augment + generate + citations (16-bob)
import chromadb
from chromadb.utils import embedding_functions
from config import client, CHAT_MODEL, EMBED_MODEL
from config import DB_YOL, KOLLEKSIYA, TOP_K
import os
# Bazaga ulanish. Embedding funksiyasi indekslashdagi bilan AYNAN bir xil model.
_db = chromadb.PersistentClient(path=DB_YOL)
_embed_fn = embedding_functions.OpenAIEmbeddingFunction(
api_key=os.environ["OPENAI_API_KEY"],
model_name=EMBED_MODEL,
)
_kol = _db.get_collection(name=KOLLEKSIYA, embedding_function=_embed_fn)
def chunklarni_top(savol: str, top_k: int = TOP_K) -> list[dict]:
"""Savolga eng yaqin top_k chunkni matn + metadata bilan qaytaradi."""
natija = _kol.query(
query_texts=[savol],
n_results=top_k,
include=["documents", "metadatas", "distances"],
)
chunklar = []
for matn, meta, masofa in zip(
natija["documents"][0], natija["metadatas"][0], natija["distances"][0]
):
chunklar.append({"matn": matn, "meta": meta, "masofa": masofa})
return chunklar
TIZIM_KORSATMA = (
"Sen kompaniya hujjatlari bo'yicha yordamchi savol-javob tizimisan.\n"
"QOIDALAR:\n"
"1. FAQAT quyida berilgan KONTEKSTGA tayanib javob ber.\n"
"2. Agar javob kontekstda bo'lmasa, o'zingdan TO'QIMA β "
"'Bu savolga hujjatlarda javob topilmadi' deb ayt.\n"
"3. Javobing oxirida foydalangan manbalarni [1], [2] ko'rinishida ko'rsat.\n"
"4. Qisqa, aniq va o'zbek tilida javob ber."
)
def _kontekst_yig(chunklar: list[dict]) -> str:
"""Chunklarni raqamlangan, manbali matnga aylantiradi (citation uchun)."""
qismlar = []
for i, c in enumerate(chunklar, start=1):
manba = c["meta"].get("manba", "noma'lum")
qismlar.append(f"[{i}] (manba: {manba})\n{c['matn']}")
return "\n\n".join(qismlar)
def _manbalar_royxati(chunklar: list[dict]) -> str:
"""Foydalanuvchiga ko'rsatish uchun [n] -> manba jadvali."""
qatorlar = []
for i, c in enumerate(chunklar, start=1):
manba = c["meta"].get("manba", "noma'lum")
qatorlar.append(f"[{i}] {manba}")
return "\n".join(qatorlar)
def rag_javob(savol: str, top_k: int = TOP_K) -> dict:
"""Bitta savolga RAG orqali javob: retrieve -> augment -> generate -> citation."""
chunklar = chunklarni_top(savol, top_k=top_k)
if not chunklar:
return {"javob": "Bu savolga hujjatlarda javob topilmadi.",
"manbalar": "", "chunklar": []}
foydalanuvchi_xabari = (
f"KONTEKST:\n{_kontekst_yig(chunklar)}\n\n"
f"SAVOL: {savol}\n\nYuqoridagi kontekstga tayanib javob ber."
)
resp = client.chat.completions.create(
model=CHAT_MODEL,
messages=[
{"role": "system", "content": TIZIM_KORSATMA},
{"role": "user", "content": foydalanuvchi_xabari},
],
temperature=0.2, # past harorat β faktlarga sodiq (6-bob)
)
return {
"javob": resp.choices[0].message.content,
"manbalar": _manbalar_royxati(chunklar),
"chunklar": chunklar,
}
Bu fayl 16-bobdagi rag_javob bilan deyarli bir xil β lekin endi u bitta savolga javob beradi va suhbat tarixini bilmaydi. Suhbatni keyingi bosqichda chat.pyda qo'shamiz.
Nega rag.py tarixni bilmaydi?
Har fayl bitta vazifa bajarsin. rag.pyning ishi β "berilgan savolga hujjatdan javob top". Tarix, qaysi foydalanuvchi, qachon β bu chat.pyning ishi. Bu ajratish (separation of concerns) kodni soddalashtiradi: rag_javobni alohida sinash, qayta ishlatish (masalan, FastAPI'da) oson bo'ladi.
4-bosqich: suhbat xotirasi va savolni qayta yozish (8-bob)¶
Mana eng qiziq yangi qism. Hozirgi rag_javob bir martalik savolga ishlaydi. Lekin haqiqiy chatbot β ko'p navbatli. Muammoni ko'raylik:
Siz: Ta'til yiliga necha kun?
Bot: Yiliga 21 ish kuni beriladi [1].
Siz: Uni qanday rasmiylashtiraman? <- "uni" nima? Tarixsiz tushunarsiz!
Ikkinchi savol β "Uni qanday rasmiylashtiraman?" β o'zicha to'liq emas. "Uni" so'zi oldingi navbatdagi "ta'til"ga ishora qiladi. Agar shu savolni to'g'ridan-to'g'ri vektor bazaga bersak, qidiruv chalkashadi β chunki "uni qanday rasmiylashtiraman" hech qaysi chunkga aniq mos kelmaydi.
Yechim (8-bobdagi g'oyaga tayanadi): savolni vektor bazaga berishdan oldin, tarix yordamida standalone (mustaqil, to'liq) shaklga qayta yozamiz. Ya'ni LLM'dan so'raymiz: "shu suhbat va yangi savolga qarab, savolni o'zicha tushunarli qilib qayta yoz".
"Uni qanday rasmiylashtiraman?" + tarix -> qayta yozish -> "Ta'tilni qanday rasmiylashtiraman?"
^ endi bu savol o'zicha to'liq, retrieve to'g'ri ishlaydi
Hayotiy o'xshatish. Suhbatda biz "u", "uni", "o'sha" kabi so'zlarni ishlatamiz β chunki suhbatdoshimiz kontekstni eslaydi. Lekin kutubxonachiga (vektor baza) borib "uni topib bering" desangiz, u tushunmaydi β to'liq nomini aytishingiz kerak. Savolni qayta yozish β aynan suhbat tilidan "kutubxona tiliga" tarjima.
Qayta yozish funksiyasi (LLM'ga arzon chaqiruv):
def savolni_qayta_yoz(savol: str, tarix: list[dict]) -> str:
"""Tarix yordamida savolni o'zicha tushunarli (standalone) qilib qayta yozadi.
Tarix bo'sh bo'lsa, savolni o'zgartirmay qaytaradi."""
if not tarix:
return savol
# Faqat oxirgi bir-ikki navbatni berish kifoya (arzon, kontekst yetarli)
oxirgi = tarix[-4:]
suhbat_matni = "\n".join(f"{m['role']}: {m['content']}" for m in oxirgi)
prompt = (
"Quyidagi suhbat va yangi savol berilgan. Yangi savolni suhbatdan "
"mustaqil (standalone) β o'zicha to'liq tushunarli qilib qayta yoz. "
"'u', 'uni', 'o'sha' kabi ishoralarni aniq nom bilan almashtir. "
"Faqat qayta yozilgan savolni qaytar, boshqa hech narsa yozma.\n\n"
f"SUHBAT:\n{suhbat_matni}\n\nYANGI SAVOL: {savol}"
)
resp = client.chat.completions.create(
model=CHAT_MODEL,
messages=[{"role": "user", "content": prompt}],
temperature=0.0, # aniq, barqaror qayta yozish
)
return resp.choices[0].message.content.strip()
Birinchi savol uchun qayta yozish shart emas
if not tarix: return savol β birinchi savolda tarix bo'sh, savol allaqachon to'liq. Shunda keraksiz LLM chaqiruvini (pul + vaqt) tejaymiz. Bu kichik optimallashtirish ko'p-navbatli ilovada sezilarli farq qiladi.
5-bosqich: chat.py β hammasini birlashtirgan CLI¶
Endi to'rt qismni birlashtiramiz: suhbat xotirasi (8-bob) + savolni qayta yozish + RAG javob (16-bob), ustiga oddiy CLI interfeys. Bu β to'liq ishlaydigan chatbot.
# chat.py β suhbat xotirasi + savolni qayta yozish + RAG + CLI
import json
from pathlib import Path
from config import client, CHAT_MODEL
from rag import rag_javob
def savolni_qayta_yoz(savol: str, tarix: list[dict]) -> str:
if not tarix:
return savol
suhbat_matni = "\n".join(f"{m['role']}: {m['content']}" for m in tarix[-4:])
prompt = (
"Quyidagi suhbat va yangi savol berilgan. Yangi savolni suhbatdan "
"mustaqil (standalone) β o'zicha to'liq tushunarli qilib qayta yoz. "
"'u', 'uni', 'o'sha' kabi ishoralarni aniq nom bilan almashtir. "
"Faqat qayta yozilgan savolni qaytar.\n\n"
f"SUHBAT:\n{suhbat_matni}\n\nYANGI SAVOL: {savol}"
)
resp = client.chat.completions.create(
model=CHAT_MODEL,
messages=[{"role": "user", "content": prompt}],
temperature=0.0,
)
return resp.choices[0].message.content.strip()
class RAGChatbot:
"""O'z hujjatlari bo'yicha gaplashadigan, esi bor, manbali chatbot."""
def __init__(self, tarix_fayl: str = "suhbat.json"):
self.tarix_fayl = tarix_fayl
self.tarix: list[dict] = [] # user/assistant xabarlari (8-bob)
self._yukla()
def _yukla(self):
"""Avvalgi suhbatni diskdan tiklaydi (8-bob: davomiylik)."""
if Path(self.tarix_fayl).exists():
self.tarix = json.loads(Path(self.tarix_fayl).read_text(encoding="utf-8"))
print(f"(avvalgi suhbat tiklandi β {len(self.tarix)} xabar)")
def saqla(self):
Path(self.tarix_fayl).write_text(
json.dumps(self.tarix, ensure_ascii=False, indent=2), encoding="utf-8")
def sora(self, savol: str) -> dict:
# 1. Savolni tarix bilan standalone qilamiz (retrieve to'g'ri ishlashi uchun)
standalone = savolni_qayta_yoz(savol, self.tarix)
# 2. RAG: standalone savol bo'yicha hujjatdan javob (16-bob)
natija = rag_javob(standalone)
# 3. Tarixga ASL savol va javobni qo'shamiz (keyingi navbat uchun)
self.tarix.append({"role": "user", "content": savol})
self.tarix.append({"role": "assistant", "content": natija["javob"]})
natija["standalone"] = standalone # debug uchun foydali
return natija
def main():
bot = RAGChatbot()
print("RAG chatbot. Savolingizni yozing. Chiqish: 'chiqish'.\n")
while True:
savol = input("Siz: ").strip()
if savol.lower() in ("chiqish", "exit", "quit", ""):
bot.saqla()
print("Suhbat saqlandi. Xayr!")
break
try:
natija = bot.sora(savol)
except Exception as e: # ishonchlilik (23-bob) β qisqa himoya
print(f"Xato yuz berdi: {e}\nQaytadan urinib ko'ring.\n")
continue
print(f"\nBot: {natija['javob']}")
if natija["manbalar"]:
print(f"\nManbalar:\n{natija['manbalar']}")
print()
if __name__ == "__main__":
main()
Mana butun tizim ishlayapti. E'tibor bering, sora() ichida uch qadam aniq ko'rinadi: savolni standalone qilish -> RAG javob -> tarixga qo'shish. Tarixga biz asl savolni ("Uni qanday rasmiylashtiraman?") qo'shamiz, chunki keyingi qayta yozishda tabiiy suhbat oqimi kerak; lekin retrieve'ga standaloneni beramiz.
Nega tarixga RAG kontekstini qo'shmaymiz?
Tarixga faqat savol va javob qo'shildi β topilgan chunklar (kontekst) emas. Sababi: kontekst har savolda qaytadan retrieve qilinadi (bazadan eng yangi, eng mosini). Eski kontekstni tarixda saqlash β token isrofi (8-bob) va eskirgan ma'lumot xavfi. Suhbat xotirasi faqat savol-javob oqimini eslaydi, hujjat kontekstini emas.
Xarajat va xato boshqaruvini ulash (22 va 23-bob)¶
Tizim ishlayapti β lekin production'ga chiqarishdan oldin ikki narsani eslash kerak. Ularni bu yerda qisqa ulaymiz (chuqur o'rganish β 22 va 23-bobda).
Xarajat (22-bob). Har savol endi kamida ikki LLM chaqiruvi: (1) savolni qayta yozish, (2) RAG javob β ustiga embedding so'rovi (retrieve). Bu sezilarli bo'lishi mumkin. Kamaytirish yo'llari:
- Birinchi savolda qayta yozishni o'tkazib yuborish (yuqorida qildik β tarix bo'sh bo'lsa).
- Arzon model qayta yozish uchun (
gpt-5.4-minikabi) β qayta yozish oddiy vazifa, kuchli model shart emas. - Embedding keshi (22-bob): bir xil savol qayta kelsa, embeddingni qayta hisoblamang.
Xato boshqaruvi (23-bob). Tarmoq uziladi, provayder RateLimitError qaytaradi, kalit eskiradi. CLI'da yuqorida try/except bilan eng oddiy himoyani qo'ydik. Production'da esa retry + backoff (23-bob) kerak:
import time, openai
def ishonchli_javob(savol: str, urinish=3) -> dict:
"""rag_javob'ni retry + backoff bilan o'raydi (23-bob)."""
for i in range(urinish):
try:
return rag_javob(savol)
except openai.RateLimitError:
kut = 2 ** i # eksponensial backoff: 1s, 2s, 4s
print(f"Limit. {kut}s kutamiz...")
time.sleep(kut)
return {"javob": "Hozir xizmat band. Birozdan keyin urinib ko'ring.",
"manbalar": "", "chunklar": []}
Production = kapston + 22-23-24-bob
Bu kapston tizim mantig'ini to'liq ko'rsatadi. Lekin haqiqiy foydalanuvchiga chiqarishdan oldin: xarajat kuzatuvi va kesh (22-bob), retry/backoff/fallback (23-bob) va prompt injection'dan himoya (24-bob β hujjat ichidagi yashirin buyruqlar) shart. Kapston β poydevor; bu boblar β uni mustahkamlovchi armatura.
Interfeys: CLI o'rniga FastAPI /chat (26-bob)¶
Yuqorida CLI (while True: input()) interfeysini to'liq qurdik β lokal sinov va o'rganish uchun ideal. Lekin chatbotni veb yoki mobil ilovaga ulash uchun unga HTTP API kerak. 26-bobda ko'rgan FastAPI bilan buni oson qilamiz β butun mantiq tayyor, faqat chat.pydagi RAGChatbotni endpointga o'raymiz:
# app.py β FastAPI interfeys (26-bobga tayanadi); CLI o'rniga HTTP
from fastapi import FastAPI
from pydantic import BaseModel
from chat import RAGChatbot
app = FastAPI(title="RAG chatbot")
# Soddalik uchun: har sessiya_id ga alohida bot. Production'da bazaga saqlang.
_botlar: dict[str, RAGChatbot] = {}
class Sorov(BaseModel):
sessiya_id: str
savol: str
@app.post("/chat")
def chat(s: Sorov):
bot = _botlar.setdefault(s.sessiya_id, RAGChatbot(f"suhbat_{s.sessiya_id}.json"))
natija = bot.sora(s.savol)
bot.saqla()
return {"javob": natija["javob"], "manbalar": natija["manbalar"]}
Ishga tushirish: uvicorn app:app --reload, so'ng POST /chat ga {"sessiya_id": "olim", "savol": "..."} yuborasiz. CLI va FastAPI β bir xil yadro (RAGChatbot), faqat boshqa "eshik". Bu β toza arxitekturaning mevasi.
Bitta yadro, ko'p interfeys
E'tibor bering: app.py chat.pydagi RAGChatbotni shunchaki import qiladi va ishlatadi β biror RAG mantig'ini takrorlamaydi. Yadro (config, ingest, rag, chat) bir marta yozilgan; CLI va FastAPI β uning ikki "yuzi". Telegram bot, Slack bot yoki veb-chat ham xuddi shu yadroga ulanadi. Mana shuning uchun loyihani fayllarga ajratdik.
Foydalanish ko'rsatmasi (boshidan oxirigacha)¶
Butun loyihani noldan ishga tushirish β qadamma-qadam:
1. Kutubxonalarni o'rnatish.
python -m venv .venv
.venv\Scripts\Activate.ps1 # Windows; macOS/Linux: source .venv/bin/activate
pip install openai python-dotenv chromadb pypdf fastapi uvicorn
2. Kalitni sozlash β .env faylini yarating (2-bob; .gitignorega .env qo'shing):
3. Hujjatlarni joylashtirish β hujjatlar/ papkasini yarating va unga .md, .txt yoki .pdf fayllaringizni soling (masalan, kompaniya qo'llanmasi, FAQ).
4. Indekslash (bir marta):
python ingest.py
# -> qollanma.md: 12 ta chunk
# -> Tayyor: 1 ta fayl, 12 ta chunk indekslandi -> ./chroma_db
5. Suhbatlashish (CLI):
python chat.py
# Siz: Ta'til yiliga necha kun?
# Bot: Yiliga 21 ish kuni beriladi [1].
# Manbalar: [1] qollanma.md
# Siz: Uni qanday rasmiylashtiraman? <- tarix bilan tushunadi!
6. (Ixtiyoriy) FastAPI interfeysi: uvicorn app:app --reload, so'ng http://localhost:8000/docs.
Hayotiy o'xshatish. Bu olti qadam β uyga ko'chib o'tish kabi: avval asboblar (1-2), keyin mebel keltirish (3 β hujjatlar), joylashtirish (4 β indekslash), va nihoyat yashashni boshlash (5-6 β suhbat). Bir marta joylashtirgach (indekslash), har kuni qaytadan emas, to'g'ridan-to'g'ri yashaysiz (chat).
Loyiha tuzilishi
Yakuniy papka shunday ko'rinadi: config.py, ingest.py, rag.py, chat.py, app.py (ixtiyoriy), .env, .gitignore, hujjatlar/ (sizning fayllaringiz) va chroma_db/ (ingest.py yaratadi). Toza, har fayl bir vazifa β buni istalgan loyihaga namuna qilib oling.
Keyingi qadamlar: bu chatbotni qanday yaxshilash mumkin¶
Bu β ishlaydigan, lekin boshlang'ich tizim. Uni professional darajaga ko'tarish g'oyalari (ko'pi kitobda ko'rilgan):
- Rerank (17-bob) β retrieve qilingan chunklarni qo'shimcha model bilan qayta saralash; eng tegishlilari yuqoriga chiqadi, sifat oshadi.
- Masofa chegarasi (threshold) (16-17-bob) β juda uzoq (tegishsiz) chunklarni umuman olmaslik; shovqin va xarajat kamayadi.
- Streaming javob (7-bob) β javobni token-token chiqarish; foydalanuvchi kutmaydi, jonli his qiladi.
- Veb UI β FastAPI ustiga oddiy HTML/JS chat oynasi yoki Streamlit/Gradio bilan tezkor interfeys.
- Ko'proq format (15-bob) β
.docx, HTML, veb-sahifa yuklash; har formatga mos yuklagich qo'shish. - Inkremental indekslash (15-bob mashqi) β faqat o'zgargan faylni qayta indekslash (xesh yoki vaqt bo'yicha).
- Kuzatuv va baholash (17, 25-bob) β javob sifatini o'lchash, qaysi savollarga "topilmadi" deyilganini logga yozish.
- Multimodal (12-bob) β rasm yoki jadval ichidagi ma'lumotni ham indekslash.
O'zingizni sinab ko'ring
Bu chatbotni o'z hujjatlaringiz (masalan, universitet qo'llanmasi yoki shaxsiy yozuvlaringiz) bilan ishga tushiring. So'ng ataylab noaniq, ko'p-navbatli savol bering: "Imtihon qachon? / Unga qanday tayyorlanaman? / Birinchi savolimni eslaysizmi?". Tizim qayta yozishni, retrieve'ni va xotirani qanday bog'lashini kuzating. Keyin yuqoridagi yaxshilashlardan birini (masalan, threshold filtri) qo'shib ko'ring.
Xulosa¶
- Kapston = bilimni birlashtirish. Alohida ko'nikmalar (2, 8, 15, 16, 22, 23, 26-bob) bittada ishlaydigan yaxlit tizimga yig'iladi β bu sizni dasturchidan ilova quruvchiga aylantiradi.
- RAG chatbot to'rt faylga ajratiladi, har biri bir vazifa:
config.py(sozlama),ingest.py(indekslash),rag.py(so'rov),chat.py(suhbat + interfeys). Bu toza ajratish kodni sinash, qayta ishlatish va kengaytirishni osonlashtiradi. - Sozlamani markazlashtirish (
config.py) β provayder/modelni bir joyda almashtirish imkonini beradi; qolgan fayllar o'zgarmaydi (ko'p-provayderli g'oyaning amaliy ko'rinishi). - Indekslash bir marta (
PersistentClientbilan diskka), chat har savolda β bu RAG'ning ikki fazasi (15-16-bob). Embedding modeli ikkala fazada bir xil bo'lishi shart. - Ko'p-navbatli suhbatning kaliti β savolni tarix bilan standalone (mustaqil) qilib qayta yozish: "uni" -> "ta'tilni". Aks holda retrieve chalkashadi. Birinchi savolda buni o'tkazib yuborib tejash mumkin.
- Suhbat xotirasiga faqat savol va javob qo'shiladi (kontekst emas) β kontekst har savolda qaytadan retrieve qilinadi; bu token tejaydi va eskirgan ma'lumotdan saqlaydi (8-bob).
- Bitta yadro, ko'p interfeys: CLI va FastAPI bir xil
RAGChatbotni ishlatadi. Production uchun xarajat/kesh (22-bob), retry/backoff (23-bob) va prompt injection himoyasi (24-bob) ulanadi. - Yaxshilash yo'li: rerank, threshold, streaming, veb UI, ko'proq format, inkremental indekslash, baholash.
Amaliy mashqlar¶
-
(Oson) To'rt faylni (
config.py,ingest.py,rag.py,chat.py) tering,hujjatlar/papkasiga 1-2 ta.md/.txtfayl soling,python ingest.pybilan indekslang vapython chat.pybilan bitta savol bering. Manba[1]to'g'ri ko'rsatilganini tekshiring. -
(Oson)
chat.pydasora()ichidaprint("Standalone:", standalone)qo'shing. So'ng ko'p-navbatli suhbat qiling: "X nima? / Uni qanday ishlataman?". Ikkinchi savol qanday qayta yozilganini o'z ko'zingiz bilan ko'ring β "uni" nimaga almashdi? -
(O'rtacha)
savolni_qayta_yozfunksiyasidagiif not tarix: return savolqatorini vaqtincha olib tashlang. Birinchi savolda qo'shimcha LLM chaqiruvi ketishini kuzating (masalan, soddaprintyokitimebilan). Nega bu optimallashtirish ko'p-navbatli ilovada muhim β qisqa izoh yozing. -
(O'rtacha)
rag.pyga masofa chegarasi (threshold) qo'shing:chunklarni_topdan keyinmasofa > 0.6bo'lgan chunklarni olib tashlang. Agar hech bir chunk qolmasa,rag_javobto'g'ridan-to'g'ri "topilmadi" qaytarsin (LLM'ga umuman bormasin). Bu xarajat va shovqinni qanday kamaytirishini izohlang (16-bob 5-mashqiga tayanadi). -
(Qiyin)
app.py(FastAPI) variantini ishga tushiring (uvicorn app:app --reload).http://localhost:8000/docsorqali ikki xilsessiya_idbilan suhbat qiling va har biri alohida tarix saqlashini (suhbat_*.jsonfayllari) tasdiqlang. So'ngishonchli_javob(retry + backoff) funksiyasinirag.pyga qo'shib,chat.pydarag_javobo'rniga ishlatib ko'ring. Bu chatbotni production'ga bir qadam yaqinlashtiradi (23-bob).
β¬ οΈ Oldingi: 26 β Deploy: FastAPI bilan LLM xizmati Β· π Kitob boshi Β· Keyingi: 28 β Kapston II: agent-asoslangan avtomatlashtirish β‘οΈ