SSD Nodes Learn 🎉 VPS $5.50/aydan başlayan
Rehberler Matt ConnorYazan Matt Connor · Güncellendi 2026-08-13

Ajan belleği neden güncelliğini yitirir ve nasıl

Ajan belleklerinin zamanla güncelliğini yitirmesi sessiz hatalara yol açar. Verilerinize son kullanma tarihi ekleyin, SQLite veritabanınızı sqlite3 ile düzenli olarak denetleyin.

Ajan belleği neden güncelliğini yitirir

Ajan belleği, bir bilginin bir kez yazılıp bir daha asla kontrol edilmemesi nedeniyle güncelliğini yitirir. Depolama katmanı bu bilgiyi sürekli döndürür, geri getirme katmanı ise bilgiyi herhangi bir tarih etiketi olmaksızın düz metin olarak isteme (prompt) yerleştirir; model de bu bilgiyi, yazıldığı günkü güvenle tekrarlar. Hiçbir süreç hata üretmez. Tüm zorluk buradadır: güncelliğini yitirmiş bir bellek, hem model hem de sizin için güncel bir bellek ile tamamen aynı görünür.

Kayıt anında daha iyi yazım yapmak bu sorunu çözmez. Çözüm, geçerlilik süresi olan bilgiler için bir son kullanma tarihi belirlemek ve süresi olmayan bilgiler için bir gözden geçirme rutini oluşturmaktır. Her ikisi de küçük bir veritabanı için olağan bakım işlemleridir ve işin büyük kısmı SQL (structured query language) ile yapılır.

Eskime ve sapma farklı hata türleridir

Eskime, doğal bir bitiş tarihine sahip bir gerçektir. "Bu hafta seyahatteyim." "Staging sunucusu taşıma nedeniyle kapalı." "Bütçe taslağı inceleniyor." Bunlar yazıldıkları anda doğruydu ve yazıldıkları an itibarıyla ömürlerini belirleyebilirsiniz. Eskime çözülebilir bir durumdur. Bir son kullanma tarihi, bazen TTL (time to live) olarak adlandırılan bir süre ekleyin ve bu süre dolduğunda ilgili kaydı silin.

Sapma, bir kez kaydedilen ve bir daha asla kontrol edilmeyen bir gerçektir. "pnpm tercih ediliyor." "Veritabanı Postgres 15." "Dağıtımlar staging dalı üzerinden yapılıyor." Hiçbir saat bunları yanlış kılmaz. Başka bir yerdeki bir karar bunları yanlış kılar ve bellek deponuzun bundan haberi olmaz.

Sapmanın temiz ve otomatik bir çözümü yoktur. Bir depo, hiç gözlemlemediği bir değişikliği tespit edemez; bu nedenle depoyu okuyup üzerinde mantık yürüten bir iş, yalnızca aynı eski metni tekrar okumuş olur. İşe yarayan mekanizma, gerçeği tanımladığı şeyle karşılaştırarak yeniden kontrol etmektir; bu da bir insan veya mevcut durumu okuyabilen bir araca sahip bir temsilci gerektirir.

Bu nedenle plan ikiye ayrılır. Eskiyenleri süresi dolacak şekilde ayarlayın. Sapanları ise gözden geçirin. İkinci sorunu birincisiymiş gibi ele almayın.

Zamana bağlı olgular için bir son kullanma tarihi belirleyin

Her bellek satırı, çoğu veri deposunun sağlamadığı üç sütuna ihtiyaç duyar: olgunun kaynağı, en son ne zaman doğrulandığı ve ne zaman geçerliliğini yitirdiği. Bu, yalnızca sqlite3 kullanarak oluşturabileceğiniz bir depodur; aynı sütunlar halihazırda kullandığınız bir depoya da eklenebilir.

CREATE TABLE memory (
  id            TEXT PRIMARY KEY,
  subject       TEXT NOT NULL,
  fact          TEXT NOT NULL,
  source        TEXT NOT NULL,
  created_at    TEXT NOT NULL DEFAULT (datetime('now')),
  confirmed_at  TEXT NOT NULL DEFAULT (datetime('now')),
  expires_at    TEXT,
  superseded_by TEXT REFERENCES memory(id) ON DELETE CASCADE
);

