Claude Agent SDK ile kodlama ajanı yazmak
Bir LLM'e "bu dosyayı düzelt" dediğinizde aslında arka planda tekrarlayan bir döngü çalışır: model bir araç çağrısı üretir, çağrı çalıştırılır, sonuç modele geri beslenir, model ya yeni bir çağrı yapar ya da durur. Bu döngüyü elle kurmak mümkün ama Anthropic'in Mayıs 2025'te yayınladığı Claude Agent SDK (Python, claude-code-sdk değil doğrudan claude-code CLI üzerine kurulu SDK değil; anthropic-sdk içindeki agent primitifleri) bu işi birkaç düzine satıra indiriyor. Bu yazıda sıfırdan bir kodlama ajanı kuracağım: agent loop mantığı, araç tanımları, dosya sistemi sandbox'ı ve token harcamasını kontrol altına almak için kullandığım yöntemler.
Agent loop nasıl çalışır?
Klasik bir chat completion isteğinde tek bir istek-yanıt döngüsü var. Agent loop ise modelin yanıtında tool_use bloğu geldiği sürece döngüyü sürdürür. Pseudocode şöyle:
mesajlar = [ilk_kullanıcı_mesajı]
while True:
yanıt = model.chat(mesajlar)
eğer yanıtta tool_use yoksa: break
tool sonucunu çalıştır
mesajlara tool sonucunu ekleAnthropic'in Python SDK'sında (sürüm 0.49+) bu döngüyü client.messages.create çağrısını tekrarlayarak kendiniz kurabilirsiniz. Tek kritik nokta: her tool çağrısının sonucunu tool_result content bloğu olarak doğru formatta geri göndermek.
Proje iskeleti
Önce bağımlılıkları kuralım:
pip install anthropic>=0.49.0Temel dosya yapısı:
coding-agent/
├── agent.py # ana agent loop
├── tools.py # araç tanımları ve çalıştırıcılar
├── sandbox.py # dosya sistemi kısıtlamaları
└── workspace/ # ajanın çalışacağı dizinAraç tanımları
Bir kodlama ajanının en az üç araca ihtiyacı var: dosya okuma, dosya yazma, komut çalıştırma. Anthropic API'si araçları JSON Schema formatında bekler.
# tools.py
import subprocess
import os
TOOL_DEFINITIONS = [
{
"name": "read_file",
"description": "Belirtilen dosyanın içeriğini döndürür.",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Okunacak dosya yolu"}
},
"required": ["path"]
}
},
{
"name": "write_file",
"description": "Belirtilen dosyaya içerik yazar. Dosya yoksa oluşturur.",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string"},
"content": {"type": "string"}
},
"required": ["path", "content"]
}
},
{
"name": "run_command",
"description": "Shell komutu çalıştırır ve stdout/stderr döndürür.",
"input_schema": {
"type": "object",
"properties": {
"command": {"type": "string"}
},
"required": ["command"]
}
}
]
def execute_tool(name: str, args: dict, workspace: str) -> str:
if name == "read_file":
full_path = os.path.join(workspace, args["path"])
with open(full_path, "r") as f:
return f.read()
elif name == "write_file":
full_path = os.path.join(workspace, args["path"])
os.makedirs(os.path.dirname(full_path), exist_ok=True)
with open(full_path, "w") as f:
f.write(args["content"])
return f"{args['path']} yazıldı."
elif name == "run_command":
result = subprocess.run(
args["command"],
shell=True,
cwd=workspace,
capture_output=True,
text=True,
timeout=30
)
output = result.stdout + result.stderr
return output[:4000] # token tasarrufu icin kirp
return f"Bilinmeyen araç: {name}"execute_tool fonksiyonunda birkaç bilinçli karar var. run_command çıktısını 4000 karakterle kırpıyorum çünkü büyük bir test çıktısı veya hata logu modelin context window'unu gereksiz yere şişirir. timeout=30 da sonsuz döngüye giren bir scriptin ajanı kilitlemesini önler.
Sandbox: dosya sistemi kısıtlaması
Ajana run_command verip serbest bırakmak tehlikeli. En azından dosya operasyonlarını belirli bir dizinle sınırlamak gerekiyor. Basit bir yaklaşım:
# sandbox.py
import os
def validate_path(path: str, workspace: str) -> str:
"""Yolu workspace içine zorlar. Dışarı çıkmaya çalışırsa hata verir."""
abs_workspace = os.path.abspath(workspace)
abs_target = os.path.abspath(os.path.join(workspace, path))
if not abs_target.startswith(abs_workspace):
raise PermissionError(
f"Erişim reddedildi: {path} workspace dışına çıkıyor"
)
return abs_targetBu kontrol ../../../etc/passwd gibi path traversal denemelerini yakalar. Ancak run_command hâlâ herhangi bir shell komutu çalıştırabilir. Gerçek bir production ortamında bu aracı bir Docker container veya bubblewrap (bwrap) içinde çalıştırmak gerekir. Basit bir Docker sarmalayıcı:
def run_sandboxed(command: str, workspace: str) -> str:
import subprocess
docker_cmd = [
"docker", "run", "--rm",
"--network=none",
"-v", f"{os.path.abspath(workspace)}:/work",
"-w", "/work",
"python:3.12-slim",
"bash", "-c", command
]
result = subprocess.run(
docker_cmd,
capture_output=True,
text=True,
timeout=60
)
return (result.stdout + result.stderr)[:4000]--network=none bayrağı container'ın dışarıya bağlantı kurmasını engeller. Ajan yanlışlıkla curl ile bir şey indirmeye çalışırsa başarısız olur.
Ana agent loop
Şimdi hepsini birleştirelim:
# agent.py
import anthropic
from tools import TOOL_DEFINITIONS, execute_tool
from sandbox import validate_path
WORKSPACE = "./workspace"
MAX_ITERATIONS = 15
MAX_TOKENS_PER_TURN = 4096
client = anthropic.Anthropic() # ANTHROPIC_API_KEY env'den okur
def run_agent(task: str) -> str:
messages = [
{"role": "user", "content": task}
]
system_prompt = (
"Sen bir kodlama asistanısın. Verilen görevi tamamlamak için "
"araçları kullan. Dosyaları oku, değiştir, komut çalıştır. "
"İşin bittiğinde kullanıcıya ne yaptığını açıkla."
)
total_input_tokens = 0
total_output_tokens = 0
for iteration in range(MAX_ITERATIONS):
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=MAX_TOKENS_PER_TURN,
system=system_prompt,
tools=TOOL_DEFINITIONS,
messages=messages
)
total_input_tokens += response.usage.input_tokens
total_output_tokens += response.usage.output_tokens
# Yanıtı mesajlara ekle
messages.append({"role": "assistant", "content": response.content})
# Tool çağrısı var mı kontrol et
tool_calls = [
block for block in response.content
if block.type == "tool_use"
]
if not tool_calls:
# Model araç çağırmadı, iş bitti
break
# Her tool çağrısını çalıştır
tool_results = []
for tool_call in tool_calls:
try:
# Dosya işlemlerinde path doğrula
if tool_call.name in ("read_file", "write_file"):
validate_path(tool_call.input["path"], WORKSPACE)
result = execute_tool(
tool_call.name,
tool_call.input,
WORKSPACE
)
except Exception as e:
result = f"Hata: {e}"
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_call.id,
"content": result
})
messages.append({"role": "user", "content": tool_results})
# Son text bloğunu bul
final_text = ""
for block in response.content:
if hasattr(block, "text"):
final_text = block.text
print(f"Toplam: {total_input_tokens} input, {total_output_tokens} output token")
print(f"İterasyon sayısı: {iteration + 1}")
return final_text
if __name__ == "__main__":
result = run_agent(
"workspace dizininde bir Python Flask uygulaması oluştur. "
"/ endpoint'i JSON dönsün. Sonra pytest ile test yaz ve çalıştır."
)
print(result)Burada dikkat edilecek birkaç nokta var. response.content bir liste; içinde hem TextBlock hem ToolUseBlock olabilir. Model bazen bir açıklama yazıp ardından araç çağrısı yapar, bazen doğrudan araç çağırır. tool_use_id eşleşmesi kritik: her tool_result'ın hangi tool_use'a ait olduğunu belirtmeniz gerekir, yoksa API hata verir.
Maliyet kontrolü
Bir kodlama ajanı kolayca onlarca iterasyon yapabilir. Claude Sonnet 4'ün fiyatlandırması (Haziran 2025 itibarıyla) input için $3/MTok, output için $15/MTok. 15 iterasyonluk bir oturumda ortalama 50K input, 10K output token harcarsanız yaklaşık $0.30 eder. Ama model uzun dosyalar okuyor, büyük testler çalıştırıyorsa bu rakam birkaç dolara çıkabilir.
Maliyet kontrolü için kullandığım dört yöntem:
MAX_ITERATIONS ile döngü sayısını sınırlamak en basit fren. 15, çoğu görev için yeterli. Model 15 iterasyonda bitiremiyorsa büyük ihtimalle yanlış yolda ilerliyor.
Tool çıktılarını kırpmak da token tasarrufu sağlıyor. run_command çıktısını 4000 karakterle sınırladım. Büyük bir npm install logunu modele olduğu gibi göndermek israf.
Üçüncü yöntem, toplam token sayacını canlı tutmak ve bir eşik aşıldığında döngüyü kesmek:
TOKEN_BUDGET = 100_000 # toplam token limiti
# loop içinde:
if total_input_tokens + total_output_tokens > TOKEN_BUDGET:
print("Token bütçesi aşıldı, durduruluyor.")
breakDördüncü yöntem model seçimi. Her görev Sonnet gerektirmiyor. Basit dosya düzenleme işleri için Haiku çok daha ucuz (input $0.25/MTok, output $1.25/MTok). Agent loop'a model parametresini dışarıdan geçirmek iyi bir pratik.
Araç setini genişletmek
Üç temel araç çoğu kodlama görevini karşılar ama birkaç ekleme işleri kolaylaştırır. list_files aracı, modelin dizin yapısını görmesini sağlar ve gereksiz ls komutlarını önler. search_files aracı (basit bir grep sarmalayıcısı) büyük projelerde modelin doğru dosyayı bulmasını hızlandırır.
{
"name": "list_files",
"description": "Belirtilen dizindeki dosya ve klasörleri listeler.",
"input_schema": {
"type": "object",
"properties": {
"directory": {
"type": "string",
"description": "Listelenecek dizin yolu. Varsayılan: ."
}
}
}
}Araç sayısını artırdıkça system prompt'a araç kullanım stratejisi eklemek faydalı. "Önce list_files ile yapıyı gör, sonra değişiklik yap" gibi bir yönerge, modelin rastgele dosya adları denemesini azaltır.
Gerçek dünya için eksikler
Bu yazıdaki ajan bir prototip. Production'a taşımak için birkaç şey daha gerekir.
Conversation state'ini diske yazmak, uzun süren görevlerde kaldığı yerden devam etmeyi mümkün kılar. messages listesini her iterasyonda JSON olarak kaydetmek yeterli.
Streaming yanıtlar kullanıcı deneyimini iyileştirir. client.messages.stream() ile yanıtı token token alabilir, kullanıcıya ajanın ne yaptığını gerçek zamanlı gösterebilirsiniz.
Hata kurtarma da gerekli. API rate limit'e takılırsanız (Anthropic'in rate limitleri tier'a göre değişir), exponential backoff ile yeniden denemek gerekir. anthropic SDK'sı bunu otomatik yapar ama timeout değerlerini ayarlamak isteyebilirsiniz.
Bu konularda daha geniş bir perspektif için MCP sunucusu yazma rehberine bakabilirsiniz; orada TypeScript tarafından tool tanımlarının standartlaştırılması anlatılıyor. AI chat arayüzü tarafı için de assistant-ui ile composable AI chat bileşenleri ve Vercel AI SDK useChat ile streaming yazıları tamamlayıcı olacaktır.
Ne zaman kendi ajanınızı yazmalısınız?
Claude Code CLI zaten bir kodlama ajanı ve çoğu bireysel kullanım için yeterli. Kendi ajanınızı yazmanız gereken durumlar spesifik: CI/CD pipeline'ına entegre etmek istiyorsanız, özel araçlar (veritabanı sorgusu, API çağrısı, deployment) eklemeniz gerekiyorsa, ya da maliyet ve güvenlik politikalarınız özel sandbox gerektiriyorsa. Bu durumların dışında, hazır çözümleri kullanmak daha mantıklı. Ama bir kez agent loop'un nasıl çalıştığını anladığınızda, her LLM tabanlı otomasyon projesinde aynı kalıbı tekrar tekrar kullanırsınız. Döngü hep aynı: model düşünür, araç çağırır, sonucu görür, tekrar düşünür.