Ana içeriğe geç
Kesit Python
İleri · Yapay Zekâ

LLM ajanları: araç döngüsü, hafıza, MCP ve güvenli sınırlar

İş akışı ile ajan farkından ReAct döngüsüne, pydantic ile araç şemalarına, hata gözlemine, adım sınırı ve takılma tespitine, hafıza kırpmaya, MCP protokolüne, insan onayı ve istem enjeksiyonu savunmasına, maliyet hesabına kadar ajan mühendisliğini sahte bir LLM ile uygular; Anadolu Market text-to-SQL ajanı ve değerlendirme setiyle bitirirsin.

Uygulamada aç, 20 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

Ajan nedir, ne zaman gerekir?

LLM API'leriyle çalışmak: istemci, JSON ve araç çağrısı dersinde modele tek turda bir araç çağırttın: model "şu fonksiyonu şu argümanla çağır" dedi, sen çağırıp sonucu geri verdin. Bir ajan (agent) bu turu döngüye çevirir: model düşünür, bir araç seçer, sonucu gözlemler, gerekirse başka bir araç çağırır ve görev bitince cevap verir. Kaç adım süreceğine ve hangi aracı çağıracağına model karar verir.

Bu güç bedelsiz değildir: her adım gecikme, token maliyeti ve hata ihtimali ekler; model döngüde takılabilir, yanlış aracı çağırabilir ya da araç çıktısındaki kötü niyetli bir talimata uyabilir. Bu yüzden ilk soru şudur: Gerçekten ajana ihtiyacım var mı?

İş akışı (workflow)Ajan (agent)
Adımları kim belirler?Sen, kodda (sabit sıra, if/else)Model, çalışma anında
ÖngörülebilirlikYüksek; test etmesi kolayDüşük; aynı girdi farklı yol izleyebilir
Maliyet ve gecikmeSabit ve düşükAdım sayısına göre değişken
Uygun iş"Şikâyeti sınıflandır → özetle → ilgili ekibe ata""Bu müşterinin neden iade istediğini bul ve çözüm öner" (açık uçlu)
Basit başlaÇoğu üretim sistemi tek bir LLM çağrısı, birkaç sabit adımdan oluşan bir zincir ya da yönlendirici (router) ile çözülür. Ajanı, adım sayısı ve sırası önceden bilinemeyen görevlere sakla.

1. ReAct döngüsü: düşün, eylem yap, gözlemle

ReAct (Reason + Act) kalıbında model her turda ya bir araç çağrısı ya da son cevap üretir. Senin kodun (ajan çerçevesi) aracı çalıştırır, sonucu gözlem (observation) olarak mesaj geçmişine ekler ve modeli tekrar çağırır. Döngüyü mutlaka sınırlarsın:

Aşağıdaki sahte model önceden yazılmış eylemleri sırayla döndürür; araç ise Lezzet Kurye'nin gerçek sipariş tablosunu okur:

class SahteLLM:
    """Ağsız ajan modeli: önceden yazılmış eylemleri sırayla döndürür, gördüğü mesajları kaydeder."""
    def __init__(self, eylemler):
        self.eylemler = list(eylemler)
        self.gorulen = []

    def cevapla(self, mesajlar):
        self.gorulen.append([dict(m) for m in mesajlar])
        return self.eylemler.pop(0) if self.eylemler else {"cevap": "Bilmiyorum"}


import veri

SIPARIS = {s["order_id"]: s for s in veri.kayitlar("lezzet_kurye", "lk_orders")}

def siparis_durumu(no):
    s = SIPARIS[no]
    return {"durum": s["status"], "kurye": s["courier_id"], "teslim": s["delivered_at"]}

ARACLAR = {"siparis_durumu": siparis_durumu}

def ajan(llm, soru, max_adim=4):
    mesajlar = [{"role": "user", "content": soru}]
    for adim in range(1, max_adim + 1):
        eylem = llm.cevapla(mesajlar)
        if "cevap" in eylem:
            return eylem["cevap"], adim
        mesajlar.append({"role": "assistant", "eylem": eylem})
        try:
            gozlem = str(ARACLAR[eylem["arac"]](**eylem["girdi"]))
        except Exception as e:
            gozlem = f"HATA: {type(e).__name__}: {e}"
        print(f"  adım {adim}: {eylem['arac']}({eylem['girdi']}) → {gozlem}")
        mesajlar.append({"role": "arac", "ad": eylem["arac"], "content": gozlem})
    return "Adım sınırına ulaşıldı", max_adim

llm = SahteLLM([
    {"dusunce": "Sipariş numarasıyla durumu sorgulayayım.", "arac": "siparis_durumu", "girdi": {"no": 99999}},
    {"dusunce": "Numara yanlış olabilir, müşterinin ilk yazdığı 28'i deneyeyim.", "arac": "siparis_durumu", "girdi": {"no": 28}},
    {"cevap": "28 numaralı siparişiniz iptal edilmiş görünüyor."},
])
print(ajan(llm, "28 numaralı siparişim nerede?"))