CREATE INDEX memory_expires ON memory(expires_at);
CREATE INDEX memory_superseded ON memory(superseded_by);

datetime('now'), YYYY-MM-DD HH:MM:SS formatında UTC (eşgüdümlü evrensel zaman) döndürür; bu format metin olarak doğru şekilde sıralanır ve karşılaştırılır, dolayısıyla aşağıdaki her tarih sorusu basit bir WHERE yan tümcesidir. source sütunu isteğe bağlı değildir. Bir mesaja, dosyaya veya komut çıktısına dayandırılamayan bir olgu asla yeniden kontrol edilemez ve yeniden kontrol edilemeyen bir olgu yalnızca silinebilir.

Süresi dolan bir bellek yazma:

INSERT INTO memory (id, subject, fact, source, expires_at)
VALUES ('m_0191', 'availability', 'Away from keyboard, replies are delayed',
        'chat 2026-08-08', datetime('now', '+7 days'));

Veri çekme işlemi asla doğrudan tabloyu okumamalıdır. Süresi dolmuş ve geçersiz kılınmış satırları gizleyen bir görünümü okumalıdır:

CREATE VIEW live_memory AS
SELECT id, subject, fact, source, confirmed_at, expires_at
FROM memory
WHERE superseded_by IS NULL
  AND (expires_at IS NULL OR expires_at > datetime('now'));

Görünüm, işin önemli yarısıdır çünkü atlanan bir temizleme işlemini zararsız hale getirir. Süresi dolmuş bir satır, silme işi çalışsa da çalışmasa da süresi dolduğu anda çekilmemeye başlar. Silme işi bu noktada yalnızca disk kullanımını ve inceleme yükünü kontrol eder, doğruluğu değil.

sqlite3 memory.db "SELECT count(*) FROM memory;" ile aradaki farkı ve live_memory karşısındaki aynı sayımı kontrol edin. Sağlıklı bir depo, birbirine yakın iki sayı gösterir. Büyük bir fark, ölü satırlardan oluşan birikmiş iş yükünüzdür.

Bir bellek silindiğinde eskisinin neden geride kaldığı

Düzeltmeler çiftler halinde gelir. Ajan, npm'den pnpm'e geçtiğinizi öğrenir, yeni bir satır yazar ve eski satırı buna yönlendirir:

UPDATE memory SET superseded_by = 'm_0207' WHERE id = 'm_0140';

Eski satır artık live_memory için görünmezdir ve zincir neyin değiştiğini kaydetmeye devam eder. Şimdi yanlış olduğu ortaya çıktığı için m_0207 öğesini silin. superseded_by üzerindeki ON DELETE CASCADE, eski satır bu ilişkideki alt öğe olduğu için m_0140 öğesini de beraberinde götürmelidir. Genellikle bunu yapmaz, çünkü SQLite siz açmadığınız sürece foreign key kısıtlamalarını görmezden gelir ve varsayılan ayar kapalıdır:

sqlite3 memory.db "PRAGMA foreign_keys;"

Bu, standart bir derlemede 0 çıktısını verir. Foreign key kısıtlamaları kapalıyken DELETE FROM memory WHERE id = 'm_0207'; başarılı olur ve m_0140, artık var olmayan bir id değerini işaret ederek geride kalır. Hiçbir şey sizi uyarmaz. O satır artık yanlış bir nedenle gizlenmiştir ve sarkan işaretçileri NULL değerine sıfırlayan ilk temizlik betiği, "prefers npm" ifadesini doğrudan live_memory içine geri koyar.

Bozuk zincirleri bulun:

sqlite3 memory.db "PRAGMA foreign_key_check;"

foreign_key_check, zorlama kapalı olsa bile ihlalleri rapor eder, bu nedenle mevcut karmaşanız üzerinde çalışır. İhlal başına bir satır yazdırır: tablo, rowid, üst tablo ve başarısız olan foreign key. Boş çıktı, zincirlerin sağlam olduğu anlamına gelir.

