Bir LLM'in siyasi pozisyon halüsine ettiği hafta — ve bir gazetecinin az kalsın bunu alıntılayacağı
Otomatik üretilen bir TBMM özeti, oylanmamış bir yasada bir üyenin oy verdiğini söyledi. Gazeteci haberi 24 saat bekletti. Sonra 'hayır' diyebilen bir doğrulama katmanı inşa edildi: dört gün.
Ekim sonunda, bir Çarşamba. NLP Parliament projesini takip eden bir gazeteci bana ekran görüntüsü gönderdi. Tek bir TBMM oturumu için ürettiğim otomatik özet, küçük bir muhalefet partisinden bir milletvekilinin tartışmalı bir yasaya lehte oy verdiğini söylüyordu. Cümle temiz, kendinden emindi. Ama oylama olmamıştı. Yasa o oturumda gündeme bile gelmemişti. Milletvekili konuşma yapmamıştı. Gazeteci, modelimden alıntı yapan bir haberi yayına almak üzereydi. 24 saat beklemesini istedim.
Bekledi. Haber o yanlış cümleyle yayınlanmadı. Bu yazı, ondan sonraki dört günü anlatıyor.
Aktörleri anonimleştireceğim. Gazeteci, "gazeteci." Milletvekili, "küçük bir partiden bir muhalefet üyesi." Yasa, "tartışmalı bir kamu ihale yasası." Bu detaylar teknik hikâye için önemsiz. Ama yanlış milletvekili veya yanlış parti olsaydınız, çok önemli olurdu.
Özetleyici aslında neydi
TBMM oturum özetleyici bir tutanağı alıyor, önceki yazıda anlattığım konuşmacı atfı pipeline'ından geçiriyor ve her konuşmacı için iki şey üretiyordu:
- O konuşmacının değindiği konuların kısa listesi.
- Her konuda aldığı pozisyonların kısa listesi.
İçeride önce extractive bir geçiş vardı (cümle sıralama), sonra generative bir geçiş (Claude'un öne çıkanları temiz prozaya yeniden yazması). Beni ısıran kısım, ikincisiydi.
Pipeline kayan bir bağlam penceresi kullanıyordu, çünkü TBMM oturumları uzun. Chunk'lar arasında yaklaşık 1.500 token örtüşme bırakmıştım. Amaç, konuşması chunk sınırını aşan bir milletvekilinin yine de tam bağlama sahip olmasıydı. Beyaz tahtada mantıklı geldi. Olanların kök nedeni de tam olarak buydu.
Halüsinasyon nasıl gerçekleşti
Peş peşe gelen iki oturum vardı. Bunlara A ve B diyelim.
- Oturum A: ihale yasası üzerine asıl tartışma. Muhalefet milletvekili, yasanın sözleşme maddeleri üzerine uzun ve şüpheci bir konuşma yaptı. Nasıl oy kullanacağını söylemedi, çünkü o gün oylama yoktu.
- Oturum B: farklı bir yasa için planlanmış bir oylama ve ihale diline değen prosedürel bir oylama.
Üretim batch'i oturumları haftaya göre grupladığı için, her iki oturum da aynı chunked context'in içine düştü.
1.500 token'lık örtüşme penceresi iki spesifik fragmanı kapsıyordu:
- Oturum A'dan milletvekilinin adı, hemen ardından
kamu ihale yasasıkonusu. - Oturum B'den ilgisiz bir konuşmacının ihale diline ilişkin bir oylama sonucundan söz etmesi.
LLM deseni tamamladı. Şunu üretti:
[Muhalefet partisinden] milletvekili, sözleşme maddelerine yapılan reformları gerekçe göstererek ihale yasasına lehte oy kullandı.
O cümlenin öznesi var, fiili var, nesnesi var, gerekçesi var, tonu da var. Ve hiçbir oturumda en ufak bir çıpası yok. Milletvekili "oy vereceğim" demedi. O yasada o gün oylama olmadı. "Reformlar" sonradan gazetecinin kullandığı kelimeydi, milletvekilinin değil.
Doğru görünüyordu. Sorun da buydu.
Bu hata kategorisi neden tehlikeli
Üç şey bu hatayı tehlikeli kılıyordu.
Birincisi, kendinden emin ton. LLM "emin değilim" ya da "muhtemelen" demedi. "Lehte oy kullandı" dedi. Üretken modeller akıcılık için ayarlanır, epistemik alçakgönüllülük için değil. Eksik fiili, eğitim dağılımındaki en olası seçenekle doldurur. Parlamento metni için "oy verdi" gayet olası.
İkincisi, yapısal alıntı. Özetleyici, ürettiği her cümleye bir source_session_id iliştiriyordu. Bu da yalanı yanlış oturuma izlenebilir kılıyordu: insan spot-check yapsa Oturum A'ya iner, ihale konuşmasını görür ve başını sallardı. Evet, bu milletvekili ihale konusuna değinmiş. Oylama iddiası kontrolden temizce sıyrılır.
Üçüncüsü, QA'da örnekleme önyargısı. Özetleri rastgele cümleler örnekleyip etrafındaki tutanağı okuyarak kontrol ediyorduk. Kontrol her zaman "konu doğru mu?" oluyordu. Hiçbir zaman "fiil gerçekten oldu mu?" değildi.
Gazetecinin ekran görüntüsü, birinin bir fiili gerçekten doğrulamaya çalıştığı ilk seferdi. O da sadece yayınlamayı düşündüğü için yaptı.
Bunu yapan naif prompt
Halüsinasyonu üreten prompt aşağıda. Makul göründüğü için saklıyorum.
def summarise_speaker(name, party, chunks):
prompt = (
"Aşağıda bir parlamento oturumundan tek bir milletvekiline ait "
"tutanak parçaları verilmiştir. Bu milletvekilinin söylediklerini, "
"aldığı pozisyonlar da dahil, 2-4 cümleyle özetleyin.\n\n"
f"MİLLETVEKİLİ: {name} ({party})\n\n"
f"PARÇALAR:\n{join_chunks(chunks)}\n"
)
return llm.generate(prompt, temperature=0.2, max_tokens=200)Bug, prompt'ta değil. Bug, LLM'in kendini parçalara bağlı tutacağı varsayımında. Tutmaz. Model, bir milletvekilinin bir yasa hakkında konuştuğu ve sonra oy verdiği milyonlarca haber metniyle eğitilmiştir. Başka bir şey araya girmedikçe, desen tamamlama o boşluğu kapatır.
İnşa ettiğim şey: iddia düzeyinde doğrulama
Dört gün. Aşağı yukarı.
Temel fikir basit: LLM çıktısı yapısal iddialara parse edilir ve her iddia OCR'a karşı yeniden doğrulanır. Bir iddia, kaynakta spesifik bir cümle aralığına bağlanamıyorsa düşürülür. Yumuşatılmaz, düşürülür.
Birinci adım: prompt'u serbest proza yerine yapısal iddialar üretecek şekilde değiştir.
CLAIM_SCHEMA = {
"type": "object",
"properties": {
"claims": {
"type": "array",
"items": {
"type": "object",
"properties": {
"topic": {"type": "string"},
"verb": {"enum": [
"addressed", "criticised", "supported",
"questioned", "voted_for", "voted_against",
"abstained"
]},
"object": {"type": "string"},
"evidence_span":{"type": "string"}
},
"required": ["topic", "verb", "object", "evidence_span"]
}
}
}
}Anahtar, evidence_span. Model, iddiayı destekleyen aralığı tutanaktan birebir ya da birebire yakın alıntılamak zorunda. Aralık yoksa iddia üretilemez.
İkinci adım: her aralığı OCR'a karşı yeniden çıpala.
def verify_span(evidence_span, transcript, speaker_name, threshold=0.85):
# Aramayı gerçekten bu konuşmacıya atfedilen satırlara kısıtla.
speaker_lines = [l for l in transcript if l.speaker == speaker_name]
if not speaker_lines:
return None
# Bulanık eşleştirme. Karakter başına düzenleme mesafesi, uzunlukla normalize.
best = max(speaker_lines, key=lambda l: fuzz.partial_ratio(l.text, evidence_span))
score = fuzz.partial_ratio(best.text, evidence_span) / 100
return best if score >= threshold else Noneverify_span None döndürürse, iddia düşürülür. Notlanmaz, bayraklanmaz — düşürülür. Özetin, modelin istediğinden kısa olmasına izin verilir. Disiplin tam olarak budur.
Üçüncü adım: fiile özel kapılar.
ACT_VERBS = {"voted_for", "voted_against", "abstained"}
def gate_act_verb(claim, vote_record_api):
if claim["verb"] not in ACT_VERBS:
return True
# Oylama fiilleri, resmi oylama kaydında açık bir satır gerektirir.
row = vote_record_api.lookup(
session=claim["session_id"],
topic=claim["object"],
member=claim["speaker_id"],
)
if row is None:
return False
# Kayıtlı oy, LLM'in iddia ettiği fiille uyuşmalı.
return row["vote"] == claim["verb"]Halüsinasyonu yakalayacak olan bu kapıydı. Vote record API, "Oturum A'da ihale yasası oylaması" sorgusuna None dönerdi, çünkü öyle bir oylama yoktu. İddia düşerdi, özet "ihale yasasına değindi" derdi ve gazetecinin gönderecek bir ekran görüntüsü olmazdı.
Sonuçlar
Doğrulama katmanını geri katalog üzerinde çalıştırdım. İlk tarama rahatsız ediciydi.
- Orijinal iddiaların %17'si düşürüldü, çünkü
evidence_span'leri eşik üzerinde çıpalanamadı. - Bunların yaklaşık %6'sı (tüm iddiaların kabaca %1'i) kötü türdendi — gerçekleşmemiş spesifik bir fiil iddia eden iddialar. Halüsinasyon pozisyonlar, halüsinasyon oylar, halüsinasyon "katılıyordu"lar.
- Kalan %11 yumuşak düşüştü — evidence span'i mevcut olan, ama LLM'in çok agresif yeniden ifade ettiği iddialar. Bilgi gerçekti; ifade çıpalanamadı. Eşiği eylem dışı fiiller için sonradan 0.78'e gevşettik ve çoğunu geri kazandık.
- Kapı tarafından düşürülen oy-fiili iddiaları: tüm oy iddialarının %4.2'si. Her biri yanlış pozitifti.
Düzeltme neden sistem düzeyinde olmak zorunda
Bir LLM halüsinasyonu keşfettiğinizde ilk dürtü, prompt'u düzeltmektir. "Desteklenmeyen iddialar yapma" ekle. "Sadece parçalardan gerçekleri belirt" ekle. İkisini de denedim. Halüsinasyon oranını belki %30 düşürürler. Ortadan kaldırmazlar. Kaldıramazlar.
Model eğitildiği şeyi yapıyor: olası dizileri tamamlamak. Olasılık, gerçek değildir. Düzeltme modelin dışında, bir iddiayı yayınlamayı reddedebilen bir katmanda yaşamak zorunda. Ve o katmanın "hayır" demeye istekli olması gerekir.
O cümle aslında tüm yazının özeti: katmanın hayır demeye istekli olması gerekir.
Bir konu hakkında hiçbir şey söylemeyen bir özet kurtarılabilir. Bir konu hakkında yanlış şeyi, kendinden emin bir fiil ve bir alıntıyla söyleyen özet, bir kamu kayıt sorunudur. En kötü durum "bir pozisyonu kaçırdık" değildir. En kötü durum "bir tane uydurduk"tur.
Sorumluluk üzerine bir not
Bunu dikkatli söylemek istiyorum, çünkü AI yazısının vaaza dönüştüğü kısım tam burası. Vaaz vermemek için epey uğraşıyorum.
Parlamento kayıtları, gazetecilerin, araştırmacıların, öğrencilerin ve vatandaşların seçilmiş yetkililer hakkında iddialarda bulunmak için kullandığı belgelerdir. Üretken bir model, bir haber metnine yapıştırılan akıcı bir cümle üretirse, fiilen kamu kaydına katkıda bulunmuştur. Model hesap verebilir değildir. Sistemi inşa eden kişi verebilir.
Bu yüzden geri döndüm, pipeline'ın her parçasını tek bir soruyla okudum: bu yalan nereye sığabilir, ve kim yakalardı? Cevabın "hiç kimse" olduğu her yere, yakalayabilen bir katman ekledim.
Bunun sistemi güvenli yaptığını düşünmüyorum. Daha az güvensiz yaptığını düşünüyorum. Aralarında bir fark var, ve o fark işin kendisi.
İki ay sonra gazeteci bana ikinci bir ekran görüntüsü gönderdi. Modelimden çıkan haber bu kez doğruydu. Doğrulamak için altı saat harcamıştı. Ona, ona o altı saati kazandıracak bir şeyi dört günde inşa ettiğimi söyledim. Bir an sustu, sonra şöyle yazdı: "Evet ama yine de güvenmezdim." Doğru cevap o.
// madem buradasın
- 12 dk okuma
Gürültülü OCR üzerinde konuşmacı atfı: akşam akşam tutulmuş bir defter
Bir regex'in Meclis Başkanı'nın sessizlik ricasını on iki kez yanlış kişiye nasıl yapıştırdığı, ve 850 oturumda doğruluk oranını 95.6%'dan 99.1%'e taşıyan beş akşamın hikâyesi.
nlpcivic-techocrproduction - 7 dk okuma
Agentic AI guardrail'leri: while(true) döngüsünü token bütçeni yakmasından durdurmak
Vibes-döngüsünü production'da gerçekten çalıştırabileceğin bir şeye çeviren dört guardrail: bütçe zarfı, retry eğrileri, break koşulları ve gerçek bir fallback zinciri.
agentic-aiproductionengineering-lessons