SSD Nodes Learn 🎉 VPS เริ่ม $5.50/เดือน
คู่มือ Matt Connorโดย Matt Connor · อัปเดตเมื่อ 2026-08-13

วิธีจัดการหน่วยความจำเอเจนต์ล้าสมัยและขั้นตอนการลบข้อมูล

หน่วยความจำเอเจนต์มักเสื่อมสภาพโดยไม่มีการแจ้งเตือน เรียนรู้วิธีตั้งค่าวันหมดอายุของข้อมูล การลบแบบ Cascade และการตรวจสอบฐานข้อมูล SQLite ด้วยคำสั่ง sqlite3 เพื่อแก้ปัญหาความคลาดเคลื่อน

เหตุใดหน่วยความจำของเอเจนต์จึงล้าสมัย

หน่วยความจำของเอเจนต์ล้าสมัยเนื่องจากข้อเท็จจริงถูกบันทึกไว้เพียงครั้งเดียวและไม่เคยถูกตรวจสอบอีกเลย ระบบจัดเก็บข้อมูลจะส่งคืนข้อมูลนั้นออกมาเรื่อยๆ ชั้นการดึงข้อมูลจะนำข้อมูลไปใส่ใน prompt เป็นข้อความธรรมดาโดยไม่มีการระบุวันที่ และโมเดลจะนำข้อมูลนั้นไปตอบซ้ำด้วยความมั่นใจเท่าเดิมกับวันที่บันทึกไว้ โดยไม่มีข้อผิดพลาดใดๆ แจ้งเตือน นี่คือความยากทั้งหมดของปัญหา: สำหรับโมเดลและสำหรับคุณแล้ว หน่วยความจำที่ล้าสมัยจะมีลักษณะเหมือนกับหน่วยความจำที่เพิ่งบันทึกใหม่ทุกประการ

การเขียนข้อมูลให้ดีขึ้นในขณะบันทึกไม่สามารถแก้ไขปัญหานี้ได้ สิ่งที่แก้ไขได้คือการกำหนดวันหมดอายุให้กับข้อเท็จจริงที่มีอายุขัย และการสร้างกระบวนการตรวจสอบสำหรับข้อเท็จจริงที่ไม่มีวันหมดอายุ ทั้งสองอย่างนี้เป็นการบำรุงรักษาฐานข้อมูลขนาดเล็กตามปกติ และงานส่วนใหญ่คือการใช้ SQL (structured query language)

ความเสื่อมสภาพ (Decay) และความคลาดเคลื่อน (Drift) คือความล้มเหลวที่ต่างกัน

ความเสื่อมสภาพ (Decay) คือข้อเท็จจริงที่มีวันหมดอายุตามธรรมชาติ เช่น "สัปดาห์นี้เดินทาง" "เซิร์ฟเวอร์ staging ปิดปรับปรุงเพื่อย้ายระบบ" "กำลังตรวจสอบร่างงบประมาณ" ข้อความเหล่านี้เป็นความจริงในขณะที่เขียน และคุณสามารถระบุอายุการใช้งานของมันได้ทันทีที่เขียนขึ้น ความเสื่อมสภาพเป็นสิ่งที่แก้ไขได้ ให้กำหนดวันหมดอายุหรือที่เรียกว่า TTL (time to live) และลบข้อมูลนั้นทิ้งเมื่อถึงกำหนด

ความคลาดเคลื่อน (Drift) คือข้อเท็จจริงที่ถูกบันทึกไว้ครั้งเดียวและไม่เคยถูกตรวจสอบซ้ำ เช่น "ต้องการใช้ pnpm" "ฐานข้อมูลคือ Postgres 15" "การ deploy ทำผ่าน branch staging" ไม่มีนาฬิกาใดที่จะทำให้ข้อความเหล่านี้กลายเป็นเท็จ แต่การตัดสินใจจากที่อื่นต่างหากที่ทำให้มันเปลี่ยนไป และไม่มีสิ่งใดแจ้งให้แหล่งเก็บข้อมูลของคุณทราบถึงการเปลี่ยนแปลงนั้น

