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ü:

Neden Local LLM?

Cloud LLM'in Dezavantajları:

Local LLM'in Avantajları:

Teknoloji Stack

yaml Tech 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)

bash Ollama Kurulumu
# 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)

bash Open WebUI 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:

plaintext System Prompt (Final Version)
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ı:

1/12
Gemma 3 4B
5/12
Llama 3.1 8B
6/12
Gemma 3 12B
12/12
Mistral Small 24B ✅

Detaylı Analiz:

❌ Gemma 3 4B (1/12 - En Kötü)

🟡 Llama 3.1 8B (5/12 - Kötü)

🟡 Gemma 3 12B (6/12 - Orta)

✅ Mistral Small 24B (12/12 - PERFECT)

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

java OllamaService.java
@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

~200ms
Avg Response Time
100%
Accuracy (12/12)
$0
Monthly Cost

Maliyet Karşılaştırması: Cloud vs Local

plaintext Cost Analysis (Monthly)
# 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%).

bash Quantized Model
# 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 Invalid Output Example
```json
{
  "matches": [...]
}
```

Çözüm: Backend'de regex ile Markdown fence'leri temizledim:

java JSON Cleanup
String cleanJson = llmOutput
    .replaceAll("```json\\n?", "")
    .replaceAll("```\\n?", "")
    .trim();

return objectMapper.readValue(cleanJson, MenuAssistantResponse.class);

Production Deployment

Docker Compose Setup

yaml docker-compose.yml
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:

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.

Sorularınız mı var?

Local LLM, Ollama veya prompt engineering hakkında soru ve deneyimlerinizi email veya LinkedIn üzerinden benimle paylaşabilirsiniz.