RAG vyhľadávanie vo firemných smerniciach: Ako dostať presné odpovede bez halucinácií

Od nolimeo · 3. apríla 2026
banner image

Predstavte si situáciu: nový zamestnanec reklamačného oddelenia rieši neštandardný prípad vrátenia tovaru poškodeného pri preprave. Namiesto rýchleho vybavenia musí otvoriť zdieľaný sieťový disk, na ktorom ho čaká tridsať PDF dokumentov so stovkami strán interných smerníc, právnych výkladov a prevádzkových poriadkov. Hľadanie kľúčového slova „vrátenie poškodeného obalu“ cez klasické vyhľadávanie v systéme Windows zlyhá alebo vráti dvadsať nesúvisiacich výsledkov. Zamestnanec potom trávi hodinu čítaním právneho textu alebo v horšom prípade rozhodne od oka, čo firmu môže neskôr stáť peniaze na reklamáciách, pokutách alebo stratenej marži.

Ak by zamestnanec skopíroval text smerníc do verejného ChatGPT a spýtal sa ho na riešenie, riskuje únik dôverných interných postupov a osobných údajov. Ak by sa spýtal verejného chatbota bez firemného kontextu, model by odpovedal len zo všeobecných tréningových dát. Detaily slovenskej legislatívy, interných výnimiek alebo firemných lehôt by si mohol jednoducho domyslieť.

V technologickom štúdiu nolimeo tento problém riešime pomocou architektúry Retrieval-Augmented Generation (RAG). Tá spája sémantické vyhľadávanie vo vektorových databázach s veľkými jazykovými modelmi (LLM). Výsledkom je interný AI asistent, ktorý dokáže nájsť relevantný odstavec v rozsiahlej dokumentácii a sformulovať odpoveď podporenú odkazom na konkrétnu stranu alebo článok internej smernice.


1. Architektúra RAG: Ako to funguje pod kapotou

Tradičné jazykové modely sú uzavreté systémy. Ich znalosti končia momentom, kedy bolo dokončené ich trénovanie (tzv. knowledge cutoff), a nemajú prístup k interným dátam uloženým vo vašom SharePointe, Confluence, ERP alebo na lokálnych diskoch.

Retrieval-Augmented Generation (RAG) tento problém rieši tak, že pred odoslaním otázky používateľa do LLM vyhľadá najrelevantnejšie úryvky z vašich firemných dokumentov a priloží ich k otázke ako kontext. Model potom neháda odpoveď len zo svojej pamäte, ale pracuje s konkrétnym textom, ktorý mu systém poskytol.

 [ Otázka používateľa ] 
        │
        ├─► [ Generovanie embeddingu otázky ] 
        │            │
        │      (Similarity Search)
        │            ▼
        ├─► [ Vyhľadanie najrelevantnejších chunkov v pgvector ]
        │            │
        │     (Prompt Augmentation)
        │            ▼
        └─► [ Spojenie Otázky + Kontextu z dokumentov ] ──► [ LLM (napr. GPT-4o) ] ──► [ Presná odpoveď s citáciou ]

Celý proces môžeme rozdeliť na dve hlavné fázy:

  1. Indexácia dokumentov (Offline fáza): Firemné smernice sa načítajú, vyčistia od balastu, rozdelia na logické celky (chunky), premenia na číselné vektory (embeddingy) a uložia do vektorovej databázy.
  2. Dopytovanie a generovanie (Online fáza): Používateľ položí otázku. Systém premení jeho otázku na vektor, nájde sémanticky najbližšie úryvky dokumentov, zostaví rozšírený prompt pre LLM a model vygeneruje odpoveď opretú o nájdené zdroje.

2. Technická implementácia RAG pipeline v TypeScript

Nižšie uvádzame zjednodušenú backendovú implementáciu v jazyku TypeScript. Kód ukazuje, ako načítať text dokumentu, rozdeliť ho na chunky s prekrytím (overlap), vygenerovať embeddingy pomocou OpenAI API a vykonať sémantické vyhľadávanie s následným generovaním odpovede nad nájdeným kontextom.

Krok A: Chunkovanie a indexácia dokumentov do pgvector

Na to, aby vyhľadávanie fungovalo sémanticky, musíme text rozdeliť na optimálne časti. Ak by sme uložili celú smernicu ako jeden obrovský blok, embedding by bol príliš nepresný. Ak by boli chunky príliš malé (napr. jednotlivé slová), stratili by sme vetný kontext. Ideálnym prístupom je chunkovanie s definovanou veľkosťou a prekrytím, ktoré zabezpečí, že dôležité informácie na hraniciach chunkov nebudú stratené.

