Ollama ile Local LLM: Kurumsal Menü Asistanı Geliştirme
Kurumsal bir web uygulamasında 100+ menü öğesi arasında kullanıcıları doğru sayfaya yönlendiren bir LLM asistanı geliştirdim. Cloud LLM yerine local Ollama altyapısı kullanarak hem maliyeti sıfırladım hem de veri güvenliğini sağladım. Bu yazıda, 4 farklı modeli test etme sürecimi ve Mistral Small 24B ile %100 doğruluk elde etme hikayemi paylaşıyorum.
Problem: 100+ Menü İçinde Kaybolmak
Geliştirdiğimiz kurumsal ERP sisteminde 100'den fazla menü öğesi vardı. Kullanıcılar sık sık "beyanname girmek istiyorum" veya "rapor görmek istiyorum" gibi sorunlarla destek ekibine geliyordu. Klasik arama çözümleri yetersiz kalıyordu çünkü:
- Nested (iç içe) menüler: 3-4 seviye derinlikte alt menüler
- Benzer isimler: "Rapor" kelimesi 12 farklı menüde geçiyordu
- Türkçe keyword matching: "gb" → "Gümrük Beyannamesi" eşleşmesi zordu
- Rol bazlı erişim: Her kullanıcı kendi yetkili olduğu menüleri görmeli
Neden Local LLM?
Cloud LLM'in Dezavantajları:
- Maliyet: API call başına ücret (ayda binlerce sorgu = yüksek fatura)
- Veri güvenliği: Menü yapısı ve kullanıcı bilgileri 3rd party'ye gidiyor
- Latency: Her istek internet üzerinden gidip geliyor
- Bağımlılık: API down olursa sistem çalışmıyor
Local LLM'in Avantajları:
- ✅ $0 maliyet: Sadece GPU elektriği
- ✅ Tam veri kontrolü: Hiçbir şey dışarı çıkmıyor
- ✅ Düşük latency: Local network üzerinden ~200ms
- ✅ Offline çalışma: İnternet bağımlılığı yok
Teknoloji Stack
# Local LLM Infrastructure
Platform: Ollama v0.1.22
Frontend: Open WebUI v0.2.5
GPU: NVIDIA RTX 4090 (24GB VRAM)
# Models Tested
- Mistral Small 24B (WINNER)
- Gemma 3 12B
- Gemma 3 4B
- Llama 3.1 8B
- Qwen3-Coder 14B
- DeepSeek R1 14B
# Backend Integration
Spring Boot 3.2
RestTemplate for API calls
JSON parsing with Jackson
Ollama Kurulumu
1. Ollama Installation (Ubuntu/Linux)
# Ollama install
curl -fsSL https://ollama.com/install.sh | sh
# GPU driver check
nvidia-smi
# Model download (Mistral Small 24B - ~14GB)
ollama pull mistral-small:24b
# Test run
ollama run mistral-small:24b
>>> Merhaba!
Merhaba! Size nasıl yardımcı olabilirim?
2. Open WebUI Kurulumu (Docker)
# Docker ile Open WebUI
docker run -d -p 3000:8080 \
--gpus all \
--add-host=host.docker.internal:host-gateway \
-v open-webui:/app/backend/data \
--name open-webui \
--restart always \
ghcr.io/open-webui/open-webui:main
# UI'ye erişim
# http://localhost:3000
System Prompt Engineering
LLM'in doğru çalışması için kritik olan kısım: System Prompt. İşte 20+ iterasyon sonrası optimize ettiğim prompt:
Sen bir web uygulaması menü yönlendirme asistanısın.
## KRİTİK KURALLAR:
1. SADECE menü yönlendirmesi yap
2. Menü dışı sorulara: "Üzgünüm, ben sadece menü yönlendirmesi yapabilirim"
3. MUTLAKA JSON formatında yanıt ver
4. Nested (iç içe) menüleri recursive ara
5. Belirsiz durumlarda TÜM eşleşmeleri döndür
## MENÜ YAPISI:
100+ menü öğesi, 3-4 seviye derinlik
Örnek: Ana Menü → Beyanname → Gümrük Beyannnamesi → GB İzleme
## YANIT FORMATI:
{
"matches": [
{
"path": "Sol menü → Beyanname → GB İzleme",
"route": "/declaration/list?type=GB",
"permission": "READ_DECLARATION"
}
],
"total": 1,
"query": "kullanıcının sorusu"
}
## ÖRNEK SORGULAR:
"rapor görmek istiyorum" → 12 farklı rapor menüsünü döndür
"beyanname girmek istiyorum" → Beyanname Giriş sayfası
"gb izlemek istiyorum" → GB İzleme sayfası
Model Karşılaştırması: 4 Farklı Test
"rapor" kelimesini arayarak her modeli test ettim. Sistem menülerinde 12 farklı rapor sayfası var. İdeal sonuç: 12/12 eşleşme.
Test Sonuçları:
Detaylı Analiz:
❌ Gemma 3 4B (1/12 - En Kötü)
- Sadece 1 menü buldu, 11 tanesini kaçırdı
- JSON formatını bozdu, path_description'a prompt metnini kopyaladı
- Nested arama yapamıyor
- 4B parametre küçük görevler için bile yetersiz
🟡 Llama 3.1 8B (5/12 - Kötü)
- 5 menü buldu ama 7 tanesini kaçırdı
- Kural uyumu zayıf
- Format doğruluğu bozuk
- 8B bile bu görev için yetersiz
🟡 Gemma 3 12B (6/12 - Orta)
- Yarısını buldu, yarısını kaçırdı
- JSON formatı iyi
- Nested aramayı kısmen yapıyor
- Production için güvenilir değil
✅ Mistral Small 24B (12/12 - PERFECT)
- Tüm 12 rapor menüsünü buldu
- Mükemmel JSON formatı
- Recursive nested arama yapıyor
- Instruction-following çok iyi
- Production-ready
Neden Mistral Small 24B Kazandı?
1. Model Parametresi Önemli
24B parametre, complex instruction'ları anlama ve JSON gibi yapılandırılmış output üretmede kritik fark yaratıyor. 4B/8B modeller basit task'ler için yeterli ama kurumsal seviye ihtiyaçlar için yetersiz kalıyor.
2. Instruction-Following Kalitesi
Mistral Small, system prompt'taki kuralları harfiyen takip etti. Diğer modeller sık sık prompt'tan sapma gösterdi.
3. Nested Search Capability
3-4 seviye derinlikteki menüleri recursive olarak tarayabilme yeteneği sadece Mistral Small'da var. Diğerleri yüzeysel arama yapıyor.
Spring Boot Entegrasyonu
@Service
@Slf4j
public class OllamaMenuAssistantService {
private static final String OLLAMA_API_URL = "http://localhost:11434/api/generate";
private static final String MODEL_NAME = "mistral-small:24b";
@Autowired
private RestTemplate restTemplate;
@Autowired
private ObjectMapper objectMapper;
public MenuAssistantResponse findMenu(String userQuery) {
try {
// Ollama request payload
Map request = new HashMap<>();
request.put("model", MODEL_NAME);
request.put("prompt", userQuery);
request.put("system", getSystemPrompt());
request.put("stream", false);
request.put("format", "json");
// API call
log.info("Ollama API call: {}", userQuery);
long startTime = System.currentTimeMillis();
ResponseEntity response = restTemplate.postForEntity(
OLLAMA_API_URL,
request,
String.class
);
long duration = System.currentTimeMillis() - startTime;
log.info("Ollama response time: {}ms", duration);
// Parse JSON response
JsonNode root = objectMapper.readTree(response.getBody());
String llmOutput = root.get("response").asText();
return objectMapper.readValue(llmOutput, MenuAssistantResponse.class);
} catch (Exception e) {
log.error("Ollama API error", e);
return MenuAssistantResponse.error("Menü arama hatası");
}
}
private String getSystemPrompt() {
// System prompt buraya (yukarıda paylaştım)
return "Sen bir web uygulaması menü yönlendirme asistanısın...";
}
}
Performance Metrics
Maliyet Karşılaştırması: Cloud vs Local
# Cloud LLM (OpenAI GPT-4)
10,000 queries/month × $0.03/1K tokens = ~$300/month
+ Latency: 500-1000ms
# Local LLM (Mistral Small 24B)
Electricity: ~$20/month (GPU power)
+ Latency: 200ms
= $280/month savings 💰
Zorluklar ve Çözümler
1. VRAM Limiti
Problem: Mistral Small 24B, full precision'da ~48GB VRAM istiyor. RTX 4090'ım sadece 24GB.
Çözüm: 4-bit quantization (Q4_K_M) kullandım. Model boyutu 48GB'den 14GB'ye düştü, accuracy kaybı minimal (~2%).
# Quantized model pull
ollama pull mistral-small:24b-q4_K_M
# VRAM usage check
nvidia-smi
# 14GB / 24GB (safe margin)
2. Prompt Drift (Kural Sapmasi)
Problem: LLM bazen system prompt'taki kuralları unutuyor, menü dışı sorulara cevap vermeye çalışıyor.
Çözüm: Prompt'ta "ASLA", "MUTLAKA", "SADECE" gibi güçlü keyword'ler kullandım. Ayrıca her istek başında kuralları tekrarladım.
3. JSON Parse Hatası
Problem: Bazen LLM valid JSON yerine Markdown wrapped JSON döndürüyordu:
```json
{
"matches": [...]
}
```
Çözüm: Backend'de regex ile Markdown fence'leri temizledim:
String cleanJson = llmOutput
.replaceAll("```json\\n?", "")
.replaceAll("```\\n?", "")
.trim();
return objectMapper.readValue(cleanJson, MenuAssistantResponse.class);
Production Deployment
Docker Compose Setup
version: '3.8'
services:
ollama:
image: ollama/ollama:latest
container_name: ollama
ports:
- "11434:11434"
volumes:
- ollama-data:/root/.ollama
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
restart: unless-stopped
open-webui:
image: ghcr.io/open-webui/open-webui:main
container_name: open-webui
ports:
- "3000:8080"
environment:
- OLLAMA_BASE_URL=http://ollama:11434
volumes:
- open-webui-data:/app/backend/data
depends_on:
- ollama
restart: unless-stopped
volumes:
ollama-data:
open-webui-data:
Öğrendiklerim ve Tavsiyeler
✅ Yapılması Gerekenler:
- Model seçimi kritik: 24B+ parametre production için minimum
- Quantization kullan: VRAM'i optimize et, accuracy kaybı minimal
- Prompt engineering'e zaman ayır: 20+ iterasyon normal
- Fallback mekanizması: LLM fail ederse klasik search devreye girsin
- Monitoring ekle: Response time, error rate, accuracy tracking
- A/B testing yap: Farklı modelleri gerçek kullanıcılarla test et
❌ Kaçınılması Gerekenler:
- Küçük modellere güvenme: 4B/8B production için risk
- Prompt'u değiştirmeden canlıya alma: Mutlaka test et
- VRAM limitini aşma: OOM (Out of Memory) crash'e sebep olur
- Error handling ihmal etme: LLM bazen beklenmedik output verir
- Cloud LLM'e körü körüne geçme: Local alternatif genelde daha iyi
Gelecek Planları
1. RAG (Retrieval Augmented Generation) Entegrasyonu
Menü yapısını vector database'e (Chroma/Qdrant) indexleyerek semantic search ekleyeceğim. Böylece "fatura" → "invoice" gibi cross-language query'ler de çalışacak.
2. Fine-tuning
Kullanıcı sorgu geçmişinden dataset oluşturup model'i fine-tune edeceğim. Domain-specific performansı daha da artıracak.
3. Multi-modal Support
Screenshot'tan menü bulma: Kullanıcı ekran görüntüsü atınca LLM o sayfaya giden yolu gösterecek.
Sonuç
Local LLM ile kurumsal bir problemi çözdüm: 100+ menü arasında yönlendirme. Cloud LLM yerine Ollama + Mistral Small 24B kullanarak:
- ✅ $280/ay maliyet tasarrufu
- ✅ %100 veri güvenliği (hiçbir veri dışarı çıkmıyor)
- ✅ 200ms latency (cloud'dan 3-5x hızlı)
- ✅ 12/12 accuracy (production-ready)
En önemli ders: Model parametresi kritik. 4B/8B modeller hobi projeleri için yeterli ama kurumsal ihtiyaçlar için 24B+ gerekli. Prompt engineering ve quantization ile local LLM'ler artık production-ready seviyede.