Bunu takip eden kural kısadır. PRAGMA foreign_keys = ON; bağlantı başına yapılan bir ayardır, bu nedenle her bağlantının buna ihtiyacı vardır: uygulamanız, temizleme betiğiniz ve içine yazmakta olduğunuz sqlite3 oturumu. Herhangi bir şeyi silen her SQL dosyasının ilk satırı olarak bunu ekleyin.

Anılarınızın gerçekte nerede yaşadığı

Herhangi bir şeyi silmeden önce kaç adet depolama alanınız olduğunu tespit edin. Self-hosted bir bellek servisi, genellikle bellek metnini ve bunun embedding verisini bir vektör veritabanında tutar, değişiklik günlüğünü (change log) ise SQLite içinde saklar. Bunlar farklı yaşam döngülerine sahip farklı dosyalardır ve birbirinden bağımsız olarak hata verebilirler.

mem0 iyi bir örnektir ve aynı yapı başka yerlerde de karşımıza çıkar. Açık kaynak kütüphanesi varsayılan olarak /tmp/qdrant konumunda, mem0 adlı bir koleksiyonda Qdrant vektör deposunu ve MEM0_DIR ortam değişkenini takip eden ~/.mem0/history.db konumunda bir SQLite değişiklik günlüğünü kullanır. history tablosu memory_id, old_memory, new_memory, event, created_at ve is_deleted verilerini tutar.

Sütun listesini tekrar okuyun. SQLite dosyası bir değişiklik günlüğüdür. Belleklerin kendisi Qdrant içindedir; bu nedenle history.db içinden satır silmek, yalnızca bir şeyin değiştiğine dair kaydı kaldırır ve belleğin geri çağrılabilir kalmasına neden olur. Silme işlemleri, her iki yerin de güncellenmesi için kütüphanenin kendi API (uygulama programlama arayüzü) üzerinden yapılmalıdır:

from mem0 import Memory

memory = Memory()
memory.delete(memory_id="mem_123")
memory.delete_all(user_id="alice")

/tmp varsayılanı özel bir uyarıyı hak eder. Ubuntu 24.10 ve sonraki sürümlerde /tmp, bellekte tutulan bir dosya sistemi olan tmpfs'tir; bu nedenle her yeniden başlatmadan sonra boştur ve tüm depo silinmiş olur. Kendi sisteminizi findmnt /tmp ile kontrol edin. tmpfs ifadesini gösteren bir satır, yolu bugün taşımanız gerektiği anlamına gelir:

config = {
    "vector_store": {
        "provider": "qdrant",
        "config": {"collection_name": "mem0", "path": "/srv/agent/qdrant"},
    }
}
memory = Memory.from_config(config)

Aynı soru ne çalıştırırsanız çalıştırın geçerlidir. Yapılandırmayı okuyun ve servisin veri yazdığı her yolu not edin. Kendi VPS'inizde mem0 bellek sunucusu çalıştırma konusu bunun servis tarafını ele alır; ajan belleğini tek bir makinede yerel tutma ise aynı bakım ihtiyaçlarına sahip daha küçük bir depolama yapısını kapsar.

sqlite3 ile depoyu okuma

Eğer yüklü değilse, sudo apt install -y sqlite3 komutu ile CLI (komut satırı arayüzü) aracını kurun. Ardından, disk üzerindeki herhangi bir depo hakkında sorulan soruların çoğunu dört komut yanıtlar.

  • sqlite3 ~/.mem0/history.db ".tables" tabloları listeler. Çıktının boş olması, yanlış dosyayı açtığınız anlamına gelir.
  • sqlite3 ~/.mem0/history.db ".schema history" tam sütun adlarını yazdırır; bu, bir deponun yapısına dair tek güvenilir dokümantasyondur.
  • sqlite3 -cmd ".mode line" ~/.mem0/history.db "SELECT * FROM history ORDER BY created_at DESC LIMIT 5;" en son yapılan beş değişikliği her alan bir satırda olacak şekilde gösterir; bu yöntem, bir sütun uzun bir metin içerdiğinde okunabilirliği korur.
  • sqlite3 ~/.mem0/history.db "SELECT event, count(*) FROM history GROUP BY event;" deponun neler yaptığını ve kütüphanenizin aslında hangi olay adlarını yazdığını gösterir.

