LocalAI ile Konuşmalı Müşteri Destek Botu: Özel Bilgi Tabanı ve Fine-Tuned Model Dağıtımı
Müşteri destek ekibinin her gece mesai yapması, aynı soruları tekrar tekrar yanıtlaması ve bilgi tabanının sürekli güncellenmesi gerektiğinde ne kadar yorucu bir süreç olduğunu hepimiz biliyoruz. Bu sorunu çözmek için büyük bulut sağlayıcılarına aylık yüzlerce dolar ödemek zorunda değilsiniz. LocalAI kullanarak kendi sunucunuzda çalışan, özel bilgi tabanınızla desteklenmiş ve ihtiyacınıza göre fine-tune edilmiş bir müşteri destek botu kurabilirsiniz. Üstelik verileriniz hiçbir zaman dışarı çıkmaz.
Mimariye Genel Bakış
Bu yazıda kuracağımız sistem üç ana katmandan oluşuyor. LocalAI sunucu tarafında model inference işini yapıyor, ChromaDB ya da basit bir vektör veritabanı bilgi tabanını indeksliyor, ve bir Python FastAPI servisi bunları birleştirerek kullanıcıya sunum yapıyor. Tüm bunlar Docker Compose ile ayağa kaldırılacak, yönetimi kolay olacak.
Senaryo olarak şunu düşünelim: Orta ölçekli bir e-ticaret şirketinin destek botu. Ürün iade politikası, kargo bilgileri, üyelik sorunları ve sık sorulan sorulara otomatik yanıt vermesi gerekiyor. Şu an 3 kişilik bir destek ekibi bu işi yapıyor ve gecenin 2’sinde gelen “siparişim nerede?” sorularına kimse bakmıyor.
Sunucu Gereksinimleri ve LocalAI Kurulumu
Önce donanım tarafını konuşalım. GPU olmadan da çalışabilirsiniz ama performans farkı ciddi olur.
- GPU’lu senaryo: NVIDIA RTX 3090 veya A100, 7B model için yeterli
- CPU’lu senaryo: En az 16 çekirdek, 32GB RAM, Mistral 7B Q4 quantized model çalıştırılabilir
- Disk: Model dosyaları için en az 20GB boş alan
Sunucuya Docker ve Docker Compose kurulu olduğunu varsayıyorum. LocalAI için proje dizinini hazırlayalım:
mkdir -p /opt/support-bot/{models,config,knowledge-base,api}
cd /opt/support-bot
# Model dizini için alt klasörler
mkdir -p models/mistral-7b
mkdir -p knowledge-base/raw
mkdir -p knowledge-base/processed
Şimdi Docker Compose dosyasını oluşturalım:
cat > /opt/support-bot/docker-compose.yml << 'EOF'
version: '3.8'
services:
localai:
image: quay.io/go-skynet/local-ai:latest-aio-cpu
container_name: localai
ports:
- "8080:8080"
volumes:
- ./models:/models
- ./config:/config
environment:
- MODELS_PATH=/models
- CONTEXT_SIZE=4096
- THREADS=8
- PARALLEL_REQUESTS=2
restart: unless-stopped
networks:
- botnet
chromadb:
image: chromadb/chroma:latest
container_name: chromadb
ports:
- "8000:8000"
volumes:
- ./chroma-data:/chroma/chroma
environment:
- IS_PERSISTENT=TRUE
- ANONYMIZED_TELEMETRY=FALSE
restart: unless-stopped
networks:
- botnet
bot-api:
build: ./api
container_name: bot-api
ports:
- "9000:9000"
volumes:
- ./knowledge-base:/knowledge-base
environment:
- LOCALAI_URL=http://localai:8080
- CHROMA_URL=http://chromadb:8000
- MODEL_NAME=mistral-7b-support
depends_on:
- localai
- chromadb
restart: unless-stopped
networks:
- botnet
networks:
botnet:
driver: bridge
EOF
Model İndirme ve LocalAI Konfigürasyonu
Mistral 7B’nin quantized versiyonunu kullanacağız. Q4_K_M formatı hem boyut hem kalite dengesi açısından müşteri destek senaryosu için ideal:
cd /opt/support-bot/models/mistral-7b
# Mistral 7B Instruct Q4_K_M modelini indir
wget https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF/resolve/main/mistral-7b-instruct-v0.2.Q4_K_M.gguf
-O mistral-7b-instruct.gguf
# Dosya boyutunu kontrol et, yaklaşık 4.4GB olmalı
ls -lh mistral-7b-instruct.gguf
LocalAI için model konfigürasyon dosyasını hazırlayalım. Bu dosya modelin nasıl davranacağını belirliyor:
cat > /opt/support-bot/config/mistral-7b-support.yaml << 'EOF'
name: mistral-7b-support
backend: llama
model: mistral-7b/mistral-7b-instruct.gguf
context_size: 4096
threads: 8
f16: true
mmap: true
mmlock: false
parameters:
temperature: 0.3
top_p: 0.9
top_k: 40
repeat_penalty: 1.1
max_new_tokens: 512
template:
chat: |
<s>[INST] <<SYS>>
Sen bir e-ticaret şirketinin müşteri destek asistanısın. Adın Asistan.
Sadece şirketin politikaları ve ürünleri hakkında bilgi ver.
Bilgi tabanında olmayan konularda "Bu konuda size yardımcı olamıyorum, lütfen destek ekibimizle iletişime geçin" de.
Kısa, net ve yardımsever ol. Türkçe yanıt ver.
<<SYS>>
Bağlam: {{.Context}}
Kullanıcı sorusu: {{.Input}} [/INST]
EOF
Bilgi Tabanı Hazırlama ve İndeksleme
Bilgi tabanı, botun “beyni” olan kısım. Ham belgeleri hazırlayıp vektör veritabanına yükleyeceğiz. Önce örnek dökümanları oluşturalım:
# İade politikası dökümanı
cat > /opt/support-bot/knowledge-base/raw/iade-politikasi.txt << 'EOF'
İADE VE DEĞİŞİM POLİTİKASI
Ürün iade süresi: Teslimat tarihinden itibaren 30 gün içinde iade yapılabilir.
Kullanılmamış ve orijinal ambalajında olan ürünler iade edilebilir.
Kozmetik ürünler açıldıktan sonra iade edilemez.
İndirimli ürünlerde iade süresi 14 gündür.
İade prosedürü:
1. Web sitesindeki "İadelerim" bölümünden talep oluşturun
2. Size özel iade kodu e-posta ile gönderilir
3. Kodu kargo görevlisine verin, ücretsiz teslim edin
4. Para iadesi 3-5 iş günü içinde hesabınıza yansır
5. Kredi kartı iadeleri bankanıza göre 7-10 gün sürebilir
Hasar/ayıplı ürün: 2 yıl garanti kapsamındadır, ücretsiz değişim yapılır.
EOF
# Kargo bilgileri
cat > /opt/support-bot/knowledge-base/raw/kargo-bilgileri.txt << 'EOF'
KARGO VE TESLİMAT BİLGİLERİ
Standart kargo: 2-4 iş günü, 29.90 TL
Hızlı kargo: 1 iş günü, 49.90 TL
500 TL ve üzeri alışverişlerde kargo ücretsiz
Kargo takibi: Sipariş onayı e-postasındaki takip linki ile anlık takip yapılabilir.
SMS bildirimleri: Kargo çıkışında ve teslimat günü SMS gönderilir.
Hafta sonu teslimat: Cumartesi günleri teslim yapılmaktadır, Pazar yapılmamaktadır.
Saat aralığı: 09:00-21:00 arası teslimat gerçekleşir.
Kapıda bulunamazsanız: Komşuya teslim seçeneği mevcuttur veya şubeye bırakılır.
EOF
Şimdi bu dökümanları işleyip ChromaDB’ye yükleyecek Python scriptini yazalım:
cat > /opt/support-bot/api/ingest.py << 'EOF'
import os
import glob
import chromadb
from chromadb.utils import embedding_functions
import hashlib
CHROMA_URL = os.getenv("CHROMA_URL", "http://localhost:8000")
KNOWLEDGE_PATH = "/knowledge-base/raw"
def chunk_text(text, chunk_size=500, overlap=50):
"""Metni örtüşen parçalara böl"""
words = text.split()
chunks = []
start = 0
while start < len(words):
end = min(start + chunk_size, len(words))
chunk = " ".join(words[start:end])
chunks.append(chunk)
start += chunk_size - overlap
return chunks
def ingest_documents():
client = chromadb.HttpClient(
host=CHROMA_URL.replace("http://", "").split(":")[0],
port=int(CHROMA_URL.split(":")[-1])
)
# Sentence transformer ile embedding, CPU'da da çalışır
ef = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="paraphrase-multilingual-MiniLM-L12-v2"
)
collection = client.get_or_create_collection(
name="support_knowledge",
embedding_function=ef,
metadata={"hnsw:space": "cosine"}
)
txt_files = glob.glob(f"{KNOWLEDGE_PATH}/*.txt")
total_chunks = 0
for filepath in txt_files:
filename = os.path.basename(filepath)
print(f"İşleniyor: {filename}")
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
chunks = chunk_text(content)
for i, chunk in enumerate(chunks):
doc_id = hashlib.md5(f"{filename}_{i}".encode()).hexdigest()
collection.upsert(
documents=[chunk],
ids=[doc_id],
metadatas=[{"source": filename, "chunk_index": i}]
)
total_chunks += 1
print(f"Toplam {total_chunks} chunk yüklendi.")
if __name__ == "__main__":
ingest_documents()
EOF
Ana Bot API’sini Yazma
FastAPI ile ana servisi oluşturalım. Bu servis kullanıcıdan gelen soruyu alıyor, bilgi tabanından ilgili bağlamı çekiyor ve LocalAI’ya gönderiyor:
cat > /opt/support-bot/api/main.py << 'EOF'
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import httpx
import chromadb
from chromadb.utils import embedding_functions
import os
import logging
from datetime import datetime
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI(title="Müşteri Destek Botu API")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
LOCALAI_URL = os.getenv("LOCALAI_URL", "http://localhost:8080")
CHROMA_URL = os.getenv("CHROMA_URL", "http://localhost:8000")
MODEL_NAME = os.getenv("MODEL_NAME", "mistral-7b-support")
chroma_host = CHROMA_URL.replace("http://", "").split(":")[0]
chroma_port = int(CHROMA_URL.split(":")[-1])
chroma_client = chromadb.HttpClient(host=chroma_host, port=chroma_port)
ef = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="paraphrase-multilingual-MiniLM-L12-v2"
)
class ChatRequest(BaseModel):
message: str
session_id: str = "default"
history: list = []
class ChatResponse(BaseModel):
response: str
sources: list
session_id: str
timestamp: str
def get_relevant_context(query: str, n_results: int = 3) -> tuple:
try:
collection = chroma_client.get_collection(
name="support_knowledge",
embedding_function=ef
)
results = collection.query(
query_texts=[query],
n_results=n_results
)
context_parts = results["documents"][0]
sources = [m["source"] for m in results["metadatas"][0]]
context = "nn".join(context_parts)
return context, list(set(sources))
except Exception as e:
logger.error(f"ChromaDB hatası: {e}")
return "", []
@app.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest):
context, sources = get_relevant_context(request.message)
messages = [{"role": "system", "content": f"Bilgi tabanı bağlamı:n{context}"}]
for h in request.history[-4:]:
messages.append(h)
messages.append({"role": "user", "content": request.message})
payload = {
"model": MODEL_NAME,
"messages": messages,
"temperature": 0.3,
"max_tokens": 512,
"stream": False
}
async with httpx.AsyncClient(timeout=60.0) as client:
try:
response = await client.post(
f"{LOCALAI_URL}/v1/chat/completions",
json=payload
)
response.raise_for_status()
result = response.json()
bot_response = result["choices"][0]["message"]["content"]
except Exception as e:
logger.error(f"LocalAI hatası: {e}")
raise HTTPException(status_code=503, detail="Model servisi şu an kullanılamıyor")
return ChatResponse(
response=bot_response,
sources=sources,
session_id=request.session_id,
timestamp=datetime.now().isoformat()
)
@app.get("/health")
async def health():
return {"status": "ok", "model": MODEL_NAME}
EOF
Fine-Tuning ile Modeli Özelleştirme
Eğer elinizde geçmiş müşteri destek konuşmaları varsa, modeli bu verilerle fine-tune ederek çok daha iyi sonuçlar alabilirsiniz. Bunun için PEFT/LoRA yöntemi kullanacağız:
# Fine-tuning için gerekli paketleri kurun (GPU sunucusunda)
pip install transformers peft datasets trl torch bitsandbytes
# Eğitim verisini hazırlama scripti
cat > /opt/support-bot/finetune/prepare_data.py << 'EOF'
import json
import csv
# Örnek: Eski destek konuşmalarını JSONL formatına çevir
def convert_csv_to_training_data(input_csv, output_jsonl):
training_samples = []
with open(input_csv, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
sample = {
"instruction": row["soru"],
"input": "",
"output": row["cevap"]
}
# Alpaca formatına dönüştür
formatted = {
"text": f"<s>[INST] {sample['instruction']} [/INST] {sample['output']}</s>"
}
training_samples.append(formatted)
with open(output_jsonl, "w", encoding="utf-8") as f:
for sample in training_samples:
f.write(json.dumps(sample, ensure_ascii=False) + "n")
print(f"{len(training_samples)} örnek hazırlandı")
convert_csv_to_training_data(
"destek-konusmalari.csv",
"training-data.jsonl"
)
EOF
Fine-tuning tamamlandıktan sonra LoRA adaptörünü base model ile birleştirip GGUF formatına export edin. Bu işlem için llama.cpp’nin convert araçlarını kullanabilirsiniz. Birleştirilmiş modeli LocalAI’ın models dizinine kopyalamanız yeterli.
Monitoring ve Log Yönetimi
Production ortamında botun ne kadar iyi çalıştığını takip etmek kritik:
cat > /opt/support-bot/monitoring/log-analyzer.sh << 'EOF'
#!/bin/bash
LOG_DIR="/opt/support-bot/logs"
DATE=$(date +%Y-%m-%d)
echo "=== Günlük Bot Raporu: $DATE ==="
# Toplam konuşma sayısı
TOTAL=$(grep -c "POST /chat" $LOG_DIR/access.log 2>/dev/null || echo 0)
echo "Toplam sorgu: $TOTAL"
# Hata oranı
ERRORS=$(grep -c "503|500" $LOG_DIR/access.log 2>/dev/null || echo 0)
echo "Hata sayısı: $ERRORS"
# Ortalama yanıt süresi (nginx log formatından)
if [ -f "$LOG_DIR/response-times.log" ]; then
AVG_TIME=$(awk '{sum+=$1; count++} END {printf "%.2f", sum/count}' $LOG_DIR/response-times.log)
echo "Ortalama yanıt süresi: ${AVG_TIME}s"
fi
# En çok sorulan kategoriler
echo ""
echo "En çok sorulan kaynak dökümanlar:"
grep "sources" $LOG_DIR/bot.log | grep -oP '"[^"]+.txt"' | sort | uniq -c | sort -rn | head -5
EOF
chmod +x /opt/support-bot/monitoring/log-analyzer.sh
# Cron job ekle
echo "0 8 * * * /opt/support-bot/monitoring/log-analyzer.sh >> /var/log/bot-daily-report.log 2>&1" | crontab -
Widget Entegrasyonu
Botu mevcut web sitenize entegre etmek için basit bir JavaScript widget’ı:
cat > /opt/support-bot/widget/chat-widget.js << 'EOF'
const SupportBot = {
sessionId: Math.random().toString(36).substring(7),
history: [],
apiUrl: "https://bot.sirketiniz.com/chat",
async sendMessage(message) {
const response = await fetch(this.apiUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: message,
session_id: this.sessionId,
history: this.history
})
});
if (!response.ok) {
return "Üzgünüm, şu an yanıt veremiyorum. Lütfen destek ekibimizi arayın.";
}
const data = await response.json();
// Konuşma geçmişini güncelle
this.history.push({ role: "user", content: message });
this.history.push({ role: "assistant", content: data.response });
// Son 6 mesajı tut
if (this.history.length > 6) {
this.history = this.history.slice(-6);
}
return data.response;
}
};
EOF
Güvenlik ve Rate Limiting
Botu herkese açık yapıyorsanız mutlaka rate limiting ekleyin:
# Nginx konfigürasyonu ile rate limiting
cat > /etc/nginx/conf.d/support-bot.conf << 'EOF'
limit_req_zone $binary_remote_addr zone=bot_limit:10m rate=10r/m;
server {
listen 443 ssl;
server_name bot.sirketiniz.com;
ssl_certificate /etc/letsencrypt/live/bot.sirketiniz.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/bot.sirketiniz.com/privkey.pem;
location /chat {
limit_req zone=bot_limit burst=5 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:9000;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 90s;
# LocalAI'ya direkt erişimi engelle
# 8080 portu sadece iç ağdan erişilebilir olmalı
}
location /health {
proxy_pass http://127.0.0.1:9000;
allow 10.0.0.0/8;
deny all;
}
}
EOF
nginx -t && systemctl reload nginx
Sistemi Ayağa Kaldırma
Her şey hazır olduğunda sırasıyla başlatın:
cd /opt/support-bot
# Tüm servisleri başlat
docker-compose up -d
# LocalAI'ın hazır olmasını bekle (model yükleme 2-3 dakika sürebilir)
echo "LocalAI hazırlanıyor..."
until curl -s http://localhost:8080/readyz > /dev/null 2>&1; do
sleep 5
echo "Bekleniyor..."
done
echo "LocalAI hazır!"
# Bilgi tabanını yükle
docker-compose exec bot-api python ingest.py
# Test sorusu gönder
curl -X POST http://localhost:9000/chat
-H "Content-Type: application/json"
-d '{"message": "İade politikanız nedir?", "session_id": "test-001"}'
Gerçek Dünyada Karşılaşılan Sorunlar ve Çözümleri
Yavaş yanıt süresi: CPU üzerinde Mistral 7B Q4 yaklaşık 8-15 saniye yanıt üretiyor. Bunu kabul edilebilir kılmak için streaming response kullanın, kullanıcı ilk tokenları hemen görsün. Alternatif olarak Phi-2 veya TinyLlama gibi daha küçük modelleri deneyin.
Halüsinasyon sorunları: Model bilgi tabanında olmayan şeyleri uydurabiliyor. Sistem prompt’una “Eğer bağlamda bilgi yoksa kesinlikle cevap verme” gibi katı yönergeler ekleyin ve ChromaDB’den dönen similarity score’u threshold ile filtreleyin. Score 0.7’nin altındaysa bağlamı modele göndermeyin.
Bellek tüketimi: Her model yükleme yaklaşık 4-5GB RAM istiyor. Birden fazla model kullanmak zorundaysanız LocalAI’ın model unloading özelliğini aktif edin. PRELOAD_MODELS=false ve WATCHDOG_STABILITY=5 ayarları ile kullanılmayan modeller bellekten temizlenir.
Türkçe karakter sorunları: GGUF modellerde bazen Türkçe karakterler bozulabiliyor. Embedding modelinin çok dilli versiyon olduğundan emin olun. paraphrase-multilingual-MiniLM-L12-v2 bu iş için test edilmiş ve güvenilir bir seçenek.
Sonuç
Bu mimarinin en güzel yanı tamamen kontrolünüzde olması. Veriler sunucunuzda kalıyor, aylık API faturası yok ve ihtiyaca göre ölçeklenebilir. Bizim test ettiğimiz 8 çekirdek, 32GB RAM’li bir sunucuda günlük 500-600 konuşmayı sorunsuz kaldırabiliyor.
Bir sonraki adım olarak bilgi tabanını otomatik güncelleyen bir pipeline ekleyebilirsiniz. Destek ekibinin yeni bir politika dökümanı yüklediğinde ingest.py scriptinin otomatik çalışması, veya müşteri konuşmalarından beğenilen yanıtların fine-tuning verisine eklenmesi sistemi zamanla daha da iyi hale getirir. Fine-tuning döngüsünü kurduğunuzda model gerçekten şirketinizin sesini ve tarzını öğreniyor, genel bir modelden çok daha tutarlı yanıtlar üretiyor.
Sunucunuzda GPU varsa modeli GPU’ya aldığınız anda yanıt süreleri 1-2 saniyeye düşüyor ve kullanıcı deneyimi tamamen değişiyor. O noktada bu sistemi telefon hattınızdaki IVR sistemiyle de entegre edebilirsiniz ama bu başka bir yazının konusu.
