Local LLM Engineering: saját gépen futó AI stack Python Docker
A kurzus célja egy lokális, privacy-first LLM gateway felépítése. Nem csak azt nézzük meg, hogyan indíts el egy modellt a gépeden, hanem azt is, hogyan döntesz modellméret, quantization, latency, RAG, eszközhívás, observability és cloud fallback között.
A WebShop Pro ügyfélszolgálati asszisztense először lokális modellt használ, érzékeny vagy lassú esetekben pedig szabályozottan tud fallbackelni külső providerre.
Lesz egy tervezési mátrixod, egy OpenAI-kompatibilis lokális kliensed, egy lokális RAG prototípusod, mérési scriptjeid és egy hybrid routing mintád production gondolkodáshoz.
Döntési térkép: mikor jó a local LLM?
A local LLM nem olcsóbb vagy jobb automatikusan. Akkor jó, ha a privacy, offline működés, stabil költség, testreszabhatóság vagy gyors iteráció fontosabb, mint a legerősebb modellminőség.
| Szempont | Local LLM előny | Kockázat | Tipikus döntés |
|---|---|---|---|
| Privacy | Az adat nem hagyja el a gépet vagy VPC-t | Lokális hozzáférésvédelem kell | PII-s első kör lokálisan |
| Latency | Kis modellel gyors roundtrip | Nagy modell CPU-n lassú | 7B-8B quantized modell napi UX-hez |
| Minőség | Domainprompttal stabil lehet | Komplex reasoningben gyengébb | Hybrid router nehéz kérdésekre |
| Költség | Nincs token alapú számla | Hardware és üzemeltetés van | Nagy volumenű, ismétlődő feladatok |
| Operáció | Teljes kontroll | Patch, model drift, monitoring rád marad | Gateway + eval + rollback |
Hardware és modell budget Python
A legelső mérnöki kérdés: mekkora modellt bír el a gép? A paraméterszám, quantization és context window együtt határozza meg a memóriát. A becslés nem helyettesíti a mérést, de segít elkerülni a rossz első választást.
Q4 quantization mellett a 7B-8B modellek jellemzően laptopon is tanulhatóak. 30B+ modellhez már GPU VRAM vagy erős szerver kell.
def estimate_model_ram(params_billion, bits_per_weight=4, context_tokens=8192):
weights_gb = params_billion * 1_000_000_000 * bits_per_weight / 8 / 1e9
kv_cache_gb = context_tokens / 8192 * params_billion * 0.10
overhead_gb = 1.2
return round(weights_gb + kv_cache_gb + overhead_gb, 1)
models = [
("small-support", 3, 4),
("daily-driver", 8, 4),
("reasoning-heavy", 14, 4),
("server-grade", 32, 4),
]
for name, params, bits in models:
print(f"{name:16} {params:>2}B Q{bits}: ~{estimate_model_ram(params, bits)} GB RAM")small-support 3B Q4: ~3.0 GB RAM daily-driver 8B Q4: ~6.0 GB RAM reasoning-heavy 14B Q4: ~9.6 GB RAM server-grade 32B Q4: ~21.2 GB RAM
Ollama első lokális chat OpenAI API shape
A leggyorsabb tanulási út az, ha a lokális runtime OpenAI-kompatibilis API-t ad. Így a meglévő LLM klienskódod nagy része változatlan marad, csak a base_url és a modellnév cserélődik.
| Lépés | Parancs | Mit bizonyít? |
|---|---|---|
| Runtime indítás | ollama serve | Lokális API fut a 11434 porton |
| Modell letöltés | ollama pull llama3.1:8b | Van futtatható modell |
| API smoke test | curl http://localhost:11434/api/tags | A gateway látja a modelleket |
from openai import OpenAI
local_client = OpenAI(
base_url="http://localhost:11434/v1",
api_key="ollama",
)
response = local_client.chat.completions.create(
model="llama3.1:8b",
messages=[
{"role": "system", "content": "Válaszolj röviden, magyarul."},
{"role": "user", "content": "Mi a local LLM legnagyobb előnye egy webshop support botnál?"},
],
)
print(response.choices[0].message.content)A legnagyobb előny, hogy az érzékeny ügyféladatokat lokálisan tarthatod, miközben gyorsan prototipizálhatsz és kontrollálhatod a futtatási költséget.
GGUF és quantization döntések
A GGUF a llama.cpp ökoszisztéma praktikus modellformátuma. A quantization csökkenti a memóriát és gyorsíthat, de minőségvesztést okozhat. Mérnökként nem a legkisebb fájl a cél, hanem a legjobb quality / latency / RAM kompromisszum.
quantization = {
"Q8_0": {"quality": 0.98, "ram": "high", "use": "quality-sensitive eval"},
"Q6_K": {"quality": 0.96, "ram": "medium-high", "use": "good laptop/server default"},
"Q5_K_M": {"quality": 0.94, "ram": "medium", "use": "balanced daily driver"},
"Q4_K_M": {"quality": 0.90, "ram": "low", "use": "laptop-friendly default"},
"Q3_K_M": {"quality": 0.82, "ram": "very-low", "use": "only if memory constrained"},
}
for q, meta in quantization.items():
print(f"{q:7} quality~{meta['quality']:.2f} ram={meta['ram']:11} use={meta['use']}")Q8_0 quality~0.98 ram=high use=quality-sensitive eval Q6_K quality~0.96 ram=medium-high use=good laptop/server default Q5_K_M quality~0.94 ram=medium use=balanced daily driver Q4_K_M quality~0.90 ram=low use=laptop-friendly default Q3_K_M quality~0.82 ram=very-low use=only if memory constrained
OpenAI-kompatibilis lokális gateway FastAPI
Éles környezetben ne az app minden pontján legyen runtime-specifikus logika. Legyen egy vékony gateway réteg, amely egységesíti a provider hívást, a timeoutot, a logolást és később a fallbacket.
from dataclasses import dataclass
from openai import OpenAI
@dataclass
class LLMConfig:
name: str
base_url: str
api_key: str
default_model: str
timeout_s: int = 30
local_cfg = LLMConfig(
name="local-ollama",
base_url="http://localhost:11434/v1",
api_key="ollama",
default_model="llama3.1:8b",
)
client = OpenAI(base_url=local_cfg.base_url, api_key=local_cfg.api_key, timeout=local_cfg.timeout_s)
print(f"provider={local_cfg.name} model={local_cfg.default_model} timeout={local_cfg.timeout_s}s")provider=local-ollama model=llama3.1:8b timeout=30s
Prompting kis modellekkel
A kisebb lokális modellek kevesebb implicit kontextust tartanak fejben. Itt a jó prompt nem dísz, hanem kontrollfelület: rövid role, explicit scope, output séma, fallback válasz és tiltott viselkedés.
def webshop_support_prompt(question, context):
return f"""
Te a WebShop Pro lokális support asszisztense vagy.
Csak a CONTEXT alapján válaszolj. Ha nincs benne elég információ, mondd ezt:
"Erre nincs elég információm a lokális tudásbázisban."
Válaszformátum:
- válasz: 2-4 mondat magyarul
- forrás: dokumentum neve vagy "nincs"
- bizonyosság: low | medium | high
CONTEXT:
{context}
USER QUESTION:
{question}
""".strip()
print(webshop_support_prompt("Mikor kapom meg a rendelésem?", "shipping_policy.md: Budapesten 2 munkanap."))Te a WebShop Pro lokális support asszisztense vagy. Csak a CONTEXT alapján válaszolj. Ha nincs benne elég információ, mondd ezt: "Erre nincs elég információm a lokális tudásbázisban." Válaszformátum: - válasz: 2-4 mondat magyarul - forrás: dokumentum neve vagy "nincs" - bizonyosság: low | medium | high CONTEXT: shipping_policy.md: Budapesten 2 munkanap. USER QUESTION: Mikor kapom meg a rendelésem?
Lokális embeddings és tudásbázis ChromaDB
Local LLM mellett a RAG még fontosabb, mert a modell mérete kisebb. A tudásbázis tartsa a vállalati tényeket, a modell pedig fogalmazzon és kössön össze.
documents = [
{"id": "shipping-1", "source": "shipping_policy.md", "text": "Budapesten a standard szállítás 2 munkanap, vidéken 3 munkanap."},
{"id": "returns-1", "source": "return_policy.md", "text": "A vásárló 14 napon belül jelezheti az elállást sérülésmentes termékre."},
{"id": "warranty-1", "source": "warranty.md", "text": "Elektronikai termékekre 24 hónap jótállás vonatkozik."},
]
chunks = []
for doc in documents:
chunks.append({
"id": doc["id"],
"text": doc["text"],
"metadata": {"source": doc["source"], "privacy": "public-support"},
})
print(f"Prepared {len(chunks)} chunks for local embedding")Prepared 3 chunks for local embedding
WebShop Pro RAG lokálisan
A lokális RAG pipeline két részből áll: retrieval és generation. Ha a retrieval rossz, a local LLM nem fogja megmenteni. Ezért mentsd a forrást, score-t és chunk id-t minden válasz mellé.
def simple_retrieve(question, chunks, top_k=2):
terms = set(question.lower().split())
scored = []
for chunk in chunks:
score = sum(1 for t in terms if t in chunk["text"].lower())
scored.append((score, chunk))
return [chunk for score, chunk in sorted(scored, reverse=True, key=lambda x: x[0])[:top_k]]
question = "Hány nap alatt érkezik meg Budapestre a rendelés?"
hits = simple_retrieve(question, chunks)
context = "\n".join(f"{h['metadata']['source']}: {h['text']}" for h in hits)
print(context)shipping_policy.md: Budapesten a standard szállítás 2 munkanap, vidéken 3 munkanap. return_policy.md: A vásárló 14 napon belül jelezheti az elállást sérülésmentes termékre.
Tool calling lokális modellel
Nem minden local runtime ad ugyanakkora eszközhívás támogatást. A robusztus minta az, hogy a modelltől strukturált JSON döntést kérsz, majd a saját kódod validálja és futtatja az eszközt.
allowed_tools = {
"lookup_order": {"required": ["order_id"]},
"search_policy": {"required": ["query"]},
}
model_plan = {
"tool": "lookup_order",
"arguments": {"order_id": "ORD-1042"},
"reason": "A felhasználó konkrét rendelés állapotát kérdezi.",
}
def validate_tool_call(plan):
if plan["tool"] not in allowed_tools:
raise ValueError("Unknown tool")
missing = set(allowed_tools[plan["tool"]]["required"]) - set(plan["arguments"])
if missing:
raise ValueError(f"Missing args: {missing}")
return True
print(validate_tool_call(model_plan), model_plan["tool"], model_plan["arguments"])True lookup_order {'order_id': 'ORD-1042'}
Benchmark: latency, throughput, RAM
Local LLM-nél a benchmark nem vanity metrika. A felhasználói élményt a time-to-first-token, a tokens/sec, a memória és a timeout arány határozza meg. Minden modellváltás előtt és után mérj.
bench = [
{"model": "3b-q4", "ttft_ms": 280, "tok_s": 42, "ram_gb": 3.2, "quality": 0.74},
{"model": "8b-q4", "ttft_ms": 520, "tok_s": 24, "ram_gb": 6.1, "quality": 0.86},
{"model": "14b-q4", "ttft_ms": 950, "tok_s": 14, "ram_gb": 9.8, "quality": 0.91},
]
def score(row):
latency_penalty = row["ttft_ms"] / 1000
speed_bonus = row["tok_s"] / 50
return round(row["quality"] + speed_bonus - latency_penalty, 2)
for row in sorted(bench, key=score, reverse=True):
print(row["model"], "score=", score(row), "ttft=", row["ttft_ms"], "ms", "ram=", row["ram_gb"], "GB")8b-q4 score=0.82 ttft=520 ms ram=6.1 GB 3b-q4 score=0.74 ttft=280 ms ram=3.2 GB 14b-q4 score=0.24 ttft=950 ms ram=9.8 GB
Hybrid routing: local vs cloud
A legerősebb production minta nem vallási kérdés: használd a lokális modellt ott, ahol elég, és küldd tovább a kérdést, amikor komplex reasoning, nagy context vagy erős tool support kell.
def route_request(question, contains_pii=False, complexity=0.3, user_tier="standard"):
if contains_pii:
return "local-private"
if complexity > 0.75:
return "cloud-frontier"
if user_tier == "free":
return "local-small"
return "local-balanced"
cases = [
("Mi a visszaküldési határidő?", False, 0.2, "standard"),
("Elemezd ezt a panaszt és írj kompenzációs tervet.", True, 0.8, "premium"),
("Írj SQL-t a rendelési kohorszokra.", False, 0.85, "premium"),
]
for question, pii, complexity, tier in cases:
print(route_request(question, pii, complexity, tier), "-", question[:42])local-balanced - Mi a visszaküldési határidő? local-private - Elemezd ezt a panaszt és írj kompenzáci cloud-frontier - Írj SQL-t a rendelési kohorszokra.
Local Docker Lab profil Docker
A kurzus Docker profilja a közös AI lab service-eket indítja: FastAPI gateway, ChromaDB, Streamlit demo, MLflow és monitoring. A modell runtime maradhat hoston futó Ollama, így nem kényszerítünk rá több gigabájtos image-et minden indításra.
docker compose --profile local-llm up -d
# Lab indítása a repo gyökeréből docker compose --profile local-llm up -d # Hoston futó Ollama ellenőrzése curl http://localhost:11434/api/tags # Gateway dokumentáció open http://localhost:8000/docs
Services: api, chroma, streamlit, mlflow, prometheus, grafana, lab-runner Local LLM runtime: host Ollama on http://localhost:11434
Evaluáció: lokális modell vs cloud modell
Ha nem mérsz, a local LLM döntés vélemény marad. A minimum eval: retrieval hit, answer groundedness, policy compliance, latency, és user-visible fallback rate.
eval_results = [
{"provider": "local-8b", "grounded": 0.86, "policy": 0.94, "latency_ms": 1450, "cost": 0.00},
{"provider": "cloud-frontier", "grounded": 0.94, "policy": 0.97, "latency_ms": 2100, "cost": 0.018},
]
for r in eval_results:
pass_mark = r["grounded"] >= 0.85 and r["policy"] >= 0.90 and r["latency_ms"] < 2500
print(f"{r['provider']:15} pass={pass_mark} grounded={r['grounded']} latency={r['latency_ms']}ms cost_units={r['cost']}")local-8b pass=True grounded=0.86 latency=1450ms cost_units=0.0 cloud-frontier pass=True grounded=0.94 latency=2100ms cost_units=0.018
Privacy, governance és threat model
A "lokális" nem egyenlő a "biztonságos"-sal. A runtime logolhat promptot, a modell cache-elhet adatot, a notebook kimenthet PII-t, a fejlesztői gép pedig lehet gyengén védett. A local LLM governance első verziója legyen egyszerű, de explicit.
| Kockázat | Kontroll | Ellenőrzés |
|---|---|---|
| Promptban PII | Redaction gateway előtt | Mintavételes audit |
| Lokális log szivárgás | Structured logging PII mezők nélkül | Log scan CI-ben |
| Nem jó modellválasz | Golden eval és policy tests | Release gate |
| Shadow AI runtime | Engedélyezett modell lista | Tooling manifest |
| Fallback adatküldés | PII-blokkoló router szabály | Trace review |
Összefoglalás és következő lépések
A local LLM engineering lényege nem az, hogy minden cloud modellt lecserélj. A jó rendszer pontosan tudja, melyik feladatot bírja el a lokális modell, melyikhez kell retrieval, mikor kell fallback, és hogyan bizonyítod méréssel, hogy a döntés működik.
| Megtanultad | Mit viszel tovább? |
|---|---|
| Hardware és quantization | Reális modellválasztás laptopra vagy szerverre |
| OpenAI-kompatibilis lokális API | Provider-csere minimális app módosítással |
| Lokális RAG | Kontrollált, forrásolt WebShop Pro válaszok |
| Benchmark és eval | Vélemény helyett mérhető döntés |
| Hybrid routing | Privacy és minőség egy rendszerben |
Dokumentáld a választott modellt, quantizationt, benchmark eredményt, routing szabályokat és governance kontrollokat. Ez egy seniorabb AI engineering gondolkodást mutató projekt, nemcsak egy "futtattam egy modellt" demo.