İMEimertekin.com

Varsayım yerine ölçüm: küçük bir coding agent yazarken yanlış çıkan yedi varsayım

Devam eden bir projeden ara not.

TarihDate
2026-09
DurumStatus
devam ediyorongoing
SürümVersion
0.1
DilLanguage
TR EN →
OkumaReading
~12 dkmin

Bu bir ara not. Proje devam ediyor; “harness mi, model mi?” sorusunun cevabı geldiğinde son bölüm güncellenecek ve değişiklik kaydına işlenecek.This is an interim note. The project is ongoing; the last section will be updated once the “harness or model?” question is answered.

Kısaca

Framework kullanmadan, sıfırdan küçük bir coding agent yazıyorum. Amacım bir ürün çıkarmak değil; “agent” dediğimiz şeyin gerçekte ne olduğunu kendi elimle kurarak öğrenmek. Yol boyunca makul görünen varsayımlarımın önemli bir kısmı ölçüme dayanamadı. Bu yazıda bunlardan yedisini anlatıyorum, ve hepsinin ortak dersi aynı çıktı: tahmin etme, ölç.

Anahtar kelimelerKeywords: coding agent · harness · tool calling · tekrarlanabilir ölçümreproducible measurement · trace · memory

Proje ne?

Agentic coding üzerine okurken en sık karşılaştığım cümle şuydu: Agent = Model + Harness. Model, metin alıp metin üreten LLM. Harness ise onun etrafındaki her şey: modele hangi tool’ların tarif edildiği, context’in nasıl kurulduğu, state’in nerede tutulduğu, döngünün ne zaman durduğu. Model hiçbir şey çalıştırmaz, sadece ister. Dosyayı okuyan, testi koşan, sonucu geri veren kod harness’ta.

Not · Bu deneyin çıkış noktası SWE-agent ekibinin mini-swe-agent çalışması [1]: küçük ve okunabilir bir agent döngüsünün ne kadar ileri gidebildiğini göstermesi [2].

Bu cümleyi okumak ile anlamak arasında fark olduğunu düşündüm ve harness’ı kendim yazmaya karar verdim. Birkaç kural koydum:

  • Agent framework’ü yok. LangGraph veya Google ADK gibi araçlara, neden var olduklarını kendi eksikliklerimden görünce bakacağım.
  • LLM SDK’sı yok. Sağlayıcıyla ham HTTP üzerinden konuşuyorum; tek runtime bağımlılığı httpx.
  • Model: Gemini Flash-Lite, ücretsiz katman. Yerel GPU’m şu an kullanılabilir değildi ve bu projeyi bloklamamalıydı.
  • Hedef repo: tomlkit, sabit bir commit’e pinlenmiş. Sıfır runtime bağımlılığı, 1.052 test, ~1,3 saniyede koşuyor.
  • Her şey kayıt altında. Her model çağrısı, her tool çağrısı, her sonuç satır satır bir JSONL trace’e yazılıyor ve sonradan kayıpsız okunabiliyor.

Çalışma şeklim de deneyin bir parçası: kodu Claude Code yazıyor, ben okuyup üzerine gidiyorum; her adımın tasarım sorularını ve kararlarını ayrı bir Claude oturumunda hazırlıyorum. Aşağıdaki yanlış tahminlerin bir kısmı benim, bir kısmı bu iki asistanın. İlginç olan, üç tarafın da aynı tür hatayı yapması.

Bu çalışma düzeni, ileride agent’a eklemeyi planladığım Project Historian fikrinin elle yapılan erken bir hâli sayılabilir. Kararlar koddan önce ayrı bir oturumda yazılıyor, duran kararlar bir tabloda tutuluyor ve her adım bir durum belgesiyle bir sonrakine devrediliyor. Tam bir uygulaması değil. Ama aşağıdaki derslerin bir kısmı, bu düzenin nerede tuttuğunu ve nerede tutmadığını gösteriyor.

1“Dokümantasyonu okuyup provider’ı yazarım”