Her bellek deposu bir veritabanı değildir. Her oturumun başında okunan basit bir not dosyası, hem hata yapmaya müsaittir hem de hiçbir araç desteğine sahip değildir: son kullanma tarihi sütunu, onaylanmış bir tarih bilgisi veya geçersiz satırları gizleyecek bir görünüm yoktur. Elle yazdığınız her satıra tarih ekleyin ve bunları aylık olarak tekrar gözden geçirin. Claude Code oturumları arasında taşınan bellek, daha küçük bir ölçekte aynı soruna sahiptir.

Süresi dolmayan gerçeklerin gözden geçirilmesi

Drift bir kuyruğa, bir sınıra ve bir alışkanlığa ihtiyaç duyar. Kuyruk, en eski doğrulamalardan oluşur:

SELECT id, subject, fact, source, confirmed_at
FROM live_memory
WHERE confirmed_at < datetime('now', '-90 days')
ORDER BY confirmed_at
LIMIT 20;

Haftada yirmi satır, birinin gerçekten yapabileceği bir gözden geçirmedir. Dört yüz satır ise kimsenin yapmadığı bir gözden geçirmedir ve bu sizi başladığınız noktaya geri döndürür. Her satır için iki sonuç vardır. Satırı source ile tekrar kontrol edin ve damgalayın:

UPDATE memory SET confirmed_at = datetime('now') WHERE id = 'm_0140';

Ya da değiştirin: yeni gerçeği ekleyin, eski satırın superseded_by değerini yeni kimliğe ayarlayın ve zincirin geçmişi tutmasını sağlayın.

İki alışkanlık bu süreci daha düşük maliyetli hale getirir. Depoyu küçük tutun, çünkü sadece büyüyen bir depo gözden geçirmeyi imkansız kılar: bir last_used_at sütunu ekleyin, bir satır gerçekten alındığında bu sütunu güncelleyin ve altı aydır kullanılmayan satırları silinme adayları olarak değerlendirin. Bu işlem, her erişim başına bir yazma maliyeti getirir, bu nedenle aracı çok sık veri çekiyorsa işlemi toplu (batch) hale getirin.

İkinci alışkanlığın hiçbir maliyeti yoktur. Yaş bilgisini isteme (prompt) kısmına ekleyin. Eğer alıcınızın oluşturduğu bellek bloğu her gerçeğin yanında confirmed 2026-05-02 bilgisini taşıyorsa, model bunu doğrudan belirtmek yerine "Mayıs ayı itibarıyla pnpm kullanıyordunuz" şeklinde ifade edebilir. Tarih içermeyen bir gerçek, dil modeli tarafından her seferinde şimdiki zaman olarak okunur.

Budama işlemini bir zamanlamaya bağlayın

Hatırladığınızda çalıştırılan bir budama işlemi, aslında çalıştırılmamış demektir. SQL komutunu /srv/agent/prune.sql içine yerleştirin:

PRAGMA foreign_keys = ON;

DELETE FROM memory
WHERE expires_at IS NOT NULL AND expires_at <= datetime('now');

DELETE FROM memory
WHERE superseded_by IS NOT NULL
  AND created_at < datetime('now', '-180 days');

/etc/systemd/system/memory-prune.service dosyasını kaydedin:

[Unit]
Description=Prune expired agent memories

[Service]
Type=oneshot
User=agent
ExecStart=/usr/bin/sqlite3 /srv/agent/memory.db ".read /srv/agent/prune.sql"

Ve /etc/systemd/system/memory-prune.timer:

[Unit]
Description=Run the agent memory prune daily

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now memory-prune.timer
systemctl list-timers memory-prune.timer

list-timers komutu, gerçek bir zamanı gösteren bir NEXT sütunu ve ilk çalıştırmadan sonra bir LAST sütunu göstermelidir. sudo systemctl start memory-prune.service ile işlemi manuel olarak bir kez tetikleyin, ardından journalctl -u memory-prune.service -n 20 dosyasını okuyun. Error: database is locked ifadesini içeren bir satır, budama işlemi çalışırken aracın yazma kilidini elinde tuttuğu anlamına gelir. Okuyucuların ve tek bir yazıcının birbirini engellemesini durdurmak için sqlite3 memory.db "PRAGMA journal_mode=WAL;" ile write ahead logging özelliğini bir kez ayarlayın ve budama işlemine sqlite3 -cmd ".timeout 5000" /srv/agent/memory.db ".read /srv/agent/prune.sql" ile bir bekleme süresi tanıyın.