// src/services/rag-indexer.ts
import { OpenAI } from "openai";
import { createClient } from "@supabase/supabase-js";

interface DocumentChunk {
  documentId: string;
  content: string;
  chunkIndex: number;
  embedding: number[];
  metadata: Record<string, any>;
}

export class SafeDocumentIndexer {
  private openai: OpenAI;
  private supabase;

  constructor() {
    const apiKey = process.env.OPENAI_API_KEY;
    const supabaseUrl = process.env.SUPABASE_URL;
    const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;

    if (!apiKey || !supabaseUrl || !supabaseKey) {
      throw new Error("Kritická chyba: Chýbajú konfiguračné premenné prostredia.");
    }

    this.openai = new OpenAI({ apiKey });
    this.supabase = createClient(supabaseUrl, supabaseKey);
  }

  /**
   * Rozdelí dlhý text na menšie logické chunky s definovaným prekrytím
   */
  public chunkText(text: string, chunkSize: number = 800, overlap: number = 150): string[] {
    const chunks: string[] = [];
    let currentIndex = 0;

    const normalizedText = text.replace(/\s+/g, " ").trim();

    while (currentIndex < normalizedText.length) {
      const chunk = normalizedText.substring(currentIndex, currentIndex + chunkSize);
      chunks.push(chunk);
      currentIndex += (chunkSize - overlap);
    }

    return chunks;
  }

  /**
   * Vygeneruje vektorový embedding pre zadaný text
   */
  public async generateEmbedding(text: string): Promise<number[]> {
    const response = await this.openai.embeddings.create({
      model: "text-embedding-3-small", // 1536 dimenzionálny embedding s vysokou efektivitou
      input: text,
    });

    const embedding = response.data[0]?.embedding;
    if (!embedding) {
      throw new Error("Zlyhalo generovanie embeddingu cez OpenAI.");
    }
    return embedding;
  }

  /**
   * Spracuje dokument, rozseká ho a uloží s embeddingami do Supabase pgvector databázy
   */
  public async indexDocument(documentId: string, title: string, fullText: string, category: string): Promise<void> {
    const rawChunks = this.chunkText(fullText);
    const dbChunks: DocumentChunk[] = [];

    console.log(`Začína indexácia dokumentu ${title}, počet chunkov: ${rawChunks.length}`);

    for (let i = 0; i < rawChunks.length; i++) {
      const content = rawChunks[i];
      const embedding = await this.generateEmbedding(content);

      dbChunks.push({
        documentId,
        content,
        chunkIndex: i,
        embedding,
        metadata: {
          title,
          category,
          source_length: fullText.length,
          indexed_at: new Date().toISOString()
        }
      });
    }

    // Hromadný zápis (bulk insert) do PostgreSQL tabuľky vybavenej pgvector
    const { error } = await this.supabase
      .from("document_chunks")
      .insert(
        dbChunks.map(chunk => ({
          document_id: chunk.documentId,
          content: chunk.content,
          chunk_index: chunk.chunkIndex,
          embedding: chunk.embedding,
          metadata: chunk.metadata
        }))
      );

    if (error) {
      console.error("Chyba pri ukladaní chunkov do pgvector:", error);
      throw new Error(`Uloženie indexu zlyhalo: ${error.message}`);
    }

    console.log(`Dokument ${title} bol úspešne zaindexovaný.`);
  }
}

Krok B: Vyhľadávanie sémantického kontextu a generovanie odpovede cez LLM

Keď používateľ položí otázku, neodpovedáme hneď. Najprv vyhľadáme sémanticky najbližšie textové úseky pomocou kosínusovej podobnosti (cosine similarity) nad vektormi v databáze a až tie použijeme ako kontext pre generovanie odpovede.

// src/services/rag-query-engine.ts
import { OpenAI } from "openai";
import { createClient } from "@supabase/supabase-js";

interface SearchResult {
  content: string;
  similarity: number;
  metadata: {
    title: string;
    category: string;
  };
}

export class SafeRAGQueryEngine {
  private openai: OpenAI;
  private supabase;