İlk iş olarak Gemini API’sine istek atan sınıfı yazacaktım. Yazmadan önce ham istekler atıp protokolün gerçekte ne döndürdüğünü ölçtüm. Dokümantasyondan yazsaydım dört sessiz hatayla başlayacaktım:

  • thoughtsTokenCount Flash-Lite’ta sıfır değil, hiç yok. usage["thoughtsTokenCount"] diye okuyan kod ilk çağrıda KeyError verirdi.
  • Düşünme token’ları candidatesTokenCount’a dahil değil, ama totalTokenCount’a dahil. Çıktı token’larını 3–4 kat eksik sayardım, ve bu projede maliyet ana metriklerden biri.
  • Tool çağrısı içeren bir cevapta bile finishReason "STOP". “Model tool mu istedi” sorusunu finishReason’a bakarak cevaplayan bir döngü hiçbir tool çağrısını görmezdi.
  • thoughtSignature. Modelin iç muhakemesinin opak bir imzası, tool çağrısıyla aynı yapının içinde geliyor. Okunamıyor, üretilemiyor, doğrulanamıyor; sadece bir sonraki istekte aynen geri gönderilebiliyor. Cevabı temiz bir iç formata çevirip sonra yeniden kuran kod bu alanı sessizce düşürürdü.

Ölçümden sonra yazılan sınıf, gerçek API’yle ilk temasında sıfır sürprizle çalıştı. Yolda bir şey daha öğrendim: tool calling bir modelin “yeteneği” değil. İstekteki tools bloğuyla açılan bir sözleşme, ve o sözleşmenin zarfı sağlayıcıdan sağlayıcıya değişiyor. Gemini functionCall döndürüyor, OpenAI tool_calls, Anthropic tool_use. Aynı açık modeli ham servis ettiğinizde ise etiketleri kendiniz parse ediyorsunuz. Format ağırlıklardan değil, servis katmanından geliyor.

2“Bir değişikliğin etkisini tek koşuyla görürüm”

Agent gerçek bir kod tabanı hakkındaki bir soruyu doğru cevaplamayı başardıktan sonra, system prompt’taki bir cümlenin davranışı ne kadar etkilediğini görmek istedim. Cümleyi çıkarıp aynı görevi yeniden koştum. Aynı görev, neredeyse aynı kurulum, dört koşu:

koşu turn token sonuç
baseline 10 43.843 tam doğru
ablation 1 10 41.144 cevapsız, turn sınırına takıldı
ablation 2 5 17.900 kısmi
ablation 3 5 14.557 kısmi

2,8 kat maliyet farkı, dörtte biri cevapsız. Sadece baseline ile ablation 2’yi karşılaştırsaydım, o cümlenin maliyeti 2,4 kat artırdığı sonucuna varırdım. Bu verinin söylediği tek şey, n=1’lik bir karşılaştırmanın hiçbir şey söylemediği.

Sebebi basit: LLM deterministik değil ve agent bu rastgeleliği yutmuyor, büyütüyor. Birinci turn’deki küçük bir fark, onuncu turn’de bambaşka bir yol demek. Sonradan yazma görevlerinde de ölçtüm: üç koşunun üçünün de başarılı olduğu bir görevde yayılım 2,6 kat çıktı.

Aynı tablodan ikinci bir tuzak da çıktı: “bitti” başarı demek değil. İki koşu cevap vererek bitti ama üç doğru konumdan yalnızca birini buldu. Ucuz koşular ucuzdu çünkü daha az iş yaptılar. Koşulsuz bir “token / episode” sıralaması, tam doğru cevabı sonuncu yapardı.

Bu tablo planın geri kalanını değiştirdi. Her kol için çok sayıda koşu, dereceli sonuç (cevapsız / kısmi / tam), sonuç kalitesine koşullu maliyet, ve ana ölçüm olarak test ile doğrulanan bug-fix görevleri geldi.

3“Yazdığım bir karar, uygulanmış bir karardır”

Model cevabını temsil eden veri yapısında retries ve retry_wait_ms alanları vardı. Karar netti: her çağrının kaç kez yeniden denendiği trace’e yazılacaktı. Ama trace’i yazan kod bu iki alanı hiç yazmıyordu. Trace’i okuyan araç da onları .get(..., 0) ile okuyordu, yani ekranda “0 retry” görünüyordu.

