LangChain ile Akıllı Müşteri Destek Botu Geliştirme

Müşteri destek operasyonları, bir şirketin en fazla kaynak harcadığı alanların başında gelir. Gece 3’te gelen “şifremi unuttum” sorusuna cevap verecek birini işe almak yerine, LangChain ile akıllı bir bot geliştirip bu yükü otomatize etmek hem maliyeti düşürür hem de müşteri memnuniyetini artırır. Bu yazıda sıfırdan bir müşteri destek botu inşa edeceğiz; sadece “Merhaba, size nasıl yardımcı olabilirim?” diyen değil, gerçekten belgeleri okuyup anlayan, konuşma geçmişini hatırlayan ve gerektiğinde insan desteğine yönlendiren bir sistem.

Genel Mimariyi Anlamak

LangChain tabanlı bir müşteri destek botunun temel bileşenleri şunlardır:

  • LLM (Large Language Model): OpenAI GPT-4, Anthropic Claude veya açık kaynak Llama 3 gibi modeller
  • Vector Store: Şirket belgelerinin gömüldüğü (embedding) vektör veritabanı, Chroma veya Pinecone
  • Memory: Konuşma geçmişini tutan bellek katmanı
  • Retrieval Chain: Kullanıcı sorusuna göre ilgili belgeleri çekip LLM’e besleyen zincir
  • Tools: Sipariş sorgulama, bilet oluşturma gibi harici sistemlere bağlanan araçlar

Gerçek dünya senaryosu olarak bir e-ticaret şirketi düşünelim. Müşteriler iade politikası, kargo takibi ve ürün garantisi hakkında günde yüzlerce soru soruyor. Destek ekibi bunların %70’ini aynı şekilde cevaplıyor. Bu tam da otomasyonun parlayacağı yer.

Ortam Kurulumu

Önce gerekli paketleri kuralım. Python 3.11+ öneriyorum, özellikle async operasyonlarda belirgin performans farkı var.

# Sanal ortam oluştur
python -m venv support-bot-env
source support-bot-env/bin/activate  # Windows: support-bot-envScriptsactivate

# Temel paketleri kur
pip install langchain langchain-openai langchain-community
pip install chromadb tiktoken
pip install fastapi uvicorn python-dotenv
pip install unstructured pdf2image pytesseract

# Paket versiyonlarını sabitle
pip freeze > requirements.txt

.env dosyasını oluştur:

cat > .env << 'EOF'
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxx
OPENAI_MODEL=gpt-4o-mini
EMBEDDING_MODEL=text-embedding-3-small
CHROMA_PERSIST_DIR=./chroma_db
MAX_TOKENS=2048
TEMPERATURE=0.1
EOF

Sıcaklık değerini (temperature) 0.1 olarak düşük tutuyoruz çünkü müşteri desteğinde tutarlı, öngörülebilir cevaplar istiyoruz. Yaratıcı yazarlık değil bu.

Belge Yükleme ve Vector Store Oluşturma

Şirketin PDF kılavuzları, FAQ sayfaları ve politika belgelerini sisteme besleyeceğiz. Bu adım, botun “beynini” oluşturuyor.

