İstek başarılı oldu. Yanıt geçerli bir JSON idi. SDK sessiz kaldı. Yine de uygulama çöktü.

Bu, bir LLM sağlayıcısı değişikliğini yapısal bir kumar yerine bir yapılandırma değişikliği gibi ele aldığınızda neler yaşandığının hikayesidir. Yeni bir temel URL (base URL) yapıştırıyor, API anahtarını değiştiriyor ve dokümantasyon OpenAI uyumlu bir uç nokta (endpoint) vaat ettiği için istek gövdesini (request body) aynı tutuyorsunuz. Temel bir "hello world" istemi (prompt) için işe yarar. Kutlama yaparsınız. Sonra gerçek trafik gelir ve dikişler patlar.

Protokol Uyumluluğu İllüzyonu

HTTP katmanındaki uyumluluk yüzeyseldir. 200 durum kodu ve bir JSON gövdesi, sunucunun mesajınızı kabul ettiği anlamına gelir. Bu, sunucunun bir öncekiyle aynı şekilde düşündüğü anlamına gelmez. OpenAI uyumlu uç noktalar bir istek yapısını paylaşır ancak davranışsal bir sözleşmeyi paylaşmazlar. İki sağlayıcı, özdeş veri yüklerini (payload) alabilir ve ince, yıkıcı şekillerde farklılaşan yanıtlar döndürebilir.

Kodunuz varsayımlarda bulunur. message.content kısmının her zaman olduğu gibi bir dize (string) olduğunu varsayarsınız. Bir araç çağrısının (tool call) temiz, ayrıştırılabilir bir JSON ile geldiğini varsayarsınız. finish_reason değerinin düşündüğünüz şeyi işaret ettiğini varsayarsınız. Bu varsayımlar, ölümcül olana kadar görünmezdir.

Her şeyi başlatan çökmeyi ele alın:

const text = response.choices[0].message.content.trim();

Bu satır masum görünüyor. Haftalarca çalıştı. Sonra yeni sağlayıcı bir araç çağrısı döndürdü. O anda, message.content boş bir dize değildi. null idi. Asıl veri yükü message.tool_calls içinde yer alıyordu ancak ayrıştırıcı (parser) çoktan ilerlemiş ve hiçbir şey üzerinde .trim() metodunu çağırmıştı. API hata fırlatmadı. Ağ katmanı şikayet etmedi. İsteği kendi ayrıştırıcınız öldürdü.

Sağlayıcıların Sessizce Ayrıştığı Noktalar

Farklılıklar değişiklik günlüklerinde (changelogs) kendilerini duyurmazlar. Yanıt nesnesinin kenarlarında, uç durumlar (edge cases) için beklerler.

Araç çağrısı (tool-call) biçimlendirmesi. Bir sağlayıcı araç argümanlarını önceden doğrulanmış bir JSON nesnesi olarak gönderir. Bir diğeri bunları bir alan içindeki kaçış karakterli (escaped) bir dize olarak gönderir. Üçüncüsü, uzun bir araç çağrısını birden fazla akış (streaming) deltasına bölebilir ve yapının geçerli olup olmadığını bile görmeden önce parçaları (chunks) tamponlamaya (buffer) zorlayabilir. Eğer uygulamanız tek bir ayrıştırılabilir veri bloğu bekliyorsa, boğulur.

Bitiş nedenleri (finish reasons). OpenAI "stop", "length", "tool_calls" ve "content_filter" gibi belirli dize değerleri kullanır. Uyumlu bir sağlayıcı, model token sınırına ulaştığında "end_turn" döndürebilir veya alanı tamamen atlayabilir. Eğer yeniden deneme (retry) veya yedekleme (fallback) mantığınız kesilmeyi tespit etmek için "length" değerini bekliyorsa, kullanıcı yarım kalmış bir yanıt görürken sisteminiz boşta bekleyecektir.

Kullanım alanları (usage fields). Bazı sağlayıcılar, gecikmeyi (latency) milisaniyeler düzeyinde azaltmak için akış yanıtlarından token sayılarını çıkarır. Diğerleri kullanımı yalnızca son parçaya ekler veya akış olmayan (non-streaming) çağrılarda tamamen atlar. Eğer müşterilerden token başına ücret alıyorsanız ve muhasebe kodunuz her yanıt nesnesinde usage.total_tokens alanının bulunmasını bekliyorsa, faturalandırma hattınız sessizce sıfırları kaydedecektir.

Akış (streaming) davranışı. Sunucu tarafından gönderilen olayların (server-sent events) standart olması gerekir, ancak sağlayıcılar tamponları (buffers) farklı frekanslarda boşaltır. Olay sınırları (event boundaries) değişiklik gösterir. Bir sağlayıcı akışı bir [DONE] sinyali ile sonlandırır. Bir diğeri ise hiçbir işaretçi (sentinel) kullanmadan bağlantıyı temiz bir şekilde keser. Eğer istemciniz belirli bir kapatma işaretçisini beklerken bloklanıyorsa, asılı kalır.

Hatalar ve zaman aşımları (timeouts). Bir hız sınırı (rate limit), bir sağlayıcıdan retry-after başlığı ile birlikte 429 olarak gelebilirken, bir diğerinden belirsiz bir 502 olarak gelebilir. Bazı sağlayıcılar isteği kabul eder ve ardından bir ağ zaman aşımından önce iki dakika boyunca sessizliğe bürünür. OpenAI SDK'sı bunları sihirli bir şekilde günlüklerinizin (logs) beklediği istisna türlerine (exception types) dönüştürmeyecektir.

Öngörülemeyen Yapılar İçin Savunmacı Ayrıştırma

Çözüm şemaya güvenmek değildir. Çözüm, her yanıtı bir şüpheli olarak ele almaktır.

content kısmının bir dize olduğunu varsaymayın. Ona dokunmadan önce kontrol edin.

const content = response.choices?.[0]?.message?.content;
const text = typeof content === "string" ? content.trim() : "";

Araç argümanlarının geçerli bir JSON olduğunu varsaymayın. Model bir eylem önerir. Kodunuz, bu önerinin yürütülmek için yeterince güvenli olup olmadığına karar vermelidir. Her araç argümanı ayrıştırma işlemini bir try-catch bloğuna alın. Eğer JSON.parse hata fırlatırsa, araç çağrısını hatalı bir çöp veri olarak değerlendirin ve bir hata işleyicisine (failure handler) yönlendirin. Halüsinasyon görmüş bir parantez veya eksik bir tırnak işareti asla işlenmemiş bir istisna (unhandled exception) olarak yukarı fırlamamalıdır.

Eğer tool_calls mevcutsa ancak content eksikse, uygulamanız bir durum geçişini (state transition) fark etmelidir. Kullanıcı bir sohbet yanıtı almadı; sistem bir iş emri aldı. Bunlar iki farklı yoldur ve yönlendiriciniz (router), dize manipülasyonuna çalışmadan önce aradaki farkı bilmelidir.

Dağıtım Öncesi Davranışsal Testler

Uç noktaya (endpoint) "hi" mesajı ile ping atmak ağın çalıştığını kanıtlar. Uygulamanız hakkında hiçbir şey kanıtlamaz.