Ölçümün yokluğu bir ölçüm gibi görünüyordu. Hatayı gizleyen şey, “güvenli” olsun diye yazılmış savunmacı bir satırdı.

Bu örüntü defalarca tekrarladı. “Bildirilmeyen bir değer sıfır değildir” ilkesi bir dosyada yazılıydı, başka bir dosyada ihlal ediliyordu. Agent’ın repoyu her görevden sonra temiz duruma döndürmesini sağlayan kod finally içinde değildi; ilk crash pinlenmiş repoyu değiştirilmiş halde bıraktı. Aynı kod, görev bittiğinde agent’ın yaptığı değişikliği de siliyordu. Bir bug-fix episode’unun en bilgilendirici çıktısı buydu, ve bir kez yalnızca şans eseri kurtuldu. Kendi raporundaki cümleyle: “guardrail vardı, yarısı yoktu.”

Düzeltmesi basit oldu. Projenin duran kararlarını tuttuğum tabloya “bu karar nerede zorlanıyor” diye bir sütun ekledim. İçine bir test adı ya da kod satırı yazılamayan her satır, yarım kalmış bir karar demek.

4“Kaydettiğim bir ders, öğrenilmiş bir derstir”

Testlerde gerçek API cevaplarını kaydedip isteğin hash’iyle eşleştiriyorum (bu kayıtlara cassette deniyor). Neredeyse her adımda “bu değişiklik kayıtları geçersiz kılacak” diye tahmin edildi, ve kılmadı. Aynı yanlış tahmin, iki kez düzeltildikten sonra üçüncü kez yapıldı.

Düzeltme bir yerde yazılıydı: öğrenme günlüğünde. Ama o dosya bir sonraki adımın planı yazılırken okunmuyordu. Kayıt vardı; getirilmedi.

Başka bir vakada kural her oturumda okunan dosyadaydı. Getiriliyordu, ama kodda yarım uygulanıyordu. Bu da başka bir kapıda başarısızlık.

İkisini yan yana koyunca bir taksonomi çıktı:

1 · Saklandı mı?bilgi bir yere yazıldı 2 · Getirildi mi?gerektiğinde okundu 3 · Uygulandı mı?davranış değişti düzeltme öğrenme günlüğündeydi,plan yazılırken okunmadı kural her oturumda okunuyordu,kodda yarım uygulandı
Şekil 1 — Her kapı ayrı ayrı başarısız olabiliyor. Yalnızca ilk kapıyı ölçen bir memory deneyi, işe yaramayan bir sistemi “çalışıyor” diye raporlar.

Her kapı ayrı ayrı başarısız olabiliyor. Bu önemli, çünkü projenin ikinci yarısı agent’a memory eklemek. Yalnızca ilk kapıyı ölçen bir memory deneyi (“bunu kaydetti mi?”), işe yaramayan bir sistemi “çalışıyor” diye raporlar. Doğru soru şu: agent’ın makul olarak soracağı bir sorgu bu bilgiyi getirir miydi, ve getirdiğinde davranış değişti mi?

Kendim için pratik çözüm, her adımda yeniden ürettiğim tek durum belgesine “bilinen yanlış inançlar” diye bir bölüm eklemek oldu. Düzeltmeyi, gerçekten okunan yere taşıdım.

5“İyi bir özet, kaynağın yerini tutar”

Yukarıdaki dört koşuluk tabloda ablation 2 “kısmi” diye işaretliydi. Bu etiket birkaç tartışma boyunca sorgulanmadan taşındı ve “yanlış cevap” gibi okunmaya başladı. Bir noktada “ama bu aynı çıktı değil mi?” diye sordum. Cevaplara tek tek bakınca durum netleşti: ablation 2 doğruydu, sadece dardı. Yorum satırlarının korunmasından sorumlu Trivia sınıfını doğru dosya ve satırla buldu; Comment sınıfını ve parser’daki ilgili fonksiyonu kaçırdı.

