← Back to list

LangGraph ile Çok Ajanlı Text-to-SQL Chatbot: Finans Verilerinizi Doğal Dille Sorgulayın 🚀

GPT-4o-mini, LangGraph ve Chainlit ile sıfırdan uçtan uca bir yapay zeka asistanı nasıl inşa edilir?

Musa Peker · 2026-06-13 09:50 · 1 claps · 9.3 min read
#langgraph #llm #text-2-sql #multi-agent #chainlit
Open on Medium ↗
Wiki topics: LLM · Large Language Models AGT · AI Agents

LangGraph ile Çok Ajanlı Text-to-SQL Chatbot: Finans Verilerinizi Doğal Dille Sorgulayın 🚀

GPT-4o-mini, LangGraph ve Chainlit ile sıfırdan uçtan uca bir yapay zeka asistanı nasıl inşa edilir?

Bugün sizlere, doğal dilde sorduğunuz finansal soruları anında SQL sorgularına çeviren, veritabanında çalıştıran, sonuçları yorumlayan ve gerektiğinde interaktif grafikler üreten çok ajanlı bir chatbot projesini adım adım anlatacağım.

Bu proje, modern LLM (Large Language Model) tabanlı ajan mimarilerinin gerçek dünyada nasıl uygulanabileceğine dair uçtan uca bir örnek sunuyor. Hadi başlayalım!

📌 İçindekiler

  1. Proje Nedir?
  2. Neden Çok Ajanlı Mimari?
  3. Mimariye Kuşbakışı
  4. LangGraph İş Akışı — Derinlemesine
  5. 9 Ajan, 9 Görev
  6. Veritabanı Tasarımı
  7. Teknoloji Yığını
  8. Kurulum ve Çalıştırma
  9. Örnek Senaryolar
  10. Kodun Kalbine Yolculuk
  11. Karşılaşılan Zorluklar ve Çözümleri
  12. Sonuç ve Gelecek Vizyonu

Proje Nedir?

“Finans Asistanı”, kullanıcının Türkçe olarak sorduğu finansal soruları alıp, arka planda SQL sorgularına çeviren, bu sorguları SQLite veritabanında çalıştıran, sonuçları anlaşılır bir dille yanıtlayan ve talebe göre interaktif Plotly grafikleri üreten bir sohbet botudur.

💡 Örnek: “Bu yıl en çok harcama yaptığım 5 kategori hangisi?” → Chatbot bunu SQL’e çevirir → Veritabanında çalıştırır → Sonucu listeler → Bar chart gösterir.

Proje, 2024–2025 dönemine ait sentetik (yapay olarak üretilmiş) finans verisiyle çalışır. Veriler şunları kapsar:

  • 💳 5 banka hesabı ve bakiyeleri
  • 📊 15 gelir/gider kategorisi
  • 🧾 Yaklaşık 510 finansal işlem
  • 🎯 240 aylık bütçe kaydı
  • 📄 200 fatura

Neden Çok Ajanlı Mimari?

Tek bir LLM çağrısıyla “sor → SQL üret → çalıştır → cevapla” yapmak mümkün. Peki neden 9 ayrı ajan?

Çünkü gerçek dünyada işler her zaman yolunda gitmez:

Her ajan tek bir işi çok iyi yapar (Single Responsibility Principle). Bu sayede sistem daha güvenilir, daha öngörülebilir ve hata durumunda kendi kendini düzeltebilir hale gelir.

Mimariye Kuşbakışı

Sistemin yüksek seviye mimarisi şu şekilde:

Kullanıcı → Chainlit Web Arayüzü → LangGraph Ajan Motoru → SQLite Veritabanı → Plotly Görselleştirme → Kullanıcıya Yanıt

Akış şu adımları izler:

Kullanıcı Sorusu
    ↓
🛡️  Guardrails Agent     → "Bu soru finansla ilgili mi?"
    ↓ (evet)
📝 SQL Agent             → Doğal dil → SQLite sorgusu
    ↓