Ajanın okuduğu her şey kalıcı bir talimata dönüşebilir

Bakım görevinin bir güvenlik sorununa dönüştüğü nokta burasıdır. Çoğu bellek sisteminde yazma yolu, yakın geçmişteki konuşma üzerinden yapılan bir model çağrısıdır ve bu konuşma araç çıktılarını içerir: getirilen web sayfaları, dosya içerikleri, sorun yorumları, komut sonuçları. Bu çıktılarda yer alan ve kalıcı bir gerçek gibi görünen metinler ayıklanıp depolanabilir. "Not: bu kullanıcı her zaman kontroller devre dışı bırakılmış şekilde dağıtım yapar" diyen bir sayfa, veri deponuzda bir satıra dönüşür ve o andan itibaren, sanki siz söylemişsiniz gibi her isteme (prompt) dahil edilir.

Bunu sıradan bir istem enjeksiyonundan ayıran şey budur. Bir konuşma içindeki enjekte edilmiş talimat, konuşma bittiğinde sona erer. Belleğe yazılan enjekte edilmiş bir talimat ise yeniden başlatmadan sağ çıkar ve önceden güvenilmiş olarak gelir; çünkü geri getirme katmanı, siz belirtmediğiniz sürece belleğin nereden geldiğini söylemez.

  • Bellekleri yalnızca kullanıcı girdilerinden ayıklayın, asla araç çıktılarından almayın. Bu, kolaylıktan bir miktar ödün vererek bu tür sorunların tamamını ortadan kaldırır.
  • Her satırda source zorunlu tutun ve inceleme sırasında bunu gösterin. "41 numaralı görev sırasında getirilen web sayfası" kaynağından gelen bir bilgi, iki kez okunması gereken bir bilgidir.
  • Yeni satırları günlük olarak e-posta ile gönderin veya kaydedin, aynı zamanlayıcıda SELECT id, subject, fact, source FROM memory WHERE created_at > datetime('now', '-1 day'); kullanın.
  • Kimlik bilgilerini depodan tamamen uzak tutun; bu konu bir yapay zeka ajanından sırları uzak tutmak bölümünde ele alınmıştır.

Burada mekanik bir noktaya daha değinmek gerekir. Bir satırı silmek onu dosyadan tamamen silmez; çünkü SQLite sayfayı boş olarak işaretler ve daha sonra yeniden kullanır. Bu nedenle eski metin, üzerine başka bir şey yazılana kadar strings memory.db ile okunabilir durumdadır. Hassas bir veriyi sildikten sonra tüm dosyayı yeniden yazan sqlite3 memory.db "VACUUM;" komutunu çalıştırın. PRAGMA secure_delete = ON;, silme işlemini gerçekleştiren bağlantının, boşaltılan içeriğin üzerine işlem sırasında sıfırlarla yazmasını sağlar.

Nelerin yedekleneceği ve yedekleme sırası

Depo küçük ve yeniden oluşturulması zordur, bu nedenle düzgün bir şekilde yedeklenmelidir. Canlı bir veritabanı dosyasını asla cp ile kopyalamayın; çünkü yazma işlemi sırasında alınan bir kopya açılamayabilir. SQLite içinde yerleşik olarak bulunan snapshot özelliğini kullanın:

sqlite3 /srv/agent/memory.db "VACUUM INTO '/srv/backup/memory-$(date +%F).db'"
sqlite3 /srv/backup/memory-$(date +%F).db "PRAGMA integrity_check;"

integrity_check çıktısını ok ile yazdırmak, bir yedekleme dosyasının kullanılabilir olduğunun tek kanıtıdır. Bunun dışındaki her durum, önceki yedeği saklamanız ve üzerine yazmadan önce sorunu araştırmanız gerektiği anlamına gelir.

