</> AI Engineering Crash Course

0 / 20 szakasz kész
Utoljára szakmailag felülvizsgálva: 2026-05-14 Verzió-mátrix Output: előre megírt eredmény
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

Éles környezetben: 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

Chat Completions API alapok Chat Completions API

Az OpenAI Python SDK-ban ma két fontos mintával találkozol: az újabb Responses API-val és a sok meglévő projektben használt Chat Completions API-val. Ebben a blokkban Chat Completions-kompatibilis példát használunk, mert a messages lista nagyon jól megmutatja az alapgondolatot: minden üzenetnek van role mezője (system / user / assistant) és content mezője, vagyis az API pontosan látja, ki mit mondott.

MezőMire való
client.chat.completions.create()A hívás belépési pontja
messages=[{"role": "system", ...}, {"role": "user", ...}]A teljes beszélgetési kontextus
response.choices[0].message.contentA modell válaszának szövege
[2]
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "Te egy webshop ügyfélszolgálati asszisztens vagy. Rövid, pontos válaszokat adj."},
        {"role": "user", "content": "Mennyi idő alatt érkezik meg a csomag?"}
    ]
)

print(response.choices[0].message.content)
Output:
[openai] chat.completions.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 válasz szövege a response.choices[0].message.content alatt érhető el. A choices egy lista, mert elvileg több jelölt válasz is kérhető (n paraméter), de RAG-ban szinte mindig csak [0]-t használjuk.

Section 05

System message vs User message OpenAI

system message — rendszer-szintű viselkedési szabályok. Rögzített, általában nem változik hívásonként.
user message — a konkrét felhasználói kérdés, hívásonként változik.

Biztonsági szempontból fontos: a két szerepet külön messages elemekre kell osztani, nem pedig egyetlen f-stringbe ágyazni. Ha a felhasználói inputot (kerdes) közvetlenül a system promptba interpolálod, az utat nyit a prompt injection-nek (a felhasználó beleírhat olyan instrukciót, ami felülírja a rendszerprompt szabályait). A role alapú elválasztás biztonságosabb, mert a modell tudja, melyik üzenet honnan származik.

[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.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": SYSTEM_INSTRUCTIONS},
        {"role": "user", "content": kerdes}
    ]
)
print(response.choices[0].message.content)
Output:
[openai] chat.completions.create() with system + user messages
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]"
Indirect prompt injection — RAG-specifikus veszély

A direct prompt injection (a felhasználó közvetlen 'Ignore previous' utasítás) ellen a system/user message szétválasztás véd. RAG rendszerekben azonban van egy másik támadási vektor: az indirect prompt injection. Ha a retrieve-elt dokumentumokba (céges wiki, ügyfél-feltöltés) malicious utasítást rejtenek el, az LLM kontextusában jelenik meg, és úgy fogja olvasni, mintha az lenne az utasítás.

Védekezés: kontextus sanitizáció (HTML/Markdown escape), strukturális szétválasztás (a kontextus külön role-ban, nem inline), és LLM-output validálás. Részletek a RAG Evaluation & AI Safety kurzusban.

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.

Az OpenAI API-k strukturált kimenetet is támogatnak: megadhatsz egy JSON sémát, és a modell válaszát ehhez a sémához kötheted. Ez sokkal megbízhatóbb, mint egyszerűen beleírni a promptba, hogy „válaszolj JSON-ban", mert a séma gépileg ellenőrizhető szerződéssé válik a modell és az alkalmazás között. Éles rendszerben ettől lesz stabil a downstream feldolgozás: a frontend, a validátor és a monitoring ugyanazokra a mezőkre számíthat.

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

A válasz továbbra is a response.choices[0].message.content alatt érhető el, viszont JSON schema módban ez egy JSON string lesz, amit json.loads()-szal kell objektummá parse-olni. Strict módban a séma minden propertyje kötelező és additionalProperties: false kell — különben az API hibát dob.

[4]
import json

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "Válaszolj JSON-ban a megadott sémának megfelelően."},
        {"role": "user", "content": "Mennyibe kerül a szállítás?"}
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "support_answer",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "answer": {"type": "string"},
                    "category": {"type": "string", "enum": ["szallitas", "visszakuldes", "garancia", "egyeb"]},
                    "confidence": {"type": "number"}
                },
                "required": ["answer", "category", "confidence"],
                "additionalProperties": False
            }
        }
    }
)