🔍 SQL Validator Agent   → Fan-out riski var mı? CTE dönüşümü
    ↓
⚙️  Execute SQL          → Sorguyu finance.db üzerinde çalıştır
    ↓ (hata varsa)
🔧 Error Agent           → Hatayı analiz et, düzelt, tekrar dene (max 3×)
    ↓ (başarılı)
🧠 Sanity Check Agent    → Sonuçlar mantıklı mı?
    ↓
💬 Analysis Agent        → Ham veri → okunabilir metin
    ↓
📊 Decide Graph Need     → Grafik gerekli mi? Hangi tür?
    ↓ (gerekiyorsa)
📈 Viz Agent             → LLM ile Plotly kodu üret ve çalıştır
    ↓
✅ Kullanıcıya Yanıt     → Metin + SQL + İnteraktif Grafik

LangGraph İş Akışı — Derinlemesine

Projenin kalbi LangGraph ile oluşturulmuş bir state machine (durum makinesi). LangGraph, ajanlar arası geçişleri, koşullu yönlendirmeleri ve döngüleri yöneten bir orkestrasyon çerçevesidir.

LangGraph’in otomatik ürettiği gerçek iş akışı diyagramı:

State (Durum) Yapısı

Tüm ajanların paylaştığı AgentState şu alanları taşır:

class AgentState(TypedDict):
    question:         str    # Kullanıcının sorusu
    sql_query:        str    # Üretilen/düzeltilmiş SQL
    query_result:     str    # Sorgu sonuçları (JSON)
    final_answer:     str    # Kullanıcıya gösterilecek metin
    error:            str    # Hata mesajı
    iteration:        int    # Kaçıncı deneme
    needs_graph:      bool   # Grafik üretilsin mi?
    graph_type:       str    # bar | line | pie | scatter
    graph_json:       str    # Plotly figür JSON'u
    is_in_scope:      bool   # Finans kapsamında mı?
    sanity_passed:    bool   # Sonuçlar mantıklı mı?
    sanity_issue:     str    # Tespit edilen sorun
    sanity_retried:   bool   # Döngü koruması

Koşullu Yönlendirmeler

LangGraph’te her adımdan sonra nereye gidileceği koşul fonksiyonları ile belirlenir:

# Kapsam kontrolü — devam mı, dur mu?
def check_scope(state):
    return "in_scope" if state.get("is_in_scope") else "out_of_scope"
# SQL hatası - düzeltmeyi dene mi, vazgeç mi?
def should_retry(state):
    if state.get("error"):
        return "retry" if state.get("iteration", 0) <= 3 else "end"
    return "success"
# Sanity check - SQL'i yeniden üret mi, devam mı?
def should_regenerate_sql(state):
    return "regenerate" if not state.get("sanity_passed") else "proceed"

Bu yapı sayesinde iş akışı doğrusal değil, dinamik — tıpkı gerçek bir uzman ekibin çalışması gibi.

9 Ajan, 9 Görev

Her ajan, kendi uzmanlık alanında çalışan bir LLM (GPT-4o-mini) çağrısıdır. Sistem mesajları ve sıcaklık değerleri her ajan için özelleştirilmiştir.

🛡️ 1. Guardrails Agent (Kapsam Kontrolü)

Görev: Kullanıcının sorusu finans verileriyle ilgili mi, selamlama mı, yoksa kapsam dışı mı?

def guardrails_agent(state: AgentState) -> AgentState:
    # Selamlama → "Merhaba! Ben Finans Asistanınım..."
    # Kapsam dışı → "Üzgünüm, bu soru finans kapsamı dışında..."
    # Kapsam içi → is_in_scope = True, devam et

📝 2. SQL Agent (SQL Üretimi)

Görev: Doğal dildeki soruyu geçerli bir SQLite sorgusuna çevirmek.