Etiket, bir özet kaynağı açmaktan daha ucuz olduğu için hayatta kaldı. Oysa kaynak tek bir komut uzaklıktaydı; trace’leri kayıpsız okumak için yazdığım araç tam olarak bunun içindi.

Buradan çıkardığım ders şu: tehlikeli olan kötü özet değil, iyi özet. İyi özet çoğu zaman doğrudur, o yüzden güvenilir, o yüzden kaynağın yerine geçer. Aynı olay bana bir şey daha gösterdi: 160 episode’u tutarlı notlandırması gereken bir insan, bir metrik değildir. Bu, test ile doğrulanan görevlere geçmenin üçüncü gerekçesi oldu.

Memory deneyi için de bir tahmin çıkardım: memory kolunda read_file / search_code çağrılarının azalması, bir doğruluk ölçümüyle eşleşene kadar verimlilik değil, uyarı işaretidir.

6“Agent’ın ilk sorunu kötü düzenleme olacak”

Agent’a yazma yetenekleri ekledim: bir dosyada birebir metin değiştiren edit_file, testleri koşup başarısız test isimlerini döndüren run_tests, salt-okunur git komutları, allowlist’li bir shell. Görev tipi de değişti. tomlkit’in geçmişinde hem kaynağı hem testi değiştiren commit’lerden seçip kaynak değişikliğini geri alıyorum, testi bırakıyorum ve agent’a “bu testi geçir” diyorum.

Koşmadan önce edit_file için beş hata modu tahmin ettim: boşluk/girinti uyuşmazlığı, birden fazla eşleşme, var olmayan bir metnin uydurulması ve benzerleri. Sonra üç görevi üçer kez koştum:

görev 3 koşu turn düzenleme
kolay geçti · geçti · geçti 11 / 12 / 18 1 / 1 / 1
orta çöktü · sınır · sınır 23 / 25 / 25 0 / 0 / 0
zor sınır · çöktü · sınır 25 / 22 / 25 0 / 0 / 0

Kolay görevde yol her seferinde aynıydı ve mantıklıydı: testi koş, başarısız testin adını al, testi bul, kaynağı bul, oku, tek düzenleme yap, testi tekrar koş, git diff ile kendi değişikliğini kontrol et.

Diğer altı episode’da ise agent 22–25 turn boyunca okudu, aradı ve bir kez bile düzenleme yapmadı. Tahmin ettiğim beş hata modunun hiçbiri gerçekleşemezdi, çünkü ortada düzenleme yoktu. Sorduğum soru yanlıştı. Asıl soru “doğru mu düzenliyor?” değil, “düzenlemeye karar veriyor mu?” idi. Bu da 4. maddedeki hatanın aynısı: bir sonraki kapıyı ölçmek için tasarım yapıp, ondan önceki kapının var olup olmadığını kontrol etmemek.

İki ucuz açıklamayı birer koşuyla test ettim:

  • Yer mi kalmadı? Turn sınırını 25’ten 40’a çıkardım. Fazladan 15 turn, aynı ara-oku-ara-oku salınımının 15 turn’ü daha oldu. Projenin en pahalı episode’u buydu (366.359 token), ve yine sıfır düzenleme.
  • Bütçesini bilmiyor mu? Her turn’de context’e “turn 7/25” gibi bir satır ekledim. Hatalı fonksiyonu 7. turn’de buldu ve kalan 18 turn’ü aramaya devam ederek geçirdi.

Sonra trace’leri tek tek okudum. Altı başarısızlığın dördünde agent hatalı fonksiyonu bulmuş ve okumuştu, ama düzenlemedi. Biri o fonksiyonu 4. turn’de ekranına aldı ve 21 turn daha aradı. Kalan ikisi aynı görevdi: testin adı “dotted inline table” diyordu, agent 25 turn parser’da kaldı, bug ise Container.append içindeydi.

