AI Engineering: RAG chatbot építése a nullától
OpenAI
ChromaDB
Ebben a kurzusban egy webshop ügyfélszolgálati RAG chatbotot építünk lépésról lépésre. A végén lesz egy můködő rendszered, ami dokumentumokat tölt be, embeddingeket generál, szemantikus keresést végez, választ generál, eval metrikákat mér és cost/latency-t monitoroz.
A WebShop Pro egy fiktív webshop termékkatalógussal, visszaküldési szabályzattal, szállítási FAQ-val.
A kurzus végére elkészítesz egy lokálisan futó RAG chatbotot dokumentumbetöltéssel, chunkinggal, embeddinggel, vektoros kereséssel, válaszgenerálással, eval metrikákkal és cost/latency monitoringgal.
Fogalmi térkép: LLM, RAG, Agent, Embedding
OpenAI
| Fogalom | Magyarázat | Analógia |
|---|---|---|
| LLM | Nagy nyelvi modell - szövegből szöveget generál | Mint egy nagyon olvasott szakember |
| RAG | Retrieval-Augmented Generation: külső tudásbázisból keres, az alapján válaszol | A szakember felolvas egy dokumentumot, abból válaszol |
| Embedding | Szöveg sůrített vektor-ábrázolása | Dokumentum koordinátája egy értelem-térben |
| Vector DB | Hasonlóság alapján keres vektorok között | Könyvtár, ahol a hasonló könyvek egymás mellett vannak |
| Agent | LLM, ami eszközöket hívhat, tervez és iterál | A szakember telefonál, keres, számol, aztán válaszol |
LLM egyedül: "Mit tudsz a visszaküldési szabályzatról?" → generalizál, hallucinálhat
LLM + RAG: → keres a dokumentumban → pontos válasz forrással
Architektúra-térkép: a RAG pipeline
ChromaDB
OpenAI
A végcél egy ilyen pipeline:
Demóban: néhány dokumentum, lokális ChromaDB, egy API hívás
Productionben: dokumentum verziókezelés, embedding cache, rate limiting, fallback modellek, observability, auth, SLA
Környezet előkészítése
Python
Mielőtt elkezdenénk a RAG chatbot építését, be kell állítanunk a fejlesztői környezetünket. Ehhez három fő Python csomagra van szükségünk: az openai könyvtár az LLM és embedding API hívásokhoz, a chromadb a vektoradatbázis műveleteihez, és a numpy a numerikus számításokhoz, például a koszinusz-hasonlóság kiszámításához.
Az OpenAI() kliens automatikusan az OPENAI_API_KEY környezeti változóból olvassa be az API kulcsot. Ha ez nincs beállítva, a hívások hibát dobnak. Éles környezetben soha ne hardkódold az API kulcsot a forráskódba -- használj .env fájlt vagy secret managert.
A kódban látod az alapvető importokat: az os és json modulok fájl- és adatkezeléshez, a time pedig a válaszidő méréséhez kell majd a későbbi szekciókban. Minden további szakasz ezekre az alapokra épül.
Futtasd a terminálban: pip install openai chromadb numpy. Az API kulcsot a export OPENAI_API_KEY="sk-..." paranccsal állítsd be, vagy hozz létre egy .env fájlt.
# pip install openai chromadb numpy import os, json, time from openai import OpenAI client = OpenAI() # OPENAI_API_KEY a környezeti változóból print("OpenAI client initialized")
[openai] Client initialized successfully API key loaded from environment variable
Responses API alapok
Responses API
Az OpenAI új Responses API-ja a javasolt mód LLM hívásokhoz. A régi Chat Completions támogatott, de új projektekhez a Responses API-t használjuk.
| Chat Completions (régi) | Responses API (új) |
|---|---|
client.chat.completions.create() | client.responses.create() |
messages=[...] | instructions="..." + input="..." |
| Nincs beépített tool | Beépített file_search, code_interpreter |
response = client.responses.create(
model="gpt-4o",
instructions="Te egy webshop ügyfélszolgálati asszisztens vagy. Rövid, pontos válaszokat adj.",
input="Mennyi idő alatt érkezik meg a csomag?"
)
print(response.output_text)[openai] responses.create() model=gpt-4o Response received: A szállítási idő a választott módtól függ: - Standard: 3-5 munkanap - Expressz: 1-2 munkanap - Aznapi: rendelés 12:00-ig
A response.output_text a válasz szövege. Ne keverd a response.choices[0].message.content-tel (az a régi Chat Completions).
Instructions vs Input
OpenAI
instructions — rendszer-szintű, hogyan viselkedjen a modell. Rögzített.
input — a konkrét kérdés, hívásonként változik.
SYSTEM_INSTRUCTIONS = """ Te a WebShop Pro ügyfélszolgálati botja vagy. Szabályok: 1. Csak a megadott tudásbázisból válaszolj 2. Ha nem tudod: "Továbbítom a csapatnak." 3. Soha ne találj ki információt 4. Mindig magyarul válaszolj """ kerdes = "Vissza tudok küldeni egy 14 napja vásárolt terméket?" response = client.responses.create( model="gpt-4o", instructions=SYSTEM_INSTRUCTIONS, input=kerdes ) print(response.output_text)
[openai] responses.create() with system instructions Response received: Igen, a terméket a vásárlástól számított 30 napon belül visszaküldheted, feltéve ha eredeti állapotú.
Ne "think step by step"-et használj. Helyette:
- Structured reasoning: "Elemzd a kérdést, azonosítsd a kulcsfogalmakat, aztán válaszolj."
- Decomposition: "Bontsd fel a problémát részfeladatokra."
- Explainable format: "Formátum: [Forrás] [Érvelés] [Válasz]"
Structured outputs: JSON válasz
OpenAI
A structured output (strukturált kimenet) azt jelenti, hogy az LLM válaszát nem szabad szabad formátumú szövegként kezelnünk, hanem egy előre meghatározott JSON sémának megfelelő objektumként kapjuk vissza. Ez kulcsfontosságú RAG rendszerekben, ahol a válasz mellé forrást, megbízhatósági pontszámot és kategóriát is vissza kell adni a downstream rendszerek számára.
A Responses API beépítetten támogatja a JSON schema kikényszerítést a text.format paraméterén keresztül. Nem kell kérned az LLM-et, hogy "válaszolj JSON-ban" és remélni a legjobbakat -- az API garantálja, hogy a válasz illeszkedik a megadott sémára. Ez azért forradalmi, mert megszünteti a kimenet parsing hibáit, amelyek a termelési rendszerek egyik leggyakoribb hibaforrásai.
A példában egy support_answer sémát definiálunk három mezővel: answer (a tényleges válaszszöveg), category (enum: szallitas, visszakuldes, garancia, egyeb), és confidence (0-1 közötti szám). A modell mindig ezt a struktúrát adja vissza, így a frontend kód biztonsággal dolgozhatja fel.
Ne keverd a response.output_text-et a response.choices[0].message.content-tel. Előbbi az új Responses API, utóbbi a régi Chat Completions formátum. A structured output esetén az output_text egy JSON string, amit json.loads()-szal kell parse-olni.
import json response = client.responses.create( model="gpt-4o", instructions="Válaszolj JSON-ban a megadott sémának megfelelően.", input="Mennyibe kerül a szállítás?", text={ "format": { "type": "json_schema", "name": "support_answer", "schema": { "type": "object", "properties": { "answer": {"type": "string"}, "category": {"type": "string", "enum": ["szallitas", "visszakuldes", "garancia", "egyeb"]}, "confidence": {"type": "number"} }, "required": ["answer", "category", "confidence"] } } } ) result = json.loads(response.output_text) print(json.dumps(result, indent=2, ensure_ascii=False))
[openai] responses.create() structured output (JSON schema) Parsed JSON response: { "answer": "A standard szállítás 990 Ft, expressz 2490 Ft.", "category": "szallitas", "confidence": 0.95 }
Demóban: JSON schema validáció. Productionben: schema verziókezelés, backward compat, fallback, validation middleware.
Tool calling: függvények hívása
OpenAI
A tool calling (eszközhívás) az a képesség, amellyel az LLM képes külső függvényeket meghívni a válaszgenerálás során. Nem te magad döntöd el, mikor hívod a függvényt -- a modell maga ismeri fel a szituációt, és automatikusan kéri a függvény meghívását, ha szükséges. Ez teszi az LLM-et "ügynökké": önállóan tervez és cselekszik.
A folyamat lépései: (1) elküldöd a kérdést az LLM-nek a definiált eszközökkel együtt, (2) a modell felismeri, hogy egy tool-t kell hívnia, és visszaadja a függvény nevét és paramétereit, (3) te futtatod a saját kódodat az eredménnyel, (4) visszaküldöd az eredményt a modellnek, (5) a modell a végleges választ generálja az eredmény alapján.
A példában egy get_order_status függvényt definiálunk, amely rendelésszám alapján lekérdezi a rendelés státuszát. Amikor a felhasználó azt kérdezi, "Mi a helyzet a WSP-12345 rendeléssel?", a modell felismeri, hogy rendelésszám van a szövegben, és automatikusan meghívja a tool-t.
A tool definícióban a description mező kritikus: a modell ebből érti meg, mikor használja az eszközt. Minél pontosabb a leírás, annál kevesebb a téves hívás. Mindig adj magyar nyelvű, egyértelmű leírást.
# Tool definíció tools = [{ "type": "function", "function": { "name": "get_order_status", "description": "Rendelés státuszának lekérdezése rendelésszám alapján", "parameters": { "type": "object", "properties": { "order_id": {"type": "string", "description": "Rendelésszám, pl. WSP-12345"} }, "required": ["order_id"] } } }] # Mock implementáció def get_order_status(order_id): orders = { "WSP-12345": {"status": "szállítás alatt", "eta": "2025-01-15"}, "WSP-67890": {"status": "kézbesítve", "delivered": "2025-01-10"}, } return orders.get(order_id, {"status": "nem található"}) response = client.responses.create( model="gpt-4o", instructions="Te a WebShop Pro support bot vagy.", input="Mi a helyzet a WSP-12345 rendeléssel?", tools=tools ) print(response.output_text)
[openai] tool_call: get_order_status(order_id="WSP-12345") Tool response received: {"status": "szállítás alatt", "eta": "2025-01-15"} Final answer: A WSP-12345 számú rendelés jelenleg szállítás alatt van, várható kézbesítés: 2025. január 15.
🔬 Feladat: Adj hozzá egy új tool-t
Definiálj egy search_products tool-t, ami termékekre keres. Paraméter: query (string). A mock implementáció adjon vissza 2-3 terméket.
Embedding fogalmak: szövegekből vektorok
OpenAI
Az embedding a szöveg numerikus ábrázolása. Minden szövegrészlet egy vektor lesz, ami a jelentését kódolja.
Példa: "A szállítás 3 nap." → [0.12, -0.34, 0.78, ..., 0.05] (1536 dimenzió)
| Kérdés | Leghasonlóbb | Hasonlóság |
|---|---|---|
| "szállítási idő" | "csomag érkezése" | 0.91 |
| "szállítási idő" | "visszaküldés" | 0.42 |
# Koszinusz-hasonlóság demó import numpy as np def cosine_similarity(a, b): return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) # Szimulált embeddingek (valóságban 1536 dimenziós) vec_szallitas = np.array([0.8, 0.2, 0.1]) vec_csomag = np.array([0.7, 0.3, 0.1]) vec_vissza = np.array([0.1, 0.2, 0.8]) print(f"szállítás vs csomag: {cosine_similarity(vec_szallitas, vec_csomag):.2f}") print(f"szállítás vs visszaküldés: {cosine_similarity(vec_szallitas, vec_vissza):.2f}")
[similarity] Computing cosine distances... szállítás vs csomag: 0.97 szállítás vs visszaküldés: 0.30
A kulcsszó-keresés nem találja meg a szinonimákat. "Futár" és "szállító" ugyanazt jelenti, de más szó. Az embeddingek ezt "értik".
OpenAI Embeddings API
OpenAI
Az OpenAI text-embedding-3-small modellje a legköltséghatékonyabb embedding megoldás: 1536 dimenziós vektorokat generál, és nagyjából $0.02 dollárba kerül 1 millió tokenenként. Ez azt jelenti, hogy 1000 átlagos dokumentum (kb. 500K token) feldolgozása mindössze 1 centbe kerül -- ez teszi praktikussá a kísérletezést és a prototípusépítést.
Az embeddings.create() hívás egy vektorlistát ad vissza: minden bemeneti szöveghez egy 1536 lebegőpontos számból álló tömböt. Ezek a számok a szöveg szemantikai jelentését kódolják -- a hasonló jelentésű szövegek vektorai közel helyezkednek el egymáshoz a vektortérben. Az API válasza tartalmazza a felhasznált tokenek számát is, ami a költségszámításhoz szükséges.
Éles környezetben érdemes az embeddingeket gyorsítótárazni, mert ugyanarra a szövegre mindig ugyanazt a vektort kapod. Ha a dokumentumaid nem változnak, felesleges minden alkalommal újra generálni az embeddingeket -- tárold őket a vektoradatbázisban, és csak az új vagy módosított dokumentumokat töltsd fel.
A text-embedding-3-small olcsó és gyors, a legtöbb RAG használathoz elegendő. Ha maximális pontosság kell, próbáld a text-embedding-3-large modellt (3072 dimenzió, kb. 3x drágább). A dimenziócsökkentés is támogatott az API-ban.
emb_response = client.embeddings.create(
model="text-embedding-3-small",
input="Mennyi idő alatt érkezik meg a csomag?"
)
vector = emb_response.data[0].embedding
print(f"Dimenzió: {len(vector)}")
print(f"Első 5 érték: {vector[:5]}")
print(f"Token használat: {emb_response.usage.total_tokens}")[openai] embeddings.create() model=text-embedding-3-small Embedding generated: Dimenzió: 1536 Első 5 érték: [0.0123, -0.0456, 0.0789, -0.0012, 0.0345] Token használat: 12
text-embedding-3-small: ~$0.02 / 1M token. 1000 dokumentum ~ 500K token ~ $0.01
ChromaDB alapok: vektoradatbázis
ChromaDB
A ChromaDB egy nyílt forráskódú vektoradatbázis, amelyet kifejezetten AI alkalmazásokhoz terveztek. Lokálisan futtatható, nem kell hozzá külön szerver vagy felhőszolgáltatás -- ez teszi ideálissá a fejlesztéshez és prototípusokhoz. Három alapvető művelete van: add (dokumentumok hozzáadása), query (hasonlósági keresés) és delete (eltávolítás).
A ChromaDB kulcsfontosságú eleme a RAG rendszernek: itt tároljuk a dokumentumok embeddingjeit, és ide küldjük a felhasználói kérdés embeddingjét kereséskor. A ChromaDB automatikusan generálja az embeddingeket, ha nem adsz meg saját embedding funkciót -- a háttérben egy alapértelmezett modellt használ ehhez. Éles környezetben érdemes saját OpenAI embedding funkciót megadni a jobb minőség érdekében.
A kódban egy webshop_docs nevű gyűjteményt (collection) hozunk létre, majd három dokumentumot adunk hozzá. Minden dokumentum kap egy egyedi azonosítót (doc1, doc2, doc3), és a ChromaDB automatikusan elkészíti a vektorábrázolásukat. A count() metódus visszaadja a tárolt dokumentumok számát.
A demóban chromadb.Client() in-memory módot használ -- a adatok a program leállásakor elvesznek. Éles használatra chromadb.PersistentClient(path="./chroma_data") használatával a adatok lemezre íródnak, és újraindítás után is megmaradnak.
import chromadb # Lokális ChromaDB (in-memory demo) chroma_client = chromadb.Client() collection = chroma_client.create_collection( name="webshop_docs", metadata={"description": "WebShop Pro dokumentumok"} ) # Dokumentumok hozzáadása collection.add( documents=[ "A standard szállítás 3-5 munkanapot vesz igénybe.", "A visszaküldési határidő 30 nap a vásárlástól számítva.", "A garancia 2 év, a számla szükséges hozzá." ], ids=["doc1", "doc2", "doc3"] ) print(f"Dokumentumok száma: {collection.count()}")
[chroma] create_collection() name="webshop_docs" [chroma] add() 3 documents added Dokumentumok száma: 3
# Keresés a gyűjteményben results = collection.query( query_texts=["meddig tarthatja meg a terméket?"], n_results=2 ) print("Legjobb találatok:") for doc, dist in zip(results["documents"][0], results["distances"][0]): print(f" [{dist:.3f}] {doc}")
[chroma] query() query="meddig tarthatja meg a terméket?" n_results=2 Legjobb találatok: [0.234] A visszaküldési határidő 30 nap a vásárlástól számítva. [0.567] A garancia 2 év, a számla szükséges hozzá.
Demóban: in-memory, 3 dokumentum. Productionben: persist_directory, több ezer dokumentum, metadata filtering, embedding function override.
Chunking stratégiák
Python
A dokumentumokat darabolni kell, mert az LLM kontextusablaka véges és a vektoros keresés kis egységeken működik a legjobban.
| Stratégia | Előny | Hátrány |
|---|---|---|
| Fix méret (pl. 500 karakter) | Egyszerű | Vághat mondat közben |
| Mondat alapú | Értelmes egységek | Változó méret |
| Átfedéses (overlap) | Nem vész el kontextus | Duplikáció |
def chunk_text(text, chunk_size=200, overlap=50): chunks = [] start = 0 while start < len(text): end = start + chunk_size chunks.append(text[start:end]) start += chunk_size - overlap return chunks szabalyzat = """A WebShop Pro visszaküldési szabályzata: 1. A terméket a vásárlástól számított 30 napon belül vissza lehet küldeni. 2. A terméknek eredeti állapotban kell lennie. 3. A csomagolásnak érintetlennek kell lennie. 4. A visszaküldés ingyenes, a futár viszi a csomagot. 5. A visszatérítés 5 munkanapon belül megtörténik. 6. Elektromos termékekre a garancia 2 év érvényes.""" chunks = chunk_text(szabalyzat, chunk_size=150, overlap=30) for i, chunk in enumerate(chunks): print(f"--- Chunk {i+1} ({len(chunk)} chars) ---") print(chunk[:80] + "...")
[chunk] chunk_text(size=150, overlap=30) --- Chunk 1 (150 chars) --- A WebShop Pro visszaküldési szabályzata: 1. A terméket a vásárlástól számított 30 napon belül vissza lehet küldeni... --- Chunk 2 (150 chars) --- letben kell lennie. 3. A csomagolásnak érintetlennek kell lennie. 4. A visszaküldés ingyenes, a futár viszi a csomagot... --- Chunk 3 (79 chars) --- napon belül megtörténik. 6. Elektromos termékekre a garancia 2 év érvényes....
Dokumentumok betöltése: WebShop Pro tudásbázis
Python
A WebShop Pro tudásbázisa négy dokumentumból áll: szállítási tájékoztató, visszaküldési szabályzat, garanciális feltételek és gyakori kérdések. A valóságban ezek PDF dokumentumok, Notion oldalak, Confluence wikik vagy akár adatbázis-lekérdezések eredményei lennének. Most egyszerű Python dictionary-kben tároljuk őket a demonstráció kedvéért.
A dokumentumok betöltése után a korábban definiált chunk_text() függvénnyel daraboljuk őket, és minden chunkhoz metaadatot rendelünk -- konkrétan a forrásdokumentum címét. Ez a metaadat később lehetővé teszi, hogy a RAG válaszban hivatkozzunk a forrásra, ami kritikus a felhasználói bizalom és a válaszok ellenőrizhetősége szempontjából.
Az all_chunks lista végül az összes feldarabolt szövegrészt tartalmazza azonosítóval, szöveggel és forrással. Ez a strukturált formátum teszi lehetővé a ChromaDB-be való feltöltést a következő szekcióban. Figyeld meg, hogy minden chunk egyedi ID-t kap (szallitas_chunk0, szallitas_chunk1, stb.), ami a későbbi frissítések és törlések esetén is fontos.
Éles rendszerben a dokumentumok betöltése gyakran a legbonyolultabb rész: PDF-ekből szöveget kinyerni, táblázatokat kezelni, képek OCR-ét elvégezni. Ehhez érdemes olyan könyvtárakat használni, mint a unstructured, PyPDF2 vagy docling.
# WebShop Pro tudásbázis DOCUMENTS = [ {"id": "szallitas", "title": "Szállítási tájékoztató", "text": """A WebShop Pro szállítási lehetőségei: - Standard szállítás: 990 Ft, 3-5 munkanap - Expressz szállítás: 2490 Ft, 1-2 munkanap - Aznapi kézbesítés: 3990 Ft, rendelés 12:00-ig - Ingyenes szállítás 15000 Ft feletti rendelések esetén - Nemzetközi szállítás: 5-10 munkanap"""}, {"id": "visszakuldes", "title": "Visszaküldési szabályzat", "text": """Visszaküldési feltételek: - 30 nap a vásárlástól - Eredeti csomagolás - Érintetlen állapot - Ingyenes visszaküldés - 5 munkanapon belüli visszatérítés - Elektromos termékekre 2 év garancia"""}, {"id": "garancia", "title": "Garanciális feltételek", "text": """Garanciális feltételek: - 2 év garancia minden elektromos termékre - Számla szükséges - A gyártói hibát a szerviz állapítja meg - 14 napon belül csere vagy javítás - Nem fedett: szándékos rongálás, vízrongálás"""}, {"id": "faq", "title": "Gyakori kérdések", "text": """GYIK: - Nyomon követhető a csomag? Igen, a rendeléskor kapott linken. - Lehet különböző címre szállítani? Igen, a pénztárnál megadható. - Elfogadtok utalványt? Igen, WebShop Pro utalvány és kupon is. - Telefonon is lehet rendelni? Igen, hétköznap 8-16 óra között."""} ] # Chunking + metadata all_chunks = [] for doc in DOCUMENTS: chunks = chunk_text(doc["text"], chunk_size=200, overlap=50) for i, chunk in enumerate(chunks): all_chunks.append({ "id": f"{doc['id']}_chunk{i}", "text": chunk, "source": doc["title"] }) print(f"Dokumentumok: {len(DOCUMENTS)}, Chunkok: {len(all_chunks)}")
[load] Processing 4 documents with chunk_text(size=200, overlap=50) Dokumentumok: 4, Chunkok: 7
Indexelés: ChromaDB feltöltése
ChromaDB
OpenAI
Az indexelés az a lépés, ahol a feldarabolt dokumentumchunkokat betöltjük a ChromaDB-be. A ChromaDB automatikusan generál embeddingeket minden chunkhoz, és indexet épít a gyors hasonlósági kereséshez. Ez a lépés "előkészíti a terepet" a későbbi lekérdezések számára -- indexelés nélkül nem tudunk keresni.
A kódban egy új webshop_rag gyűjteményt hozunk létre, és az add() metódussal feltöltjük a chunkokkal. Három paramétert adunk át: documents (a chunk szövegei), ids (egyedi azonosítók) és metadatas (forrásinformáció). A metadata filtering lehetővé teszi, hogy később csak bizonyos forrásokra keressünk -- például csak a szállítási dokumentumok között.
Éles környezetben az indexelés egy batch folyamat, amely ütemezetten fut (pl. naponta), és frissíti a vektoradatbázist az új vagy módosított dokumentumokkal. Inkrementális frissítés esetén csak a változott chunkokat kell újra indexelni, nem az egész tudásbázist. A ChromaDB upsert művelete is támogatja ezt a használati esetet.
Néhány ezer dokumentum indexelése másodpercek alatt megtörténik. Nagyobb tudásbázisok (millió dokumentum) esetén érdemes dedikált vektoradatbázist használni (Pinecone, Weaviate, Qdrant), amelyek oszlopos indexeléssel és shardinggal gyorsítják a keresést.
rag_collection = chroma_client.create_collection(name="webshop_rag") rag_collection.add( documents=[c["text"] for c in all_chunks], ids=[c["id"] for c in all_chunks], metadatas=[{"source": c["source"]} for c in all_chunks] ) print(f"Indexed chunks: {rag_collection.count()}")
[chroma] create_collection() name="webshop_rag" [chroma] add() 7 chunks with metadata Indexed chunks: 7
Vektoros keresés: a releváns chunkok megkeresése
ChromaDB
A vektoros keresés (vector search) a RAG rendszer szíve: a felhasználói kérdést embedding-gé alakítjuk, majd a ChromaDB megkeresi a hozzá legközelebbi vektorokat a vektortérben. Ez a retrieval (visszakeresés) lépés -- a RAG rövidítés első betűje. Minél relevánsabb chunkokat találunk, annál jobb választ tud generálni az LLM.
A retrieve() függvény a ChromaDB query() metódusát használja: elküldi a kérdés szövegét, és a ChromaDB automatikusan embedding-gé alakítja, majd koszinusz-hasonlóság alapján megkeresi a n_results darab legközelebbi chunkot. Az eredmény tartalmazza a dokumentum szövegét, a metaadatokat (forrás) és a távolságot -- minél kisebb a távolság, annál relevánsabb a találat.
A példában a "Mennyibe kerül a szállítás?" kérdésre a rendszer a szállítási dokumentum chunkjait találja meg a legkisebb távolsággal. A távolság értékek tipikusan 0 és 2 között mozognak, ahol a 0 tökéletes egyezést jelent. Éles környezetben érdemes beállítani egy távolság-küszöböt: ha a legjobb találat is túl messze van, a rendszer jelezheti, hogy nem talál releváns információt.
A sima vektoros keresés négyzetesen rossz lehet kulcsszó-pontos lekérdezéseknél (pl. "WSP-12345 rendelés"). Éles környezetben érdemes hibrid keresést használni: a vektoros keresés eredményeit kombinálja kulcsszavas kereséssel (BM25), így mind a szemantikai, mind a lexikai pontosság megmarad.
def retrieve(query, n_results=3): results = rag_collection.query( query_texts=[query], n_results=n_results ) docs = results["documents"][0] sources = [m["source"] for m in results["metadatas"][0]] distances = results["distances"][0] return list(zip(docs, sources, distances)) # Teszt results = retrieve("Mennyibe kerül a szállítás?") for doc, source, dist in results: print(f"[{dist:.3f}] ({source})") print(f" {doc[:80]}...")
[chroma] query() query="Mennyibe kerül a szállítás?" n_results=3 Retrieved 3 results: [0.189] (Szállítási tájékoztató) A WebShop Pro szállítási lehetőségei: - Standard szállítás: 990 Ft, 3-5 munkanap... [0.312] (Szállítási tájékoztató) - Nemzetközi szállítás: 5-10 munkanap... [0.567] (Gyakori kérdések) Elfogadtok utalványt? Igen, WebShop Pro utalvány és kupon is...
🔬 Feladat: Keresés tesztelése
Próbáld meg a retrieve("garancia") hívást. Melyik dokumentum a legközelebbi? Milyen távolsággal?
RAG pipeline: retrieve → augment → generate
OpenAI
ChromaDB
Most összerakjuk az egészet! A RAG pipeline három lépése:
- Retrieve: kérdés → vektoros keresés → releváns chunkok
- Augment: chunkok + kérdés → egyetlen prompt
- Generate: prompt → LLM → válasz
def rag_query(question, n_results=3): # 1. RETRIEVE retrieved = retrieve(question, n_results=n_results) context = "\n\n".join([doc for doc, _, _ in retrieved]) # 2. AUGMENT augmented_input = f"""Kérdés: {question} Tudásbázis: {context} Válaszolj a kérdésre a tudásbázis alapján. Ha nincs benne, mondd: "Ezt a kérdést továbbítom a csapatnak.""" # 3. GENERATE response = client.responses.create( model="gpt-4o", instructions=SYSTEM_INSTRUCTIONS, input=augmented_input ) return response.output_text # Teszteljük! for q in ["Mennyibe kerül a szállítás?", "Meddig garanciás a telefon?"]: print(f"Q: {q}") print(f"A: {rag_query(q)}") print()
[chroma] retrieve() → context gathered [openai] responses.create() → generating answer... Q: Mennyibe kerül a szállítás? A: A standard szállítás 990 Ft (3-5 munkanap), az expressz 2490 Ft (1-2 munkanap), az aznapi 3990 Ft. 15000 Ft felett ingyenes. [chroma] retrieve() → context gathered [openai] responses.create() → generating answer... Q: Meddig garanciás a telefon? A: Elektromos termékekre 2 év garancia érvényes. A számla szükséges hozzá. 14 napon belül csere vagy javítás.
Demóban: egy függvény, mindig ugyanaz. Productionben: conversation history, streaming, fallback modellek, rate limiting, cache, re-ranking, hybrid search (keyword + vector), guardrails.
🔬 Feladat: Hallucináció teszt
Kérdezz olyat, ami nincs a tudásbázisban: rag_query("Mennyi a kedvezmény VIP vásárlóknak?"). Mit válaszol?
Evaluáció: a RAG minőségének mérése
OpenAI
Honnan tudjuk, hogy a RAG rendszer jól működik? Eval metrikák:
| Metrika | Mit mér? | Jó érték |
|---|---|---|
| Faithfulness | A válasz a forrásból származik-e | > 0.9 |
| Relevance | A válasz releváns-e a kérdésre | > 0.8 |
| Hallucination rate | Aránya a nem forrás-alapú válaszoknak | < 0.1 |
| Retrieval precision | A visszakeresett chunkok relevancia aránya | > 0.7 |
# Egyszerű eval: LLM-as-judge def evaluate_answer(question, answer, context): eval_response = client.responses.create( model="gpt-4o", instructions="""Értékeld a válasz minőségét. Adj 0-1 közötti pontszámot: - faithfulness: a válasz a kontextusból származik-e - relevance: a válasz releváns-e a kérdésre Válaszolj JSON-ban.""", input=f"""Kérdés: {question} Válasz: {answer} Kontextus: {context}""", text={"format": {"type": "json_schema", "name": "eval", "schema": {"type": "object", "properties": { "faithfulness": {"type": "number"}, "relevance": {"type": "number"}, "reasoning": {"type": "string"}}, "required": ["faithfulness", "relevance", "reasoning"]}}} ) return json.loads(eval_response.output_text) # Eval teszt q = "Mennyibe kerül a szállítás?" a = "A standard szállítás 990 Ft." ctx = "A WebShop Pro szállítási lehetőségei: Standard: 990 Ft, 3-5 munkanap" result = evaluate_answer(q, a, ctx) print(json.dumps(result, indent=2, ensure_ascii=False))
[openai] evaluate_answer() LLM-as-judge Eval result: { "faithfulness": 0.95, "relevance": 0.9, "reasoning": "A válasz pontosan a kontextusból származik és közvetlenül megválaszolja a kérdést." }
Cost és latency tracking
Grafana
A költség- és válaszidő-monitorozás (cost és latency tracking) elengedhetetlen minden termelési RAG rendszernél. Egyetlen GPT-4o hívás $0.01-0.05 dollárba kerülhet, és egy rosszul optimalizált RAG pipeline könnyen napi tíz vagy száz dolláros számlát generálhat. A válaszidő pedig közvetlenül befolyásolja a felhasználói élményt -- ha a chatbot 5 másodpercig gondolkodik, a felhasználók elhagyják az oldalt.
A rag_query_tracked() függvény egy wrapper, amely megméri a teljes folyamat és az LLM hívás külön-külön eltelt idejét. Ez a felbontás fontos, mert a retrieval lokális és gyors (ezredmásodpercek), míg az LLM hívás távoli API és lassabb (száz milliszekundumoktól másodpercekig). A szűk keresztmetszet azonosítása segíti az optimalizálást.
A token szám alapján kiszámítható a költség: GPT-4o esetén $2.50/1M input token és $10/1M output token. A response.usage objektum tartalmazza a bemeneti és kimeneti tokenek számát, amiből automatizáltan számolható a költség. Éles rendszerben ezeket az adatokat Prometheus vagy Grafana dashboardra küldjük, és alertinget állítunk be a küszöbértékekre.
A költség csökkentésére: (1) használj kisebb modellt (GPT-4o-mini) egyszerű kérdéseknél, (2) cache-eld a gyakori kérdések válaszait, (3) korlátozd a kontextus hosszát. A válaszidő javítására: (4) használj streaming választ, (5) párhuzamosítsd a retrieval-t, (6) válassz gyorsabb embedding modellt.
# Cost és latency wrapper def rag_query_tracked(question): start = time.time() # Retrieval (lokális, gyors) retrieved = retrieve(question) context = "\n\n".join([doc for doc, _, _ in retrieved]) # LLM hívás (API, lassú) llm_start = time.time() response = client.responses.create( model="gpt-4o", instructions=SYSTEM_INSTRUCTIONS, input=f"Kérdés: {question}\n\nTudásbázis:\n{context}" ) llm_time = time.time() - llm_start total_time = time.time() - start # GPT-4o pricing: $2.50/1M input, $10/1M output # A response.usage tartalmazza a token számot print(f"Retrieval: {(total_time - llm_time)*1000:.0f}ms") print(f"LLM: {llm_time*1000:.0f}ms") print(f"Total: {total_time*1000:.0f}ms") return response.output_text print(rag_query_tracked("Mennyi a szállítás?"))
[chroma] retrieve() 12ms [openai] responses.create() 834ms Total: 846ms Answer: A standard szállítás 990 Ft (3-5 munkanap), expressz 2490 Ft (1-2 munkanap). 15000 Ft felett ingyenes.
Demóban: print. Productionben: Prometheus/Grafana dashboard, alerting ha latency > 2s vagy cost/nap > $X, per-endpoint breakdown, token usage per model.
Streamlit UI: chat frontend
Streamlit
A Streamlit egy Python webes keretrendszer, amely pár sornyi kóddal lehetővé teszi interaktív webes alkalmazások készítését -- HTML, CSS vagy JavaScript ismerete nélkül. A RAG chatbot esetén ez a leggyorsabb módja annak, hogy a háttérben futó Python logikát felhasználói felülettel lássuk el, és a chatbotot böngészőből lehessen elérni.
A kód három fő részből áll: (1) a chat előzmények tárolása a session_state-ben, ami minden felhasználói munkamenethez külön memóriát biztosít, (2) a korábbi üzenetek megjelenítése a st.chat_message() widgettel, amely automatikusan formázza a beszélgetést, és (3) az új üzenetek fogadása a st.chat_input() widgettel, amely egy beviteli mezőt jelenít meg a chat alján.
Amikor a felhasználó beír egy kérdést, a Streamlit meghívja a korábban definiált rag_query() függvényt, megkapja a választ, és megjeleníti az "assistant" szerepkörű üzenetként. A teljes beszélgetés előzmények megmaradnak a munkamenet alatt, így a felhasználó görgethet és visszanézheti a korábbi kérdéseket és válaszokat.
Mentsd el a kódot app.py néven, majd futtasd a streamlit run app.py paranccsal. A Streamlit automatikusan megnyitja a böngészőt a localhost:8501 címen. A kód minden módosítás után automatikusan újratöltődik a böngészőben.
# streamlit run app.py # pip install streamlit import streamlit as st st.title("WebShop Pro Support Bot") # Chat history a session_state-ben if "messages" not in st.session_state: st.session_state.messages = [] # Korábbi üzenetek megjelenítése for msg in st.session_state.messages: st.chat_message(msg["role"]).write(msg["content"]) # Új üzenet if prompt := st.chat_input("Kérdezz valamit..."): st.chat_message("user").write(prompt) st.session_state.messages.append({"role": "user", "content": prompt}) # RAG válasz response = rag_query(prompt) st.chat_message("assistant").write(response) st.session_state.messages.append({"role": "assistant", "content": response})
$ streamlit run app.py You can now view your Streamlit app in your browser. Local URL: http://localhost:8501 Network URL: http://192.168.1.100:8501
Összefoglalás
OpenAI
ChromaDB
Egy teljes RAG chatbot rendszer:
- ✅ OpenAI Responses API (nem Chat Completions)
- ✅ Instructions + Input szétválasztás
- ✅ Structured outputs (JSON schema)
- ✅ Tool calling (függvény hívás)
- ✅ Embedding fogalmak + OpenAI Embeddings API
- ✅ ChromaDB vektoradatbázis
- ✅ Chunking stratégiák
- ✅ Dokumentumbetöltés + indexelés
- ✅ Vektoros keresés (retrieval)
- ✅ RAG pipeline (retrieve → augment → generate)
- ✅ LLM-as-judge evaluáció
- ✅ Cost és latency tracking
- ✅ Streamlit chat UI
- AI Data Engineer kurzus: feature store, adat pipeline, webshop adatok
- MLOps kurzus: modell deployment, monitoring, drift detektálás
- Production RAG: re-ranking, hybrid search, guardrails, A/B testing
Architecture recap:
Embedding · RAG · Chunking