← Notlara dön

Her Şeyi Trace Ettim. Bu Langfuse'u Daha Az Kullanışlı Yaptı.

Observability'nin amacı her fonksiyonu kaydetmek değil. Bir şeyler ters gittiğinde sistemi anlamayı kolaylaştırmak.

CairnOps’taki ilk observability versiyonum fazla heveslıydı.

Bir fonksiyon önemli görünüyorsa trace ediyordum. Sonra çağırdığı helper’ı trace ediyordum. Sonra o helper’ın altındaki helper’ı.

Sonuç teknik olarak detaylı, pratikte can sıkıcıydı.

Bir retrieval işlemi şöyle bir şeye dönüşebiliyordu:

retrieve
├── build_query
├── normalize_query
├── search
├── join_chunks
└── clean_sources

Hiçbir şey eksik değildi. Yine de dashboard’u her açtığımda gerçek iş akışını zihnimde yeniden kurmak zorunda kalıyordum.

İşte o zaman “daha fazla span”ı “daha iyi observability” olarak görmeyi bıraktım.

Bunun yerine hata sınırlarını trace etmeye başladım

Şu an kullandığım kural basit: bir işlem, anlamlı derecede farklı bir sebeple başarısız olabiliyorsa ya da performansı anlamlı derecede farklı bir karaktere sahipse kendi span’ini hak ediyor.

Hava durumu çekme işlemi iyi bir örnek.

CairnOps’ta bir hava durumu isteği, hava durumu sağlayıcısı çağrılmadan bile başarısız olabiliyor çünkü önce konumun geocode edilmesi gerekiyor.

Bu yüzden şu faydalı:

weather_fetch
└── geocode

geocode başarısız olursa, önce uygulama loglarını okumadan nereye bakacağımı biliyorum.

Sadece bir query string’i formatlayan bir helper bana böyle bir teşhis değeri sunmuyor. O bir implementasyon detayı olarak kalıyor.

LLM çağrıları zaten gözlemlenebilirdi

Neredeyse yaptığım başka bir hata, her model çağrısını manuel olarak sarmalamaktı.

LangChain’in callback entegrasyonu zaten prompt, output, latency, token, model bilgisi ve mevcutsa maliyet içeren generation observation’ları oluşturuyordu.

Aynı çağrının etrafına başka bir manuel span eklemek, yeni bir cevap eklemeden yeni bir katman eklemek olurdu.

Bana yardımcı olan ayrım şuydu:

Trace      -> tek bir kullanıcı isteği
Span       -> tek bir anlamlı işlem
Generation -> tek bir model çağrısı

Bu rolleri ayrı tuttuktan sonra trace ağacı çok daha okunabilir hale geldi.

Karmaşık node’lar hâlâ çocuklara sahip olabilir

Nested span’lerden tamamen kaçınmıyorum.

RouteAgent birkaç farklı türde iş yapıyor: bir GPX dosyası okuyabiliyor, bir topografya servisiyle rota verisini zenginleştirebiliyor, sonra sonucu yorumlaması için LLM’e sorabiliyor.

Bunların farklı hata modları ve farklı latency profilleri var, bu yüzden bu yapı karmaşıklığını hak ediyor:

route_assess
├── gpx_load
├── topo_enrich
└── LLM generation

Rota node’u dört saniye sürerse, bu dört saniyenin dosya I/O’sundan mı, harici servisten mi yoksa modelden mi geldiğini hemen görebiliyorum.

Bir trace’ten istediğim şey bu.

İsimlendirme de önemli

Aslında aynı işlem olan branch’ler için ayrı işlem isimleri oluşturmayı da bıraktım.

Örneğin standart ve yüksek riskli plan üretimi farklı kod yolları kullanıyordu. Onları şöyle isimlendirebilirdim:

plan_write_standard
plan_write_high_risk

Bunun yerine tek bir işlem ismi tuttum ve ayrımı metadata’ya taşıdım:

plan_write
risk_level=HIGH

Bu, toplam latency ve token analizini çok daha temiz hale getiriyor. Aynı iş operasyonunu birden fazla yapay kategoriye bölmeden yüksek riskli planları hâlâ filtreleyebiliyorum.

Şu anki trace’lerim ilk versiyondan daha az bilgi içeriyor.

Ama çok daha kullanışlılar.

Artık Langfuse’u açtığımda sistemin şeklini ve nerede kırılabileceğini görmek istiyorum—çalışan her Python fonksiyonunun call graph’ını değil.