result = json.loads(response.choices[0].message.content)
print(json.dumps(result, indent=2, ensure_ascii=False))
Output:
[openai] chat.completions.create() structured output (JSON schema, strict)
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ó. Éles környezetben: schema verziókezelés, backward compat, fallback, validation middleware.

Section 07

Tool calling: függvények hívása OpenAI

A eszközhívás (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.

Chat Completions API-ban a tool hívásokat a response.choices[0].message.tool_calls listán kapjuk vissza (nem a content-en). Minden elem tartalmazza a függvény nevét (tc.function.name) és a JSON-stringként szerializált argumentumokat (tc.function.arguments) — ezt json.loads()-szal kell parse-olni. A tool_choice="auto" azt jelenti, hogy a modell maga dönti el, hív-e tool-t, vagy egyenes választ ad.

[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.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "Te a WebShop Pro support bot vagy."},
        {"role": "user", "content": "Mi a helyzet a WSP-12345 rendeléssel?"}
    ],
    tools=tools,
    tool_choice="auto"
)

# Ha a modell tool-t hív, a tool_calls listán kapjuk vissza, content=None
tool_calls = response.choices[0].message.tool_calls
if tool_calls:
    for tc in tool_calls:
        args = json.loads(tc.function.arguments)
        result = get_order_status(**args)
        print(f"Tool {tc.function.name} →", result)
else:
    print(response.choices[0].message.content)
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 olcsó, gyors embedding megoldás: alapértelmezés szerint 1536 dimenziós vektorokat generál, és az OpenAI aktuális modelloldala 1 millió tokenenkénti árazást ad meg hozzá. Ez azt jelenti, hogy embeddingekkel prototípust építeni általában nagyságrendekkel olcsóbb, mint minden kérdésre nagy nyelvi modellt futtatni. A pontos árat mindig az aktuális pricing oldalon ellenőrizd, mert a modellárak idővel változhatnak.

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, amely alapértelmezés szerint 3072 dimenziós vektorokat ad. A dimensions paraméterrel a harmadik generációs embedding modelleknél csökkenthető a vektorméret.

Megjegyzés: új projektekben a text-embedding-3-small / -large családot érdemes választani; a régebbi embedding modelleket csak meglévő rendszereknél tartsd meg indokolt kompatibilitási okból.

[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

Az embedding költségét mindig az aktuális szolgáltatói árlista és a tényleges tokenhasználat alapján számold. A kurzusban a lényeg a módszer: mérd a usage.total_tokens értéket, az egységárakat tartsd konfigurációban, és abból számolj becsült dokumentumonkénti vagy futásonkénti költséget.

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á.
Distance metrika

A Chroma alapból squared L2 distance-t ad vissza (kisebb = közelebb), nem cosine similarity-t. Cosine-hoz használd metadata={'hnsw:space':'cosine'} collection paramétert létrehozáskor — pl. create_collection(name="...", metadata={"hnsw:space": "cosine"}). Ekkor a distances 0 (azonos) és 2 (ellentétes) között mozog, ami 1 - cosine_similarity.

Demo vs Production

Demóban: in-memory, 3 dokumentum. Éles környezetben: 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 értelmezése függ a használt metrikától: a Chroma alapértelmezetten squared L2 distance-t használ (kisebb = közelebb, 0 = azonos vektor, nincs felső korlát). Ha cosine-t szeretnél (0 = azonos, 2 = teljesen ellentétes), állítsd be a collection-t metadata={'hnsw:space':'cosine'}-szal. É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 — a kontextust a system message-be tesszük,
    #    a felhasználó kérdését pedig külön user message-be.
    #    Ez biztonságosabb, mint egyetlen f-stringbe ágyazni
    #    (kevésbé vulnerable prompt injection-re).
    system_msg = f"""{SYSTEM_INSTRUCTIONS}

Tudásbázis (csak ebből válaszolj):
{context}

Ha a kérdés nem szerepel a tudásbázisban, mondd: "Ezt a kérdést továbbítom a csapatnak."""

    # 3. GENERATE
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": system_msg},
            {"role": "user", "content": question}
        ]
    )
    return response.choices[0].message.content