  constructor() {
    this.openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
    this.supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!);
  }

  /**
   * Vyhľadá top-K najrelevantnejších úryvkov dokumentov pomocou kosínusovej similarity v pgvector
   */
  private async searchRelevantContext(queryEmbedding: number[], limit: number = 3, minSimilarity: number = 0.65): Promise<SearchResult[]> {
    // Volanie PostgreSQL RPC funkcie match_chunks, ktorá vykonáva vektorové porovnávanie
    const { data, error } = await this.supabase.rpc("match_chunks", {
      query_embedding: queryEmbedding,
      match_threshold: minSimilarity,
      match_count: limit
    });

    if (error) {
      console.error("Chyba pri RPC match_chunks:", error);
      throw new Error("Chyba pri sémantickom prehľadávaní databázy.");
    }

    return data || [];
  }

  /**
   * Prijme dopyt používateľa, vykoná sémantické vyhľadávanie a vráti odpoveď s presnými citáciami
   */
  public async askEnterpriseAssistant(userQuery: string): Promise<{ answer: string; sources: string[] }> {
    try {
      // 1. Vygenerovanie embeddingu pre otázku používateľa
      const queryResponse = await this.openai.embeddings.create({
        model: "text-embedding-3-small",
        input: userQuery,
      });
      const queryEmbedding = queryResponse.data[0]?.embedding;

      if (!queryEmbedding) {
        throw new Error("Zlyhalo generovanie embeddingu otázky.");
      }

      // 2. Vyhľadanie sémantického kontextu z vlastnej databázy pgvector
      const contextMatches = await this.searchRelevantContext(queryEmbedding);

      if (contextMatches.length === 0) {
        return {
          answer: "Pre vašu otázku som nenašiel žiadne relevantné informácie v oficiálnych firemných smerniciach. Nemôžem na ňu odpovedať, aby som predišiel špekuláciám.",
          sources: []
        };
      }

      // 3. Zostavenie kontextového bloku pre LLM
      const contextText = contextMatches
        .map((match, index) => `[Zdroj ${index + 1}: ${match.metadata.title}]:\n${match.content}`)
        .join("\n\n");

      // 4. Augmentácia promptu: LLM dostane pokyny nešpekulovať a citovať zdroje
      const systemPrompt = `Ste interný firemný asistent, ktorý odpovedá výhradne na základe priložených úryvkov z firemných smerníc.
Vaša odpoveď musí spĺňať tieto prísne kritériá:
1. Odpovedajte striktne na základe priloženého kontextu. Ak kontext neobsahuje odpoveď, jasne napíšte, že informáciu neviete dohľadať a nešpekulujte.
2. V odpovedi uvádzajte presné citácie na zdroje vo formáte [Zdroj X].
3. Nepoužívajte žiadne informácie z vašich globálnych tréningových dát, ktoré nie sú podložené priloženým kontextom.
4. Odpovedajte profesionálnym a vecným tónom v slovenskom jazyku.`;

      const response = await this.openai.chat.completions.create({
        model: "gpt-4o",
        temperature: 0.0, // Nízka teplota obmedzuje kreatívne dopĺňanie mimo kontextu
        messages: [
          { role: "system", content: systemPrompt },
          { role: "user", content: `Dostupné firemné smernice:\n${contextText}\n\nOtázka zamestnanca: ${userQuery}` }
        ]
      });

      const answer = response.choices[0]?.message?.content || "Chyba pri generovaní odpovede.";
      const uniqueSources = Array.from(new Set(contextMatches.map(m => m.metadata.title)));

      return {
        answer,
        sources: uniqueSources
      };

    } catch (error) {
      console.error("Zlyhala RAG dopytovacia pipeline:", error);
      throw new Error("Systémový výpadok interného asistenta.");
    }
  }
}

3. Prečo pgvector v Supabase a nie externé vektorové databázy?

Pri návrhu RAG systémov sa mnohé vývojárske tímy nechajú zlákať na samostatné vektorové databázy, ako Pinecone alebo Milvus. Pre zavedenú firmu však takýto prístup často prináša ďalšie licenčné náklady, potrebu synchronizovať dáta medzi hlavnou PostgreSQL databázou a vektorovou databázou a nové bezpečnostné riziká.

V nolimeu často presadzujeme integráciu vektorového vyhľadávania priamo do relačnej databázy pomocou rozšírenia pgvector v PostgreSQL alebo Supabase.

Výhoda 1: Row Level Security (RLS) pre prístupové práva

Najväčším rizikom zdieľaného firemného vyhľadávania je neoprávnený prístup k citlivým dátam. Ak má asistent prístup k mzdovým smerniciam, zmluvám manažmentu a zároveň k bežným prevádzkovým poriadkom, radový zamestnanec by sa nemal spýtať na platové podmienky svojich kolegov.

Pri použití pgvector v PostgreSQL môžeme definovať Row Level Security (RLS) pravidlá, vďaka ktorým sémantické vyhľadávanie prehľadá len tie dokumenty, na ktoré má prihlásený používateľ oprávnenie podľa svojej roly v systéme.

