Asosiy qismga o'tish

Generation (Javob Yaratish)

9-bob 32 kod bloki 4922 so'z

RAG arxitekturasida G harfi Generation degan ma'noni bildiradi. Retrieval eng yaxshi chunk-larni topdi, heuristik tanlash eng tegishlilarini ajratdi, endi LLM bu chunk-larni o'qib javob yozishi kerak. Bu oxirgi qadam -- foydalanuvchi ko'radigan natija aynan shu yerda yaratiladi.

Lekin "LLM-ga chunk-larni berish va javob so'rash" degani oddiy emas. LLM-ni nazorat qilish, to'g'ri formatda javob olish, hallucination-ni oldini olish, streaming-ni sozlash -- bularning barchasi generation bosqichining vazifasi.


9.1 System Prompt Engineering

System prompt -- LLM uchun "ish ko'rsatmasi". Bu foydalanuvchi ko'rmaydi, lekin LLM har bir javobda unga amal qiladi. Nihol loyihasida 11 ta qoida:

python20 qator
SYSTEM_PROMPT = (
    "You are a strict factual assistant for a company knowledge base. "
    "RULES:\n"
    "1. Answer ONLY using the provided CONTEXT. Never use outside knowledge.\n"
    "2. If the context lacks information, say: 'Информация не найдена в базе знаний.'\n"
    "3. For each project or item found, include: name, client, what was done, "
    "equipment/technologies used, and results if available.\n"
    "4. Respond ONLY in the language the user wrote in. If user writes in Russian, "
    "answer entirely in Russian. If in Uzbek, answer entirely in Uzbek. "
    "If the same project exists in the context in multiple languages, use ONLY the version "
    "matching the user's language. SKIP duplicate projects in other languages entirely.\n"
    "5. NEVER mix Cyrillic and Latin scripts in one response.\n"
    "6. Use ONLY plain text. No markdown: no **, no ##, no *, no - bullets. "
    "Use numbered lists (1. 2. 3.) only. Separate sections with blank lines.\n"
    "7. No filler phrases. No emojis. No offers to help further.\n"
    "8. Do NOT include URLs or source references.\n"
    "9. Do NOT repeat the same information. Each item must be unique.\n"
    "10. Do NOT invent or assume information not present in the context.\n"
    "11. Keep your answer under 15 items maximum."
)

Har bir qoida nima uchun kerak

Qoida 1: Faqat kontekstdan javob ber

Bu eng muhim qoida -- anti-hallucination himoyasi. LLM-lar o'z training data-sidan ma'lumot "to'qishi" mumkin. Masalan, siz Nihol loyihalari haqida so'rasangiz, LLM o'zi bilgan boshqa IT kompaniya ma'lumotlarini aralashtirib yuborishi mumkin. Bu qoida: "Faqat men bergan kontekstdan javob ber, boshqa hech narsadan emas."

Test paytida aniqlangan muammo: bu qoidasiz LLM "Nihol shuningdek bulut xizmatlari ham taklif qiladi" deb yozgan -- kontekstda bunday ma'lumot yo'q edi.

Qoida 2: Ma'lumot yo'q bo'lsa, ayt

Agar kontekstda javob bo'lmasa, LLM "bilmayman" deyishi kerak. Bu qoidasiz LLM umumiy javob to'qiydi. Masalan: "Toshkentda ob-havo qanday?" -- kontekstda ob-havo ma'lumoti yo'q, lekin LLM o'zi javob berishga harakat qiladi.

Qoida 3: Har bir loyiha uchun tafsilot

Tuzilgan javob: ism, mijoz, nima qilingan, texnologiyalar, natijalar. Bu qoidasiz LLM faqat loyiha nomlarini sanab o'tadi, tafsilot bermaydi.

Qoida 4: Foydalanuvchi tilida javob ber

Ruscha savol -- ruscha javob. O'zbekcha savol -- o'zbekcha javob. Kontekstda bir loyiha ikki tilda bo'lishi mumkin -- faqat mos tilini ishlatish kerak.

Test muammosi: foydalanuvchi ruscha yozdi, LLM javobiga o'zbekcha chunk-dan ma'lumot qo'shdi. Natijada aralash, o'qib bo'lmaydigan javob.

Qoida 5: Skriptlarni aralashtirishma

Kirill va lotin alifbosini bitta javobda ishlatish o'qishni qiyinlashtiradi. Bu qoida 4-ni kuchaytiradi.

Qoida 6: Faqat oddiy matn

Lokal modellar (Qwen, LLaMA) ko'pincha markdown formatlashni qo'shadi: **qalin**, ## sarlavha, - ro'yxat. Telegram yoki oddiy matn interfeysi uchun bu yomon ko'rinadi. Faqat raqamli ro'yxat (1. 2. 3.) va bo'sh qatorlar bilan ajratish.

Qoida 7: Bo'sh so'zlar yo'q

"Albatta, sizga yordam berishdan xursandman!" -- bu foydalanuvchiga hech narsa bermaydi. Faqat javobning o'zi kerak.

Qoida 8: URL-lar yo'q

Kontekstda URL-lar bo'lishi mumkin, lekin javobda ularni ko'rsatish kerak emas -- ular eskirgan yoki buzilgan bo'lishi mumkin.