# 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] chat.completions.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] chat.completions.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. Éles környezetben: 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.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": """É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."""},
            {"role": "user", "content": f"""Kérdés: {question}
Válasz: {answer}
Kontextus: {context}"""}
        ],
        response_format={"type": "json_schema", "json_schema": {
            "name": "eval",
            "strict": True,
            "schema": {"type": "object",
                "properties": {
                    "faithfulness": {"type": "number"},
                    "relevance": {"type": "number"},
                    "reasoning": {"type": "string"}},
                "required": ["faithfulness", "relevance", "reasoning"],
                "additionalProperties": False}}}
    )
    return json.loads(eval_response.choices[0].message.content)

# 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 éles RAG rendszernél. A költség nem fix „chatbot díj", hanem főleg tokenfogyasztásból, modellválasztásból, tool-hívásokból és cache-hit arányból áll össze. Egy rosszul optimalizált RAG pipeline könnyen váratlan számlát generálhat. A válaszidő pedig közvetlenül befolyásolja a felhasználói élményt: ha a chatbot túl sokáig 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 automatizáltan számolható a becsült költség. A response.usage objektum tartalmazza a bemeneti és kimeneti tokenek számát; ezt szorozzuk az aktuális modellárakkal. Éles rendszerben ezeket az adatokat Prometheus vagy Grafana dashboardra küldjük, és riasztást állítunk be a küszöbértékekre. A modellárakat ne hard-kódold örökre: kezeld konfigurációként, és időnként egyeztesd az OpenAI billing/pricing felülettel.

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.

TTFB ≠ total latency

A stream=True az LLM válaszában time-to-first-byte-ot (TTFB) javítja: a felhasználó az első token megjelenésekor látja, hogy "valami történik". A teljes válasz latency azonban változatlan vagy minimálisan rosszabb (a streamelt átvitel többlete miatt). Tehát: jobb UX-érzet, de a system throughput nem nő. Batch inferenciánál (pl. tömeges dokumentum-feldolgozás) nincs előnye — ott a sima request-response a hatékonyabb.

Production tipp — prompt caching (2024+)

RAG kontextusoknál a system message + dokumentum-kontextus gyakran ismétlődik. Mind az Anthropic, mind az OpenAI támogat natív prompt caching-et:

  • Anthropic: explicit cache_control={"type": "ephemeral"} markert teszünk a prompt statikus részére. Cache hit: input token költség 10% (90% megtakarítás). 5 perces TTL.
  • OpenAI: automatikus, 1024+ tokenes prompt prefix esetén. Cache hit: input költség 50%.

RAG/agentic munkafolyamatoknál a prompt caching az egyik legfontosabb költségcsökkentési technika, különösen akkor, ha hosszú, de sok kérésben ismétlődő system promptot vagy dokumentum-kontextust használsz. Részletek és példák: LLMOps kurzus 11. szakasza.

[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.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": f"{SYSTEM_INSTRUCTIONS}\n\nTudásbázis:\n{context}"},
            {"role": "user", "content": question}
        ]
    )
    llm_time = time.time() - llm_start

    total_time = time.time() - start

    # Modellárakat konfigurációból tölts, mert idővel változhatnak.
    PRICING = {"gpt-4o": {"input_per_1m": 2.50, "output_per_1m": 10.00}}  # példaérték
    price = PRICING["gpt-4o"]
    # A response.usage tartalmazza a token számot:
    #   prompt_tokens, completion_tokens, total_tokens
    usage = response.usage
    in_cost = usage.prompt_tokens * price["input_per_1m"] / 1_000_000
    out_cost = usage.completion_tokens * price["output_per_1m"] / 1_000_000
    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")
    print(f"Tokens: {usage.prompt_tokens} in / {usage.completion_tokens} out — ${in_cost+out_cost:.5f}")
    return response.choices[0].message.content

print(rag_query_tracked("Mennyi a szállítás?"))
Output:
[chroma] retrieve() 12ms
[openai] chat.completions.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. Éles környezetben: 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 Chat Completions API (client.chat.completions.create)
  • ✅ System message + User message szétválasztás (prompt injection védelem)
  • ✅ Structured outputs (response_format + JSON schema, strict mode)
  • ✅ Tool calling (tools=[{"type":"function",...}], tool_calls kiolvasás)
  • ✅ Embedding fogalmak + OpenAI Embeddings API (text-embedding-3-small)
  • ✅ ChromaDB vektoradatbázis (squared L2 vs cosine distance)
  • ✅ 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 (response.usage)
  • ✅ 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?