-- Príklad SQL definície RLS pre tabuľku document_chunks v PostgreSQL
ALTER TABLE document_chunks ENABLE ROW LEVEL SECURITY;

CREATE POLICY user_access_policy ON document_chunks
    FOR SELECT
    USING (
        -- Chunk sa sprístupní len vtedy, ak rola používateľa zodpovedá oprávneniam dokumentu
        EXISTS (
            SELECT 1 FROM documents d
            WHERE d.id = document_chunks.document_id
            AND d.required_role <= auth.jwt()->>'user_role'
        )
    );

Výhoda 2: Databázová integrita a transakcie

Ak zamestnanec aktualizuje interný dokument v ERP alebo CMS, zmena sa môže v pgvector prejaviť v rámci jednej databázovej transakcie. Znižuje sa tak riziko, že dokument zmažete, no jeho vektorové chunky zostanú visieť v externej databáze a asistent ich bude ďalej citovať.


4. Skrytá realita RAG systémov: Na čo si dať pozor

RAG vyzerá na papieri jednoducho, no prechod z prototypu do stabilnej firemnej prevádzky prináša reálne inžinierske výzvy. Robustný systém sa spozná podľa toho, ako rieši nasledujúce hraničné stavy:

4.1. Zastaralé informácie (Stale Data)

Ak sa zmení smernica o BOZP (Bezpečnosť a ochrana zdravia pri práci), stará verzia dokumentu musí byť okamžite zneplatnená. RAG systém nesmie miešať informácie z roku 2022 s novými pravidlami z roku 2025.

  • Ako to riešime: Každý dokument má priradené číslo verzie a príznak aktívnosti. Pri vyhľadávaní sa filtrujú iba úryvky z aktuálne platných verzií dokumentov. Pri nahratí novej smernice systém prepíše alebo nanovo naimportuje chunky prislúchajúce danému ID dokumentu.

4.2. Lost in the Middle

Veľké jazykové modely venujú najviac pozornosti začiatku a koncu poskytnutého kontextu. Informácie umiestnené v strede dlhého textu môžu prehliadnuť. Ak do promptu vložíte 10 veľkých chunkov, dôležitý detail uprostred sa môže stratiť.

  • Ako to riešime: Používame zoradenie výsledkov podľa relevancie a limitujeme počet odoslaných chunkov, spravidla na top-3 až top-5. Pri náročnejších projektoch implementujeme aj re-ranking, napríklad pomocou Cohere ReRank, aby sa najdôležitejšie pasáže dostali na začiatok kontextu.

4.3. Nízke sémantické skóre (No-Match scenár)

Ak sa zamestnanec spýta na recept na domáci guláš, vektorové vyhľadávanie v databáze firemných zmlúv aj tak vráti nejaké výsledky, pretože kosínusová podobnosť vždy nájde „najbližšie“ vektory, aj keď vecne nesúvisia.

  • Ako to riešime: V kóde (Krok B) overujeme prah podobnosti (match_threshold). Ak najlepší výsledok dosiahne podobnosť nižšiu ako 0.65, dopytovanie sa zastaví a asistent odpovie preddefinovanou správou: „Na vašu otázku som nenašiel relevantné informácie vo firemných dokumentoch.“ Tým znižujeme riziko, že si model začne odpoveď domýšľať.

Záver: Firemné vyhľadávanie musí byť presné, overiteľné a bezpečné

Interné vyhľadávanie postavené na architektúre RAG a pgvector dokáže výrazne zrýchliť prácu s firemnými smernicami, zmluvami a technickou dokumentáciou. Zamestnanci nemusia čítať stovky strán právnych a technických textov, ale pracujú s odpoveďou, ktorú si môžu overiť v zdrojovom dokumente.

Pri implementácii je však kľúčové vyhnúť sa krehkým no-code riešeniam, nejasným prístupovým právam a architektúre, ktorá nevie udržať aktuálnosť dokumentov. Firemné znalosti sú príliš cenné na to, aby boli napojené na náhodne poskladaný prototyp.

Sme technologické štúdio nolimeo. Navrhujeme firemné integrácie, RAG systémy a bezpečné AI riešenia pre B2B firmy, ktoré potrebujú pracovať s internými dátami bez chaosu, únikov a neoveriteľných odpovedí.

Chcete nasadiť bezpečné interné vyhľadávanie nad firemnými smernicami, dokumentmi alebo zmluvami? Napíšte nám a prejdeme si dáta, prístupy aj vhodnú RAG architektúru.

Máte záujem posunúť váš projekt vpred?