</> AI Engineering Crash Course

0 / 20 section completed
Intro

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.

Projekt: WebShop Pro Support Bot

A WebShop Pro egy fiktív webshop termékkatalógussal, visszaküldési szabályzattal, szállítási FAQ-val.

Learning outcome

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.

Section 01

Fogalmi térkép: LLM, RAG, Agent, Embedding OpenAI

FogalomMagyarázatAnalógia
LLMNagy nyelvi modell - szövegből szöveget generálMint egy nagyon olvasott szakember
RAGRetrieval-Augmented Generation: külső tudásbázisból keres, az alapján válaszolA szakember felolvas egy dokumentumot, abból válaszol
EmbeddingSzöveg sůrített vektor-ábrázolásaDokumentum koordinátája egy értelem-térben
Vector DBHasonlóság alapján keres vektorok közöttKönyvtár, ahol a hasonló könyvek egymás mellett vannak
AgentLLM, ami eszközöket hívhat, tervez és iterálA szakember telefonál, keres, számol, aztán válaszol
Mentális modell

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

Section 02

Architektúra-térkép: a RAG pipeline ChromaDB OpenAI

A végcél egy ilyen pipeline:

📄 Dokumentumok
✂️ Chunking
🔢 Embedding
🗄️ ChromaDB
❓ Kérdés
🔍 Vector search
🧠 LLM válasz
Demo vs Production

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

Section 03

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.

Telepítés

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.

[1]
# 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")
Output:
[openai] Client initialized successfully
API key loaded from environment variable
Section 04

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 toolBeépített file_search, code_interpreter
[2]
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)
Output:
[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
Gyakori hiba

A response.output_text a válasz szövege. Ne keverd a response.choices[0].message.content-tel (az a régi Chat Completions).

Section 05

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.

[3]
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)
Output:
[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ú.
Prompt engineering stratégiák

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]"
Section 06

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.

Gyakori hiba

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.

[4]
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))
Output:
[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
}
Demo vs Production

Demóban: JSON schema validáció. Productionben: schema verziókezelés, backward compat, fallback, validation middleware.

Section 07

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.

Fontos

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.

[5]
# 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)
Output:
[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.

Section 08

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ésLeghasonlóbbHasonlóság
"szállítási idő""csomag érkezése"0.91
"szállítási idő""visszaküldés"0.42
[6]
# 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}")
Output:
[similarity] Computing cosine distances...
szállítás vs csomag:       0.97
szállítás vs visszaküldés: 0.30
Miért nem kulcsszó-keresés?

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".

Section 09

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.

Modell választás

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.

[7]
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}")
Output:
[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
Cost

text-embedding-3-small: ~$0.02 / 1M token. 1000 dokumentum ~ 500K token ~ $0.01

Section 10

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.

Persistencia

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.

[8]
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()}")
Output:
[chroma] create_collection() name="webshop_docs"
[chroma] add() 3 documents added
Dokumentumok száma: 3
[9]
# 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}")
Output:
[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á.
Demo vs Production

Demóban: in-memory, 3 dokumentum. Productionben: persist_directory, több ezer dokumentum, metadata filtering, embedding function override.

Section 11

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égiaElőnyHátrány
Fix méret (pl. 500 karakter)EgyszerűVághat mondat közben
Mondat alapúÉrtelmes egységekVáltozó méret
Átfedéses (overlap)Nem vész el kontextusDuplikáció
[10]
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] + "...")
Output:
[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....
Section 12

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.

Gyakorlati tanács

É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.

[11]
# 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)}")
Output:
[load] Processing 4 documents with chunk_text(size=200, overlap=50)
Dokumentumok: 4, Chunkok: 7
Section 13

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.

Teljesítmény

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.

[12]
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()}")
Output:
[chroma] create_collection() name="webshop_rag"
[chroma] add() 7 chunks with metadata
Indexed chunks: 7
Section 14

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.

Hibrid keresés

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.

[13]
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]}...")
Output:
[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?

Section 15

RAG pipeline: retrieve → augment → generate OpenAI ChromaDB

Most összerakjuk az egészet! A RAG pipeline három lépése:

  1. Retrieve: kérdés → vektoros keresés → releváns chunkok
  2. Augment: chunkok + kérdés → egyetlen prompt
  3. Generate: prompt → LLM → válasz
[14]
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()
Output:
[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.
Demo vs Production

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?

Section 16

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:

MetrikaMit mér?Jó érték
FaithfulnessA válasz a forrásból származik-e> 0.9
RelevanceA válasz releváns-e a kérdésre> 0.8
Hallucination rateAránya a nem forrás-alapú válaszoknak< 0.1
Retrieval precisionA visszakeresett chunkok relevancia aránya> 0.7
[15]
# 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))
Output:
[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."
}
Section 17

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.

Optimalizálási tippek

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.

[16]
# 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?"))
Output:
[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.
Demo vs Production

Demóban: print. Productionben: Prometheus/Grafana dashboard, alerting ha latency > 2s vagy cost/nap > $X, per-endpoint breakdown, token usage per model.

Section 18

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.

Futtatás

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.

[17]
# 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})
Output:
$ 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
Section 19

Összefoglalás OpenAI ChromaDB

Mit építettünk?

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
Következő lépések
  • 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:

📄 Docs
✂️ Chunk
🔢 Embed
🗄️ ChromaDB
🔍 Search
🧠 GPT-4o
💬 Chat
Szójegyzék

Embedding · RAG · Chunking

Quiz: Mi a RAG?

Quiz: Mi a ChromaDB?