İkinci adımda model, ilk aracın hata gözlemini görüp düzeltti. Bu yüzden araç hataları ajanı çökertmemeli, gözlem olarak modele geri verilmelidir. Gerçek SDK'larda aynı döngü şöyle görünür:

# pip install anthropic
import anthropic
client = anthropic.Anthropic()
mesajlar = [{"role": "user", "content": "28 numaralı siparişim nerede?"}]
for _ in range(8):                                   # adım sınırı
    yanit = client.messages.create(model=MODEL_ADI, max_tokens=1024,
                                   tools=ARAC_TANIMLARI, messages=mesajlar)
    mesajlar.append({"role": "assistant", "content": yanit.content})
    if yanit.stop_reason != "tool_use":
        break                                        # son cevap
    sonuclar = []
    for blok in yanit.content:
        if blok.type == "tool_use":
            try:
                cikti, hata = str(ARACLAR[blok.name](**blok.input)), False
            except Exception as e:
                cikti, hata = f"HATA: {e}", True
            sonuclar.append({"type": "tool_result", "tool_use_id": blok.id,
                             "content": cikti, "is_error": hata})
    mesajlar.append({"role": "user", "content": sonuclar})

2. Araç şemaları: pydantic'ten JSON Schema

Modele hangi araçların olduğunu ad, açıklama ve girdi şeması (JSON Schema) ile anlatırsın. Şemayı elle yazmak yerine Veri doğrulama ve pydantic v2 dersindeki modellerden üretmek hem tekrarı önler hem de modelin gönderdiği argümanları aynı modelle doğrulamanı sağlar. Açıklamalar modelin aracı doğru seçmesi için kritiktir: "ne yapar, ne zaman kullanılır, ne döndürür".

from typing import Literal
from pydantic import BaseModel, Field, ValidationError

class IadeTalebi(BaseModel):
    siparis_no: int = Field(gt=0, description="Lezzet Kurye sipariş numarası")
    neden: Literal["hasarli", "yanlis_urun", "eksik"] = Field(description="İade nedeni")
    tutar: float = Field(gt=0, le=5000, description="İade edilecek tutar (TL)")

arac = {
    "name": "iade_baslat",
    "description": "Teslim edilmiş bir sipariş için iade talebi açar. Yalnızca müşteri iade istediğini açıkça söylediyse kullan.",
    "input_schema": IadeTalebi.model_json_schema(),
}
print(arac["input_schema"]["required"])
print(arac["input_schema"]["properties"]["neden"])

try:
    IadeTalebi.model_validate({"siparis_no": 28, "neden": "pahali", "tutar": 9000})
except ValidationError as e:
    print([(".".join(map(str, h["loc"])), h["type"]) for h in e.errors()])

Doğrulama hatasını modele gözlem olarak geri verirsen ("tutar: 5000'den küçük olmalı"), model çoğu zaman bir sonraki adımda düzeltir. Ama yeniden deneme sayısını sınırla; aynı hatayı üç kez yapan model dördüncüde de yapar.

3. Durum ve hafıza

Model hafızasızdır; "hafıza" senin her çağrıda gönderdiğin mesajlardır. Ajan döngüsünde geçmiş her adımda büyür, bu yüzden üç katman kullanılır:

KatmanNe tutar?Nasıl?
Kısa dönem (çalışma belleği)Bu görevin mesajları, araç çağrıları ve gözlemleriMesaj listesi; bütçe aşılınca kırpma ya da özetleme
Konuşma özetiEski turların sıkıştırılmış hâliLLM ile özet, ilk görev mesajı her zaman korunur
Uzun dönemKullanıcı tercihleri, geçmiş çözümlerVektör deposu (Embedding ve vektör arama: anlamsal arama ve RAG'in getirme (retrieval) katmanı), anahtar-değer deposu
def kirp(mesajlar, max_mesaj):
    """İlk görev mesajını korur, araç sonucunu çağrısından ayırmaz."""
    if len(mesajlar) <= max_mesaj:
        return list(mesajlar)
    kuyruk = mesajlar[-(max_mesaj - 2):]
    while kuyruk and kuyruk[0]["role"] == "arac":   # çağrısı kesilmiş sonuç kalmasın
        kuyruk = kuyruk[1:]
    atilan = len(mesajlar) - 1 - len(kuyruk)
    return [mesajlar[0], {"role": "user", "content": f"[{atilan} eski mesaj özetlendi]"}] + kuyruk

gecmis = [{"role": "user", "content": "Görev: iade sürecini başlat"}]
for i in range(3):
    gecmis += [{"role": "assistant", "content": f"çağrı {i}"}, {"role": "arac", "content": f"sonuç {i}"}]
for m in kirp(gecmis, 5):
    print(m["role"], "|", m["content"])
Yetim araç sonucuGerçek API'lerde bir araç sonucu (tool_result), kendisini isteyen araç çağrısı (tool_use) olmadan gönderilirse istek reddedilir. Kırparken çağrı-sonuç çiftlerini birlikte tut.

4. MCP: Model Context Protocol

Her uygulama her araç için ayrı entegrasyon yazınca N × M sorunu doğar. MCP (Model Context Protocol), araçları ve verileri LLM uygulamalarına standart bir yolla sunmak için açık bir protokoldür. Bir MCP sunucusu üç şey sunar: araçlar (tools: modelin çağırabileceği fonksiyonlar), kaynaklar (resources: okunabilir veri, ör. bir dosya ya da tablo) ve istemler (prompts: hazır şablonlar). Claude Desktop, Claude Code ve birçok IDE gibi istemciler sunucuya bağlanıp araç listesini alır ve modelin isteğiyle çağırır. Mesajlar JSON-RPC 2.0 biçimindedir.

# pip install "mcp[cli]"  — resmi Python SDK ile küçük bir MCP sunucusu
from mcp.server.fastmcp import FastMCP
import sqlite3

mcp = FastMCP("anadolu-market")

@mcp.tool()
def siparis_sayisi(durum: str) -> int:
    """Verilen durumdaki (delivered, cancelled, returned, shipped) sipariş sayısını döndürür."""
    con = sqlite3.connect("anadolu_market.db")
    return con.execute("SELECT COUNT(*) FROM orders WHERE status = ?", (durum,)).fetchone()[0]

@mcp.resource("sema://tablolar")
def sema() -> str:
    """Veritabanı şeması (salt okunur)."""
    return "orders(order_id, customer_id, order_date, status, channel)"

if __name__ == "__main__":
    mcp.run()          # stdio üzerinden; istemci yapılandırmasına bu komutu eklersin

Arka planda istemci ile sunucu şu mesajları değiş tokuş eder. Protokolü anlamak için küçük bir dağıtıcı yazalım:

import json

ARAC_TANIMLARI = [{"name": "siparis_sayisi", "description": "Durumdaki sipariş sayısı",
                   "inputSchema": {"type": "object", "properties": {"durum": {"type": "string"}}, "required": ["durum"]}}]
SAYILAR = {"delivered": 712, "cancelled": 72}

def isle(istek):
    if istek["method"] == "tools/list":
        return {"jsonrpc": "2.0", "id": istek["id"], "result": {"tools": ARAC_TANIMLARI}}
    if istek["method"] == "tools/call":
        durum = istek["params"]["arguments"]["durum"]
        return {"jsonrpc": "2.0", "id": istek["id"],
                "result": {"content": [{"type": "text", "text": str(SAYILAR.get(durum, 0))}], "isError": False}}
    return {"jsonrpc": "2.0", "id": istek["id"], "error": {"code": -32601, "message": "Method not found"}}

print(json.dumps(isle({"jsonrpc": "2.0", "id": 1, "method": "tools/call",
                       "params": {"name": "siparis_sayisi", "arguments": {"durum": "cancelled"}}}), ensure_ascii=False))
Kendi bilgisayarında denepip install "mcp[cli]" ile SDK'yı kur, yukarıdaki sunucuyu bir dosyaya kaydet ve mcp dev sunucu.py ile tarayıcıdaki denetleyicide araçları dene. Ardından Claude Desktop ya da Claude Code yapılandırmasına sunucuyu ekleyebilirsin; güncel adımlar için modelcontextprotocol.io belgelerine bak.

5. Güvenlik: yetki, onay ve istem enjeksiyonu

Ajan, senin adına iş yapan bir programdır; ona verdiğin her araç bir yetkidir.

import re

YASAK = re.compile(r"\b(insert|update|delete|drop|alter|create|replace|attach|pragma)\b", re.I)

def sql_bekci(sorgu, izinli, limit=50):
    s = sorgu.strip().rstrip(";").strip()
    if ";" in s or "--" in s or "/*" in s:
        raise ValueError("tek bir sorgu olmalı, yorum içeremez")
    if not re.match(r"(select|with)\b", s, re.I):
        raise ValueError("yalnız SELECT sorgularına izin var")
    if YASAK.search(s):
        raise ValueError("yasak anahtar kelime")
    tablolar = {t.lower() for t in re.findall(r"\b(?:from|join)\s+([A-Za-z_]\w*)", s, re.I)}
    yetkisiz = tablolar - izinli
    if yetkisiz:
        raise ValueError(f"izin verilmeyen tablo: {', '.join(sorted(yetkisiz))}")
    if not re.search(r"\blimit\s+\d+\s*$", s, re.I):
        s = f"{s} LIMIT {limit}"
    return s


IZINLI = {"customers", "orders", "order_items", "products"}
for sorgu in ["SELECT city, COUNT(*) FROM customers GROUP BY city",
              "select * from orders; DROP TABLE orders",
              "SELECT * FROM employees",
              "DELETE FROM customers"]:
    try:
        print("OK  ", sql_bekci(sorgu, IZINLI))
    except ValueError as e:
        print("RED ", e)
Bekçi tek savunma değildirDüzenli ifadeyle SQL denetimi her hileyi yakalayamaz. Asıl güvence veritabanı düzeyindedir: yalnız SELECT yetkisi olan bir kullanıcı, salt okunur bağlantı (SQLite'ta mode=ro), sorgu zaman aşımı ve satır sınırı. Bekçi, hataları erken ve anlaşılır biçimde modele geri vermek içindir.

6. Maliyet ve gecikme

Her adımda tüm geçmiş yeniden gönderildiği için girdi tokenları adım sayısıyla karesel büyür. 10 adımlı bir ajan, tek çağrılık bir işin 20–50 katı token harcayabilir.

sistem, yeni_girdi, cikti = 1500, 300, 200    # token
toplam_girdi = 0
gecmis = 0
for adim in range(1, 11):
    girdi = sistem + gecmis + yeni_girdi
    toplam_girdi += girdi
    gecmis += yeni_girdi + cikti
    if adim in (1, 5, 10):
        print(f"{adim:>2}. adım girdi {girdi:>5} token, toplam {toplam_girdi:>6}")

Azaltma yolları: adım sınırı, uzun araç çıktılarını kısaltma, istem önbellekleme (prompt caching), basit adımlarda küçük model, paralel araç çağrıları ve en önemlisi gereksiz ajan kullanmamak.

ÇerçeveOdakNe zaman?
Sağlayıcı SDK'sı (anthropic, openai)Ham araç çağırma döngüsüTam kontrol, az bağımlılık; bu dersteki yaklaşım
LangGraphAjanı durum makinesi/graf olarak modelleme, kalıcı durum, insan onayı düğümleriÇok adımlı, dallanan, kesintiye uğrayıp devam eden akışlar
LangChainHazır bileşenler (yükleyiciler, araç sarmalayıcılar)Hızlı prototip; soyutlamaların bedelini bil
LlamaIndexVeri bağlayıcıları ve RAG odaklı ajanlarBelge ağırlıklı getirme (retrieval) uygulamaları

7. Text-to-SQL ajanı ve değerlendirme seti

Yapay zekâ yolunun bitirme projesi bir Anadolu Market text-to-SQL ajanıdır: kullanıcı "İptal edilen sipariş sayısı?" diye sorar; ajan önce sema aracıyla tabloları görür, SQL yazar, sql_calistir aracıyla bekçiden geçirip çalıştırır, hata gelirse düzeltir ve sonucu döndürür. Kaliteyi yürütme doğruluğu (execution accuracy) ile ölçersin: ajanın sonucu, altın SQL'in sonucu ile (satır sırası önemsizse sıralanarak) aynı mı? Altın sete "Tüm müşterileri sil" gibi reddedilmesi gereken sorular da konur.

Gerçek işteAjan projeleri genelde "demo çalıştı" aşamasında takılır. Üretime geçen ekipler önce 30–100 soruluk bir değerlendirme seti kurar, her istem ya da araç değişikliğinde seti otomatik çalıştırır, her adımın girdisini, çıktısını, süresini ve token sayısını iz (trace) olarak kaydeder ve geri alınamaz araçları insan onayına bağlar. Hukuk ve bilgi güvenliği ekipleri hangi araçların hangi verilere erişebileceğini baştan onaylar.
Uzmanlaş

Çoklu ajan ve yönlendirme

Bir "yönetici" ajanın işi alt ajanlara (araştırmacı, SQL uzmanı, yazar) dağıttığı mimariler bağlamı böler ve paralellik sağlar; ama maliyeti ve hata yüzeyini çarpar. Önce tek ajan + iyi araç tasarımıyla başla.

Araç tasarımı = arayüz tasarımı

Az sayıda, iyi adlandırılmış, açıklaması net ve hata mesajı yol gösteren araçlar, çok sayıda ince araçtan daha iyi sonuç verir. Araç çıktısını kısa ve yapılandırılmış tut; 10 000 satırlık sonucu modele değil, özetini ve ilk birkaç satırı ver.

Değerlendirme katmanları

Son cevap doğruluğu yanında yörünge (trajectory) değerlendirmesi yap: doğru araçlar doğru sırayla mı çağrıldı, gereksiz adım var mı, yasak araç denendi mi? LLM-as-judge kullanıyorsan hakemi insan etiketleriyle kalibre et.

Alıştırmalara geç (20)