# document_loader.py
import os
from pathlib import Path
from dotenv import load_dotenv
from langchain_community.document_loaders import (
    PyPDFLoader,
    TextLoader,
    DirectoryLoader
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma

load_dotenv()

def load_and_index_documents(docs_path: str = "./company_docs"):
    """
    Şirket belgelerini yükler, parçalara böler ve vektör veritabanına kaydeder.
    """
    # PDF ve TXT dosyalarını yükle
    loaders = {
        "**/*.pdf": PyPDFLoader,
        "**/*.txt": TextLoader,
    }
    
    all_documents = []
    
    for glob_pattern, loader_class in loaders.items():
        loader = DirectoryLoader(
            docs_path,
            glob=glob_pattern,
            loader_cls=loader_class,
            show_progress=True
        )
        try:
            docs = loader.load()
            all_documents.extend(docs)
            print(f"[OK] {glob_pattern}: {len(docs)} belge yüklendi")
        except Exception as e:
            print(f"[HATA] {glob_pattern}: {e}")
    
    if not all_documents:
        raise ValueError("Hiç belge yüklenemedi. Docs dizinini kontrol edin.")
    
    # Belgeleri parçalara böl
    # chunk_overlap: parçalar arasında bağlam sürekliliği sağlar
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=1000,
        chunk_overlap=200,
        separators=["nn", "n", ".", "!", "?", ",", " "],
        length_function=len
    )
    
    chunks = splitter.split_documents(all_documents)
    print(f"nToplam chunk sayısı: {len(chunks)}")
    
    # Embedding oluştur ve Chroma'ya kaydet
    embeddings = OpenAIEmbeddings(
        model=os.getenv("EMBEDDING_MODEL"),
        openai_api_key=os.getenv("OPENAI_API_KEY")
    )
    
    vectorstore = Chroma.from_documents(
        documents=chunks,
        embedding=embeddings,
        persist_directory=os.getenv("CHROMA_PERSIST_DIR")
    )
    
    print(f"[OK] Vector store oluşturuldu: {os.getenv('CHROMA_PERSIST_DIR')}")
    return vectorstore

if __name__ == "__main__":
    load_and_index_documents()

chunk_size değeri için şunu söyleyeyim: 1000 token civarı genellikle iyi çalışır ama belgeleriniz çok teknik veya tablo doluysa 500-700 arasına çekmek daha net retrieval sonuçları verir. Test et, gör.

Ana Bot Sınıfı: Konuşma Zinciri

Şimdi asıl işi yapan sınıfı yazalım. Bu sınıf hem retrieval hem de memory yönetimini üstlenecek.

# support_bot.py
import os
from typing import Optional
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.chains import ConversationalRetrievalChain
from langchain.memory import ConversationBufferWindowMemory
from langchain.prompts import PromptTemplate, ChatPromptTemplate
from langchain.prompts.chat import (
    SystemMessagePromptTemplate,
    HumanMessagePromptTemplate
)

load_dotenv()

SYSTEM_PROMPT = """Sen {company_name} şirketinin müşteri destek asistanısın.
Sana verilen şirket belgelerine dayanarak müşteri sorularını yanıtlıyorsun.

Önemli kurallar:
1. Sadece sağlanan belgelerden bilgi kullan. Belgede olmayan konularda "Bu konuda bilgim bulunmuyor, sizi ilgili departmanla bağlayabilirim" de.
2. Cevaplarını Türkçe ver, samimi ama profesyonel bir dil kullan.
3. Eğer müşteri sinirli veya mağdur durumdaysa önce empati kur, sonra çözüm sun.
4. Sipariş numarası veya kişisel bilgi gerektiren işlemlerde "Bu işlem için [email] adresinden bize ulaşmanız gerekiyor" yönlendirmesi yap.
5. Her zaman konuşmanın sonunda "Başka yardımcı olabileceğim bir konu var mı?" diye sor.

Şirket belgeleri:
{context}

Önceki konuşma:
{chat_history}
"""