ความคลาดเคลื่อนไม่มีวิธีแก้ไขอัตโนมัติที่ชัดเจน แหล่งเก็บข้อมูลไม่สามารถตรวจพบการเปลี่ยนแปลงที่มันไม่เคยสังเกตเห็น ดังนั้นงานที่ทำหน้าที่อ่านและวิเคราะห์ข้อมูลจากแหล่งเก็บเดิมจึงเป็นเพียงการอ่านข้อความเก่าซ้ำไปซ้ำมา กลไกที่ได้ผลคือการตรวจสอบข้อเท็จจริงเทียบกับสิ่งที่มันอธิบายอยู่จริง ซึ่งจำเป็นต้องอาศัยคน หรือตัวแทน (agent) ที่ถือเครื่องมือซึ่งสามารถอ่านสถานะปัจจุบันได้

ดังนั้น แผนการทำงานจึงแบ่งออกเป็นสองส่วน คือ กำหนดวันหมดอายุให้กับสิ่งที่เสื่อมสภาพ และตรวจสอบซ้ำในสิ่งที่อาจคลาดเคลื่อน อย่าจัดการปัญหาที่สองด้วยวิธีเดียวกับปัญหาแรก

กำหนดวันหมดอายุให้กับข้อเท็จจริงที่มีเงื่อนไขด้านเวลา

แถวข้อมูลในหน่วยความจำทุกแถวต้องการคอลัมน์ 3 คอลัมน์ที่ที่จัดเก็บข้อมูลส่วนใหญ่ไม่มีให้ ได้แก่ แหล่งที่มาของข้อเท็จจริง, เวลาที่ได้รับการยืนยันล่าสุด และเวลาที่ข้อเท็จจริงนั้นจะหมดความถูกต้อง นี่คือที่จัดเก็บข้อมูลที่คุณสามารถสร้างขึ้นได้ด้วย sqlite3 เพียงอย่างเดียว และคุณสามารถเพิ่มคอลัมน์เดียวกันนี้ลงในที่จัดเก็บข้อมูลที่คุณใช้งานอยู่แล้วได้

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') จะส่งค่ากลับเป็น UTC (coordinated universal time) ในรูปแบบ YYYY-MM-DD HH:MM:SS ซึ่งสามารถเรียงลำดับและเปรียบเทียบเป็นข้อความได้อย่างถูกต้อง ดังนั้นคำถามเกี่ยวกับวันที่ทุกข้อด้านล่างจึงเป็นเพียงประโยค WHERE ธรรมดา คอลัมน์ source เป็นสิ่งที่ขาดไม่ได้ ข้อเท็จจริงที่คุณไม่สามารถสืบย้อนกลับไปยังข้อความ ไฟล์ หรือผลลัพธ์ของคำสั่งได้นั้นจะไม่สามารถตรวจสอบซ้ำได้ และข้อเท็จจริงที่ไม่สามารถตรวจสอบซ้ำได้จะมีทางเดียวคือต้องลบทิ้ง

การเขียนข้อมูลในหน่วยความจำที่มีวันหมดอายุ:

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'));

การดึงข้อมูลต้องไม่ใช้วิธีอ่านจากตารางโดยตรง แต่ต้องอ่านผ่าน view ที่ซ่อนแถวที่หมดอายุหรือถูกแทนที่แล้วไว้:

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'));

View คือส่วนสำคัญ เพราะมันช่วยให้การพลาดการลบข้อมูล (prune) ไม่ส่งผลเสีย แถวที่หมดอายุจะหยุดถูกดึงข้อมูลทันทีที่ถึงเวลาหมดอายุ ไม่ว่างานลบข้อมูลจะทำงานหรือไม่ก็ตาม งานลบข้อมูลจึงทำหน้าที่เพียงควบคุมการใช้พื้นที่ดิสก์และภาระในการตรวจสอบเท่านั้น ไม่ใช่เรื่องของความถูกต้องของข้อมูล

ตรวจสอบช่องว่างของข้อมูลด้วย sqlite3 memory.db "SELECT count(*) FROM memory;" และเปรียบเทียบจำนวนกับ live_memory ที่จัดเก็บข้อมูลที่มีสถานะปกติจะแสดงตัวเลขสองค่าที่ใกล้เคียงกัน หากพบช่องว่างขนาดใหญ่ แสดงว่าคุณมีแถวข้อมูลที่ตายแล้วค้างอยู่ในระบบจำนวนมาก

เหตุใดการลบหน่วยความจำจึงทิ้งข้อมูลเก่าไว้เบื้องหลัง

การแก้ไขข้อมูลจะมาเป็นคู่เสมอ เอเจนต์จะเรียนรู้ว่าคุณย้ายจาก npm ไปยัง pnpm จากนั้นจะเขียนแถวใหม่และชี้แถวเก่าไปยังแถวนั้น:

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

แถวเก่าจะถูกซ่อนจาก live_memory และสายโซ่ข้อมูลยังคงบันทึกสิ่งที่เปลี่ยนแปลงไว้ ตอนนี้ให้ลบ m_0207 ออกเพราะพบว่าข้อมูลนั้นไม่ถูกต้อง ON DELETE CASCADE บน superseded_by ควรจะลบ m_0140 ออกไปด้วย เนื่องจากแถวเก่าเป็นลูกในความสัมพันธ์นั้น โดยปกติแล้วมันจะไม่ถูกลบ เพราะ SQLite จะเพิกเฉยต่อ foreign key เว้นแต่คุณจะเปิดใช้งาน ซึ่งค่าเริ่มต้นคือปิด:

sqlite3 memory.db "PRAGMA foreign_keys;"

คำสั่งนั้นจะแสดงผล 0 ในการติดตั้งแบบมาตรฐาน เมื่อ foreign key ถูกปิด DELETE FROM memory WHERE id = 'm_0207'; จะทำงานสำเร็จและ m_0140 จะยังคงค้างอยู่ โดยชี้ไปยัง id ที่ไม่มีอยู่จริง ไม่มีสิ่งใดแจ้งเตือนคุณ แถวข้อมูลนั้นจะถูกซ่อนด้วยเหตุผลที่ผิด และสคริปต์ทำความสะอาดตัวแรกที่รีเซ็ต dangling pointer ไปยัง NULL จะนำค่า "prefers npm" กลับเข้าไปใน live_memory ทันที

ค้นหาสายโซ่ที่เสียหาย:

sqlite3 memory.db "PRAGMA foreign_key_check;"

foreign_key_check จะรายงานการละเมิดแม้ว่าจะปิดการบังคับใช้ไว้ ดังนั้นมันจึงทำงานกับข้อมูลที่ยุ่งเหยิงที่คุณมีอยู่แล้ว มันจะแสดงผลหนึ่งแถวต่อการละเมิดหนึ่งรายการ ได้แก่ ตาราง, rowid, ตารางแม่ และ foreign key รายการใดที่ล้มเหลว หากผลลัพธ์ว่างเปล่าแสดงว่าสายโซ่ข้อมูลยังคงสมบูรณ์

กฎที่ตามมานั้นสั้นมาก PRAGMA foreign_keys = ON; เป็นการตั้งค่าต่อการเชื่อมต่อ ดังนั้นทุกการเชื่อมต่อจำเป็นต้องตั้งค่านี้ ไม่ว่าจะเป็นแอปพลิเคชันของคุณ, สคริปต์ prune ของคุณ และเซสชัน sqlite3 ที่คุณกำลังพิมพ์อยู่ ให้ใส่คำสั่งนี้เป็นบรรทัดแรกของทุกไฟล์ SQL ที่มีการลบข้อมูล

ที่อยู่จริงของข้อมูลความจำของคุณ

ก่อนจะลบข้อมูลใดๆ ให้ตรวจสอบก่อนว่าคุณมีแหล่งจัดเก็บข้อมูลกี่แห่ง โดยปกติแล้วบริการความจำแบบ self-hosted จะเก็บข้อความความจำและ embedding ไว้ใน vector database และเก็บ log การเปลี่ยนแปลงไว้ใน SQLite ซึ่งไฟล์เหล่านี้แยกจากกันและมีวงจรชีวิตที่ต่างกัน จึงอาจเกิดความล้มเหลวแยกจากกันได้

mem0 เป็นตัวอย่างที่ชัดเจนและมีโครงสร้างแบบเดียวกับบริการอื่น ไลบรารีโอเพนซอร์สของ mem0 จะใช้ Qdrant เป็นค่าเริ่มต้นที่ /tmp/qdrant ในคอลเลกชันชื่อ mem0 และมี SQLite สำหรับเก็บ log การเปลี่ยนแปลงที่ ~/.mem0/history.db ซึ่งตำแหน่งไฟล์จะอ้างอิงตามตัวแปรสภาพแวดล้อม MEM0_DIR โดยตาราง history จะเก็บข้อมูล memory_id, old_memory, new_memory, event, created_at และ is_deleted

โปรดอ่านรายการคอลัมน์เหล่านั้นอีกครั้ง ไฟล์ SQLite เป็นเพียง log การเปลี่ยนแปลง ส่วนข้อมูลความจำจริงๆ อยู่ใน Qdrant ดังนั้นการลบแถวข้อมูลจาก history.db จะเป็นการลบเพียงบันทึกว่ามีการเปลี่ยนแปลงเกิดขึ้น แต่ยังคงสามารถดึงข้อมูลความจำนั้นกลับมาได้ การลบข้อมูลต้องทำผ่าน API (application programming interface) ของไลบรารีโดยตรงเพื่อให้ทั้งสองแหล่งข้อมูลได้รับการอัปเดตพร้อมกัน:

from mem0 import Memory

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

ค่าเริ่มต้น /tmp ควรได้รับการแจ้งเตือนเป็นพิเศษ บน Ubuntu 24.10 ขึ้นไป /tmp จะเป็น tmpfs ซึ่งเป็นระบบไฟล์ที่ทำงานบนหน่วยความจำ ดังนั้นข้อมูลจะหายไปทั้งหมดหลังจากรีบูตเครื่อง ให้ตรวจสอบการตั้งค่าของคุณด้วย findmnt /tmp หากพบแถวที่แสดง tmpfs หมายความว่าคุณควรย้ายตำแหน่ง path ดังกล่าวทันที:

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

คำถามเดียวกันนี้ใช้ได้กับทุกบริการที่คุณรันอยู่ ให้ตรวจสอบไฟล์ config และจดบันทึกทุก path ที่บริการนั้นเขียนข้อมูลลงไป บทความ การรัน mem0 memory server บน VPS ของคุณเอง ครอบคลุมด้านการให้บริการในส่วนนี้ และ การเก็บความจำของ agent ไว้ในเครื่องเดียว เป็นการจัดการแหล่งเก็บข้อมูลขนาดเล็กที่มีความต้องการในการบำรุงรักษาแบบเดียวกัน

การอ่านข้อมูลจาก store ด้วย sqlite3

ติดตั้ง CLI (command line interface) หากยังไม่มีด้วยคำสั่ง sudo apt install -y sqlite3 จากนั้นคำสั่ง 4 รายการต่อไปนี้จะช่วยตอบคำถามส่วนใหญ่เกี่ยวกับ store ใดๆ ที่อยู่บนดิสก์ของคุณ

  • sqlite3 ~/.mem0/history.db ".tables" ใช้แสดงรายการตารางทั้งหมด หากผลลัพธ์ว่างเปล่า แสดงว่าคุณเปิดไฟล์ผิด
  • sqlite3 ~/.mem0/history.db ".schema history" ใช้แสดงคอลัมน์ทั้งหมด ซึ่งเป็นเอกสารอ้างอิงที่เชื่อถือได้เพียงแหล่งเดียวเกี่ยวกับโครงสร้างของ store นั้น
  • sqlite3 -cmd ".mode line" ~/.mem0/history.db "SELECT * FROM history ORDER BY created_at DESC LIMIT 5;" ใช้แสดงการเปลี่ยนแปลงล่าสุด 5 รายการ โดยแสดงผลทีละฟิลด์ต่อบรรทัด ซึ่งช่วยให้อ่านง่ายแม้ในกรณีที่คอลัมน์เก็บข้อมูลเป็นย่อหน้า
  • sqlite3 ~/.mem0/history.db "SELECT event, count(*) FROM history GROUP BY event;" ใช้แสดงประวัติการทำงานของ store และชื่อเหตุการณ์ (event names) ที่ไลบรารีของคุณเขียนลงไปจริง

ไม่ใช่ทุก memory store จะเป็นฐานข้อมูลเสมอไป ไฟล์บันทึกข้อความธรรมดาที่ถูกอ่านทุกครั้งที่เริ่ม session มักประสบปัญหาและไม่มีเครื่องมือช่วยจัดการ เช่น ไม่มีคอลัมน์วันหมดอายุ, ไม่มีการยืนยันวันที่ และไม่มีมุมมอง (view) สำหรับซ่อนแถวข้อมูลที่ไม่ได้ใช้งานแล้ว ให้ระบุวันที่ในทุกบรรทัดที่คุณเขียนด้วยตนเองและหมั่นกลับมาอ่านทบทวนทุกเดือน หน่วยความจำที่คงอยู่ข้าม session ของ Claude Code ก็ประสบปัญหาเดียวกันนี้ในขอบเขตที่เล็กกว่า

การทบทวนข้อเท็จจริงที่ไม่มีวันหมดอายุ

Drift จำเป็นต้องมีคิว (queue), การจำกัดจำนวน (cap) และนิสัยในการทำ (habit) คิวที่ว่านี้คือการยืนยันข้อมูลที่เก่าที่สุด:

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

การทบทวนข้อมูลสัปดาห์ละ 20 แถวเป็นสิ่งที่คนสามารถทำได้จริง แต่ถ้าเป็น 400 แถวจะไม่มีใครทำ และนั่นจะทำให้คุณกลับไปอยู่ที่จุดเริ่มต้น สำหรับแต่ละแถวจะมีผลลัพธ์ที่เป็นไปได้ 2 อย่าง คือตรวจสอบซ้ำกับ source ของมันแล้วประทับตรา:

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

หรือแทนที่ข้อมูลนั้น: แทรกข้อเท็จจริงใหม่เข้าไป ตั้งค่า superseded_by ของแถวเก่าให้เป็น id ใหม่ แล้วปล่อยให้สายโซ่เก็บประวัติเอาไว้

นิสัย 2 อย่างจะช่วยให้กระบวนการนี้มีต้นทุนต่ำลง อย่างแรกคือรักษาขนาดของที่เก็บข้อมูลให้เล็กอยู่เสมอ เพราะที่เก็บข้อมูลที่ขยายตัวขึ้นเรื่อยๆ จะทำให้การทบทวนเป็นไปไม่ได้: ให้เพิ่มคอลัมน์ last_used_at เข้าไป อัปเดตค่าเมื่อมีการดึงข้อมูลแถวนั้นออกมาใช้งานจริง และพิจารณาแถวที่ไม่ได้ถูกใช้งานมานานกว่า 6 เดือนว่าเป็นรายการที่ควรลบออก วิธีนี้มีต้นทุนคือการเขียนข้อมูล 1 ครั้งต่อการดึงข้อมูล 1 ครั้ง ดังนั้นหากเอเจนต์มีการสื่อสารบ่อย ให้ใช้วิธีทำเป็นชุด (batch) แทน

นิสัยอย่างที่สองไม่มีต้นทุนใดๆ คือการใส่ข้อมูลอายุของข้อมูลลงไปใน prompt หากบล็อกหน่วยความจำที่ตัวดึงข้อมูล (retriever) สร้างขึ้นมี confirmed 2026-05-02 กำกับไว้ข้างๆ แต่ละข้อเท็จจริง โมเดลจะสามารถตอบว่า "ณ เดือนพฤษภาคม คุณกำลังใช้ pnpm" แทนที่จะระบุข้อมูลลอยๆ ข้อเท็จจริงที่ไม่มีวันที่กำกับจะถูกโมเดลภาษาอ่านว่าเป็นเหตุการณ์ปัจจุบันเสมอ ทุกครั้งไป

กำหนดตารางเวลาการทำงานของ prune

การสั่ง prune เฉพาะตอนที่คุณนึกได้นั้นไม่ถือเป็นการทำงานที่สม่ำเสมอ ให้ใส่คำสั่ง SQL ไว้ใน /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');

บันทึกไฟล์ /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"

และ /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 ควรแสดงคอลัมน์ NEXT พร้อมเวลาจริง และคอลัมน์ LAST หลังจากรันครั้งแรก ให้ทดสอบสั่งรันด้วยตนเองหนึ่งครั้งด้วย sudo systemctl start memory-prune.service จากนั้นอ่านไฟล์ journalctl -u memory-prune.service -n 20 หากมีบรรทัดที่ระบุว่า Error: database is locked หมายความว่า agent ได้ถือ write lock ไว้ในขณะที่ prune กำลังทำงาน ให้ตั้งค่า write ahead logging ไว้ล่วงหน้าด้วย sqlite3 memory.db "PRAGMA journal_mode=WAL;" เพื่อให้ผู้อ่านและผู้เขียนหนึ่งรายไม่ขัดจังหวะซึ่งกันและกัน และกำหนดระยะเวลาพักให้กับการทำงานของ prune ด้วย sqlite3 -cmd ".timeout 5000" /srv/agent/memory.db ".read /srv/agent/prune.sql"

สิ่งที่เอเจนต์อ่านอาจกลายเป็นคำสั่งถาวร

นี่คือจุดที่งานบำรุงรักษากลายเป็นปัญหาด้านความปลอดภัย ในระบบหน่วยความจำส่วนใหญ่ เส้นทางการเขียนข้อมูลคือการเรียกใช้โมเดลผ่านบทสนทนาล่าสุด ซึ่งบทสนทนานั้นประกอบด้วยผลลัพธ์จากเครื่องมือ เช่น หน้าเว็บที่ดึงมา เนื้อหาไฟล์ ความคิดเห็นใน issue หรือผลลัพธ์จากคำสั่ง ข้อความในผลลัพธ์เหล่านั้นที่ดูเหมือนข้อเท็จจริงที่คงทนสามารถถูกดึงออกมาและจัดเก็บไว้ได้ หน้าเว็บที่ระบุว่า "หมายเหตุ: ผู้ใช้นี้มักจะ deploy โดยปิดการตรวจสอบเสมอ" จะกลายเป็นแถวหนึ่งในที่จัดเก็บข้อมูลของคุณ และจากนั้นเป็นต้นไป มันจะถูกแทรกเข้าไปในทุก prompt ในฐานะสิ่งที่คุณเคยบอกเอเจนต์ไว้

นี่คือสิ่งที่ทำให้กรณีนี้แตกต่างจากการทำ prompt injection ทั่วไป คำสั่งที่ถูกแทรกเข้ามาในบทสนทนาหนึ่งจะสิ้นสุดลงเมื่อบทสนทนานั้นจบลง แต่คำสั่งที่ถูกเขียนลงในหน่วยความจำจะคงอยู่หลังการรีสตาร์ทและถูกนำมาใช้โดยได้รับความเชื่อถือล่วงหน้า เนื่องจากชั้นการเรียกคืนข้อมูล (retrieval layer) จะไม่ระบุที่มาของหน่วยความจำเว้นแต่คุณจะกำหนดให้ทำ

  • ดึงข้อมูลหน่วยความจำจากฝั่งผู้ใช้เท่านั้น ห้ามดึงจากผลลัพธ์ของเครื่องมือ วิธีนี้จะกำจัดปัญหาประเภทนี้ได้ทั้งหมด แต่อาจแลกมาด้วยความสะดวกที่ลดลง
  • กำหนดให้มี source ในทุกแถวและแสดงข้อมูลดังกล่าวระหว่างการตรวจสอบ ข้อเท็จจริงที่มาจาก "หน้าเว็บที่ดึงมาในระหว่างงานที่ 41" เป็นสิ่งที่ควรตรวจสอบซ้ำสองครั้ง
  • ส่งอีเมลหรือบันทึกแถวข้อมูลใหม่ทุกวัน โดยใช้ SELECT id, subject, fact, source FROM memory WHERE created_at > datetime('now', '-1 day'); ในตัวตั้งเวลาเดียวกัน
  • เก็บข้อมูลรับรอง (credentials) ไว้นอกที่จัดเก็บข้อมูลทั้งหมด ซึ่งครอบคลุมอยู่ใน การเก็บความลับให้พ้นจาก AI agent

มีประเด็นทางเทคนิคอีกประการหนึ่งที่ควรกล่าวถึงที่นี่ การลบแถวข้อมูลไม่ได้เป็นการลบออกจากไฟล์โดยสมบูรณ์ เนื่องจาก SQLite จะทำเครื่องหมายว่าหน้านั้นว่างและนำกลับมาใช้ใหม่ในภายหลัง ดังนั้นข้อความเดิมจึงยังคงอ่านได้ด้วย strings memory.db จนกว่าจะมีข้อมูลอื่นมาเขียนทับ ให้รัน sqlite3 memory.db "VACUUM;" หลังจากลบข้อมูลที่ละเอียดอ่อน ซึ่งจะเป็นการเขียนไฟล์ใหม่ทั้งหมด ส่วน PRAGMA secure_delete = ON; จะทำให้การเชื่อมต่อที่ดำเนินการลบข้อมูลทำการเขียนทับเนื้อหาที่ถูกปล่อยว่างด้วยเลขศูนย์ในขณะที่ดำเนินการ

สิ่งที่ควรสำรองข้อมูลและลำดับความสำคัญ

ฐานข้อมูลมีขนาดเล็กและสร้างใหม่ได้ยาก จึงต้องสำรองข้อมูลอย่างถูกต้อง ห้ามคัดลอกไฟล์ฐานข้อมูลที่กำลังทำงานอยู่ด้วย cp เพราะสำเนาที่คัดลอกระหว่างการเขียนอาจไม่สามารถเปิดใช้งานได้ ให้ใช้ฟังก์ชัน snapshot ที่มีมาให้ใน 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 ok เป็นวิธีเดียวที่จะพิสูจน์ได้ว่าไฟล์สำรองข้อมูลนั้นใช้งานได้จริง หากใช้วิธีอื่น ให้เก็บไฟล์สำรองชุดก่อนหน้าไว้และตรวจสอบให้แน่ใจก่อนที่จะเขียนทับ

ให้ทำ snapshot ของ vector store ในงานเดียวกันและในเวลาเดียวกัน หากข้อมูลทั้งสองส่วนถูกสำรองข้อมูลห่างกันหลายชั่วโมง การกู้คืนจะทำให้เกิดการผสมผสานระหว่าง log การเปลี่ยนแปลงชุดใหม่กับชุดความจำเก่า ส่งผลให้ข้อมูลที่ถูกลบไปแล้วกลับมาปรากฏอีกครั้ง ให้เขียนข้อมูลทั้งสองส่วนลงในไดเรกทอรีเดียวกันที่ระบุวันที่ไว้ เพื่อให้มั่นใจว่าจะสามารถกู้คืนข้อมูลไปพร้อมกันได้เท่านั้น การใช้งาน SQLite ในสภาพแวดล้อม production บน VPS จะอธิบายรายละเอียดเชิงลึกเกี่ยวกับการล็อก (locking), การสำรองข้อมูล และการตั้งค่าที่จำเป็นสำหรับบริการที่ต้องทำงานต่อเนื่องเป็นเวลานาน

FAQ

หน่วยความจำของเอเจนต์ควรมีอายุการใช้งานนานเท่าใดก่อนที่จะหมดอายุ?

ให้กำหนดวันหมดอายุจากข้อเท็จจริง ไม่ใช่จากค่าเริ่มต้นส่วนกลาง บันทึกการเดินทางหรือบันทึก "กำลังทำโปรเจกต์นี้ในสัปดาห์นี้" ควรมีอายุ 7 วัน ส่วนข้อตกลงของทีมหรือความชอบส่วนบุคคลไม่ควรมีวันหมดอายุ แต่ควรนำไปไว้ในคิวการตรวจสอบแทน ข้อเท็จจริงเกี่ยวกับเวอร์ชันของซอฟต์แวร์ควรมีอายุการใช้งานใกล้เคียงกับรอบการออกรุ่น (release cadence) ของโปรเจกต์นั้น หากคุณไม่สามารถระบุอายุการใช้งานได้ในขณะที่เขียนข้อเท็จจริง นั่นเป็นสัญญาณว่าข้อมูลนั้นมีความคลาดเคลื่อนไปตามกาลเวลามากกว่าที่จะเสื่อมสภาพ ดังนั้นให้กำหนดวันที่ confirmed_at และตรวจสอบข้อมูลนั้นแทนการปล่อยให้หมดอายุ

ฉันสามารถตรวจจับโดยอัตโนมัติได้หรือไม่เมื่อข้อเท็จจริงที่จัดเก็บไว้ไม่ถูกต้องแล้ว?

ไม่สามารถทำได้อย่างน่าเชื่อถือ ระบบจัดเก็บข้อมูลไม่มีมุมมองต่อโลกภายนอก จึงไม่สามารถรับรู้การเปลี่ยนแปลงที่ทำให้ข้อเท็จจริงนั้นกลายเป็นเท็จได้ และงานที่อ่านข้อมูลจากระบบจัดเก็บซ้ำก็ทำได้เพียงแค่อ่านข้อความเดิม สิ่งที่คุณสามารถทำได้โดยอัตโนมัติคือการนำข้อมูลขึ้นมาแสดงผล: ให้จัดเรียงตาม confirmed_at และนำแถวที่เก่าที่สุดมาแสดงต่อหน้าผู้ใช้งาน หรือต่อหน้าเอเจนต์ที่มีเครื่องมือสำหรับอ่านสถานะปัจจุบันจาก repository, ไฟล์ config หรือ monitoring endpoint การทำคิวอัตโนมัติเป็นสิ่งที่ควรทำ แต่การตัดสินความถูกต้องโดยอัตโนมัติยังไม่สามารถทำได้ในขณะนี้

ฉันลบหน่วยความจำไปแล้วแต่ข้อมูลกลับมาอีกครั้ง เกิดจากอะไร?

โดยปกติเกิดจากการที่มีระบบจัดเก็บข้อมูลสองแห่งและคุณเขียนข้อมูลลงไปเพียงแห่งเดียว ข้อความในหน่วยความจำและ embedding ของมันมักจะอยู่ใน vector database ในขณะที่ไฟล์ SQLite จะเก็บ log การเปลี่ยนแปลง ดังนั้นการลบแถวออกจากไฟล์ SQLite จะเป็นการลบเพียงบันทึกการตรวจสอบ แต่ยังทำให้หน่วยความจำนั้นถูกดึงกลับมาได้ ให้ลบผ่าน library API เพื่อให้ข้อมูลทั้งสองส่วนได้รับการอัปเดต อีกสาเหตุที่พบบ่อยคือการกู้คืนข้อมูล (restore) ซึ่ง vector store และไฟล์ SQLite ถูกทำ snapshot ไว้คนละช่วงเวลา การกู้คืนจึงนำแถวข้อมูลที่อีกฝั่งหนึ่งลบไปแล้วกลับมา

การแก้ไขฐานข้อมูลหน่วยความจำด้วยตนเองในขณะที่เอเจนต์กำลังทำงานอยู่มีความปลอดภัยหรือไม่?

การอ่านข้อมูลมีความปลอดภัย การเขียนข้อมูลจะปลอดภัยก็ต่อเมื่ออยู่ในโหมด write ahead logging และต้องเป็นการเขียนทีละหนึ่งรายการเท่านั้น ให้รันคำสั่ง sqlite3 memory.db "PRAGMA journal_mode;" เพื่อตรวจสอบว่าคุณอยู่ในโหมดใด และ wal คือคำตอบที่คุณต้องการ หากคุณพบ Error: database is locked แสดงว่ามีกระบวนการอื่นถือ write lock อยู่ ให้กำหนดระยะเวลารอสำหรับเซสชันของคุณด้วย sqlite3 -cmd ".timeout 5000" หรือหยุดการทำงานของ service เอเจนต์ก่อน การแก้ไข vector store ด้วยตนเองนั้นแตกต่างออกไป: ควรปล่อยให้เป็นหน้าที่ของ library เนื่องจาก embedding และข้อความต้องมีความสอดคล้องกันเสมอ