Vektör deposunun snapshot'ını aynı iş içerisinde ve aynı anda alın. Eğer iki parça arasında saatler varsa, geri yükleme işlemi yeni bir değişiklik günlüğünü eski bir bellek setiyle karıştırır ve silinen veriler tekrar ortaya çıkar. Her ikisini de tarih damgalı tek bir dizine yazın; böylece yalnızca birlikte geri yüklenebilirler. Bir VPS üzerinde üretim ortamında SQLite çalıştırmak konusu; kilitleme, yedekleme ve uzun süre çalışan bir servisin ihtiyaç duyduğu ayarlar hakkında daha derinlemesine bilgi sunar.

FAQ

Bir aracı belleği, süresi dolmadan önce ne kadar süre kalmalıdır?

Süreyi genel bir varsayılan değerden değil, bilginin kendisinden yola çıkarak belirleyin. Bir seyahat notu veya "bu hafta bu proje üzerinde çalışıyorum" gibi bir not yedi günlük bir ömre sahiptir. Bir ekip kuralı veya kişisel tercih için süre sınırı koymayın; bunun yerine bu bilgiyi inceleme kuyruğuna alın. Bir yazılım sürümü hakkındaki bilginin geçerlilik süresi, o projenin sürüm yayınlama sıklığına yakın olmalıdır. Bilgiyi yazdığınız anda bir raf ömrü belirleyemiyorsanız, bu durum bilginin eskidiğini değil, zamanla değiştiğini gösterir; bu nedenle ona bir confirmed_at tarihi atayın ve süresini doldurmak yerine incelemeye tabi tutun.

Depolanan bir bilginin yanlış olduğunu otomatik olarak tespit edebilir miyim?

Güvenilir bir şekilde hayır. Depo, kendi dışındaki dünyayı göremez; bu nedenle bir bilginin yanlış olmasına neden olan değişikliği fark edemez. Depoyu tekrar okuyan bir iş, yalnızca aynı eski metni tekrar okumuş olur. Otomatikleştirebileceğiniz şey, bilginin öne çıkarılmasıdır: confirmed_at ile sıralama yapın ve en eski satırları bir insanın önüne veya bir depodan, yapılandırma dosyasından ya da izleme uç noktasından güncel durumu okuyabilen bir araca sahip olan bir ajanın önüne getirin. Kuyruğu otomatikleştirmek faydalıdır. Karar verme sürecini otomatikleştirmek ise henüz o aşamada değildir.

Bir belleği sildim ancak geri geldi. Neden?

Genellikle iki farklı depo olması ve sizin sadece birine yazmış olmanızdan kaynaklanır. Bellek metni ve vektör temsili (embedding) genellikle bir vektör veritabanında tutulurken, değişiklik günlüğü bir SQLite dosyasında saklanır. Bu nedenle, SQLite dosyasından satır silmek yalnızca denetim kaydını kaldırır ve belleği erişilebilir durumda bırakır. Her ikisinin de güncellenmesi için silme işlemini kütüphane API'si üzerinden gerçekleştirin. Diğer yaygın neden ise geri yüklemedir; vektör deposu ve SQLite dosyasının farklı zamanlarda yedeklenmiş olması, geri yükleme sırasında diğer tarafın zaten silmiş olduğu satırların geri gelmesine neden olur.

Ajan çalışırken bellek veritabanını manuel olarak düzenlemek güvenli midir?

Okuma işlemleri güvenlidir. Yazma işlemleri yalnızca write ahead logging modunda güvenlidir ve bu modda bile aynı anda yalnızca bir yazıcı işlem yapabilir. Hangi modda olduğunuzu görmek için sqlite3 memory.db "PRAGMA journal_mode;" komutunu çalıştırın; aradığınız yanıt wal değeridir. Eğer Error: database is locked görürseniz, başka bir işlem yazma kilidini elinde tutuyor demektir; bu durumda oturumunuza sqlite3 -cmd ".timeout 5000" ile bir bekleme süresi ekleyin veya önce ajan servisini durdurun. Bir vektör deposunu manuel olarak düzenlemek farklıdır: bunu kütüphaneye bırakın, çünkü vektör temsili ile metnin birbiriyle tutarlı kalması gerekir.