Dilshod.dev

RAG — asosan qidiruv muammosi

RAG tizimi noto'g'ri javob berganda, aybdor odatda model emas — kerakli bo'lak kontekstga umuman tushmagan. Buni qanday isbotlash va isbotlagandan keyin nimani tuzatish haqida.

Muallif: Dilshod Abdullayev11 daqiqa o'qish

Qo'llab-quvvatlash boti mijozga ERR_2041 xatosi sessiya muddati tugaganini bildiradi, qaytadan kiring deb aytadi. Buni toza, ishonchli nutq bilan aytadi. ERR_2041 ning haqiqiy ma'nosi — kartani oldindan avtorizatsiya qilish muvaffaqiyatsizligi, CVV ni qayta kiritish bilan tuzatiladi — bot so'rov yuboradigan o'sha Postgres bazasida yotgan hujjat sahifasida turibdi: indekslangan, embedding qilingan, tayyor. U hech qachon olinmagan.

Jamoaning birinchi instinkti tizim promptini qayta yozish bo'ldi. "Taxmin qilma" qo'shildi. "Ishonching komil bo'lmasa, bilmayman de" qo'shildi. Hech biri yordam bermadi va bera olmasdi ham, chunki model o'zida bor hujjat haqida taxmin qilayotgani yo'q edi. U umuman hech qanday tegishli kontekstsiz javob berayotgan edi — LLM har doim yomon va har doim ishonch bilan bajaradigan yagona ish.

"Model xato qildi" bilan "model buni umuman ko'rmadi" orasidagi shu farq — RAG'ni tuzatish shundan boshlanishi kerak, va deyarli hech qachon shundan boshlanmaydi.

Har qanday prompt o'zgarishidan oldingi birinchi savol

Kimdir menga yomon RAG javobini keltirsa, boshqa hech narsaga qaramasdan bitta narsani so'rayman: to'g'ri javob olingan bo'laklar ichida bormidi? Bu mexanik tekshiruv, baho emas. Nimani olganingizni logga yozing, keyin o'qing.

// Faqat javobni emas, qidiruv natijasini ham logga yozing. Busiz siz taxmin qilyapsiz.
await db.query(
  `INSERT INTO rag_traces (query, chunk_ids, scores, answer)
   VALUES ($1, $2, $3, $4)`,
  [question, chunks.map(c => c.id), chunks.map(c => c.score), answer],
);

Ikkita natija, ikkita mutlaqo boshqa loyiha.

Agar javob olingan matn ichida bo'lgan va model baribir xato qilgan bo'lsa, sizda generatsiya muammosi bor. Endi prompt ustida ishlash mantiqiy: manbaga bog'lash ko'rsatmalari, boshqa model, kontekst blokini yaxshiroq formatlash.

Agar javob u yerda bo'lmagan bo'lsa, buni hech qanday prompt tuzatmaydi. Siz faqat modelni tez-tez rad etadigan qila olasiz — bu esa xato javobni foydasiz javobga almashtiradi. Xato qidiruvda.

Mening tajribamda ikkinchi holat ustunlik qiladi. U yana kamroq jozibali, chunki qidiruvni sozlash tokenizatsiya va indeks konfiguratsiyasi haqida o'ylashni talab qiladi, promptlar esa suhbatga o'xshaydi.

Bo'laklarga bo'lish — bir marta qabul qilib, keyin u bilan yashaydigan qaror

Hech narsa embedding qilinmasdan oldin, nimadir hujjatlarni bo'laklarga kesadi. O'sha kesim keyingi hamma narsaning shiftini belgilaydi — hech qanday reranker o'z kontekstidan ajratib tashlangan ma'lumotni tiklay olmaydi.

Har bir qo'llanmada keladigan standart — qat'iy o'lchamli bo'laklash: 500 token, 50 ustma-ust, davom et. Bu oson va jimgina buzg'unchi. Oyna qayerga tushsa o'sha yerga tushadi. U jadvalni sarlavha qatoridan ajratadi — natijada raqamlar to'plami ustunlar nimani anglatishini bilmagan holda keladi. U yetti qadamli protsedurani to'rtinchi qadamdan keyin kesadi — bo'lak jarayonning yarmini tasvirlaydi va modelga to'liq jarayondek o'qiladi.

