list_altİçindekilerexpand_more
- 01FastAPI ve LLM Servis Mimarisi
- 02FastAPI neden bu iş için doğru seçim?
- 03Ollama mu, vLLM mi?
- 04Mimari akış
- 05Ortam Kurulumu
- 06Gereksinimler
- 07Kurulum adımları
- 08Ollama ile Basit LLM Endpoint
- 09Streaming Response Desteği
- 10vLLM Entegrasyonu (GPU Sunucu Senaryosu)
- 11JWT Auth Middleware
- 12Docker ile Production Deployment
- 13Dockerfile
- 14docker-compose.yml
- 15Performans ve İzleme
- 16/health endpoint
- 17Yanıt süreleri ölçümü
- 18Sonraki Adımlar
Bir uygulamada OpenAI yerine yerel LLM kullanmak istiyorsun ama Ollama’nın socket’ına doğrudan bağlanmak hem kırılgan hem de bakımı zor bir yapı. Asıl çözüm şu: kendi servis katmanını yaz. FastAPI bu iş için tam oturur; async desteği, tip doğrulaması ve otomatik OpenAPI dökümanıyla dakikalar içinde üretim kalitesinde bir LLM API elde edersin. Bu rehberde Ollama ile geliştirme ortamı kuruyoruz, ardından GPU sunucusu için vLLM’e geçişi adım adım gösteriyoruz.
FastAPI ve LLM Servis Mimarisi
FastAPI neden bu iş için doğru seçim?
Çoğu LLM uygulaması zamanla benzer bir hal alır: birkaç endpoint, token stream’i iletmek, basit bir auth katmanı. Flask ile başlamak cazip görünse de token başına birden fazla müşteriye aynı anda yanıt verirken async olmayan bir framework tıkanır. FastAPI’nin async def tabanlı mimarisi, tek bir thread’de onlarca bağlantıyı aynı anda tutabilir; bu da streaming LLM yanıtları için kritik bir avantaj.
Bunun ötesinde Pydantic ile gelen otomatik istek doğrulaması, /docs üzerinden hazır bir Swagger UI ve type hint’lerden türetilen schema, tek başına bir sprint tasarrufu demek.
Ollama mu, vLLM mi?
İkisi aynı problemi farklı profiller için çözüyor:
| Kriter | Ollama | vLLM |
|---|---|---|
| Hedef ortam | Geliştirme, CPU/tüketici GPU | Üretim, data-center GPU |
| Kurulum | Tek komut | pip install vllm, CUDA gerekli |
| Throughput | Orta | Çok yüksek (PagedAttention) |
| OpenAI uyumlu endpoint | Evet (/api/chat) | Evet (/v1/chat/completions) |
| Model format | GGUF, Llama.cpp | Hugging Face ağırlıkları |
| Bellek yönetimi | Otomatik, GGUF quantize | PagedAttention ile KV-cache |
Kaynak: vLLM GitHub deposu, Ollama docs
Geliştirme makinende Ollama; AWS p3/p4 veya bare-metal GPU sunucusunda vLLM. İki senaryoyu da aşağıda ele alıyoruz.
Mimari akış
İstemci
│
▼
FastAPI (uvicorn, 0.0.0.0:8000)
│
├── POST /chat ──► Ollama (localhost:11434)
│ veya
│ vLLM (localhost:8080)
│
└── GET /health
FastAPI bir proxy değil; istek doğrulaması, auth ve hata yönetimini üstleniyor. LLM backend’i çıkarılabilir; konfigürasyon değişkeni olarak tutuyoruz.
Ortam Kurulumu
Gereksinimler
- Python 3.11+
- Ollama (geliştirme için) veya CUDA 12.x + vLLM (üretim için)
- Docker ve Docker Compose (deployment için)
Kurulum adımları
# Sanal ortam
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# Bağımlılıklar
pip install fastapi uvicorn[standard] httpx pydantic python-jose[cryptography]
# Ollama kurulumu (macOS/Linux)
curl -fsSL https://ollama.ai/install.sh | sh
ollama pull llama3.2 # ~2GB, 8B model
Model indikten sonra Ollama otomatik olarak http://localhost:11434 adresinde dinlemeye başlar. Aşağıdaki komutla çalıştığını doğrula:
curl http://localhost:11434/api/generate \
-d '{"model":"llama3.2","prompt":"Merhaba","stream":false}'
Ollama ile Basit LLM Endpoint
Proje yapısı:
llm_api/
├── main.py
├── models.py
├── auth.py
└── config.py
config.py ile backend seçimini ortam değişkenine bağlıyoruz:
# config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
llm_backend: str = "ollama" # "ollama" veya "vllm"
ollama_url: str = "http://localhost:11434"
vllm_url: str = "http://localhost:8080"
ollama_model: str = "llama3.2"
vllm_model: str = "meta-llama/Meta-Llama-3-8B-Instruct"
secret_key: str = "change-me-in-prod"
algorithm: str = "HS256"
settings = Settings()
İstek/yanıt şemaları:
# models.py
from pydantic import BaseModel
from typing import Optional
class ChatRequest(BaseModel):
message: str
system: Optional[str] = "Sen yardımcı bir asistansın."
temperature: float = 0.7
max_tokens: int = 512
class ChatResponse(BaseModel):
reply: str
model: str
backend: str
Temel endpoint:
# main.py
from fastapi import FastAPI, HTTPException
import httpx
from models import ChatRequest, ChatResponse
from config import settings
app = FastAPI(title="LLM API", version="1.0.0")
@app.post("/chat", response_model=ChatResponse)
async def chat(req: ChatRequest):
if settings.llm_backend == "ollama":
return await _ollama_chat(req)
return await _vllm_chat(req)
async def _ollama_chat(req: ChatRequest) -> ChatResponse:
payload = {
"model": settings.ollama_model,
"messages": [
{"role": "system", "content": req.system},
{"role": "user", "content": req.message},
],
"options": {"temperature": req.temperature, "num_predict": req.max_tokens},
"stream": False,
}
async with httpx.AsyncClient(timeout=60) as client:
resp = await client.post(f"{settings.ollama_url}/api/chat", json=payload)
if resp.status_code != 200:
raise HTTPException(status_code=502, detail="Ollama hatası")
data = resp.json()
return ChatResponse(
reply=data["message"]["content"],
model=settings.ollama_model,
backend="ollama",
)
uvicorn main:app --reload ile başlat; http://localhost:8000/docs üzerinden endpoint’i hemen test edebilirsin.
Streaming Response Desteği
Büyük çıktılarda kullanıcının tüm yanıtı beklemesi kötü bir deneyim. Token-by-token akış için FastAPI’nin StreamingResponse’u ve Ollama’nın stream modunu birleştiriyoruz:
from fastapi.responses import StreamingResponse
import json
@app.post("/chat/stream")
async def chat_stream(req: ChatRequest):
async def token_generator():
payload = {
"model": settings.ollama_model,
"messages": [
{"role": "system", "content": req.system},
{"role": "user", "content": req.message},
],
"stream": True,
}
async with httpx.AsyncClient(timeout=120) as client:
async with client.stream(
"POST",
f"{settings.ollama_url}/api/chat",
json=payload,
) as response:
async for line in response.aiter_lines():
if not line:
continue
chunk = json.loads(line)
token = chunk.get("message", {}).get("content", "")
if token:
yield f"data: {json.dumps({'token': token})}\n\n"
if chunk.get("done"):
yield "data: [DONE]\n\n"
return StreamingResponse(token_generator(), media_type="text/event-stream")
Frontend tarafında bu endpoint’i EventSource veya fetch + ReadableStream ile tüketebilirsin:
const response = await fetch("/chat/stream", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: "Merhaba!" }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
// "data: {...}\n\n" satırlarını parse et
text.split("\n\n").forEach(line => {
if (line.startsWith("data: ") && !line.includes("[DONE]")) {
const chunk = JSON.parse(line.slice(6));
process.stdout.write(chunk.token);
}
});
}
vLLM Entegrasyonu (GPU Sunucu Senaryosu)
vLLM, OpenAI API formatıyla uyumlu bir HTTP sunucusu açıyor; bu yüzden FastAPI tarafındaki değişiklik minimal. Sunucuyu başlatmak için:
pip install vllm
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Meta-Llama-3-8B-Instruct \
--port 8080 \
--tensor-parallel-size 1
FastAPI tarafında vLLM proxy fonksiyonu:
async def _vllm_chat(req: ChatRequest) -> ChatResponse:
payload = {
"model": settings.vllm_model,
"messages": [
{"role": "system", "content": req.system},
{"role": "user", "content": req.message},
],
"temperature": req.temperature,
"max_tokens": req.max_tokens,
}
async with httpx.AsyncClient(timeout=120) as client:
resp = await client.post(
f"{settings.vllm_url}/v1/chat/completions",
json=payload,
)
if resp.status_code != 200:
raise HTTPException(status_code=502, detail="vLLM hatası")
data = resp.json()
return ChatResponse(
reply=data["choices"][0]["message"]["content"],
model=settings.vllm_model,
backend="vllm",
)
Ortam değişkeniyle geçiş yapılır: LLM_BACKEND=vllm uvicorn main:app.
Daha ayrıntılı bir vLLM karşılaştırması için vLLM vs SGLang vs TGI: Üretim LLM Servis Karşılaştırması yazısına bakabilirsin.
JWT Auth Middleware
Herkese açık bir endpoint üretimde tutulmamalı. FastAPI’nin Depends mekanizmasıyla Bearer token doğrulaması eklemek yalnızca birkaç satır:
# auth.py
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import JWTError, jwt
from config import settings
bearer_scheme = HTTPBearer()
def verify_token(
credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
) -> dict:
token = credentials.credentials
try:
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
return payload
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Geçersiz veya süresi dolmuş token",
)
Endpoint’e eklemek için:
@app.post("/chat", response_model=ChatResponse)
async def chat(req: ChatRequest, _: dict = Depends(verify_token)):
...
Token üretmek için küçük bir yardımcı:
from datetime import datetime, timedelta
def create_token(subject: str, expire_minutes: int = 60) -> str:
payload = {
"sub": subject,
"exp": datetime.utcnow() + timedelta(minutes=expire_minutes),
}
return jwt.encode(payload, settings.secret_key, algorithm=settings.algorithm)
Docker ile Production Deployment
Dockerfile
FROM python:3.11-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
COPY . .
ENV LLM_BACKEND=ollama
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
docker-compose.yml
services:
llm-api:
build: .
ports:
- "8000:8000"
environment:
- LLM_BACKEND=ollama
- OLLAMA_URL=http://ollama:11434
- SECRET_KEY=${SECRET_KEY}
depends_on:
- ollama
ollama:
image: ollama/ollama:latest
ports:
- "11434:11434"
volumes:
- ollama_data:/root/.ollama
# GPU erişimi için:
# deploy:
# resources:
# reservations:
# devices:
# - driver: nvidia
# count: 1
# capabilities: [gpu]
volumes:
ollama_data:
Başlatmak için:
export SECRET_KEY=$(openssl rand -hex 32)
docker compose up -d
docker compose exec ollama ollama pull llama3.2
Performans ve İzleme
/health endpoint
@app.get("/health")
async def health():
backend_ok = False
check_url = (
f"{settings.ollama_url}/api/tags"
if settings.llm_backend == "ollama"
else f"{settings.vllm_url}/health"
)
try:
async with httpx.AsyncClient(timeout=5) as client:
r = await client.get(check_url)
backend_ok = r.status_code == 200
except Exception:
pass
return {
"status": "ok" if backend_ok else "degraded",
"backend": settings.llm_backend,
"backend_reachable": backend_ok,
}
Yanıt süreleri ölçümü
FastAPI middleware’i ile her isteğin ne kadar sürdüğünü loglamak:
import time
from fastapi import Request
@app.middleware("http")
async def add_timing(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
duration_ms = (time.perf_counter() - start) * 1000
response.headers["X-Response-Time-Ms"] = f"{duration_ms:.1f}"
print(f"{request.method} {request.url.path} — {duration_ms:.1f}ms")
return response
Yük altında profil çıkarmak için locust veya k6 kullanabilirsin. Streaming endpoint’te ilk token süresi (TTFT) ve toplam yanıt süresi ayrı ayrı ölçülmeli; LLM kalitesi değerlendirmesinde TTFT genellikle daha kritik.
Sonraki Adımlar
Bu noktada çalışan bir FastAPI + Ollama servisi var; vLLM’e geçiş tek bir ortam değişkeni. Uygulamayı büyütmek istersen bakmaya değer noktalar:
- Aynı prompt’a tekrar eden istekler için Redis ile yanıt önbellekleme; Prompt Caching nedir? yazısında kavramsal zemin var.
- Server-Sent Events yerine çift yönlü iletişim gerekirse
websocketsekle. slowapiveya nginx upstream limitleri ile API kotası koy.- Prometheus + Grafana ile token/saniye ve TTFT dashboard’u kur.
- Birden fazla model arasında runtime’da geçiş için konfigürasyon katmanını genişlet.
/v1/prefix’i ve schema uyumu eklersen mevcut OpenAI Python istemcisi değişiklik gerektirmeden bu sunucuya bağlanır.
İlgili kaynaklar:



