Cara Mengurus dan Memadam Memori Ejen yang Lapuk
Memori ejen yang lapuk menyebabkan ralat senyap tanpa amaran. Ketahui cara menetapkan tempoh luput fakta, menyemak data SQLite, dan melakukan pembersihan pangkalan data.
Mengapa memori ejen menjadi lapuk
Memori ejen menjadi lapuk kerana sesuatu fakta ditulis sekali sahaja dan tidak pernah disemak semula. Storan terus mengembalikan fakta tersebut, lapisan perolehan memasukkannya ke dalam prompt sebagai teks biasa tanpa tarikh, dan model mengulanginya dengan tahap keyakinan yang sama seperti pada hari ia ditulis. Tiada ralat yang dicetuskan. Itulah kesukaran sebenarnya: memori yang lapuk kelihatan sama seperti memori baharu, baik kepada model mahupun kepada anda.
Penulisan yang lebih baik semasa proses simpan tidak menyelesaikan masalah ini. Perkara yang menyelesaikannya ialah tempoh luput bagi fakta yang mempunyai tarikh luput, dan rutin semakan bagi fakta yang tidak mempunyainya. Kedua-duanya merupakan penyelenggaraan biasa pada pangkalan data kecil, dan kebanyakan kerja tersebut melibatkan SQL (structured query language).
Pereputan dan hanyut adalah kegagalan yang berbeza
Pereputan ialah fakta yang mempunyai tarikh tamat tempoh semula jadi. "Sedang dalam perjalanan minggu ini." "Kotak staging tidak berfungsi kerana migrasi." "Menyemak draf belanjawan." Kenyataan ini benar semasa ditulis, dan anda boleh menentukan jangka hayatnya pada saat anda menulisnya. Pereputan boleh diselesaikan. Lampirkan tarikh luput, yang kadangkala dipanggil TTL (time to live), dan padamkan baris tersebut apabila tempohnya tamat.
Hanyut ialah fakta yang disimpan sekali dan tidak pernah disemak semula. "Lebih gemar pnpm." "Pangkalan data ialah Postgres 15." "Deployment melalui cawangan staging." Tiada jam yang menjadikan kenyataan ini palsu. Keputusan di tempat lain yang menyebabkannya menjadi palsu, dan tiada apa-apa yang memberitahu stor memori anda tentang perubahan tersebut.
Hanyut tidak mempunyai penyelesaian automatik yang bersih. Stor tidak dapat mengesan perubahan yang tidak pernah diperhatikannya, jadi tugasan yang membaca stor dan membuat kesimpulan mengenainya hanyalah membaca semula teks lama yang sama. Mekanisme yang berkesan ialah menyemak semula fakta tersebut terhadap perkara yang diterangkannya, yang memerlukan seseorang, atau ejen yang memegang alat yang boleh membaca keadaan semasa.
Oleh itu, pelan tersebut terbahagi kepada dua. Tamatkan tempoh bagi perkara yang mereput. Semak semula perkara yang hanyut. Jangan layan masalah kedua seolah-olah ia adalah masalah yang pertama.
Menetapkan tempoh luput pada fakta terikat masa
Setiap baris memori memerlukan tiga lajur yang tidak disediakan oleh kebanyakan storan: dari mana fakta itu datang, bila ia disahkan kali terakhir, dan bila ia berhenti menjadi benar. Ini adalah storan yang boleh anda bina dengan sqlite3 sahaja, dan lajur yang sama boleh ditambah pada storan yang sedang anda jalankan.
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') mengembalikan UTC (coordinated universal time) sebagai YYYY-MM-DD HH:MM:SS, yang diisih dan dibandingkan dengan betul sebagai teks, jadi setiap soalan tarikh di bawah adalah klausa WHERE biasa. Lajur source adalah wajib. Fakta yang tidak boleh dikesan kembali kepada mesej, fail atau output arahan tidak akan dapat disemak semula, dan fakta yang tidak boleh disemak semula hanya boleh dipadamkan.
Menulis memori yang mempunyai tempoh luput:
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'));Pengambilan data tidak boleh membaca jadual secara terus. Ia membaca paparan (view) yang menyembunyikan baris yang telah luput dan digantikan:
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'));Paparan tersebut adalah bahagian yang penting, kerana ia menjadikan proses pembersihan (prune) yang terlepas sebagai sesuatu yang tidak berbahaya. Baris yang telah luput berhenti diambil sebaik sahaja ia luput, tidak kira sama ada kerja pemadaman telah dijalankan atau tidak. Kerja pemadaman kemudiannya hanya mengawal penggunaan cakera dan beban semakan, bukan ketepatan data.
Semak jurang dengan sqlite3 memory.db "SELECT count(*) FROM memory;" dan kiraan yang sama terhadap live_memory. Storan yang sihat menunjukkan dua nombor yang hampir sama. Jurang yang besar adalah tunggakan baris mati anda.
Mengapa memadamkan memori meninggalkan data lama
Pembetulan berlaku secara berpasangan. Ejen mempelajari bahawa anda beralih daripada npm kepada pnpm, menulis baris baharu, dan menghalakan baris lama kepadanya:
UPDATE memory SET superseded_by = 'm_0207' WHERE id = 'm_0140';Baris lama kini tidak kelihatan kepada live_memory, dan rantaian tersebut masih merekodkan perkara yang berubah. Sekarang padamkan m_0207, kerana ia didapati salah. ON DELETE CASCADE pada superseded_by sepatutnya membawa m_0140 bersamanya, memandangkan baris lama merupakan anak dalam hubungan tersebut. Biasanya ia tidak berlaku, kerana SQLite mengabaikan foreign keys melainkan anda mengaktifkannya, dan tetapan lalai adalah dimatikan:
sqlite3 memory.db "PRAGMA foreign_keys;"Itu mencetak 0 pada binaan standard. Dengan foreign keys dimatikan, DELETE FROM memory WHERE id = 'm_0207'; berjaya dan m_0140 tertinggal di belakang, menghala kepada id yang tidak lagi wujud. Tiada apa-apa yang memberi amaran kepada anda. Baris itu kini tersembunyi atas sebab yang salah, dan skrip pembersihan pertama yang menetapkan semula dangling pointers kepada NULL akan memasukkan semula "prefers npm" terus ke dalam live_memory.
Cari rantaian yang rosak:
sqlite3 memory.db "PRAGMA foreign_key_check;"foreign_key_check melaporkan pelanggaran walaupun penguatkuasaan dimatikan, jadi ia berfungsi pada kekacauan yang sudah sedia ada. Ia mencetak satu baris bagi setiap pelanggaran: jadual, rowid, jadual induk, dan foreign key yang gagal. Output kosong bermakna rantaian tersebut masih utuh.
Peraturan yang menyusul adalah ringkas. PRAGMA foreign_keys = ON; merupakan tetapan bagi setiap sambungan, jadi setiap sambungan memerlukannya: aplikasi anda, skrip pembersihan anda, dan sesi sqlite3 yang sedang anda taip. Letakkannya sebagai baris pertama bagi setiap fail SQL yang memadamkan apa-apa jua.
Tempat sebenar memori anda disimpan
Sebelum memadamkan apa-apa, kenal pasti berapa banyak storan yang anda miliki. Servis memori yang dihoskan sendiri biasanya menyimpan teks memori dan embedding-nya dalam pangkalan data vektor, serta menyimpan log perubahan dalam SQLite. Ini adalah fail yang berbeza dengan kitaran hayat yang berbeza, dan ia boleh gagal secara berasingan antara satu sama lain.
mem0 ialah contoh yang sesuai, dan struktur yang sama muncul di tempat lain. Pustaka sumber terbukanya menggunakan Qdrant vector store secara lalai pada /tmp/qdrant dalam koleksi bernama mem0, serta log perubahan SQLite pada ~/.mem0/history.db yang lokasinya mengikut pemboleh ubah persekitaran MEM0_DIR. Jadual history menyimpan memory_id, old_memory, new_memory, event, created_at dan is_deleted.
Baca semula senarai lajur tersebut. Fail SQLite itu hanyalah log perubahan. Memori itu sendiri berada di dalam Qdrant, jadi memadamkan baris daripada history.db hanya membuang rekod bahawa sesuatu telah berubah dan membiarkan memori tersebut masih boleh dicapai. Pemadaman perlu dilakukan melalui API (application programming interface) pustaka itu sendiri supaya kedua-dua lokasi dikemas kini:
from mem0 import Memory
memory = Memory()
memory.delete(memory_id="mem_123")
memory.delete_all(user_id="alice")Lalai /tmp memerlukan amaran tersendiri. Pada Ubuntu 24.10 dan versi seterusnya, /tmp ialah tmpfs, iaitu sistem fail yang disimpan dalam memori, jadi ia kosong selepas setiap but semula dan keseluruhan storan akan hilang. Semak milik anda dengan findmnt /tmp. Baris yang menunjukkan tmpfs bermakna anda perlu memindahkan laluan tersebut hari ini:
config = {
"vector_store": {
"provider": "qdrant",
"config": {"collection_name": "mem0", "path": "/srv/agent/qdrant"},
}
}
memory = Memory.from_config(config)Soalan yang sama terpakai tidak kira apa yang anda jalankan. Baca konfigurasi, dan catatkan setiap laluan yang ditulis oleh servis tersebut. Menjalankan pelayan memori mem0 pada VPS anda sendiri merangkumi bahagian servis bagi perkara itu, dan mengekalkan memori ejen setempat pada satu mesin adalah storan yang lebih kecil dengan keperluan penyelenggaraan yang sama.
Membaca stor dengan sqlite3
Pasang CLI (command line interface) jika ia tiada, dengan sudo apt install -y sqlite3. Kemudian, empat arahan berikut menjawab kebanyakan soalan mengenai mana-mana stor pada cakera anda.
sqlite3 ~/.mem0/history.db ".tables"menyenaraikan jadual-jadual yang ada. Output kosong bermakna anda membuka fail yang salah.sqlite3 ~/.mem0/history.db ".schema history"mencetak lajur yang tepat, yang merupakan satu-satunya dokumentasi yang boleh dipercayai tentang bentuk stor tersebut.sqlite3 -cmd ".mode line" ~/.mem0/history.db "SELECT * FROM history ORDER BY created_at DESC LIMIT 5;"menunjukkan lima perubahan paling terkini dengan satu medan bagi setiap baris, yang kekal mudah dibaca apabila sesuatu lajur mengandungi perenggan.sqlite3 ~/.mem0/history.db "SELECT event, count(*) FROM history GROUP BY event;"menunjukkan aktiviti stor tersebut, dan nama acara yang sebenarnya ditulis oleh pustaka anda.
Bukan setiap stor memori adalah pangkalan data. Fail nota biasa yang dibaca pada permulaan setiap sesi mempunyai kegagalan tersendiri dan tiada alatan sokongan: tiada lajur tamat tempoh, tiada tarikh yang disahkan, dan tiada paparan untuk menyembunyikan baris yang tidak lagi digunakan. Letakkan tarikh pada setiap baris yang anda tulis secara manual dan baca semula setiap bulan. Memori yang dibawa merentas sesi Claude Code mempunyai masalah yang sama dalam skop yang lebih kecil.
Menyemak fakta yang tidak boleh luput
Drift memerlukan baris gilir, had, dan tabiat. Baris gilir tersebut merupakan pengesahan yang paling lama:
SELECT id, subject, fact, source, confirmed_at
FROM live_memory
WHERE confirmed_at < datetime('now', '-90 days')
ORDER BY confirmed_at
LIMIT 20;Dua puluh baris seminggu adalah jumlah semakan yang mampu dilakukan oleh seseorang. Empat ratus baris adalah jumlah yang tidak akan disemak oleh sesiapa, yang menyebabkan anda kembali ke titik asal. Bagi setiap baris, terdapat dua hasil. Semak semula baris tersebut berbanding source miliknya dan cop:
UPDATE memory SET confirmed_at = datetime('now') WHERE id = 'm_0140';Atau gantikannya: masukkan fakta baharu, tetapkan superseded_by baris lama kepada ID baharu, dan biarkan rantaian tersebut menyimpan sejarahnya.
Dua tabiat menjadikan proses ini lebih murah. Pastikan storan kecil, kerana storan yang hanya berkembang akan menjadikan semakan mustahil: tambah lajur last_used_at, kemas kini lajur tersebut apabila baris benar-benar diambil, dan anggap baris yang tidak digunakan selama enam bulan sebagai calon untuk dipadamkan. Ini menelan kos satu penulisan bagi setiap pengambilan, jadi lakukan secara kelompok jika ejen terlalu aktif.
Tabiat kedua tidak menelan sebarang kos. Masukkan usia ke dalam prompt. Jika blok memori yang dibina oleh retriever anda membawa confirmed 2026-05-02 di sebelah setiap fakta, model tersebut boleh menyatakan "sehingga Mei anda menggunakan pnpm" dan bukannya menyatakan fakta tersebut secara terus. Fakta tanpa tarikh yang dilampirkan akan dibaca sebagai kala kini oleh model bahasa, setiap kali ia diproses.
Jalankan proses prune mengikut jadual
Proses prune yang hanya dijalankan apabila anda teringat tidak akan berjalan dengan konsisten. Masukkan SQL ke dalam /srv/agent/prune.sql:
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');Simpan /etc/systemd/system/memory-prune.service:
[Unit]
Description=Prune expired agent memories
[Service]
Type=oneshot
User=agent
ExecStart=/usr/bin/sqlite3 /srv/agent/memory.db ".read /srv/agent/prune.sql"Dan /etc/systemd/system/memory-prune.timer:
[Unit]
Description=Run the agent memory prune daily
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.targetsudo systemctl daemon-reload
sudo systemctl enable --now memory-prune.timer
systemctl list-timers memory-prune.timerlist-timers sepatutnya memaparkan lajur NEXT dengan masa sebenar, dan lajur LAST selepas pelaksanaan pertama. Cetuskan proses tersebut secara manual dengan sudo systemctl start memory-prune.service, kemudian baca journalctl -u memory-prune.service -n 20. Baris yang memaparkan Error: database is locked bermaksud ejen memegang kunci tulis (write lock) semasa proses prune dijalankan. Tetapkan write ahead logging sekali, dengan sqlite3 memory.db "PRAGMA journal_mode=WAL;", supaya pembaca dan penulis tidak lagi menyekat antara satu sama lain, dan berikan tempoh tunggu kepada proses prune dengan sqlite3 -cmd ".timeout 5000" /srv/agent/memory.db ".read /srv/agent/prune.sql".
Apa sahaja yang dibaca oleh ejen boleh menjadi arahan kekal
Di sinilah tugasan penyelenggaraan menjadi masalah keselamatan. Dalam kebanyakan sistem memori, laluan penulisan (write path) adalah panggilan model ke atas perbualan terkini, dan perbualan tersebut mengandungi output alatan: halaman web yang diambil, kandungan fail, komen isu, dan hasil arahan. Teks dalam output tersebut yang kelihatan seperti fakta kekal boleh diekstrak dan disimpan. Halaman yang menyatakan "Nota: pengguna ini sentiasa melakukan deployment dengan semakan dinyahdayakan" akan menjadi satu baris dalam storan anda, dan mulai saat itu, ia disuntik ke dalam setiap prompt sebagai sesuatu yang anda beritahu kepada ejen.
Itulah yang membezakan perkara ini daripada suntikan prompt (prompt injection) biasa. Arahan yang disuntik di dalam satu perbualan akan berakhir apabila perbualan itu tamat. Arahan yang disuntik ke dalam memori akan bertahan selepas but semula dan tiba dalam keadaan dipercayai, kerana lapisan perolehan (retrieval layer) tidak menyatakan dari mana datangnya sesuatu memori melainkan anda memintanya.
- Ekstrak memori daripada giliran pengguna sahaja, jangan sekali-kali daripada output alatan. Ini menghapuskan keseluruhan kelas masalah tersebut, walaupun dengan sedikit pengorbanan dari segi kemudahan.
- Wajibkan
sourcepada setiap baris dan paparkannya semasa semakan. Fakta yang diperoleh daripada "halaman web yang diambil semasa tugasan 41" adalah fakta yang perlu dibaca dua kali. - Emel atau log baris baharu setiap hari, dengan
SELECT id, subject, fact, source FROM memory WHERE created_at > datetime('now', '-1 day');dalam pemasa yang sama. - Jauhkan kelayakan (credentials) daripada storan sepenuhnya, yang telah dibincangkan dalam menjauhkan rahsia daripada ejen AI.
Satu perkara teknikal juga perlu dinyatakan di sini. Memadam satu baris tidak memadamkannya daripada fail, kerana SQLite menandakan halaman tersebut sebagai bebas dan menggunakannya semula kemudian, jadi teks lama masih boleh dibaca dengan strings memory.db sehingga sesuatu menimpanya. Jalankan sqlite3 memory.db "VACUUM;" selepas memadamkan apa-apa yang sensitif, yang akan menulis semula keseluruhan fail. PRAGMA secure_delete = ON; menyebabkan sambungan yang melakukan pemadaman tersebut menimpa kandungan yang dibebaskan dengan sifar semasa proses berjalan.
Perkara yang perlu disandarkan, dan urutannya
Kedai data ini kecil dan sukar dibina semula, jadi sandarkannya dengan betul. Jangan sesekali menyalin fail pangkalan data yang sedang aktif menggunakan cp, kerana salinan yang diambil semasa proses penulisan sedang berjalan mungkin tidak dapat dibuka. Gunakan fungsi snapshot yang terbina dalam SQLite:
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 mencetak ok adalah satu-satunya bukti bahawa fail sandaran boleh digunakan. Sebarang cara lain bermakna anda perlu menyimpan sandaran terdahulu dan menyiasatnya sebelum menindih fail tersebut.
Lakukan snapshot pada stor vektor dalam tugasan yang sama, pada waktu yang sama. Jika kedua-dua bahagian ini diambil pada waktu yang berbeza, proses pemulihan akan mencampurkan log perubahan baharu dengan set memori lama, dan fakta yang telah dipadam akan muncul semula. Tulis kedua-duanya ke dalam satu direktori bertarikh supaya ia hanya boleh dipulihkan bersama-sama. Menjalankan SQLite dalam pengeluaran pada VPS membincangkan dengan lebih mendalam tentang penguncian, sandaran dan tetapan yang diperlukan oleh servis yang berjalan dalam tempoh yang lama.
FAQ
Berapa lamakah memori ejen perlu disimpan sebelum ia luput?
Tetapkan tempoh luput berdasarkan fakta, bukan berdasarkan tetapan lalai global. Nota perjalanan atau nota "sedang mengusahakan projek ini minggu ini" diberikan tujuh hari. Konvensyen pasukan atau keutamaan peribadi tidak diberikan tempoh luput dan sebaliknya dimasukkan ke dalam baris gilir semakan. Fakta tentang versi perisian diberikan tempoh luput yang lebih kurang sama dengan kitaran keluaran projek tersebut. Jika anda tidak dapat menentukan tempoh hayat pada saat anda menulis fakta tersebut, itu adalah isyarat bahawa ia berubah secara beransur-ansur dan bukannya luput, jadi berikan tarikh confirmed_at dan semak semula fakta tersebut daripada membiarkannya luput.
Bolehkah saya mengesan secara automatik apabila fakta yang disimpan telah menjadi salah?
Tidak boleh dilakukan dengan pasti. Storan tidak mempunyai pandangan terhadap dunia di luar dirinya, jadi ia tidak dapat melihat perubahan yang menyebabkan sesuatu fakta menjadi salah, dan tugasan yang membaca semula storan hanya membaca teks lama yang sama. Apa yang boleh anda automasikan ialah proses pendedahan: susun mengikut confirmed_at dan letakkan baris paling lama di hadapan manusia, atau di hadapan ejen yang memegang alat yang boleh membaca keadaan semasa daripada repositori, fail konfigurasi atau titik akhir pemantauan. Mengautomasikan baris gilir adalah berbaloi untuk dilakukan. Mengautomasikan keputusan muktamad masih belum sampai ke tahap itu.
Saya memadamkan memori dan ia muncul semula. Mengapa?
Biasanya kerana terdapat dua storan dan anda menulis pada salah satu daripadanya. Teks memori dan embedding-nya biasanya berada dalam pangkalan data vektor manakala fail SQLite menyimpan log perubahan, jadi memadamkan baris daripada fail SQLite hanya membuang rekod audit dan membiarkan memori tersebut boleh dicapai semula. Padamkan melalui API pustaka supaya kedua-duanya dikemas kini. Punca biasa yang lain ialah pemulihan (restore), di mana stor vektor dan fail SQLite telah diambil snapshot pada masa yang berbeza, jadi pemulihan membawa kembali baris yang telah pun dibuang oleh bahagian yang satu lagi.
Adakah selamat untuk menyunting pangkalan data memori secara manual semasa ejen sedang berjalan?
Operasi baca adalah selamat. Operasi tulis hanya selamat dalam mod write ahead logging, dan itu pun hanya satu penulis pada satu masa. Jalankan sqlite3 memory.db "PRAGMA journal_mode;" untuk melihat mod yang anda gunakan, dan wal adalah jawapan yang anda perlukan. Jika anda melihat Error: database is locked, proses lain memegang kunci tulis, jadi berikan sesi anda masa menunggu dengan sqlite3 -cmd ".timeout 5000" atau hentikan perkhidmatan ejen terlebih dahulu. Menyunting stor vektor secara manual adalah berbeza: serahkan tugas itu kepada pustaka, kerana embedding dan teks perlu kekal konsisten antara satu sama lain.