Qoida 9: Takrorlanish yo'q

LLM bir xil ma'lumotni boshqa so'zlar bilan qayta-qayta yozishi mumkin. Ayniqsa lokal modellar buni ko'p qiladi.

Qoida 10: To'qima

1-qoidani kuchaytirish. "To'qima" (hallucinate) qilma -- faqat kontekstda borini yoz.

Qoida 11: Maksimum 15 element

Juda uzun javoblar foydalanuvchini charchatadi. 15 ta element -- yetarli tafsilot, lekin qisqa.


9.2 Context Injection (Kontekstni Kiritish)

Retrieval va chunk tanlashdan keyin qolgan chunk-lar LLM-ga qanday yuboriladi? Nihol loyihasidagi format:

python25 qator
def build_prompt(
    chunks: list[dict],
    question: str,
    history: list[dict] | None = None,
) -> list[dict]:
    messages = [{"role": "system", "content": SYSTEM_PROMPT}]

    if chunks:
        context_parts = []
        for i, chunk in enumerate(chunks, 1):
            context_parts.append(f"[{i}] {chunk['chunk_text']}")
        context = "\n\n".join(context_parts)
        messages.append({"role": "system", "content": f"CONTEXT:\n\n{context}"})
    else:
        messages.append({
            "role": "system",
            "content": "No relevant information found in the knowledge base.",
        })

    if history:
        turns = history[-(MAX_HISTORY_TURNS * 2):]
        messages.extend(turns)

    messages.append({"role": "user", "content": question})
    return messages

Formatning ahamiyati

Chunk-lar [1], [2], [3] kabi raqamlanadi. Nega?

  1. LLM manba ko'rsata oladi: "1-hujjatga ko'ra..." deb yozishi mumkin.
  2. Debug oson: qaysi chunk-dan javob olganini tekshirish mumkin.
  3. Struktura: LLM raqamlangan ro'yxatni yaxshiroq tushunadi.