Görevlere verdiğim zorluk etiketleri de çöktü. Etiketleri kaynak değişikliğinin büyüklüğüne göre vermiştim (4, 9 ve 24 satır). Kolay/kolay-değil sınırında tuttu, orta ile zor arasında hiçbir şey öngörmedi. “Başarısız test adından hatalı fonksiyona kaç adım var?” ölçüsü de yanlış sırayı verdi. Şu an elimdeki hipotez şu: ayıran şey, testin dilinin doğru modülü gösterip göstermediği. Üç görevlik bir gözlem bu; bulgu değil, yanlışlanabilir bir hipotez.

7“Ölçüm aletim güvenilir”

Görevler arasında repo temizlenirken Claude Code bir şey fark etti: Python’un bytecode cache’i (.pyc), kaynak dosyanın güncel olup olmadığını saniye çözünürlüklü değişiklik zamanı ve byte boyutu ile kontrol ediyor. a - b ifadesi aynı saniye içinde a + b yapılırsa ikisi de değişmiyor, eski bytecode koşuyor, ve agent kendi doğru düzeltmesinin işe yaramadığını görüyor. Dışarıdan bakınca bu, “bug’ı düzeltemeyen agent”tan ayırt edilemez.

Mekanizmayı ayrıca ölçtüm:

dar döngüde doğal yarış          → 200 denemenin 198'inde eski bytecode koştu
PYTHONDONTWRITEBYTECODE=1        → HÂLÂ eski bytecode
__pycache__ silindikten sonra    → doğru sonuç

İlk akla gelen düzeltme işe yaramıyor. PYTHONDONTWRITEBYTECODE=1 yalnızca yeni .pyc yazılmasını engelliyor, var olan eski dosyanın okunmasını engellemiyor. Doğru düzeltme, her görevden önce __pycache__ dizinlerini silmek.

Hikâyenin ikinci yarısı da aynı derecede öğretici oldu. İlk tepki “o zaman topladığımız bütün sayılar kirli” idi. Silip yeniden koşmadan önce trace’lerdeki zaman damgalarına baktık: düzenlemeler, repo sıfırlandıktan 29, 41 ve 201 saniye sonra yapılmıştı; kalan altı episode’da hiç düzenleme yoktu. Veri temizdi. Veriyi çöpe atmadan önce de ölçmek gerekiyor.

Açık soru: harness mi, model mi?

İki ucuz açıklama elendi. Geriye iki aday kaldı:

  1. Harness hiçbir adımda karar istemiyor. Döngü agent’ın sonsuza kadar arayıp okumasına izin veriyor. Her tek çağrı kendi içinde makul, ve hiçbir aşama ondan bir hipoteze bağlanmasını istemiyor.
  2. Model bu görevi yapamıyor. Flash-Lite küçük ve ucuz bir model.

İkisi de aynı veriyle uyumlu, ve şu ana kadar koşulan on bir episode’un hepsi aynı modelde koştu. Tek modelle bu ikisini ayırt etmek mümkün değil. Bir sonraki adım bu yüzden tek bir soru soruyor: aynı görev, aynı harness, daha güçlü bir model. Projenin başından beri model katmanını bir arayüzün arkasında tutmamın sebebi tam olarak bu tür bir sorunun gelecek olmasıydı.

İki cevap da işe yarar. Güçlü model de düzenlemezse, “harness’ta karar noktası yoksa model ne olursa olsun karar vermiyor” sonucunu iki modelle ölçmüş olurum. Düzenlerse, deneyin ilk kararı model seçimi olur. Task’ların üçte ikisini hiç çözemeyen bir modelle memory’nin etkisini ölçmek mümkün değil, çünkü ölçülecek bir aralık kalmıyor.

Bariz bir düzeltme de var: system prompt’a “önce hipotez kur, sonra düzenle, sonra test et” yazmak. Muhtemelen işe yarar, ve tam da bu yüzden şimdilik yapmıyorum. O zaman ölçtüğüm şey agent değil prompt olur. Bu ayrı bir karar ve ayrı bir deney kolu olacak.

Sırada ne var?