Tuzilmani hisobga oladigan bo'laklash muallif hujjatga allaqachon qo'ygan chegaralarni hurmat qiladi. Sarlavhalar bo'yicha bo'ling, sig'sa bo'limni butun saqlang va sarlavha yo'lini bo'lak matniga olib kiring:

type Chunk = { text: string; headingPath: string[]; docId: string };
 
function chunkByHeading(doc: ParsedDoc, maxTokens = 900): Chunk[] {
  const out: Chunk[] = [];
 
  for (const section of doc.sections) {
    // Sarlavha yo'li oldiga qo'yiladi — shunda embedding kontekstni ko'radi,
    // va bo'lakni o'qigan odam ham u nima haqida ekanini biladi.
    const prefix = section.headingPath.join(' > ');
 
    if (countTokens(section.text) <= maxTokens) {
      out.push({ text: `${prefix}\n\n${section.text}`, headingPath: section.headingPath, docId: doc.id });
      continue;
    }
 
    // Faqat haddan katta bo'limlar bo'linadi, va faqat abzas chegarasida.
    for (const part of splitOnParagraphs(section.text, maxTokens)) {
      out.push({ text: `${prefix}\n\n${part}`, headingPath: section.headingPath, docId: doc.id });
    }
  }
 
  return out;
}

Halol murosa: kattaroq bo'laklar ko'proq kontekst tashiydi va to'liq javobni o'z ichiga olish ehtimoli yuqori, lekin ular embedding'ni suyultiradi. Uchta kichik mavzuni qamragan 900 tokenlik vektor o'sha mavzularning o'rtachasiga ishora qiladi va hech biriga aniq mos kelmaydi. Kichikroq bo'laklar aniq topiladi, keyin yetarlicha gapira olmaydi. Men bo'lim o'lchamidagi bo'laklarga moyilman va aniqlik yo'qotilishini qabul qilaman, chunki javobning yarmiga aniq ishora butun javobga xira ishoradan arzonroq turadi. Bu — afzallik, qonun emas.

Embedding ERR_2041 nimaligini bilmaydi

Vektor qidiruv demolari orqali RAG'ga kelganlarni hayratga soladigan qism shu. Embedding semantik o'xshashlikni kodlaydi, ERR_2041 da esa semantika yo'q. Bu — ma'nosi butunlay ma'lumotnoma jadvalida yashaydigan ixtiyoriy token, va uning vektori boshqa xato-kodga o'xshash satrlar — ERR_2040, ERR_3110 — yonida turadi. Chunki u shunday ko'rinadi, ma'nosi shunday bo'lgani uchun emas.

Mahsulot nomlari, SKU'lar, versiya raqamlari, konfiguratsiya kalitlari, familiyalar uchun ham xuddi shunday. Ya'ni aniq javob kerak bo'lganda foydalanuvchilar teradigan aynan o'sha identifikatorlar uchun.

To'liq matnli qidiruvda bunday muammo yo'q. U tokenni so'zma-so'z topadi.

-- Generatsiya qilinadigan tsvector ustuni: bir marta indekslanadi, Postgres o'zi yuritadi.
ALTER TABLE chunks
  ADD COLUMN fts tsvector
  GENERATED ALWAYS AS (to_tsvector('simple', text)) STORED;
 
CREATE INDEX chunks_fts_idx ON chunks USING GIN (fts);
 
-- 'english' emas, 'simple': stemming yo'q, shuning uchun ERR_2041 ERR_2041 bo'lib qoladi.
-- Tilga xos konfiguratsiya matn recall'ini oshirib, identifikatorlarni buzardi.

O'sha izoh — tafsilot emas, haqiqiy murosa. english stemming oddiy matnda moslikni yaxshilaydi va aynan aniq mos kelishi kerak bo'lgan satrlarni buzadi. Ikkala konfiguratsiyani ham ishlatish mumkin; sizning korpusingiz uchun qaysi biri muhimroq ekanini hal qilish — asosiy ish.

Hech bir usul yolg'iz o'zi yetarli emas. Vektorlar "to'lovim doim qaytib kelyapti" ni "Rad etilgan avtorizatsiyalar" deb nomlangan sahifa bilan bog'laydi. To'liq matnli qidiruv ERR_2041 ni topadi. Sizga ikkalasi ham kerak.

Ikki reytingni yangi shkala o'ylab topmasdan birlashtirish