Chunk-lar \n\n (ikki bo'sh qator) bilan ajratiladi -- bu LLM uchun vizual chegara, har bir chunk alohida hujjat ekanini bildiradi.

Agar chunk-lar topilmasa, maxsus xabar yuboriladi: "No relevant information found in the knowledge base." Bu LLM-ga 2-qoidani qo'llash signali -- "bilmayman" deb javob berishi kerak.

Analogiya: Imtihonda student-ga ruxsat etilgan material beriladi. [1], [2] -- bu sahifa raqamlari. Student faqat shu materialdan javob yozishi kerak, boshqa kitoblardan emas.


9.3 Chat History Management (Suhbat Tarixini Boshqarish)

RAG chatbot bir martalik savol-javob emas. Foydalanuvchi ketma-ket savollar beradi: "Bank loyihalari haqida gapirib bering" -- "Uzpromstroybank haqida batafsil" -- "Qaysi yilda bo'lgan?"

Tarixsiz LLM "Qaysi yilda bo'lgan?" savoliga javob bera olmaydi -- kontekst yo'q.

Sliding Window yondashuvi

python1 qator
MAX_HISTORY_TURNS = 6  # 6 turn = 12 xabar (user + assistant)

Oxirgi 6 ta turn (12 xabar) saqlanadi. Yangi turn qo'shilganda eng eski turn tushib ketadi.

python3 qator
if history:
    turns = history[-(MAX_HISTORY_TURNS * 2):]
    messages.extend(turns)

Nega cheksiz tarix emas?

  1. Context window to'ladi: LLM-ning context window chegarasi bor (masalan 8192 token). Har bir eski xabar joy egallaydi. 20 ta turn = taxminan 4000-6000 token. Bu chunk-lar uchun joy qoldirmaydi.

  2. Eski kontekst shovqinga aylanadi: 10 turn oldingi savol hozirgi savolga aloqasi bo'lmasligi mumkin. LLM uchun bu faqat qo'shimcha shovqin.

  3. Tezlik: ko'proq token = sekinroq inference.

6 turn -- bu taxminan 2-3 daqiqalik suhbat. Ko'p hollarda foydalanuvchi bu vaqt ichida mavzuni tugatadi. Agar uzunroq suhbat kerak bo'lsa, MAX_HISTORY_TURNS qiymatini oshirish mumkin, lekin 10 dan oshirish tavsiya etilmaydi.

Analogiya: Yozuv taxtasida joy cheklangan. Har yangi mavzu yozilganda eng eski yozuv o'chiriladi. Oxirgi 6 ta mavzu doim ko'rinib turadi.


9.4 Streaming va SSE (Server-Sent Events)

Odatiy HTTP so'rov: foydalanuvchi so'rov yuboradi, 10 soniya kutadi, butun javob bir vaqtda keladi. Bu yomon UX -- foydalanuvchi 10 soniya bo'sh ekranga qaraydi.

Streaming bilan: har bir token (so'z qismi) tayyor bo'lishi bilan yuboriladi. Foydalanuvchi javobni real-time ko'radi.

SSE (Server-Sent Events) nima?

HTTP ulanish ochiq qoladi va server ma'lumotni parchalab yuboradi. Har bir parcha formati:

yaml5 qator
data: {"token": "Ucell"}\n\n
data: {"token": " uchun"}\n\n
data: {"token": " billing"}\n\n
data: {"token": " tizimi"}\n\n
data: {"done": true, "sources": [...]}\n\n

Har bir qator data: bilan boshlanadi va \n\n (ikki yangi qator) bilan tugaydi. Bu SSE protokoli standardi.

Nihol loyihasidagi streaming

python37 qator
def stream_response(
    messages: list[dict],
    temperature: float | None = None,
) -> Generator[str, None, None]:
    settings = get_settings()
    temperature = temperature if temperature is not None else settings.RAG_TEMPERATURE

    try:
        resp = httpx.post(
            f"{settings.OLLAMA_URL}/api/chat",
            json={
                "model": settings.CHAT_MODEL,
                "messages": messages,
                "stream": False,
                "think": False,
                "options": {
                    "temperature": temperature,
                    "num_predict": 1536,
                    "repeat_penalty": 1.5,
                    "repeat_last_n": 512,
                },
            },
            timeout=300,
        )
        resp.raise_for_status()
        data = resp.json()["message"]
        raw = data.get("content", "").strip()
        if not raw:
            raw = data.get("thinking", "").strip()
        cleaned = _strip_markdown(raw)
        cleaned = _cut_repetitions(cleaned)
        for chunk in [cleaned[i:i+4] for i in range(0, len(cleaned), 4)]:
            yield chunk
    except httpx.ConnectError:
        yield "[Error: OLLAMA is unavailable]"
    except httpx.ReadTimeout:
        yield "[Error: Response timed out]"

Bu yerda qiziq yondashuv: Ollama-dan butun javob olinadi (stream: False), keyin post-processing qilinadi (markdown olib tashlash, takrorlanishlarni kesish), va shundan keyin 4 belgidan parchalab yuboriladi. Nega shunday?

  1. Post-processing uchun butun matn kerak -- markdown pattern-larni faqat to'liq matnda topish mumkin.
  2. Foydalanuvchiga "streaming effekt" beriladi -- 4 belgidan parcha sifatida.

Route darajasida SSE

python14 qator
def event_stream():
    full_response = []
    for token in stream_response(messages):
        full_response.append(token)
        yield f"data: {json.dumps({'token': token})}\n\n"

    answer = "".join(full_response)
    history.append({"role": "user", "content": question})
    history.append({"role": "assistant", "content": answer})
    _save_history(sid, history)

    yield f"data: {json.dumps({'done': True, 'sources': sources})}\n\n"

return StreamingResponse(event_stream(), media_type="text/event-stream")

Oxirgi event done: true va sources ro'yxatini o'z ichiga oladi -- frontend buning asosida streaming-ni tugatadi va manba ko'rsatadi.


9.5 Post-Processing (Javobni Tozalash)

Lokal modellar (Qwen, LLaMA) ko'pincha system prompt-dagi formatlash qoidalarini e'tiborsiz qoldiradi. Shuning uchun javobni dasturiy ravishda tozalash kerak.

Markdown Stripping

python12 qator
_MD_PATTERNS = [
    (re.compile(r"\*\*\*(.+?)\*\*\*"), r"\1"),   # ***bold italic*** -> text
    (re.compile(r"\*\*(.+?)\*\*"), r"\1"),         # **bold** -> text
    (re.compile(r"\*(.+?)\*"), r"\1"),             # *italic* -> text
    (re.compile(r"__(.+?)__"), r"\1"),             # __underline__ -> text
    (re.compile(r"_(.+?)_"), r"\1"),               # _italic_ -> text
    (re.compile(r"^#{1,6}\s+", re.MULTILINE), ""), # ## heading -> text
    (re.compile(r"^\s*[-*+]\s+", re.MULTILINE), ""), # - bullet -> text
    (re.compile(r"^\s*>\s+", re.MULTILINE), ""),   # > blockquote -> text
    (re.compile(r"`([^`]+)`"), r"\1"),             # `code` -> text
    (re.compile(r"```[\s\S]*?```"), ""),            # code block -> remove
]

Har bir pattern alohida regex. Tartib muhim: *** (uch yulduzcha) ** (ikki yulduzcha) dan oldin tekshiriladi, chunki ** patterni *** ichidagi yulduzchalarni ham ushlashi mumkin.

python4 qator
def _strip_markdown(text_input: str) -> str:
    for pattern, replacement in _MD_PATTERNS:
        text_input = pattern.sub(replacement, text_input)
    return text_input

Takrorlanishni Aniqlash

Lokal modellar eng ko'p uchraydigan muammosi -- bir xil ma'lumotni qayta-qayta yozish. Ba'zan model cheksiz loop-ga tushadi.

python20 qator
def _cut_repetitions(text_input: str) -> str:
    lines = text_input.split("\n")
    seen = set()
    result = []
    repeat_count = 0
    for line in lines:
        stripped = line.strip()
        if not stripped:
            result.append(line)
            continue
        normalized = re.sub(r"\d+\.\s*", "", stripped).strip()
        if normalized in seen:
            repeat_count += 1
            if repeat_count >= 2:
                break
            continue
        seen.add(normalized)
        repeat_count = 0
        result.append(line)
    return "\n".join(result).strip()

Qanday ishlaydi: 1. Har bir qator seen to'plamiga qo'shiladi. 2. Raqamli prefiks (1., 2.) olib tashlanadi -- normalized faqat matn. 3. Agar normalized allaqachon seen da bo'lsa -- bu takrorlanish. 4. 2 ta takrorlanishdan keyin javob kesiladi (break).

Nega 2 ta? Ba'zan model bitta qatorni tasodifan takrorlaydi (1 marta) -- bu qabul qilinadi. Lekin 2 marta takrorlash -- bu loop boshlanish signali.


9.6 Temperature, top_p, max_tokens

Bu uchta parametr LLM javobining "xarakterini" belgilaydi.

Temperature

LLM har bir keyingi token uchun ehtimolliklar ro'yxatini hisoblaydi. Temperature bu ehtimolliklarni qanchalik "keskin" yoki "tekis" qilishini nazorat qiladi.

python10 qator
Keyingi token ehtimolliklari (misol):
  "loyiha"  = 0.6
  "ish"     = 0.2
  "dastur"  = 0.15
  "narsa"   = 0.05

Temperature 0.0: har doim "loyiha" tanlanadi (eng yuqori ehtimollik).
Temperature 0.1: deyarli har doim "loyiha", juda kamdan-kam "ish".
Temperature 1.0: ehtimolliklarga proporsional tanlanadi -- "loyiha" 60%, "ish" 20%.
Temperature 2.0: deyarli teng ehtimollik -- har qanday so'z chiqishi mumkin.

RAG uchun temperature = 0.1 optimal. Nega 0.0 emas? Chunki 0.0 da ba'zan model "stuck" bo'ladi -- bitta javob shabloniga tushib qoladi. 0.1 ozgina variatsiya beradi.

top_p (Nucleus Sampling)

Temperature-ga alternativ yondashuv. top_p = 0.9 degani: ehtimolliklari yig'indisi 90% ga yetguncha bo'lgan token-lar orasidan tanlash.

python7 qator
Token ehtimolliklari (tartiblangan):
  "loyiha"  = 0.6   -> yig'indi: 0.6  (90% dan kam, kiritiladi)
  "ish"     = 0.2   -> yig'indi: 0.8  (90% dan kam, kiritiladi)
  "dastur"  = 0.15  -> yig'indi: 0.95 (90% dan oshdi, KIRITILMAYDI)
  "narsa"   = 0.05  -> kiritilmaydi

top_p = 0.9 da faqat "loyiha" va "ish" orasidan tanlanadi.

Temperature va top_p birga ishlatilmaydi -- odatda bittasini tanlang. Nihol loyihasida biz temperature = 0.1 ishlatamiz.

max_tokens

Javobning maksimal uzunligi (token-larda). 1 token taxminan 4 belgi (ingliz tilida) yoki 1-2 belgi (kirill).

matn1 qator
"num_predict": 1536,  # Nihol loyihasida

Nihol loyihasida num_predict = 1536 (Ollama terminologiyasida max_tokens ga teng). Bu taxminan 500-700 so'zlik javob.

RAG uchun tavsiya: - Qisqa javoblar (FAQ): max_tokens = 512 - O'rtacha javoblar (loyiha tavsifi): max_tokens = 1024 - Uzun javoblar (batafsil hisobot): max_tokens = 2048 - 4096 dan oshirish kamdan-kam kerak -- juda uzun javoblar sifatsiz bo'ladi.

repeat_penalty va repeat_last_n

Qo'shimcha parametrlar takrorlanishni nazorat qiladi:

matn2 qator
"repeat_penalty": 1.5,    # Takrorlangan token-lar uchun jazo
"repeat_last_n": 512,     # Oxirgi 512 token ichida tekshirish

repeat_penalty = 1.5 degani: agar token oxirgi 512 token ichida allaqachon ishlatilgan bo'lsa, uning ehtimolligi 1.5 ga bo'linadi. Bu model-ni yangi so'zlar ishlatishga undaydi.


9.7 Prompt Anti-Patterns (Noto'g'ri Yondashuvlar)

1. Juda umumiy system prompt

matn2 qator
# YOMON
"You are a helpful assistant. Answer questions."

Bu LLM-ga hech qanday chegara qo'ymaydi. LLM o'zi bilgan narsani ham aralashtirib javob beradi.

matn2 qator
# YAXSHI
"Answer ONLY using the provided CONTEXT. Never use outside knowledge."

2. Juda ko'p qoidalar

matn2 qator
# YOMON -- 30 ta qoida
"RULES:\n1. ... \n2. ... \n... \n30. ..."

Kichik modellar (7B-9B parametrli) 10 dan ortiq qoidani barqaror bajara olmaydi. Eng muhim qoidalarni tanlang. Nihol loyihasida 11 ta qoida -- bu chegarada. 9B model ba'zan 6-qoidani (markdown yo'q) buzadi, shuning uchun post-processing kerak.

3. Anti-hallucination qoidasi yo'q

matn2 qator
# YOMON -- hallucination himoyasi yo'q
"Answer the user's question based on the context."

"Based on" yetarli emas -- LLM buni "kontekstdan boshlang, keyin o'zingiz qo'shing" deb tushunishi mumkin.

matn2 qator
# YAXSHI
"Answer ONLY using the provided CONTEXT. Do NOT invent or assume information not present in the context."

"ONLY" va "Do NOT invent" -- bu aniq taqiq.

4. Qoidalar user message ichida

python4 qator
# YOMON
messages = [
    {"role": "user", "content": "RULES: ... \n\nQuestion: ..."}
]

System message LLM uchun "eganing ko'rsatmasi". User message "foydalanuvchining so'rovi". Qoidalar system message-da bo'lsa -- LLM ularni ko'rsatma sifatida qabul qiladi. User message-da bo'lsa -- LLM ularni foydalanuvchi so'rovi sifatida ko'radi va e'tiborsiz qoldirishi mumkin.

python5 qator
# YAXSHI
messages = [
    {"role": "system", "content": "RULES: ..."},
    {"role": "user", "content": "Question: ..."}
]

9.8 To'liq Generation Pipeline

Nihol loyihasidagi chat endpoint butun jarayonni birlashtiradi:

python47 qator
@router.post("/api/chat")
def chat_send(
    body: ChatMessage,
    request: Request,
    session: Session = Depends(get_session),
):
    question = body.message.strip()

    # 1. Til aniqlash
    lang_code = detect_lang(question)

    # 2. So'rovni kengaytirish (admin paneldagi QUERY_EXPANSION=Y bo'lsa)
    search_text = question
    if get_sys_param("QUERY_EXPANSION", str, "N") == "Y":
        expanded_query = expand_query(question)
        if expanded_query:
            search_text = expanded_query

    # 3. Embedding + Retrieval (sof pgvector, til va rubrika filtrlari)
    embedding = generate_query_embedding(search_text)
    chunks = retrieve_chunks(
        session, embedding,
        top_k=rag["top_k"], threshold=rag["similarity_threshold"],
        lang_code=lang_code, rubrics=effective_rubrics,
    )

    # 4. Heuristik tanlash: top-3 + kalit so'z mosliklari, max 8
    used = select_context_chunks(chunks, question)

    # 5. Prompt yaratish (system prompt + context + savol)
    messages = build_prompt(chunks, question, None)

    # 6. Streaming javob
    def event_stream():
        full_response = []
        for token in stream_response(messages):
            full_response.append(token)
            yield f"data: {json.dumps({'token': token})}\n\n"

        answer = "".join(full_response)
        history.append({"role": "user", "content": question})
        history.append({"role": "assistant", "content": answer})
        _save_history(sid, history)

        yield f"data: {json.dumps({'done': True, 'sources': sources})}\n\n"

    return StreamingResponse(event_stream(), media_type="text/event-stream")

Har bir qadam: 1. Til aniqlash -- kirill/lotin harflar soni bo'yicha. 2. Query expansion -- LLM so'rovni kengaytiradi (admin paneldan yoqib-o'chirish mumkin: QUERY_EXPANSION parametri). 3. Retrieval -- bitta embedding, pgvector cosine search, til + rubrika filtrlari. 15 tagacha chunk. 4. Heuristik tanlash -- select_context_chunks: top-3 + kalit so'z mosliklari, jami maksimum 8 chunk. 5. Prompt yaratish -- system prompt + kontekst + savol. 6. Streaming -- post-processing (markdown + takrorlanish) + 4 belgidan parchalab yuborish.

Bu pipeline 3-5 soniyada javob boshlanishini ta'minlaydi. Foydalanuvchi token-larni real-time ko'radi. Tarix saqlanadi, keyingi savollar kontekstda davom etadi.

Xulosa: Generation bosqichida eng muhim narsa -- LLM-ni nazorat qilish. System prompt qoidalari, post-processing, va parametrlar sozlash -- bularning barchasi birgalikda sifatli, ishonchli javob beradi. Lekin eslang: agar retrieval yomon chunk-lar qaytarsa, hech qanday prompt engineering buni tuzatolmaydi. Generation faqat yaxshi retrieval ustiga quriladi.


Model tanlash — o'lchang, taxmin qilmang

"Qaysi modelni tanlay?" degan savolga umumiy javob yo'q. Lekin o'lchash usuli bor.

Quyidagi natijalar haqiqiy sinovdan — o'zbek va rus tilidagi aralash kontekst, 9 ta savol, RTX GPU, Ollama:

Model To'g'ri til To'g'ri mazmun tok/s O'rtacha vaqt
qwen3:8b 9/9 9/9 136 0.9 s
gemma3:4b 6/9 6/9 173 1.8 s
qwen2.5:7b — narxlarni tushirib qoldirdi — —

Uchta xulosa chiqadi.

1. Kichik model — kam ishonch

gemma3:4b uchdan bir hollarda xato qildi: yo noto'g'ri tilda javob berdi, yo narxlarni tushirib qoldirdi. Bilim bazasi boti uchun bu "tezlik evaziga sifat" emas — bu buzuq mahsulot.

Sabab: o'zbek tili kam resursli. 4B parametrli modelda bu birinchi bo'lib yo'qoladi.

2. tok/s — bu KECHIKISH EMAS

Diqqat bilan qarang: gemma3:4b tezroq token yozadi (173 va 136), lekin javobni kechroq tugatadi (1.8 s va 0.9 s).

Nega? Chunki u bir xil gapni aytish uchun ko'proq token sarflaydi. Chalkashadi, takrorlaydi, uzoq yozadi.

Foydalanuvchi tok/s ni sezmaydi — u JAVOB KELISHINI kutadi. Shuning uchun o'lchov birligi — javob vaqti, token tezligi emas.

3. Qanday o'lchash kerak

Marketing raqamlariga ishonmang. O'z ma'lumotingizda sinang:

python24 qator
import re, httpx, time

SAVOLLAR = [
    ("Tarif qancha turadi?", "uz", ["250 000", "450 000"]),
    ("Сколько стоит тариф?", "ru", ["250 000", "450 000"]),
]

kir = lambda t: len(re.findall(r"[а-яА-Я]", t))
lot = lambda t: len(re.findall(r"[a-zA-Z]", t))

for model in ("qwen3:8b", "gemma3:4b"):
    til_ok = mazmun_ok = 0
    for savol, til, kutilgan in SAVOLLAR:
        r = httpx.post("http://localhost:11434/api/chat", json={
            "model": model,
            "messages": [{"role": "user", "content": f"{KONTEKST}\n\n{savol}"}],
            "stream": False,
            "options": {"temperature": 0.1, "num_predict": 400},
        }, timeout=180)
        j = r.json()["message"]["content"]
        k, l = kir(j), lot(j)
        til_ok += (l > k * 3) if til == "uz" else (k > l * 3)
        mazmun_ok += all(x in j for x in kutilgan)
    print(f"{model}: til {til_ok}, mazmun {mazmun_ok}")

Ikki mezon alohida o'lchanadi: javob to'g'ri TILDAmi, va javobda to'g'ri MA'LUMOT bormi. Model to'g'ri tilda noto'g'ri javob berishi mumkin.

Xotira hisobi

Model tanlashda VRAM ham chegara:

Konfiguratsiya Hajm
bge-m3 (embedding) 1.2 GB
qwen3:8b Q4 5.2 GB
jami 6.4 GB

8 GB videokartaga sig'adi, kontekst uchun 1.6 GB qoladi. gemma4:8b (9.6 GB) esa embedding bilan birga sig'maydi.

GPU umuman bo'lmasa ham ishlaydi. O'lchangan (faqat CPU):

CPU GPU
bge-m3 embedding 662 ms 245 ms
gemma3:4b javob 5.1 s 1.0 s
qwen3:8b javob 7.1 s 1.2 s

Embedding uchun CPU mutlaqo yetarli: 14 ta chunk 9 sekundda, 500 ta chunk 5 daqiqada tayyor bo'ladi. Va bu bir marta qilinadi.


Promptga NAMUNA JUMLA yozmang

Bu — eng qimmatga tushadigan va eng kam ma'lum xato.

Muammo

Prompt da shunday qoida bor edi:

python2 qator
2. Agar kontekstda ma'lumot bo'lmasa, ayt:
   'Информация не найдена в базе знаний.'

Niyat yaxshi: modelga aniq matn beramiz. Natija esa yomon — o'zbekcha savolga ruscha javob keldi. Chunki 4-qoida "foydalanuvchi tilida javob ber" desa ham, 2-qoidadagi RUSCHA JUMLA kuchliroq ta'sir qildi.

Tuzatishga urinish — yomonlashdi

Mantiqiy qadam: o'zbekcha namuna ham qo'shamiz.

python2 qator
2. ... o'zbekcha uchun 'Bu haqda ma'lumot yo'q',
       ruscha uchun 'Информация не найдена'

Natija battar bo'ldi: endi ruscha savolga o'zbekcha javob kela boshladi. Va tariflar savolida model ikki yozuvni aralashtirib yubordi.

To'g'ri yechim — namunani BUTUNLAY olib tashlash

python2 qator
2. If the context lacks the information, state that clearly
   in the SAME language as the question. Do not invent an answer.

Qoidani AYTAMIZ, lekin KO'RSATMAYMIZ.

O'lchangan natija

Aralash (o'zbek + rus) kontekstda, 4 xil savol:

2-qoida qanday yozilgan To'g'ri tilda javob
faqat ruscha namuna 3/4
ruscha + o'zbekcha namuna 2/4
namunasiz, faqat qoida 4/4

Uchinchi variant 5 marta takrorlab sinaldi: 20/20.

Nega shunday bo'ladi

LLM — bu davom ettiruvchi model. U promptdagi har qanday matnni "shunday yozish kerak ekan" deb qabul qiladi. Ruscha jumla yozsangiz — ruscha yozishga moyillik paydo bo'ladi. Bu few-shot o'rganishning teskari tomoni: siz namuna bermoqchi emassiz, lekin model uni namuna deb qabul qiladi.

Amaliy qoida: promptda til bilan bog'liq ko'rsatma bersangiz, o'sha tilda MATN yozmang. Faqat qoidani ayting.

Xuddi shu mantiq boshqa joyda ham ishlaydi:

  • Format namunasi yozsangiz — model o'sha formatga yopishib qoladi
  • "Masalan: 5 ta band" desangiz — doim 5 ta band yozadi
  • Uzunlik namunasi bersangiz — mazmundan qat'i nazar shu uzunlikda yozadi

Namuna kerak bo'lsa — atayin bering va oqibatini biling. Tasodifan bermang.


</> Loyihaning haqiqiy kodi — bu bob amalda qanday ishlaydi

app/services/chat.py — generation bosqichi

python83 qator
def _system_prompt() -> str:
    return get_sys_param("SYSTEM_PROMPT", str, _SYSTEM_PROMPT_FALLBACK)


def build_prompt(
    chunks: list[dict],
    question: str,
    history: list[dict] | None = None,


def _refine_answer(chunks: list[dict], question: str) -> str:
    settings = get_settings()
    lang = detect_lang(question)
    lang_instruction = "Ўзбек тилида жавоб бер." if lang == "UZ" else "Ответь на русском языке."

    context_chunks = select_context_chunks(chunks, question)
    context = "\n---\n".join(c["chunk_text"] for c in context_chunks)

    prompt = (
        f"{_system_prompt()}\n\n"
        "------------------------------------------------------------------------------------\n"
        f"{context}\n"
        "------------------------------------------------------------------------------------\n"
        f"Используя предоставленную информацию, ответь на этот вопрос:\n"
        f"{question}\n"
        f"{lang_instruction}"
    )

    try:
        resp = httpx.post(
            f"{settings.OLLAMA_URL}/api/generate",
            json={
                "model": settings.CHAT_MODEL,
                "prompt": prompt,
                "stream": False,
                "options": {
                    "temperature": 0.0,
                    "num_predict": 2048,
                    "repeat_penalty": 1.1,
                    "repeat_last_n": 512,
                },
            },
            timeout=300,
        )
        resp.raise_for_status()
        return resp.json().get("response", "").strip()
    except Exception:
        return "Ошибка генерации ответа."


def stream_response(
    messages: list[dict],
    temperature: float | None = None,
    chunks: list[dict] | None = None,
    question: str | None = None,


def _strip_markdown(text_input: str) -> str:
    for pattern, replacement in _MD_PATTERNS:
        text_input = pattern.sub(replacement, text_input)
    return text_input


def _cut_repetitions(text_input: str) -> str:
    lines = text_input.split("\n")
    seen = set()
    result = []
    repeat_count = 0
    for line in lines:
        stripped = line.strip()
        if not stripped:
            result.append(line)
            continue
        normalized = re.sub(r"\d+\.\s*", "", stripped).strip()
        if normalized in seen:
            repeat_count += 1
            if repeat_count >= 2:
                break
            continue
        seen.add(normalized)
        repeat_count = 0
        result.append(line)
    return "\n".join(result).strip()

9.10 Reliable RAG — javobni chiqarishdan oldin tekshirish

Kodlar: Reliable RAG — uchta darvoza 9-bob · filter → verify → cite · ~90 qator

1. Birinchi darvoza — chunk filtri

Retrieval 5 ta chunk qaytardi. Ularning hammasi ham kerak emas. Har birini LLM dan so'rab tekshiramiz va keraksizlarini LLM ga umuman bermaymiz.

python16 qator
GRADE_DOC = (
    "Bu hujjat bo'lagi savolga javob berishga yordam beradimi? "
    "Faqat HA yoki YOQ deb yoz.\n\n"
    "Savol: {question}\n\nBo'lak: {chunk}"
)

def filter_relevant(question: str, chunks: list[dict]) -> list[dict]:
    kept = []
    for c in chunks:
        out = _ollama_chat(
            [{"role": "user", "content": GRADE_DOC.format(question=question,
                                                           chunk=c["content"][:900])}],
            temperature=0.0, max_tokens=5)
        if "HA" in (out or "").upper():
            kept.append(c)
    return kept

2. Ikkinchi darvoza — hallucination tekshiruvi

Javob yozilgandan keyin uni kontekstga solishtiramiz. Muhim: promptda "bitta fakt ham tashqarida bo'lsa YOQ" deb yozilgan — qattiq mezon.

python13 qator
CHECK_GROUNDED = (
    "Javobdagi BARCHA faktlar kontekstda bormi? "
    "Bitta fakt ham kontekstdan tashqarida bo'lsa — YOQ deb yoz. "
    "Faqat HA yoki YOQ.\n\n"
    "Kontekst:\n{context}\n\nJavob: {answer}"
)

def is_grounded(answer: str, context: str) -> bool:
    out = _ollama_chat(
        [{"role": "user", "content": CHECK_GROUNDED.format(context=context,
                                                            answer=answer)}],
        temperature=0.0, max_tokens=5)
    return "HA" in (out or "").upper()

3. Uchinchi darvoza — aniq iqtibos

Javobning qaysi qismi qaysi chunk dan kelganini so'raymiz. Bu foydalanuvchiga tekshirish imkonini beradi — RAG tizimida eng qimmatli xususiyat.

python22 qator
import json

HIGHLIGHT = (
    "Javobdagi har bir da'vo uchun uni tasdiqlaydigan manba raqamini va "
    "aniq iqtibosni ko'rsat. JSON qaytar:\n"
    '[{{"davo": "...", "manba": 1, "iqtibos": "..."}}]\n'
    "Iqtibosni manbadan AYNAN ko'chir.\n\n"
    "Manbalar:\n{sources}\n\nJavob: {answer}"
)

def extract_citations(answer: str, chunks: list[dict]) -> list[dict]:
    sources = "\n\n".join(
        f"[{i + 1}] {c['content'][:700]}" for i, c in enumerate(chunks)
    )
    out = _ollama_chat(
        [{"role": "user", "content": HIGHLIGHT.format(sources=sources, answer=answer)}],
        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 []
python33 qator
def reliable_answer(session, question: str, top_k: int = 5) -> dict:
    emb, _ = generate_query_embedding(question)
    chunks = retrieve_chunks(session, emb, top_k=top_k)

    relevant = filter_relevant(question, chunks)
    if not relevant:
        return {"answer": "Bazada tegishli ma'lumot topilmadi.",
                "status": "NO_CONTEXT", "citations": []}

    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)

    if not is_grounded(answer, context):
        return {"answer": "Ishonchli javob shakllantira olmadim.",
                "status": "NOT_GROUNDED", "citations": []}

    return {
        "answer": answer,
        "status": "OK",
        "filtered": f"{len(relevant)}/{len(chunks)}",
        "citations": extract_citations(answer, relevant),
    }


r = reliable_answer(session, "To'lov shlyuzi qachon ulangan?")
print(f"Holat: {r['status']}  (chunk: {r.get('filtered')})")
print(f"\n{r['answer']}\n")
for c in r["citations"]:
    print(f"  [{c['manba']}] {c['davo'][:50]}")
    print(f"      \"{c['iqtibos'][:80]}\"")
Natija
Holat: OK  (chunk: 3/5)

Click va Payme to'lov shlyuzlari 2023-yil iyun oyida, loyihaning ikkinchi
bosqichida REST API orqali ulangan.

  [1] To'lov shlyuzlari iyun oyida ulangan
      "Ikkinchi bosqichda, iyun oyida, Click va Payme to'lov shlyuzlari"
  [1] Ulanish REST API orqali amalga oshirilgan
      "REST API orqali ulandi"

4. Uchta raqamga e'tibor bering

3/5 — ikkita chunk filtrdan o'tmadi va LLM ularni umuman ko'rmadi.

OK — javob kontekst bilan tasdiqlandi.

Iqtiboslar — foydalanuvchi har bir da'voni asl matnga solishtirib ko'rishi mumkin.

Narxi katta: 5 ta filtr + 1 generation + 1 tekshiruv + 1 iqtibos = 8 ta LLM chaqiruvi. Oddiy RAG da 1 ta.

Tejash: filtrni bitta promptga birlashtiring (5 chunk ni raqamlab, "qaysilari tegishli?" deb so'rang) — 5 chaqiruv 1 ta bo'ladi.

Qayerda majburiy: tibbiyot, huquq, moliya. Qayerda ortiqcha: ichki bilim bazasi, marketing bot — u yerda tezlik muhimroq.

Shu paytgacha qurgan pipeline imizda bitta jiddiy bo'shliq bor: javob chiqib ketgandan keyin uni tekshiradigan hech narsa yo'q. LLM yolg'on yozsa — foydalanuvchi uni o'qiydi.

Reliable RAG generation atrofiga uchta darvoza qo'yadi:

  • Kirishda — har bir chunk tegishlimi? Tegishli bo'lmagani LLM ga umuman bermaydi.
  • Chiqishda — javobdagi har bir da'vo kontekstda bormi?
  • Manba — javobning qaysi qismi qaysi chunk dan olingan?

Uchinchi darvoza eng qimmatlisi

Ko'p tizimlar javob ostida manbalar ro'yxatini ko'rsatadi. Lekin bu yolg'on ishonch beradi: ro'yxat retrieval natijasi, ya'ni "shu 5 ta chunk topildi" degani. Javob ularning qaysi biridan kelgani noma'lum.

Haqiqiy manba ko'rsatish boshqacha ishlaydi: javob yozilgandan keyin, LLM dan "javobning har bir qismi qaysi chunk dan olingan?" deb so'raladi. Natijada foydalanuvchi aniq iqtibosni ko'radi.

python9 qator
Oddiy RAG:
  Javob: "To'lov shlyuzi iyun oyida ulangan."
  Manbalar: [chunk 12, chunk 18, chunk 24, chunk 31, chunk 44]
            ^-- qaysi biri? Bilinmaydi.

Reliable RAG:
  Javob: "To'lov shlyuzi iyun oyida ulangan."
  Asos:  chunk 12 --> "Ikkinchi bosqichda, iyun oyida, Click va Payme
                       to'lov shlyuzlari REST API orqali ulandi."

Tasdiqlanmagan javobni ko'rsatmang

Eng muhim qaror shu: tekshiruvdan o'tmagan javob foydalanuvchiga chiqmasligi kerak. "Ishonchli javob shakllantira olmadim" degan xabar — yolg'on javobdan yaxshiroq.

Bu biznes qarori, texnik qaror emas. Tibbiyot, huquq, moliya sohalarida bu majburiy. Marketing chatbot da esa ortiqcha qattiqqo'llik bo'lishi mumkin.

Bu bobni bepul o'qidingiz.

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

Telegramda yozish Narxlar