Canlı trafiği yönlendirmeden önce, yeni sağlayıcıya karşı hedeflenmiş bir davranışsal test paketi çalıştırın:

  • Normal metin yanıtı. content alanının mevcut olduğunu, bir dize (string) olduğunu ve tür dönüşüm (casting) hatası vermeden temizleme (sanitization) hattınızdan geçebildiğini doğrulayın.
  • Zorunlu araç çağrısı (tool call). tool_choice değerini required olarak ayarlayın. Sağlayıcının buna uyup uymadığını teyit edin ve content alanının null, boş bir dize veya eksik bir anahtar olarak gelip gelmediğini kontrol edin. Bu durumların her birinin kendi işleyicisine (handler) ihtiyacı vardır.
  • Hatalı araç argümanları. Modelin araç argümanları içinde bozuk JSON döndürdüğü senaryolar enjekte edin. Ayrıştırıcınızın (parser), işleyiciyi (worker) çökertmek yerine bu argümanları nazikçe reddettiğinden emin olun.
  • Token sınırına yakın yanıt. Bağlam penceresini (context window) zorlayın. finish_reason değerini kontrol edin. Kırpma (truncation) gerçekleştiğinde sağlayıcı beklenmedik bir şey döndürürse, özetleme veya yeniden deneme mantığınızın nasıl tepki vereceğini bilmesi gerekir.

Bunlar birim testler (unit tests) değil, entegrasyon testleridir. Kodunuz ile sağlayıcının "kişiliği" arasındaki gerçek ilişkiyi test ederler. Migrasyonu tamamlandı saymadan önce bunları geçin.

Dahili Bir Sözleşme (Contract) Oluşturun

Sağlayıcı farklılıkları ağ sınırınızda durmalıdır. İş mantığına (business logic) sızmalarına izin vermeyin.

Ham SDK yanıtını tüketen ve uygulamanızın gerçekten sahip olduğu bir nesne (object) üreten bir normalizasyon katmanı oluşturun. Sağlayıcıya özgü tuhaflıkları kararlı bir dahili formata eşleyin (map). Eğer Sağlayıcı A araç argümanlarını dize olarak, Sağlayıcı B ise nesne olarak döndürüyorsa, eşleyiciniz (mapper) her ikisini de kendi ToolRequest yapınıza düzleştirir (flatten). Eğer kullanım (usage) bilgisi eksikse, eşleyiciniz bunu ya tahmin eder ya da boşluğu işaretler; ancak asla undefined değerinin maliyet takip modüllerinize sızmasına izin vermez.

Eğer finish_reason standart dışıysa, bunu kendi son durum enum'ınıza çevirin: COMPLETE, TRUNCATED, TOOL_CALL, FILTERED. Uygulamanız ne yapacağına üçüncü taraf bir sunucudan gelen ham dizeleri koklayarak değil, bu temiz soyutlamalara dayanarak karar vermelidir.

Bu katman, sağlayıcı değişikliklerini bir "köstebek vurma" (whack-a-mole) oyunundan tek dosyadan ibaret bir değişikliğe dönüştürür. Eşleyiciyi yeniden yazar, davranışsal testleri çalıştırır ve yolunuza devam edersiniz. Uygulamanız ise bozulmadan kalır.

Bir Yapılandırma Ayarı Değil, Bir Bağımlılık Güncellemesi

LLM sağlayıcılarını değiştirmek, CDN uç noktalarını değiştirmek gibi değildir. Veritabanınızı PostgreSQL'den MySQL'e taşımaya daha yakındır. Aynı bağlantı dizesinin (connection string) özdeş sorgu davranışı anlamına geldiğini asla varsaymazsınız. Kilitleme semantiğini (locking semantics), taşıma yollarını ve indeksleme tuhaflıklarını test edersiniz. LLM'ler de aynı saygıyı hak eder. Onlar, standart API'lar kılığına girmiş olasılıksal sistemlerdir ve yanıtları; biçimlendirme, kırpma ve kontrol akışı hakkında, tek bir ağ hatası bile vermeden uygulamanızı yerle bir edebilecek varsayımlar taşır.

Hata asla bağlantıda değildi. Hata, uyumluluğun aynılık anlamına geldiği varsayımındaydı. Öyle değildir. Yapıyı doğrulayın. Uç durumları (edges) test edin. Sözleşmeye (contract) sahip çıkın.


Kaynak: Hata Sadece LLM Sağlayıcısını Değiştirdikten Sonra Gerçekleşti

Topluluk: GyaanSetu AI on Telegram