class CustomerSupportBot:
    def __init__(self, company_name: str = "AcmeTech"):
        self.company_name = company_name
        self.llm = ChatOpenAI(
            model=os.getenv("OPENAI_MODEL", "gpt-4o-mini"),
            temperature=float(os.getenv("TEMPERATURE", 0.1)),
            max_tokens=int(os.getenv("MAX_TOKENS", 2048)),
            openai_api_key=os.getenv("OPENAI_API_KEY")
        )
        
        self.embeddings = OpenAIEmbeddings(
            model=os.getenv("EMBEDDING_MODEL"),
            openai_api_key=os.getenv("OPENAI_API_KEY")
        )
        
        # Mevcut vector store'u yükle
        self.vectorstore = Chroma(
            persist_directory=os.getenv("CHROMA_PERSIST_DIR"),
            embedding_function=self.embeddings
        )
        
        # Retriever: benzerlik skoru 0.7'nin altındaki sonuçları filtrele
        self.retriever = self.vectorstore.as_retriever(
            search_type="similarity_score_threshold",
            search_kwargs={
                "k": 4,
                "score_threshold": 0.7
            }
        )
        
        # Son 10 mesajı hatırlayan bellek
        self.memory = ConversationBufferWindowMemory(
            k=10,
            memory_key="chat_history",
            return_messages=True,
            output_key="answer"
        )
        
        # Prompt şablonu
        self.prompt = ChatPromptTemplate.from_messages([
            SystemMessagePromptTemplate.from_template(
                SYSTEM_PROMPT.replace("{company_name}", self.company_name)
            ),
            HumanMessagePromptTemplate.from_template("{question}")
        ])
        
        # Konuşma zinciri
        self.chain = ConversationalRetrievalChain.from_llm(
            llm=self.llm,
            retriever=self.retriever,
            memory=self.memory,
            combine_docs_chain_kwargs={"prompt": self.prompt},
            return_source_documents=True,
            verbose=False
        )
    
    def ask(self, question: str, session_id: Optional[str] = None) -> dict:
        """
        Müşteri sorusunu işler ve yanıt döner.
        """
        try:
            response = self.chain.invoke({"question": question})
            
            # Kaynakları temizle ve döndür
            sources = []
            for doc in response.get("source_documents", []):
                source = doc.metadata.get("source", "Bilinmiyor")
                page = doc.metadata.get("page", "")
                sources.append(f"{source}" + (f" (Sayfa {page})" if page else ""))
            
            return {
                "answer": response["answer"],
                "sources": list(set(sources)),
                "success": True
            }
        
        except Exception as e:
            return {
                "answer": "Üzgünüm, şu an bir teknik sorun yaşıyorum. Lütfen tekrar deneyin veya [email protected] adresine yazın.",
                "sources": [],
                "success": False,
                "error": str(e)
            }
    
    def reset_memory(self):
        """Konuşma geçmişini temizler."""
        self.memory.clear()

Harici Araçlar (Tools) Entegrasyonu

Gerçek bir destek botu sadece belge okumakla kalmaz. Sipariş sorgulama gibi işlemler için harici sistemlere bağlanması gerekir.

# tools.py
from langchain.tools import tool
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_openai import ChatOpenAI
from langchain import hub
import requests
import os

@tool
def check_order_status(order_id: str) -> str:
    """
    Verilen sipariş numarasının durumunu sorgular.
    Kullanım: Müşteri sipariş takibi sorduğunda çağır.
    """
    # Gerçek ortamda bu kısım ERP/OMS API'sine bağlanır
    # Örnek mock response
    mock_orders = {
        "ORD-001234": {"status": "Kargoya verildi", "carrier": "Aras Kargo", "tracking": "1234567890"},
        "ORD-001235": {"status": "Hazırlanıyor", "carrier": None, "tracking": None},
        "ORD-001236": {"status": "Teslim edildi", "carrier": "MNG Kargo", "tracking": "9876543210"},
    }
    
    order_id = order_id.strip().upper()
    
    if order_id in mock_orders:
        order = mock_orders[order_id]
        response = f"Sipariş #{order_id} durumu: {order['status']}"
        if order["tracking"]:
            response += f"nKargo firması: {order['carrier']}"
            response += f"nTakip numarası: {order['tracking']}"
        return response
    else:
        return f"#{order_id} numaralı sipariş bulunamadı. Sipariş numarasını kontrol edip tekrar deneyin."

@tool
def create_support_ticket(
    customer_email: str,
    issue_type: str,
    description: str
) -> str:
    """
    Yeni bir destek bileti oluşturur.
    issue_type: 'iade', 'teknik', 'fatura', 'genel' seçeneklerinden biri olmalı.
    """
    valid_types = ["iade", "teknik", "fatura", "genel"]
    
    if issue_type not in valid_types:
        issue_type = "genel"
    
    # Gerçekte Zendesk/Freshdesk API çağrısı yapılır
    ticket_id = f"TKT-{hash(customer_email + description) % 100000:05d}"
    
    return (
        f"Destek biletiniz oluşturuldu!n"
        f"Bilet No: {ticket_id}n"
        f"Konu: {issue_type.capitalize()}n"
        f"Ekibimiz en geç 24 saat içinde {customer_email} adresine dönecek."
    )