Bu adımdan sonra agent’a memory ve Project Historian dediğim bir bileşen ekleyeceğim. Historian, projede alınan kararları ve geçmişi görevler arasında taşıyan bir katman. Bu fikrin önceden yapılmış benzer çalışmaları var; kendi katkım fikrin kendisi değil, onu çalışan bir agent üzerinde ölçmek. Sonra aynı görevlerde memory’siz ve memory’li agent’ı karşılaştıracağım.

  1. ve 4. maddedeki hataları, Historian’ı kendi çalışma şeklime elle uygularken yaşadım: karar yazılıydı ama zorlanmıyordu; ders kayıtlıydı ama getirilmiyordu. Agent’a eklenecek Historian’ın ölçmesi gereken şey de tam olarak bu iki kapı.

Bu yazıdaki bulguların bir kısmı zaten o deneyin girdisi oldu. Örneğin “bu repoda test adları bug’ın yerini yanlış gösteriyor” bilgisi, tam olarak bir memory sisteminin taşıması gereken türden bir proje bilgisi. Yukarıdaki üç kapılık taksonomi de o deneyin nasıl değerlendirileceğini belirliyor.

Şimdiye kadar öğrendiklerim

  • Sistem davranışı hakkındaki makul öngörüler bu projede sık sık yanlış çıktı. Bir oran veremem. Yanlışlar bedel ödettiği için kaydedildi, doğrular çoğu zaman yazılmadı, yani elimdeki örneklem temiz değil. Ama yön net: tek güvenilir filtre, ölçümü koşmak.
  • Agent, modelin belirsizliğini büyütüyor. Tek koşuluk bir karşılaştırma bir şey söylemez.
  • Projenin en değerli kodu agent değil, trace. Yukarıdaki derslerin neredeyse hepsi, “ne oldu?” sorusuna tahminle değil kayıtla cevap verebildiğim için mümkün oldu.
  • Guardrail’ler koşudan önce kurulmalı, sonra değil. Ve kurulduğunu değil, zorlandığını kontrol etmek gerekiyor.
  • Memory’nin zor kısmı saklamak değil, getirmek ve uygulamak. Bunu önce kendi çalışma şeklimde öğrendim.

Proje devam ediyor. Harness mi model mi sorusunun cevabı geldiğinde bu yazının son bölümünü güncelleyeceğim.

KaynaklarReferences

  1. SWE-agent ekibiteam. mini-swe-agent. GitHub. github.com/SWE-agent/mini-swe-agent
  2. Yang, J., Jimenez, C. E., Wettig, A., Lieret, K., Yao, S., Narasimhan, K., & Press, O. (2024). SWE-agent: Agent-computer interfaces enable automated software engineering. Advances in Neural Information Processing Systems 37 (NeurIPS 2024). arXiv:2405.15793
  3. python-poetry. tomlkit. GitHub. github.com/python-poetry/tomlkit
  4. Bu deneyin deposu, trace’leri ve learning log’u. Yayınlandığında bağlantı eklenecek.This experiment’s repository, traces and learning log. Link added once public.

AtıfCite this

Ertekin, İ. M. (2026). Varsayım yerine ölçüm: Küçük bir coding agent yazarken yanlış çıkan yedi varsayım (Ara not, sürüm 0.1). https://imertekin.com/yazilar/varsayim-yerine-olcum

@misc{ertekin2026olcum,
  author       = {Ertekin, {\.I}lkay Murat},
  title        = {Varsay{\i}m Yerine {\"O}l{\c{c}}{\"u}m: K{\"u}{\c{c}}{\"u}k Bir Coding Agent Yazarken Yanl{\i}{\c{s}} {\c{C}}{\i}kan Yedi Varsay{\i}m},
  year         = {2026},
  howpublished = {\url{https://imertekin.com/yazilar/varsayim-yerine-olcum}},
  note         = {Ara not, devam eden {\c{c}}al{\i}{\c{s}}ma, s{\"u}r{\"u}m 0.1}
}

Değişiklik kaydıChangelog

  1. v0.1 · İlk ara not yayındav0.1 · First interim note published
  2. Bekleniyor · “Harness mi, model mi?” sonucuPending · “Harness or model?” result

İlgiliRelated