Sodda birlashtirish — ikkala ballni normallashtirib, vaznli yig'indi olish. Bu ishlamaydi, chunki kosinus o'xshashligi va ts_rank taqqoslab bo'lmaydigan narsalarni o'lchaydi — bitta korpusda sozlangan vaznlar keyingisida ma'nosiz.

Reciprocal rank fusion (RRF) buni chetlab o'tadi: ballarni umuman e'tiborsiz qoldirib, pozitsiyalardan foydalanadi. Har bir ro'yxat 1 / (k + rank) qo'shadi, shuning uchun ikkala ro'yxatda ham yuqorida turgan hujjatlar faqat bittasida birinchi bo'lganlardan ustun keladi.

WITH semantic AS (
  SELECT id, ROW_NUMBER() OVER (ORDER BY embedding <=> $1) AS rank
  FROM chunks
  WHERE tenant_id = $2 AND lang = $3
  ORDER BY embedding <=> $1          -- pgvector'da <=> kosinus masofasi
  LIMIT 50
),
lexical AS (
  SELECT id, ROW_NUMBER() OVER (ORDER BY ts_rank(fts, query) DESC) AS rank
  FROM chunks, plainto_tsquery('simple', $4) AS query
  WHERE tenant_id = $2 AND lang = $3 AND fts @@ query
  ORDER BY ts_rank(fts, query) DESC
  LIMIT 50
)
SELECT COALESCE(s.id, l.id) AS id,
       -- k = 60 yuqori pozitsiyalarni yumshatadi, bitta ro'yxat butunlay hukmron bo'lmaydi.
       COALESCE(1.0 / (60 + s.rank), 0) + COALESCE(1.0 / (60 + l.rank), 0) AS rrf
FROM semantic s
FULL OUTER JOIN lexical l ON s.id = l.id
ORDER BY rrf DESC
LIMIT 30;

Gibrid qidiruvning narxi — halol murakkablik: yuritiladigan ikkita indeks, noto'g'ri sozlashning ikkita yo'li, sekin ishlaganda tushuntiriladigan yana bitta so'rov. Shunga qaramay men buni identifikator bo'lgan har qanday korpus uchun deyarli majburiy deb bilaman — texnik hujjatlarning deyarli hammasi shunday.

Keng ol, keyin qattiq kes

Fusion sizga o'ttizta ishonarli nomzod beradi. O'ttizta bo'lak — modelga berish uchun juda katta kontekst: u kerakli bo'lakni chalg'ituvchilar orasiga ko'madi va buning uchun tokenlar bilan to'laydi.

Cross-encoder reranker so'rov bilan har bir nomzodni birgalikda o'qiydi va haqiqiy dolzarblikni baholaydi — mustaqil hisoblangan ikkita vektorni taqqoslash o'rniga. Aynan shu birgalikdagi kodlash uni aniqroq qiladi, va shuning uchun u sekinroq: uni oldindan hisoblab yoki indekslab bo'lmaydi.

async function retrieve(question: string, tenantId: string, lang: string) {
  // Keng tashla: bu yerda recall muhim, aniqlik hali emas.
  const candidates = await hybridSearch(question, tenantId, lang, { limit: 30 });
 
  // Keyin qattiq kes. Reranker — qimmat va aniq bosqich.
  const scored = await reranker.score(question, candidates.map(c => c.text));
 
  return candidates
    .map((c, i) => ({ ...c, relevance: scored[i] }))
    .sort((a, b) => b.relevance - a.relevance)
    .filter(c => c.relevance > RELEVANCE_FLOOR)   // bo'sh natija ham haqiqiy javob
    .slice(0, 5);
}

O'sha filter qatori sortdan muhimroq. Pastki chegara bo'lmasa siz doim beshta bo'lak qaytarasiz — korpusingiz qamramagan savollar uchun ham. Beshta ahamiyatsiz bo'lak esa yo'qlikdan yomonroq, chunki ular modelga ishonchli xato javob qurish uchun material beradi. Hech narsa qaytarmaslik esa "bu haqda menda hujjat yo'q" deyish imkonini beradi — bu rost va foydali.

Reranker kritik yo'lga kechikish qo'shadi. Bu maqbulmi yoki yo'qmi — muhandislik emas, mahsulot qarori. Muqobili xato javob bo'lgan qo'llab-quvvatlash boti uchun odatda maqbul.

Filtrlash — sozlash emas, to'g'rilik masalasi