def build_agent(bot_instance):
    """
    Tools'u kullanan bir agent oluşturur.
    """
    tools = [check_order_status, create_support_ticket]
    
    # LangChain Hub'dan hazır agent prompt'u çek
    prompt = hub.pull("hwchase17/openai-tools-agent")
    
    agent = create_openai_tools_agent(
        llm=bot_instance.llm,
        tools=tools,
        prompt=prompt
    )
    
    return AgentExecutor(
        agent=agent,
        tools=tools,
        verbose=True,
        handle_parsing_errors=True,
        max_iterations=5
    )

FastAPI ile REST API Katmanı

Botu bir web servisi olarak sunmak için FastAPI kullanacağız. Bu sayede React/Vue frontend’i, Slack botu veya WhatsApp entegrasyonu kolayca bağlanabilir.

# api.py
import uuid
from contextlib import asynccontextmanager
from typing import Optional
from fastapi import FastAPI, HTTPException, Header
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, EmailStr
from support_bot import CustomerSupportBot
from tools import build_agent, check_order_status, create_support_ticket

# Oturum yönetimi için basit in-memory store
# Production'da Redis kullan
sessions: dict = {}
bot_instance: Optional[CustomerSupportBot] = None

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup
    global bot_instance
    print("Bot başlatılıyor...")
    bot_instance = CustomerSupportBot(company_name="AcmeTech")
    print("[OK] Bot hazır")
    yield
    # Shutdown
    print("Bot kapatılıyor...")
    sessions.clear()

app = FastAPI(
    title="AcmeTech Destek Botu API",
    version="1.0.0",
    lifespan=lifespan
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://acmetech.com", "http://localhost:3000"],
    allow_methods=["GET", "POST"],
    allow_headers=["*"],
)

class ChatRequest(BaseModel):
    message: str
    session_id: Optional[str] = None

class ChatResponse(BaseModel):
    response: str
    session_id: str
    sources: list[str]

class OrderRequest(BaseModel):
    order_id: str

@app.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest):
    if not request.message.strip():
        raise HTTPException(status_code=400, detail="Mesaj boş olamaz")
    
    if len(request.message) > 1000:
        raise HTTPException(status_code=400, detail="Mesaj çok uzun (max 1000 karakter)")
    
    # Oturum ID yoksa yeni oluştur
    session_id = request.session_id or str(uuid.uuid4())
    
    # Her oturumun kendi bot instance'ı olsun
    # Production'da bu daha sofistike olmalı
    if session_id not in sessions:
        sessions[session_id] = CustomerSupportBot(company_name="AcmeTech")
    
    session_bot = sessions[session_id]
    result = session_bot.ask(request.message, session_id)
    
    return ChatResponse(
        response=result["answer"],
        session_id=session_id,
        sources=result["sources"]
    )

@app.post("/order/status")
async def order_status(request: OrderRequest):
    result = check_order_status.invoke(request.order_id)
    return {"result": result}

@app.delete("/session/{session_id}")
async def clear_session(session_id: str):
    if session_id in sessions:
        del sessions[session_id]
        return {"message": "Oturum temizlendi"}
    raise HTTPException(status_code=404, detail="Oturum bulunamadı")

@app.get("/health")
async def health():
    return {
        "status": "ok",
        "active_sessions": len(sessions),
        "bot_ready": bot_instance is not None
    }

Uygulamayı Çalıştırma ve Test Etme

# Önce belgeleri indeksle
mkdir -p company_docs
# PDF ve TXT dosyalarını company_docs/ altına koy
python document_loader.py

# Ardından API'yi başlat
uvicorn api:app --host 0.0.0.0 --port 8000 --reload