Bu ajan, veritabanı şemasının tamamını prompt’ta görür ve şu kritik kurallara uymak zorundadır:

  • Her agregasyonu ayrı CTE’de hesapla (fan-out önlemi)
  • Kategori adlarını JOIN ile getir, ham ID döndürme
  • Tarih formatlamasında printf('%04d-%02d', year, month) kullan

🔍 3. SQL Validator Agent (SQL Kalite Denetimi)

Görev: SQL çalıştırılmadan önce fan-out (kartezyen çarpım) riskini tespit etmek.

Bu ajanın en zekice yanı: önce Python ile deterministik kontrol yapar, risk yoksa LLM’i hiç çağırmaz!

def _has_fanout_risk(sql: str) -> bool:
    # JOIN var mı? + Agregasyon var mı? + CTE yok mu?
    # Üçü birden varsa → riskli, LLM'e gönder
    # Değilse → güvenli, olduğu gibi geç

Bu yaklaşım, gereksiz LLM çağrılarını önleyerek hem maliyeti hem gecikmeyi düşürür.

⚙️ 4. Execute SQL (Sorgu Çalıştırma)

Görev: SQL’i SQLite üzerinde çalıştırmak. Noktalı virgülle ayrılmış çoklu ifadeleri sırayla işler.

🔧 5. Error Agent (Hata Kurtarma)

Görev: SQL hatası aldığında, hata mesajını ve şemayı kullanarak sorguyu düzeltmek.

# En fazla 3 deneme hakkı var
if iteration > 3:
    state["final_answer"] = "Üzgünüm, bu soru için doğru SQL üretemedim..."

🧠 6. Sanity Check Agent (Sonuç Akıl Yürütme)

Görev: SQL başarıyla çalıştı ama sonuçlar gerçekten doğru mu?

Bu ajan şunları kontrol eder:

  • Parasal değerler makul mü? (Maaş 18–22 bin iken sonuç milyonlar gelmemeli)
  • Sayımlar gerçekçi mi? (200 fatura varken 13.000 overdue olamaz)
  • Aynı kategori birden fazla listelenmiş mi? (GROUP BY hatası)

Şüpheli bir durumda sanity_passed = False yaparak SQL agent'a geri döner — ama sadece bir kez (döngü koruması).

💬 7. Analysis Agent (Sonuç Yorumlama)

Görev: Ham JSON sorgu sonuçlarını kullanıcıya yönelik doğal bir metne dönüştürmek.

# temperature=0.7 ile daha doğal ve akıcı bir anlatım

📊 8. Decide Graph Need (Grafik Kararı)

Görev: Veriye bakarak grafik gerekip gerekmediğine ve hangi türün uygun olduğuna karar vermek.

📈 9. Viz Agent (Grafik Üretimi)

Görev: LLM’e Plotly kodu yazdırmak, exec() ile çalıştırmak ve JSON olarak Chainlit'e iletmek.

# LLM "fig" değişkenini oluşturan Plotly kodu yazar
exec(plotly_code, {"df": df, "go": go, "px": px})
fig = exec_env["fig"]
state["graph_json"] = fig.to_json()  # Chainlit'e ilet

Veritabanı Tasarımı

Projede SQLite kullanılıyor ve veriler tamamen db_init.py ile programatik olarak üretiliyor. Bu sayede projeyi klonlayan herkes aynı veri setiyle çalışabilir.

ER Diyagramı (Metinsel)

accounts (1) ────────── (N) transactions (N) ────────── (1) categories
   │                            │                            │
   │ account_id                 │ category_id                │ category_id
   │ account_name               │ transaction_date           │ category_name
   │ balance                    │ amount                     │ category_type
   │                            │ description                │
   │                            │ status                     │
   │                            │                            │
   └── (N) invoices             └── (N) budgets ─────────────┘
        │                            │
        │ vendor_name                │ limit_amount
        │ due_date                   │ spent_amount
        │ status                     │ month, year

Örnek Veri Dağılımı

Sentetik Veri Üretim Mantığı

db_init.py gerçekçi senaryolar için şu kuralları uygular:

# Her ayın 5'inde maaş yatar (18.000–22.000 TRY)
if day == 5:
    transactions.append((..., "Aylık maaş", ...))
# Her ayın 1'inde kira ödenir (8.000–9.000 TRY)
if day == 1:
    transactions.append((..., "Aylık kira ödemesi", ...))
# Günlük harcamalar %60 olasılıkla gerçekleşir
if random.random() < 0.60:
    transactions.append((..., random.uniform(50, 800), ...))
# Bütçeler bazen aşılır (gerçekçilik için)
spent = round(random.uniform(0.5, 1.3) * limit, 2)

Teknoloji Yığını

Kurulum ve Çalıştırma

Projeyi ayağa kaldırmak için 6 adım yeterli:

1. Depoyu Klonlayın

git clone https://github.com/kullanici-adi/finans-asistani.git
cd finans-asistani

2. Sanal Ortam Oluşturun

python -m venv venv
# Windows
venv\Scripts\activate
# macOS / Linux
source venv/bin/activate

3. Bağımlılıkları Yükleyin

pip install -r requirements.txt

4. API Anahtarınızı Tanımlayın

cp .env.example .env
# .env dosyasını açın ve OPENAI_API_KEY değerini girin

5. Veritabanını Oluşturun

python db_init.py

Çıktı:

Veritabanı oluşturuldu: finance.db
       5  hesap
      15  kategori
     512  işlem
     240  bütçe kaydı
     200  fatura

6. Uygulamayı Başlatın

chainlit run app.py

Tarayıcınızda **http://localhost:8000** adresini açın ve sorgulamaya başlayın! 🎉

Örnek Senaryolar

Senaryo 1 — Basit Sorgu & Grafik

“Bu yılki toplam gelir ve giderim ne kadar?”

Sistem şu adımlardan geçer:

  1. Guardrails: ✅ Finans kapsamında
  2. SQL Agent: Gelir ve gider toplamlarını hesaplayan sorgu üretir
  3. Validator: Risk yok, geç
  4. Execute: Sorguyu çalıştırır → Gelir: 312.450 TRY, Gider: 198.230 TRY
  5. Sanity Check: Değerler makul ✅
  6. Analysis: “2024 yılında toplam 312.450 TRY gelir elde ettiniz…”
  7. Decide Graph: Bar chart uygun
  8. Viz Agent: Mavi/Kırmızı bar chart üretir

Senaryo 2 — Karmaşık Çok Tablolu Analiz

“2024 yılında bütçe limitini en az 3 farklı ayda aşmış kategorilerin toplam harcama tutarını ve gecikmiş fatura sayısını listele.”

Bu sorgu transactions, budgets, categories ve invoices tablolarını aynı anda JOIN'ler. Fan-out riski yüksektir!

Sistemin başarısı:

  1. SQL Agent: 3 ayrı CTE’li sorgu üretir
  2. Validator: Fan-out riski tespit eder → CTE yapısını doğrular
  3. Execute: 4 tablodan veriyi hatasız çeker
  4. Sanity Check: Sonuçları onaylar
  5. Viz Agent: Çubuk grafik ile kategorileri görselleştirir

Senaryo 3 — Görsel Sunum

Kodun Kalbine Yolculuk

Proje Yapısı

finans-asistani/
├── app.py                   # Chainlit arayüzü & olay yöneticileri
├── text2sql_agent.py        # LangGraph motoru (tüm ajanlar burada)
├── db_init.py               # Sentetik veri üretimi
├── finance.db               # SQLite veritabanı (otomatik oluşur)
├── requirements.txt         # Bağımlılıklar
├── chainlit.md              # Karşılama ekranı
└── images/                  # Görseller

app.py — Chainlit ile Gerçek Zamanlı Akış

app.py, Chainlit'in event-driven yapısını kullanarak ajan adımlarını gerçek zamanlı olarak kullanıcıya gösterir:

@cl.on_message
async def main(message: cl.Message):
    async with cl.Step(name="🤖 Ajan İş Akışı") as workflow_step:
        async for event in process_question_stream(user_question):
            if event_type == "node_start":
                # Yeni bir alt adım oluştur
                node_step = cl.Step(name=display_name, parent_id=workflow_step.id)
                await node_step.send()            elif event_type == "node_end":
                # Adım çıktısını formatla ve güncelle
                node_step.output = _format_node_output(node_name, output)
                await node_step.update()
elif event_type == "node_end":
                # Adım çıktısını formatla ve güncelle
                node_step.output = _format_node_output(node_name, output)
                await node_step.update()

Bu sayede kullanıcı, chatbot’un “düşünme sürecini” adım adım izleyebilir — tam bir şeffaf yapay zeka deneyimi.

text2sql_agent.py — Ajan Motoru

Dosyanın omurgası create_finance_graph() fonksiyonudur:

def create_finance_graph():
    workflow = StateGraph(AgentState)
# Düğümleri kaydet
    workflow.add_node("guardrails_agent",    guardrails_agent)
    workflow.add_node("sql_agent",           sql_agent)
    workflow.add_node("sql_validator_agent", sql_validator_agent)
    workflow.add_node("execute_sql",         execute_sql)
    workflow.add_node("sanity_check_agent",  sanity_check_agent)
    workflow.add_node("analysis_agent",      analysis_agent)
    workflow.add_node("error_agent",         error_agent)
    workflow.add_node("decide_graph_need",   decide_graph_need)
    workflow.add_node("viz_agent",           viz_agent)
    # Koşullu kenarlar
    workflow.add_conditional_edges("guardrails_agent", check_scope, {
        "in_scope": "sql_agent", "out_of_scope": END
    })
    workflow.add_conditional_edges("execute_sql", should_retry, {
        "success": "sanity_check_agent", "retry": "error_agent", "end": "analysis_agent"
    })
    workflow.add_conditional_edges("sanity_check_agent", should_regenerate_sql, {
        "regenerate": "sql_agent", "proceed": "analysis_agent"
    })
    # ...
    return workflow.compile()

Streaming — Async Generator Pattern

process_question_stream() bir async generator olarak çalışır:

async def process_question_stream(question: str):
    async for event in finance_graph.astream_events(initial_state, ...):
        if event_type == "on_chain_start":
            yield {"type": "node_start", "node": node_name}
        elif event_type == "on_chain_end":
            yield {"type": "node_end", "node": node_name, "output": output}
    yield {"type": "final", "result": current_state}

Bu pattern, Chainlit’in async for ile olayları gerçek zamanlı tüketmesine olanak tanır.

Karşılaşılan Zorluklar ve Çözümleri

🚨 Zorluk 1: Fan-Out (Kartezyen Çarpım) Hatası

Sorun: Birden fazla tablodan aynı anda SUM, COUNT gibi agregasyonlar yapılırken tablolar doğrudan JOIN edilince satırlar çarpışıyor ve sonuçlar gerçek değerlerin katbekat üstüne çıkıyor.

Çözüm: İki katmanlı savunma:

  1. SQL Validator Agent (LLM): Fan-out desenini tespit edip CTE yapısına dönüştürür
  2. Sanity Check Agent (LLM): Çalıştırma sonrası sonuçların mantıksız olup olmadığını kontrol eder
-- YANLIŞ (fan-out)
SELECT c.category_name, SUM(t.amount), COUNT(i.invoice_id)
FROM transactions t
JOIN invoices i ON ...
GROUP BY c.category_name;
-- DOĞRU (CTE ile ayrıştırma)
WITH tx AS (
    SELECT category_id, SUM(amount) AS total FROM transactions GROUP BY category_id
),
inv AS (
    SELECT category_id, COUNT(*) AS cnt FROM invoices GROUP BY category_id
)
SELECT ... FROM categories
JOIN tx ON ... LEFT JOIN inv ON ...;

🚨 Zorluk 2: Deterministik Kontrol ile LLM Tasarrufu

Sorun: Her SQL için LLM’e “fan-out var mı?” diye sormak hem maliyetli hem yavaş.

Çözüm: _has_fanout_risk() fonksiyonu tamamen Python ile, LLM çağırmadan kontrol yapar:

def _has_fanout_risk(sql: str) -> bool:
    has_join = " JOIN " in sql.upper()
    has_agg  = any(f in sql.upper() for f in ["SUM(", "COUNT(", "AVG("])
    has_cte  = sql.upper().startswith("WITH ")
    return has_join and has_agg and not has_cte

Sadece riskli sorgular LLM’e gönderilir — %70–80 LLM çağrısından tasarruf.

🚨 Zorluk 3: Sonsuz Döngü Koruması

Sorun: Sanity check başarısız olursa → SQL yeniden üretilir → tekrar sanity check → tekrar başarısız → sonsuz döngü.

Çözüm: sanity_retried bayrağı:

if state.get("sanity_retried", False):
    state["sanity_passed"] = True  # Döngüyü kır, devam et
    return state

SQL hataları için de iteration > 3 kontrolüyle maksimum 3 deneme sınırı konulmuştur.

🚨 Zorluk 4: Türkçe Metin ve Tarih Formatı

Sorun: SQLite’da Türkçe karakterler büyük/küçük harf duyarlıdır. Ayrıca budgets tablosunda tarih month + year olarak INTEGER saklanır.

Çözüm: Prompt mühendisliği ile:

KRİTİK — Tarih formatı:
  DOĞRU:  printf('%04d-%02d', year, month)  → '2024-01'
  YANLIŞ: year || '-' || month || '-01'     → '2024-1-01' (NULL döner!)

Sonuç ve Gelecek Vizyonu

Bu proje, LangGraph tabanlı çok ajanlı mimarinin gerçek bir iş problemini nasıl çözebileceğini gösteriyor. 9 uzman ajanın koordineli çalışmasıyla:

  • ✅ Kapsam dışı sorular kibarca reddediliyor
  • ✅ Karmaşık JOIN’ler güvenle yönetiliyor
  • ✅ Hatalı SQL’ler otomatik düzeltiliyor
  • ✅ Mantıksız sonuçlar yakalanıp yeniden deneniyor
  • ✅ İnteraktif grafiklerle veri görselleştiriliyor
  • ✅ Tüm süreç kullanıcıya şeffafça gösteriliyor

🚀 Sırada Ne Var?

Projeyi bir sonraki seviyeye taşımak için planlanan özellikler:

Kaynak Kod

Projenin tam kaynak koduna GitHub üzerinden ulaşabilirsiniz:

🔗 https://github.com/m-peker/agentic-finance-assistant

Teşekkür

Bu makaleyi okuduğunuz için teşekkürler! Sorularınızı yorumlarda paylaşabilir, projeye ⭐ vererek destek olabilirsiniz.

“The best way to predict the future is to build it.” — Alan Kay

#LangGraph #LLM #Text2SQL #MultiAgent #Python #Chainlit #OpenAI #Plotly #YapayZeka #FinTech


메타데이터
post_id
27e8c1f699e6
slug
langgraph-ile-çok-ajanlı-text-to-sql-chatbot-finans-verilerinizi-doğal-dille-sorgulayın-27e8c1f699e6
url
https://medium.com/@msapeker/langgraph-ile-%C3%A7ok-ajanl%C4%B1-text-to-sql-chatbot-finans-verilerinizi-do%C4%9Fal-dille-sorgulay%C4%B1n-27e8c1f699e6
canonical_url
https://medium.com/@msapeker/langgraph-ile-%C3%A7ok-ajanl%C4%B1-text-to-sql-chatbot-finans-verilerinizi-do%C4%9Fal-dille-sorgulay%C4%B1n-27e8c1f699e6
author_url
https://medium.com/@msapeker
status
ok
fetched_at
2026-06-25 16:53:31