Ko'p tenantli tizimda A tenant uchun B tenantning hujjatini olish — bu atrofiga LLM o'ralgan ma'lumot sizib chiqishi. Bu yaxshiroq reyting bilan tuzatiladigan sifat masalasi emas. Filtr so'rovning ichida, baza tomonidan majburlanadigan joyda turishi kerak — natijalarga keyin qo'llanadigan joyda emas:

-- Kompozit indeks: tenant filtri indeksdan keyin emas, indeks orqali qo'llanadi.
CREATE INDEX chunks_tenant_lang_idx ON chunks (tenant_id, lang);
 
CREATE INDEX chunks_embedding_idx ON chunks
  USING hnsw (embedding vector_cosine_ops);

Buning narxini biling. HNSW indeksi butun jadval bo'ylab eng yaqin qo'shnilarni taqriban topadi; uning ustidagi qattiq WHERE sizni LIMIT dan ancha kam qator bilan qoldirishi mumkin, chunki filtr indeks allaqachon tanlagan nomzodlarga qo'llanadi. Postgres sizni ogohlantirmaydi — shunchaki kalta natija olasiz va nimadir yo'qolgani haqida hech qanday signal bo'lmaydi.

pgvector 0.8 bunga to'g'ridan-to'g'ri javob qo'shdi: hnsw.iterative_scan. U birinchi nomzodlar to'plamida to'xtash o'rniga, filtrlangan LIMIT qanoatlantirilguncha graf bo'ylab yurishda davom etadi. Natija tartibi aniq bo'lishi shart bo'lsa strict_order, baribir rerank qiladigan bo'lsangiz relaxed_order qo'ying; hnsw.max_scan_tuples ishni cheklaydi. Shu ham yetmasa, tenant_id bo'yicha partitsiyalashga o'ting — tenantlar ko'p bo'lganda u bashoratliroq — lekin avval sozlamani sinab ko'ring, u bitta qator.

Sana va til filtrlari kamroq xavfli, lekin ahamiyati kam emas. Bekor qilingan qoidaga havola qilayotgan bot — tashqaridan sezish qiyin bo'lgan tarzda ishonchli ravishda xato.

Qidiruvni alohida o'lchang

Boshdan-oxir javob sifati — manfaatdorlar so'raydigan raqam va sizga eng kam narsa aytadigan raqam, chunki u qidiruv, reranking va generatsiyani bitta ballga aralashtiradi — va o'sha ball siz sababini aniqlay olmaydigan sabablarga ko'ra o'zgaradi.

Qidiruvchini alohida o'lchang. Bunga infratuzilma emas, bir yarim kun va jadval kerak. O'ttiz-ellikta haqiqiy foydalanuvchi savolini oling va har biriga qaysi bo'lak javob berishini qo'lda belgilang. Keyin quvuringizdan bitta narsani so'rang: o'sha bo'lak top-k ichida chiqdimi?

// recall@k: javob bo'lagi ma'lum savollardan nechtasini yuzaga chiqardik?
function recallAtK(evalSet: EvalCase[], results: Map<string, string[]>, k: number) {
  const hits = evalSet.filter(c => results.get(c.question)!.slice(0, k).includes(c.goldChunkId));
  return hits.length / evalSet.length;
}

Uni reranking'dan oldin k = 30 da va keyin k = 5 da ishlating. Bu ikki raqam ikkita nosozlik turini toza ajratadi. 30 da recall past bo'lsa — nomzod bosqichi ishlamayapti va reranker'ning imkoniyati bo'lmagan; borib bo'laklashni yoki gibrid so'rovni tuzating. 30 da yuqori, 5 da past bo'lsa — qidiruv ishlayapti, reranker yaxshi bo'laklarni tashlab yuboryapti.

Qo'lda tuzilgan to'plamning qiymati statistik kuchda emas; ellikta misol sizning haqiqiy aniqligingiz haqida hech narsa isbotlamaydi. Ular beradigan narsa — regressiya testi: bo'laklash strategiyasini o'zgartirasiz va vaziyatni yomonlashtirganingizni uch haftadan keyin qo'llab-quvvatlash so'rovidan emas, darhol bilib olasiz.

Havolani biriktirilgan holda saqlang

Har bir bo'lak quvur bo'ylab o'z manbasi bilan birga yurishi kerak, va o'sha manba foydalanuvchiga yetib borishi kerak.

const context = chunks
  .map((c, i) => `[${i + 1}] ${c.headingPath.join(' > ')} (${c.docUrl})\n${c.text}`)
  .join('\n\n');