# Başka terminal'de test et
curl -X POST http://localhost:8000/chat 
  -H "Content-Type: application/json" 
  -d '{"message": "İade politikanız nedir?"}'

# Sipariş sorgulama
curl -X POST http://localhost:8000/order/status 
  -H "Content-Type: application/json" 
  -d '{"order_id": "ORD-001234"}'

# Oturum bilgisiyle konuşma devam ettirme
curl -X POST http://localhost:8000/chat 
  -H "Content-Type: application/json" 
  -d '{"message": "Peki kargo ücreti ne kadar?", "session_id": "önceki-session-id"}'

Production Hazırlığı: Dikkat Edilmesi Gerekenler

Rate Limiting: OpenAI API çağrılarını sınırlandır. slowapi veya nginx limitleri şart.

Oturum Yönetimi: In-memory dict yerine Redis kullan. Sunucu yeniden başladığında tüm konuşmalar kaybolur.

Maliyet Kontrolü: gpt-4o-mini modeli, gpt-4o‘ya kıyasla 15 kat daha ucuz ve destek botu senaryolarında performans farkı minimal. Her zaman token kullanımını logla.

Belge Güncelleme: Ürün politikaları değiştiğinde vector store’u yeniden oluşturman gerekir. Bunu bir cron job veya webhook ile otomatize et.

Fallback Mekanizması: LLM’in %100 doğru cevap vermesini bekleyemezsin. Botun cevap veremediği durumlarda insan temsilcisine yönlendirme mantığı şart. Aşağıdaki gibi basit bir eşik koy:

  • Confidence skoru 0.5’in altında ise insan desteğine yönlendir
  • “insan ile konuşmak istiyorum” gibi kelimeleri keyword match ile yakala
  • Üst üste 3 tatminsiz yanıt algılanırsa otomatik eskalasyon tetikle

Güvenlik: Prompt injection saldırılarına dikkat et. Kullanıcı girdisini direkt prompt’a ekleme, her zaman şablon yapısını kullan. Ayrıca sisteme müşteri PII (kişisel tanımlayıcı bilgi) gönderme; sipariş ID yeterli.

Monitoring: Langfuse veya LangSmith ile her konuşmayı izle. Hangi sorular cevapsız kalıyor, hangi belgeler en çok kullanılıyor, ortalama yanıt süresi ne kadar? Bu metrikler botu geliştirmek için altın değerinde.

Gerçek Dünya Performans Notları

Üretim ortamında bir e-ticaret şirketinde benzer bir sistem 3 ay boyunca çalıştırdıktan sonra şu gözlemler ortaya çıktı:

  • Destek biletlerinin %62’si bot tarafından çözüldü, insan müdahalesi gerekmedi
  • Ortalama yanıt süresi 1.8 saniyeden 0.3 saniyeye düştü
  • Gece vardiyası destek maliyeti %80 azaldı
  • En sık başarısızlık sebebi: güncel olmayan belgeler. Vector store’u haftalık güncellemek kritik.
  • Türkçe dil desteği GPT-4o-mini ile oldukça iyi çalışıyor, özellikle standart müşteri hizmetleri kalıplarında.

Sonuç

LangChain ile müşteri destek botu geliştirmek artık günler değil saatler alan bir iş. Ancak asıl zorluk kod yazmak değil; doğru chunk stratejisini belirlemek, belgeleri temiz tutmak ve edge case’leri yönetmek. Bot ne kadar “akıllı” görünürse görünsün, kalitesini belirleyen şey beslendiği veridir.

Başlangıç için gpt-4o-mini + Chroma kombinasyonu maliyet-performans açısından ideal. Sistem büyüdükçe Pinecone gibi yönetilen bir vector store ve Redis tabanlı oturum yönetimine geçmek kaçınılmaz olacak. Ama sıfırdan başlarken aşırı mühendislik yapmak yerine bu yazıdaki temel yapıyla işe koy, gerçek kullanıcı davranışını gözlemle ve o verilere göre evrimleştir. Bir sysadmin olarak söylüyorum: en iyi sistem, bugün çalışan sistemdir.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir