Ana sayfa → Teknik
Agent, Local LLM, MCP Server: Kim Karar Verir, Kim Çalıştırır?
“Bugün kaç dolar depozit oldu?” Cevap ödeme sisteminde duruyor. “EURUSD’de kaç hesap long?” Cevap trading platformunda. Ama her seferinde birinin SQL yazması ya da BI ekranını açması gerekiyor. İhtiyaç: bu soruları bir chat’e yazıp anında cevap almak. Şartlar üç cümleydi: müşteri ve işlem verisi şirket dışına çıkmayacak, cloud API yok; model gerektiğinde bizim yazdığımız tool’ları (depozit, çekim, pozisyon, hacim, bakiye, KYC) çağıracak; hepsi şirket içindeki bir sunucuda, tek bir GPU’da (8 GB VRAM) dönecek. Bu da küçük bir LLM demek: 4 milyar parametrelik (4B) bir model; cloud’dakilerin yüzde biri. Asıl soru modelin hangisi olacağı değildi: kim karar verecek, kim çalıştıracak, kim yönetecek?
- Üç parça, üç sorumluluk. Agent yönetir; Local LLM sadece karar verir (düz metin ya da
tool_calls); MCP Server sadece tool çalıştırır. Hiçbiri diğerinin işini yapmaz. - Yöneten agent, model değil. Hangi metnin modelin önüne konacağına, tool sonucunun nereye ekleneceğine ve loop’un ne zaman biteceğine agent karar verir. Model ve tool’lar değişir, agent aynı kalır.
- Küçük LLM’de tool seçimini docstring belirler. “NE ZAMAN KULLAN / NE ZAMAN KULLANMA + örnek” biçimi: 5 tool’da 20/20, 11 tool’da %96. Kod değişmedi, metin değişti.
- Ölçmeden model seçme. qwen3:4b 20/20 ama ~11 sn; qwen2.5:3b 17/20 ama 0,5 sn. Üç yanlış tool seçimi 11 saniyeden pahalı.
- Loop’un iki freni var. Tur sınırı (5) ve system prompt’taki “hata aldıysan aynı tool’u tekrar çağırma” kuralı. Biri olmadan diğeri yetmez.
- Client ne olursa olsun. Agent OpenAI-compatible bir HTTP API sunar; şirket içi bir chat ekranı, Open WebUI ya da curl takılır. Client’ın gönderdiği system prompt’a güvenilmez; talimat agent’tan gelir.
İhtiyaç: şirket içi raporu anında almak, veriyi dışarı çıkarmadan
Bir fintech’te her gün sorulan raporların çoğu aynı kalıpta: “bugün kaç dolar depozit oldu?”, “kaç çekim bekliyor?”, “EURUSD’de kaç hesap long, kaç hesap short?”, “altında bugünkü hacim kaç lot?”, “KYC’de kaç başvuru bekliyor?” Cevap bir sistemde duruyor; soru doğal dille geliyor. Arada bir insan var: rapor isteyen, SQL yazan, ekran görüntüsü atan. Cloud model bu arayı güzel kapatır; ama müşteri bakiyesini ve pozisyon verisini cloud’a göndermek bir fintech’te bir toplantı, bir sözleşme ve bir hukuk görüşü demek — çoğu zaman da “hayır”.
Şirket içi çözümün üç şartı vardı:
- Veri şirket dışına çıkmayacak. Model de, tool’lar da şirket içindeki sunucuda; dışarıya tek bir HTTP çağrısı yok.
- Model bizim tool’larımızı çağırabilecek. Depozit, çekim, pozisyon dağılımı, hacim, bakiye, KYC. Yeni bir rapor eklemek bir Python fonksiyonu yazmak kadar kolay olacak.
- Tek bir GPU yetecek. Sunucuda 8 GB VRAM’li tek bir kart var. Bu da 4–8 milyar parametrelik, 4-bit sıkıştırılmış (q4) modeller demek; 70 milyarlık akıl yok.
Üçüncü şart tasarımı belirledi. Küçük bir LLM az yönlendirmeyle çok karar veremez. O zaman karar yükünü modelden alıp başka bir yere koymak gerekir. O yer: agent.
Mimari: üç parça, üç sorumluluk
İlk gün verilen tek karar bu: agent yönetir, model değil. Model sadece “tool lazım mı, hangisi?” sorusuna cevap verir. Tool’u çalıştırmaz, sonucu kendisi toplamaz, ne zaman duracağına karar vermez. Bunların hepsi agent’ın işi.
Sen --> Client (chat UI) --HTTP--> AGENT (FastAPI, OpenAI-compatible API)
|
+--> Local LLM (qwen3:4b) --> duz metin ya da tool_calls
|
+--> MCP Server (bizim tool'lar) --> result --> tekrar LLM --> son cevap
| Kim | Ne yapar | Ne yapmaz |
|---|---|---|
| Client (şirket içi chat ekranı, Open WebUI, curl…) | Sadece UI. Prompt’u yollar, cevabı gösterir. | Tool bilmez, MCP bilmez. |
| Agent (bizim, Python) | Yönetir. LLM’e sorar, tool_calls gelirse MCP’ye gider, loop’u yönetir, son cevabı döner. | Kendi başına cevap üretmez, tool çalıştırmaz. |
| Local LLM (Ollama üstünde qwen3:4b) | Sadece karar. Düz metin ya da tool_calls üretir. | Network’e çıkamaz, dosyaya dokunamaz. |
| MCP Server (bizim, Python) | Tool’ları çalıştırır, sonuç döner. | LLM’i asla çağırmaz, karar vermez. |
Bu ayrımın karşılığı iki yerde alındı. Birincisi: client değişebiliyor. Agent OpenAI-compatible bir API sunduğu için şirket içi bir chat ekranı, Open WebUI ya da curl takılıyor; agent ve tool’lar değişmiyor. İkincisi: bir şey bozulduğunda “hangi kutu?” sorusunun dört cevabı var, kırk değil. Tool yanlış seçildiyse docstring, tool hata verdiyse MCP Server, cevap uydurmaysa agent’ın modele verdiği metin.
Stack
- Şirket içi sunucu, tek GPU (8 GB VRAM)
- Python 3.12 (uv),
mcpSDK 2.2.0, FastAPI + uvicorn, httpx - Ollama 0.34 (local LLM runtime), model
qwen3:4b
MCP Server: eller
mcp SDK ile stdio transport üzerinden konuşan küçük bir server. Tool’lar sıradan Python
fonksiyonları; SDK docstring’i ve tip imzasını alıp modele gidecek schema’ya çeviriyor. Yeni
bir yetenek eklemek = bir fonksiyon yazıp server.py’de kaydetmek. Agent tarafında hiçbir
değişiklik yok.
@mcp.tool()
def depozit_ozeti(tarih: str = "bugun") -> str:
"""Gunluk depozit ozeti: toplam tutar (USD), islem sayisi, ortalama, odeme yontemi kirilimi.
NE ZAMAN KULLAN: Soruda "depozit", "para yatirma", "kac dolar girdi", "yatirim geldi"
geciyorsa HER ZAMAN bu tool'u cagir.
Ornek: "bugun kac dolar depozit oldu?", "dun kac depozit geldi?"
NE ZAMAN KULLANMA: Para cekme sorulunca (cekim_ozeti), tek bir musterinin
bakiyesi sorulunca (musteri_bakiyesi).
"""
Projenin en önemli öğrenmesi bu bloğun içinde: LLM’in tool’a gidip gitmeyeceğine karar verdiği tek şey docstring. Agent bu metinleri her istekte modele gönderiyor; model soruyu bu metinle karşılaştırıyor. “Ne zaman kullan / ne zaman kullanma + örnek soru” biçimi küçük LLM’de isabeti ciddi artırdı; ölçtük: 11 tool’da %96 (26/27), 5 tool’da 20/20. Kod tarafında hiçbir şey değişmedi; değişen sadece metin.
Tool’lar iki grup. Gerçek olanlar sunucuya bakıyor: sistem_bilgisi, dosya_ara,
dosya_oku, not_kaydet, notlari_getir. İş tool’ları şimdilik sabit
response dönen mock’lar; mimariyi test etmek için var. Gerçek entegrasyon (ödeme sistemi, trading
platformu, KYC) geldiğinde sadece fonksiyon gövdesi değişecek, docstring ve agent olduğu gibi kalacak.
| Tool | Ne döner | Örnek soru |
|---|---|---|
depozit_ozeti(tarih) | Toplam depozit ($), işlem sayısı, ortalama, yöntem kırılımı | “bugün kaç dolar depozit oldu?” |
cekim_ozeti(tarih) | Toplam çekim ($), bekleyen / onaylanan / reddedilen sayısı | “kaç çekim onay bekliyor?” |
pozisyon_dagilimi(sembol) | Açık pozisyonlarda long / short hesap sayısı ve lot | “EURUSD’de kaç hesap long?” |
islem_hacmi(sembol, tarih) | Günlük hacim (lot), işlem sayısı, en çok işlem gören 5 sembol | “altında bugünkü hacim kaç lot?” |
musteri_bakiyesi(musteri) | Bakiye, equity, free margin, açık pozisyon sayısı | “Ayşe Yılmaz’ın free margin’i ne kadar?” |
kyc_bekleyenler() | Bekleyen KYC başvuruları, en eskisinin yaşı | “KYC’de kaç başvuru bekliyor?” |
Altı tool’un sınırları birbirine yakın: depozit ile çekim, pozisyon ile hacim, bakiye ile depozit. “NE ZAMAN KULLANMA” satırı bu yüzden her tool’da komşusunu adıyla gösteriyor.
stdio transport’ta print() yasak. Protocol stdout üzerinden JSON-RPC konuşuyor; araya
giren tek bir “debug: geldi” satırı bağlantıyı bozuyor ve hata mesajı sana bunu söylemiyor.
Tüm log’lar logging ile stderr’e.
Local LLM: karar veren, ama dokunamayan
Agent, httpx ile Ollama’nın /api/chat’ine tools parametresiyle
request atıyor; native tool-calling var. temperature=0 — tool seçimi deterministic olsun,
aynı soru iki kez sorulunca iki farklı tool’a gitmesin.
qwen3 bir reasoning model. think: false göndermek düşünmeyi kapatmadı; düşünceyi
content’e karıştırdı. Parametreyi hiç göndermeyince runtime thinking’i ayrı bir field’a
koyuyor. Yine de </think> artıkları geliyor; bazen açılış etiketi yutulup sadece kapanış
kalıyor. O yüzden son </think>’ten sonrası cevap sayılıyor.
Model seçimi: ölçmeden karar yok
20 soruluk bir test set: 10’u tool gerektiren (“saat kaç?”, “not al: yarın süt al”), 10’u alakasız (“2+2 kaç?”, “Python’da list comprehension nedir?”). Alakasızlar da sayılıyor: gereksiz yere tool’a giden model de yanlış.
| Model | Tool seçim isabeti | Latency / soru |
|---|---|---|
qwen3:4b | 20/20 | ~11 sn (içten düşünüyor) |
qwen2.5:3b | 17/20 | ~0,5 sn |
İsabet için qwen3:4b default; hız istersen OLLAMA_MODEL=qwen2.5:3b. 11 saniye
uzun. Ama üç yanlış tool seçimi 11 saniyeden pahalı: yanlış tool’a giden cevap uydurma sayı içeriyor
ve sen fark etmiyorsun.
Agent: yöneten
Agent’ın dört işi var: loop’u yönetmek, modele ne gideceğine karar vermek, dışarıya standart bir API sunmak ve kendini ayağa kaldırmak.
1. Loop: beş tur, sonra tool’suz son söz
for rnd in range(1, MAX_TOOL_ROUNDS + 1): # varsayilan 5
resp = await llm.chat(convo, tools=tools)
if not resp.wants_tools:
return resp.content # duz metin geldi: bitti
convo.append(resp.raw_message) # modelin tool_call mesaji gecmise
for call in resp.tool_calls:
result = await registry.call(call.name, call.arguments) # MCP'de calistir
convo.append({"role": "tool", "tool_name": call.name, "content": result})
# tur siniri asildi: elindeki tool sonuclariyla, tool'suz son bir cevap iste
resp = await llm.chat(convo, tools=None)
Dört adım: prompt + tool listesi modele gider; düz metin geldiyse döner; tool_calls geldiyse
her biri MCP’de çalışır, sonuçlar role: tool olarak geçmişe eklenir ve başa dönülür;
MAX_TOOL_ROUNDS aşıldıysa tool’suz son bir cevap istenir. Sonsuz döngü koruması
loop’ta; “tool hata verdiyse aynı tool’u tekrar tekrar çağırma” kuralı system
prompt’ta.
2. Modele ne gider: talimat bizden
Agent’ın system prompt’u kısa ve kuralcı: soru bir tool’un “NE ZAMAN KULLAN” durumuna uyuyorsa tool’u çağır, bilgiyi uydurma; saat, dosya, not gibi makineye özel şeyleri sadece tool’la öğrenebilirsin; genel bilgi ve matematikte tool kullanma; tool sonucunu özetle, ham çıktıyı yapıştırma; hata döndüyse açıkla, tekrar deneme; emin değilsen tool çağırmak uydurmaktan iyidir.
Bir de client’tan gelenlere karşı iki kural var. Client’ın gönderdiği system mesajları düşüyor
(KEEP_CLIENT_SYSTEM=false); bazı chat client’ları her isteğe binlerce token kendi
talimatını ekliyor ve küçük LLM o yığının içinde bizim 11 tool’u göremiyor. Kullanıcı mesajı bir
sarmalın içinde geldiyse (<userRequest> gibi) sadece içi alınıyor. Sarmal yoksa mesaj
olduğu gibi geçiyor; curl ve Open WebUI bu yoldan gidiyor.
def _normalize_messages(messages):
out = []
for m in messages:
role = m.get("role", "user")
if role == "system" and not settings.keep_client_system:
continue # talimat bizden gelir
content = m.get("content", "")
if role == "user":
content = _unwrap_copilot_user(content) # sarmal varsa sadece icini al
out.append({"role": role, "content": content})
return out
Debug için her gelen request .last_request.json’a yazılıyor. “Client tam olarak ne
gönderiyor?” sorusu bir dosya açmak kadar. Yukarıdaki iki kural da o dosyaya bakınca yazıldı.
3. HTTP API: OpenAI biçimi
POST /v1/chat/completions ve GET /v1/models. Streaming (SSE) var; şimdilik response
hazır olunca chunk’lanarak gönderiliyor, gerçek token akışı değil. Yedek olarak Ollama-compatible
/api/chat + /api/tags da var: Ollama provider’ı olan bir client’ı
agent’ın portuna yönlendirebiliyorsun. Her cevabın altına hangi tool’ların çağrıldığı ekleniyor:
Bugun toplam depozit: 184.250 $ (312 islem, ortalama 590 $)
Kart %61, havale %34, kripto %5
---
Kullanilan tool'lar: depozit_ozeti
4. Bootstrap: sıfır elle kurulum
uv run local-agent dediğinde sırayla: Ollama yoksa kur → servis kapalıysa
başlat → model inmemişse pull → warm-up ile VRAM’e yükle → MCP Server’ı subprocess
olarak başlat → :8000’de dinle. Her adım idempotent; ikinci çalıştırmada hepsi
atlanıyor, 1–2 sn içinde agent ayakta. Bir adım fail etse de agent kalkıyor; hata chat
response’unda görünüyor, log’un dibinde değil.
Sahadan: 5 tool’dan 11 tool’a
İlk ölçüm beş gerçek tool’la yapıldı: 20 soruda 20/20. Sonra altı mock iş tool’u eklendi — depozit, çekim, pozisyon, hacim, bakiye, KYC — ve tool sayısı 11 oldu. Model aynıydı, kod aynıydı; değişen tek şey modelin önündeki tool listesinin uzunluğuydu. Beş kısa açıklama arasında seçim yapmak kolaydı; on bir açıklama arasında sınırlar bulanıklaşıyordu: depozit mi çekim mi, pozisyon mu hacim mi, bakiye mi depozit mi?
Çözüm modelde değil, metinde arandı: her docstring’e “ne zaman kullan”, “ne zaman
kullanma” ve örnek sorular; “kullanma” satırı komşu tool’u adıyla gösteriyor
(cekim_ozeti, musteri_bakiyesi). Aynı model, aynı kod, 11 tool’da 26/27. Sınırı
kod değil, “NE ZAMAN KULLANMA” satırı çiziyor.
Loop tarafında da benzer bir ders var. Tur sınırı tek başına yetmez: hata alan bir model aynı tool’u beş tur boyunca çağırabilir ve her tur ~11 sn sürer; sınır sadece 55 saniyede bitmesini garanti eder, boşa dönmemesini değil. O yüzden system prompt’ta ayrı bir satır var: “tool hata döndürürse hatayı kullanıcıya açıkla, aynı tool’u tekrar çağırma.” Sınır loop’u durdurur; kural loop’a hiç girmemesini sağlar.
Ne izlemeli?
- Tool seçim isabeti — sabit test set (10 tool gerektiren + 10 alakasız). Her yeni tool ve her docstring değişikliğinden sonra tekrar koş.
- Soru başına latency — 11 sn normal. Katlanıyorsa model değil, modele giden metnin boyutu.
- Request’in token boyutu —
.last_request.json’un boyutu. System prompt + 11 tool birkaç bin token; bunun 5 katı geliyorsa client bir şey ekliyor. - Soru başına tur sayısı — 1 tur düz cevap, 2 tur tek tool.
MAX_TOOL_ROUNDS’a çarpan her soru bir loop ya da retry spam. - Hangi tool’lar çağrıldı — her cevabın altındaki satır. Depozit sorusunda boşsa cevap uydurma.
- MCP Server’dan dönen hata oranı — tool başına. Yükseliyorsa sorun docstring’de değil, tool’un kendisinde.
Kontrol listesi
- Üç sorumluluk ayrı mı: agent yönetiyor, LLM sadece karar veriyor, MCP Server sadece çalıştırıyor?
- Model network’e ve dosyaya dokunamıyor mu; her şey tool üzerinden mi?
- Her tool’un docstring’inde “ne zaman kullan / ne zaman kullanma” ve örnek soru var mı?
- Tool seçimini sayıyla ölçtüm mü; alakasız soruları da test setine koydum mu?
temperature=0mı? Reasoning modeldethinkparametresini gönderiyor muyum?- stdio MCP Server’ında stdout’a yazan tek bir satır var mı?
- Loop’ta tur sınırı ve hata sonrası retry yasağı var mı?
- Client’ın system mesajlarını düşürüyor muyum; talimat agent’tan mı geliyor?
- Gelen request’i bir dosyaya dump edebiliyor muyum?
- Yeni tool eklemek agent’a dokunmadan mümkün mü?
Sonuç
İhtiyaç bir cümleydi ve cevabı bir günde yazıldı: agent, MCP Server, bootstrap, benchmark. Şimdi “bugün kaç dolar depozit oldu?” diye soruyorum, 11 saniye sonra cevap geliyor, altında hangi tool’un çalıştığı yazıyor ve şirket dışına tek byte çıkmıyor. Rapor isteyen, SQL yazan, ekran görüntüsü atan zincir yok; soru ve cevap var.
İşin en zor kısmı ne Ollama’ya request atmak ne MCP protokolünü konuşmaktı. Zor olan, küçük bir LLM’in neyi yapabileceğine dürüstçe karar vermekti: sadece “tool lazım mı, hangisi?” Gerisi — sıra, sınır, hata, geçmiş, talimat — agent’ta. Model büyüyünce de bu ayrım bozulmayacak; sadece daha az yönlendirme gerekecek.
Başlıktaki sorunun cevabı: LLM karar verir, MCP Server çalıştırır, agent yönetir. Hangi metnin modelin önüne konacağını, tool sonucunun nereye ekleneceğini ve loop’un ne zaman biteceğini agent belirler. Model o kararın sadece bir adımı.
Kodun tamamı, test seti ve bootstrap:
github.com/ksksertac/ai-agent-mcpserver.
Repodaki setup script’i Ollama’yı ve modeli kurup agent’ı :8000/v1’de
başlatıyor. Client olarak curl, Open WebUI ya da OpenAI-compatible herhangi bir chat ekranını bağlayıp
“bugün kaç dolar depozit oldu?”, “EURUSD’de kaç hesap long?” gibi sorularla
test edebilirsin. İş tool’ları şimdilik mock; gerçek ödeme sistemine ve trading platformuna bağlamak
fonksiyon gövdesini değiştirmek kadar.