// Keyin prompt har bir da'vodan [n] belgisini talab qiladi.

Bu qisman foydalanuvchi ishonchi uchun. Asosan esa — muhandislik imkoniyati. Havolali javob — tekshirib bo'ladigan javob: kimdir bosib o'tib, ko'rsatilgan sahifada javob da'vo qilgan narsa yo'qligini ko'radi va haqiqatan bajarsa bo'ladigan xato hisoboti yozadi. Havolalarsiz esa sizda "o'tgan hafta bot nimadadir xato qilgan edi" qoladi — buni tuzatib bo'lmaydi.

Havolalar men boshida aytgan nosozlikni ham yuzaga chiqaradi. Agar bot ERR_2041 haqida javob berib, sessiya muddati haqidagi sahifaga havola qilsa, qidiruv xatosi hech kim o'qimaydigan logga ko'milib qolmay, javobning o'zida ko'rinib turadi.

Men qattiq turmaydigan narsalar

Bo'lak o'lchamlari, k qiymatlari, RRF konstantasi, dolzarblik chegarasi — hammasi korpusga bog'liq, va men o'zimning birorta raqamimni boshqa birovning ma'lumotida himoya qilmagan bo'lardim. Bular meros qilib olinadigan emas, o'lchanadigan narsalar.

Bu arxitektura shu shaklda qancha qolishiga ham ishonchim komil emas. Kontekst oynalari kattalashgani sari, bugun qidiruv qilayotgan ishning bir qismi shunchaki ko'proq matn uzatish bilan yutiladi. Bu qidiruvni yo'q qilmaydi — millionta hujjatdan qaysi birini uzatishni baribir hal qilish kerak, va juda uzun kontekst ustidagi e'tibor na tekin, na bir tekis — lekin chegarani surishi ehtimoldan xoli emas. Agar kimdir menga katta oynali model qo'polroq qidiruv bilan mening ehtiyotkor quvurimni haqiqiy eval to'plamida ortda qoldirishini ko'rsatsa, men soddaroq variantni tanlayman.

Men qattiq turadigan narsa — eng boshdagi tashxis harakati. Boshqa hech narsani o'zgartirishdan oldin javob kontekstda bormidi, shuni tekshiring. Bu arxitektura haqida emas, va u biz turgan arxitekturalarning barchasidan uzoq yashaydi.

Ostidagi shakl

RAG AI'ga hech qanday aloqasi yo'q bir odatni mukofotlaydi: bosqichlardan iborat quvur yomon natija bergan bo'lsa, ularning birortasini yaxshilashdan oldin qaysi bosqich ma'lumotni yo'qotganini aniqlang. Jamoalar promptga qo'l uzatadi — u ishlash ehtimoli yuqori bo'lgani uchun emas, prompt tushunarli va qiziqarli bo'lgani, qidiruv qatlami esa ikkalasi ham emasligi uchun.

Model — oxirgi bosqich. U faqat unga berilgan narsa qanchalik to'g'ri bo'lsa, shunchalik to'g'ri bo'la oladi. Ko'p hollarda javob bazada boshidan turgan bo'ladi, va tuzatishga arziydigan hamma narsa esa uni borib olmagan kodda yotadi.

O'xshash maqolalar

aiagents

Kontekst muhandisligi — yangi to'siq

Prompt muhandisligi so'z tanlash haqida edi. Kontekst muhandisligi esa cheklangan oynada qaysi tokenlar joy olishga haqli ekanini hal qiladi — va bugun ishlaydigan agent bilan deyarli ishlaydigan agentni ajratadigan narsa aynan shu.

5 daqiqa o'qish
aiagents

AI agenti aslida nima

Marketingni olib tashlasangiz, agent — uch qismli sikl. Shu qismlarni tushunish agentlar production'da nega qulashini va qaysi nosozliklarni tuzatishga arziyotganini aniq ko'rsatadi.

7 daqiqa o'qish
aicareer

Yo'qolgan birinchi zinapoya

Stanford oyiga besh million amerikalikning maoshini kuzatadi. Yigirma ikki–yigirma besh yoshdagilar bandligi AI ta'sir qilgan kasblarda kutilgandan o'n to'qqiz foiz past. Odamlar ishdan bo'shatilmayapti — ularni ishga olish to'xtagan. Va bu ikkisi butunlay boshqa muammo.

6 daqiqa o'qish