Files

54 KiB

LongTermMemory

ปลั๊กอินความจำระยะยาวสำหรับ LangBot ที่มีการออกแบบสองชั้น:

  • L1 โปรไฟล์หลัก (Core profile) ที่ฉีดเข้าไปใน prompt ของระบบ
  • L2 ความจำเหตุการณ์ (Episodic memory) ที่ดึงข้อมูลผ่านการค้นหาเวกเตอร์และฉีดเข้าไปในบริบท

สิ่งที่ปลั๊กอินทำ

  • มีเครื่องมือ remember สำหรับการเขียนความจำเหตุการณ์
  • มีเครื่องมือ recall_memory สำหรับการค้นหาความจำเหตุการณ์เชิงรุกพร้อมตัวกรองที่ควบคุมได้
  • มีเครื่องมือ update_profile สำหรับการอัปเดตโปรไฟล์ที่เสถียร
  • มีเครื่องมือ forget สำหรับการลบความจำเหตุการณ์ที่ระบุโดยเอเจนต์
  • ฉีดความจำโปรไฟล์และตัวตนของผู้พูดปัจจุบันผ่าน EventListener
  • ใช้ EventListener เพื่อดึงและฉีดความจำเหตุการณ์ที่เกี่ยวข้องก่อนการเรียกใช้โมเดล
  • มีคำสั่ง !memory สำหรับการตรวจสอบและการดีบัก
  • มีหน้าเว็บ คอนโซลความจำ (Memory Console) สำหรับการตรวจสอบแบบเห็นภาพ: โปรไฟล์, ความจำเหตุการณ์ (พร้อมการแบ่งหน้า), บันทึกการตรวจสอบ, การส่งออก, การตรวจสุขภาพการแยก metadata และสแน็ปช็อตการฉีดความจำล่าสุด
  • มีคำสั่ง !memory list [page] เพื่อเรียกดูความจำเหตุการณ์พร้อมการแบ่งหน้า
  • มีคำสั่ง !memory forget <episode_id> เพื่อลบเหตุการณ์ที่ระบุ
  • มีคำสั่ง !memory search <query> เพื่อค้นหาเหตุการณ์ (ผลลัพธ์รวมถึง ID ของเหตุการณ์)
  • มีคำสั่ง !memory export เพื่อส่งออกโปรไฟล์ L1 สำหรับเซสชันปัจจุบันเป็น JSON
  • แทนที่เหตุการณ์เก่าที่เกี่ยวข้องโดยอัตโนมัติเมื่อมีการจัดเก็บการแก้ไข/การอัปเดตข้อเท็จจริง/การชี้แจง

การออกแบบโดยรวม

ปลั๊กอินนี้ไม่ได้พยายามดัมพ์ประวัติการแชททั้งหมดลงในบริบท แต่จะแบ่งความจำระยะยาวออกเป็นสองชั้นที่มีพฤติกรรมการจัดเก็บและการดึงข้อมูลที่แตกต่างกัน:

  • L1 โปรไฟล์หลัก: ข้อเท็จจริงที่เสถียรและความถี่ต่ำ เช่น ชื่อ, ความชอบ, ตัวตน และบันทึกที่มีอายุยืนยาว
  • L2 ความจำเหตุการณ์: ข้อเท็จจริงที่อ่อนไหวต่อเวลาและตามสถานการณ์ เช่น เหตุการณ์ล่าสุด, แผนการ และประสบการณ์

การแบ่งแยกนี้มีเหตุผล:

  • ข้อมูลโปรไฟล์ที่เสถียรมีราคาถูกและเชื่อถือได้ในการฉีดเข้าไปใน prompt ของระบบ
  • ความจำเหตุการณ์จะเติบโตขึ้นเรื่อยๆ ตามเวลา ดังนั้นจึงควรดึงข้อมูลตามความต้องการแทนที่จะฉีดเข้าไปทั้งหมดในทุกเทิร์น
  • เอเจนต์ควรอัปเดตข้อเท็จจริงโปรไฟล์ที่เสถียรแตกต่างจากความจำประเภทเหตุการณ์

ความแตกต่างจากความจำสไตล์ผู้ช่วยส่วนตัวแบบ OpenClaw

เมื่อเร็วๆ นี้ ระบบเอเจนต์จำนวนมากได้พูดถึงการออกแบบเช่น OpenClaw: ความจำระยะยาวที่จัดเก็บในรูปแบบไฟล์ข้อความที่ผู้ใช้สามารถอ่านได้เป็นหลัก เช่น MEMORY.md รวมกับการสรุป, การไตร่ตรอง และตรรกะการดึงข้อมูลแบบเบา

แนวทางนั้นมีจุดแข็งที่ชัดเจน:

  • ความจำมีความโปร่งใสต่อผู้ใช้อย่างสมบูรณ์
  • ข้อความธรรมดานั้นง่ายต่อการสำรองข้อมูล, ซิงค์ และควบคุมเวอร์ชันตามธรรมชาติ
  • เหมาะอย่างยิ่งสำหรับเวิร์กโฟลว์ส่วนตัวที่มีผู้ใช้คนเดียว, ผู้ช่วยคนเดียว และมีความต่อเนื่องสูง
  • เมื่อปริมาณความจำยังน้อย การทำความเข้าใจข้อความทั้งหมดก็อาจจะ "ดีพอ"

แต่ LongTermMemory ใน LangBot กำลังแก้ปัญหาที่แตกต่างออกไป การใช้งาน LangBot โดยทั่วไปจะมีลักษณะดังนี้:

  • บอทหนึ่งตัวให้บริการกลุ่มแชทและแชทส่วนตัวหลายรายการ
  • อินสแตนซ์ปลั๊กอินหนึ่งรายการจัดการเซสชันและผู้พูดหลายคน
  • ความจำรวมถึงบริบทกลุ่มที่แชร์, โปรไฟล์ผู้พูดปัจจุบัน และข้อเท็จจริงเหตุการณ์ระดับเซสชัน
  • ขอบเขตการแยก (isolation) ที่ชัดเจนระหว่างเซสชัน, บอท และผู้พูด

ด้วยเหตุนี้ เราจึงไม่ได้ใช้การออกแบบ "ไฟล์ข้อความเดียวเป็นแหล่งข้อมูลความจริง (source of truth)" เราเลือกสถาปัตยกรรมแบบเป็นชั้นที่ตรงกับโมเดลรันไทม์แบบหลายเซสชันของ LangBot ได้ดีกว่า

ความจำแบบ OpenClaw ถูกปรับแต่งมาเพื่ออะไร

ในเชิงนามธรรม การออกแบบนั้นถูกปรับแต่งมาเพื่อ:

  • ผู้ช่วยส่วนตัวสำหรับผู้ใช้รายเดียว
  • ข้อความที่มนุษย์อ่านได้เป็นรูปแบบหลักของความจำระยะยาว
  • ความโปร่งใส, ความสามารถในการแก้ไข และความต่อเนื่องของการเล่าเรื่อง
  • สมมติฐานที่ว่าขนาดความจำจะยังคงจัดการได้และผู้ใช้ยินดีที่จะดูแลจัดการโดยตรง

นั่นเป็นสิ่งที่เหมาะสมอย่างยิ่งสำหรับเพื่อน AI ส่วนตัว, ผู้ช่วยวิจัย และเวิร์กโฟลว์ผู้ช่วยส่วนตัว

ทำไม LangBot จึงไม่เพียงแค่เลียนแบบโมเดลนั้น

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

หากเราเปลี่ยนความจำระยะยาวให้เป็นไฟล์เล่าเรื่องเพียงไฟล์เดียวเช่น MEMORY.md ปัญหาหลายอย่างจะปรากฏขึ้นอย่างรวดเร็ว:

  • การแยกส่วนจะกลายเป็นเรื่องยาก
    • ความจำจากกลุ่ม A, กลุ่ม B และแชทส่วนตัว C จะอยู่ร่วมกันอย่างปลอดภัยได้อย่างไร?
    • คุณจะแยกโปรไฟล์ที่เสถียรของผู้พูดคนเดียวออกจากบันทึกการเล่าเรื่องที่แชร์ได้อย่างไร?
  • ความละเอียดของการฉีดจะกลายเป็นไม่เสถียร
    • prompt ของระบบต้องการสถานะโปรไฟล์ที่เสถียร ไม่ใช่ไดอารี่ตามลำดับเวลาทั้งหมด
    • การเรียกคืนอัตโนมัติพ้องต้องการชิ้นส่วนความจำที่เกี่ยวข้องมากที่สุดสำหรับคำถามปัจจุบัน ไม่ใช่เรื่องราวทั้งหมด
  • ขอบเขตผู้ใช้หลายคนเป็นสิ่งสำคัญอันดับแรกใน LangBot
    • ในผู้ช่วยส่วนตัว "ผู้ใช้" มักจะเป็นคนคนเดียว
    • ใน LangBot ผู้พูดปัจจุบัน, เซสชันปัจจุบัน และบอทปัจจุบันล้วนมีความสำคัญ
  • การฉีดอัตโนมัติและการดึงข้อมูลเชิงรุกเป็นความต้องการที่แตกต่างกัน
    • ข้อมูลโปรไฟล์ที่เสถียรควรได้รับการฉีดอย่างสม่ำเสมอ
    • ความจำเหตุการณ์ควรได้รับการดึงข้อมูลแบบเลือกสรร
    • การบังคับให้ทั้งคู่เข้าสู่รูปแบบความจำแบบข้อความอย่างเดียวจะกลายเป็นเรื่องที่น่าอึดอัด

สิ่งที่เราเลือกแลกเปลี่ยน (Tradeoff)

ดังนั้นการออกแบบ LongTermMemory จึงเป็นการแลกเปลี่ยนที่สำคัญดังนี้:

  • สิ่งที่เรายืมมาจากปรัชญานั้น

    • ความจำไม่ควรถูกปฏิบัติเป็นเพียงที่เก็บเวกเตอร์แบบกล่องดำ
    • โปรไฟล์ที่เสถียร, ความจำตามเวลา และการปรับพฤติกรรมระยะยาวล้วนมีความสำคัญ
    • ไม่ใช่ทุกอย่างที่ควรถูกดัมพ์ลงในบริบทในทุกเทิร์น
  • จุดที่เราแตกต่างอย่างจงใจ

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

โดยสรุป:

  • OpenClaw ส่วนใหญ่ตอบคำถาม: "ผู้ช่วยส่วนตัวควรเก็บความจำระยะยาวที่อ่านได้, แก้ไขได้ และสะท้อนความคิดได้อย่างไร?"
  • LongTermMemory ส่วนใหญ่ตอบคำถาม: "บอทที่ทำงานข้ามกลุ่มและแชทส่วนตัวควรเก็บสถานะโปรไฟล์ที่เสถียรและความจำประสบการณ์ที่ดึงข้อมูลได้ภายใต้กฎการแยกที่ชัดเจนได้อย่างไร?"

ไม่มีทิศทางใดที่ "ดีกว่า" ในระดับสากล ทั้งคู่ปรับแต่งมาเพื่อผลิตภัณฑ์และโหมดความล้มเหลวที่แตกต่างกัน

การออกแบบ

ปลั๊กอินนี้ตั้งใจที่จะอยู่ใกล้กับจุดขยายที่มีอยู่ของ LangBot แทนที่จะต้องการแพตช์หลักที่กำหนดเอง

  • โปรไฟล์ L1 ถูกจัดเก็บในที่เก็บข้อมูลปลั๊กอินในรูปแบบ JSON
  • ความจำเหตุการณ์ L2 ถูกจัดเก็บในฐานข้อมูลเวกเตอร์
  • การดึงข้อมูลความจำถูกเปิดใช้งานต่อไปป์ไลน์โดยการแนบ KnowledgeEngine ของปลั๊กอินนี้
  • ปัจจุบันปลั๊กอินสมมติว่ามี KB ความจำเพียงรายการเดียวต่ออินสแตนซ์ปลั๊กอิน และแยกความจำตามเมทาดาตา

การใช้งานปัจจุบันสร้างขึ้นบน LangBot และ SDK API ที่มีอยู่ หากในภายหลัง LangBot เพิ่ม API ที่เน้นความจำชัดเจนยิ่งขึ้น, API ตัวตนเซสชัน หรือ API การลงทะเบียน KB ปลั๊กอินอาจจะเรียบง่ายขึ้น แต่สถาปัตยกรรมปัจจุบันจะยังคงใช้ได้

ความเข้ากันได้ของแบ็กเอนด์ฐานข้อมูลเวกเตอร์

ความจำเหตุการณ์ L2 พึ่งพาฟิลด์เมทาดาตาที่กำหนดเอง (user_key, episode_id, tags, importance, ฯลฯ) สำหรับการแยกและการกรอง ไม่ใช่แบ็กเอนด์ฐานข้อมูลเวกเตอร์ของ LangBot ทั้งหมดที่รองรับเมทาดาตาที่กำหนดเอง:

แบ็กเอนด์ เมทาดาตาที่กำหนดเอง การรองรับ LongTermMemory
Chroma (ค่าเริ่มต้น) ใช่ รองรับเต็มรูปแบบ
Qdrant ใช่ รองรับเต็มรูปแบบ
SeekDB ใช่ รองรับเต็มรูปแบบ
Milvus ไม่ (โครงสร้างคงที่: text, file_id, chunk_uuid) ไม่รองรับ
pgvector ไม่ (โครงสร้างคงที่: text, file_id, chunk_uuid) ไม่รองรับ

Milvus และ pgvector ใช้โครงสร้างคอลัมน์คงที่และจะทิ้งฟิลด์เมทาดาตาที่ไม่รู้จักโดยไม่แจ้งให้ทราบ ซึ่งหมายความว่าการแยกตามเมทาดาตา (การกรอง user_key) และคำสั่งความจำเหตุการณ์ (!memory list, !memory forget, !memory search) จะทำงานไม่ถูกต้องบนแบ็กเอนด์เหล่านี้ — ตัวกรองจะถูกละเลยและคำถามอาจส่งคืนผลลัพธ์ที่ไม่อยู่ในขอบเขต

หากคุณต้องการใช้ LongTermMemory โปรดใช้ Chroma, Qdrant หรือ SeekDB เป็นแบ็กเอนด์ฐานข้อมูลเวกเตอร์ของคุณ

วิธีการทำงาน

โฟลวความจำระยะยาวแบบครบวงจรมีสี่ส่วนหลัก:

1. การเขียนโปรไฟล์ L1

  • เอเจนต์ใช้ update_profile เพื่อเขียนข้อเท็จจริงที่เสถียร
  • ข้อมูลจะถูกจัดเก็บในที่เก็บข้อมูลปลั๊กอินในรูปแบบ JSON ที่มีโครงสร้าง
  • โปรไฟล์จะถูกจัดเก็บที่ขอบเขต session (เซสชัน) หรือ speaker (ผู้พูด)

2. การเขียนเหตุการณ์ L2

  • เอเจนต์ใช้ remember เพื่อเขียนความจำประเภทเหตุการณ์
  • ความจำแต่ละรายการจะมีเมทาดาตา เช่น ประทับเวลา, ความสำคัญ, แท็ก และขอบเขต
  • ความจำเหล่านั้นจะถูกทำ embedding และจัดเก็บในฐานข้อมูลเวกเตอร์ผ่าน KnowledgeEngine ของปลั๊กอิน

3. การฉีดข้อมูลอัตโนมัติก่อนการตอบกลับ

  • ในระหว่าง PromptPreProcessing ตัว EventListener จะตรวจสอบตัวตนของเซสชันปัจจุบัน
  • สำหรับ L1:
    • จะโหลดโปรไฟล์เซสชันที่แชร์
    • จะโหลดโปรไฟล์ผู้พูดปัจจุบัน
    • จะฉีดทั้งคู่ พร้อมกับตัวตนของผู้พูดปัจจุบัน เข้าไปใน default_prompt
  • สำหรับ L2:
    • จะรันการดึงข้อมูลเหตุการณ์หนึ่งครั้งโดยใช้ข้อความของผู้ใช้ปัจจุบัน
    • ความจำที่ดึงมาจะถูกฉีดเป็นบล็อกบริบทข้อเท็จจริง

ดังนั้นทั้ง L1 และ L2 จะเข้าสู่บริบทของโมเดลก่อนการสร้างคำตอบ แต่ในรูปแบบที่แตกต่างกัน: L1 เป็นความจำใน prompt ของระบบ, L2 เป็นบริบทที่ดึงมา

4. การค้นหาเชิงรุกและการดีบัก

  • หากการฉีดอัตโนมัติไม่เพียงพอ เอเจนต์สามารถเรียกใช้ recall_memory
  • สำหรับการตรวจสอบและการดีบัก คุณสามารถใช้ !memory, !memory profile, !memory search, !memory list และ !memory forget
  • !memory export จะส่งออกเฉพาะโปรไฟล์ L1 ของขอบเขตปัจจุบันสำหรับการสำรองข้อมูลหรือการย้ายข้อมูล

ความสัมพันธ์กับ AgenticRAG

เมื่อเปิดใช้งาน AgenticRAG พร้อมกับ LongTermMemory:

  • LongTermMemory จะนำ KB ความจำของตัวเองออกจากการประมวลผล RAG แบบทั่วไป
  • การเรียกคืน L2 อัตโนมัติยังคงได้รับการจัดการโดย LongTermMemory เอง
  • KB ความจำเดียวกันยังคงสามารถสอบถามได้ชัดเจนผ่านเครื่องมือ query_knowledge ของ AgenticRAG

สิ่งนี้ช่วยหลีกเลี่ยงการเรียกคืนที่ซ้ำซ้อนในขณะที่ยังคงรักษาทั้งสองเส้นทาง:

  • การเรียกคืนความจำอัตโนมัติ
  • การดึงข้อมูลที่ลึกซึ้งยิ่งขึ้นโดยเอเจนต์เมื่อจำเป็น

ทำไมจึงไม่มีตัวกรองเมทาดาตาฝั่งเอเจนต์

รันไทม์ที่อยู่เบื้องหลังสามารถรองรับการกรองเมทาดาตาได้ แต่ปลั๊กอินนี้ไม่ได้เปิดเผยตัวกรองเมทาดาตาแบบดิบให้กับโฟลวของเอเจนต์ในวันนี้

เหตุผล:

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

หากในภายหลัง LangBot มีวิธีการรวมศูนย์เพื่ออธิบายฟิลด์เมทาดาตาที่กรองได้ต่อฐานความรู้ ก็สามารถเพิ่มการกรองเมทาดาตาฝั่งเอเจนต์ได้

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

รูปแบบการแยก (Isolation Model)

รองรับโหมดการแยกสองโหมด:

  • session: แต่ละกลุ่มแชทหรือแชทส่วนตัวมีความจำที่เป็นอิสระต่อกัน
  • bot: ทุกเซสชันภายใต้บอทเดียวกันจะแชร์ความจําร่วมกัน

ในรูปแบบการใช้งานปัจจุบัน โดยทั่วไปเพียงพอแล้วเนื่องจากอินสแตนซ์ปลั๊กอินมักจะผูกกับสภาพแวดล้อมรันไทม์/บอท LangBot ที่เฉพาะเจาะจง

รายละเอียดกฎการแยก

มีแนวคิดขอบเขตที่เกี่ยวข้องแต่แตกต่างกันเล็กน้อยสองประการในปลั๊กอินนี้:

  • session_name: ตัวตนของการสนทนาที่ส่งผ่านเส้นทางคำถาม / การดึงข้อมูลปัจจุบัน รูปแบบเป็น {launcher_type}_{launcher_id}
  • session_key: คีย์จัดเก็บ L1 ภายในของปลั๊กอิน เมื่อมี bot_uuid จะกลายเป็น {bot_uuid}:{launcher_type}_{launcher_id} มิฉะนั้นจะกลับไปเป็น {launcher_type}_{launcher_id}
  • scope_key / user_key: คีย์จริงที่ใช้สำหรับการจัดเก็บโปรไฟล์หรือการแยกการดึงข้อมูล L2

วิธีแยกโปรไฟล์ L1

โปรไฟล์ L1 จะถูกจัดเก็บไว้ในขอบเขตการสนทนาปัจจุบันเสมอ:

  • session profile
    • โปรไฟล์ที่แชร์สำหรับการสนทนาปัจจุบัน
    • มีประโยชน์สำหรับบริบทที่เสถียรในระดับกลุ่มหรือระดับการสนทนา
  • speaker profile
    • ข้อเท็จจริงที่เสถียรเกี่ยวกับผู้พูดปัจจุบัน
    • มีประโยชน์สำหรับความชอบส่วนบุคคล, ตัวตน และบันทึกเฉพาะบุคคล

ด้วยเหตุนี้ !memory export จึงส่งออกเฉพาะโปรไฟล์ที่เป็นของ session_key ปัจจุบันเท่านั้น ไม่ใช่ทุกโปรไฟล์ในอินสแตนซ์ปลั๊กอินทั้งหมด

วิธีแยกความจำเหตุการณ์ L2

ความจำ L2 จะถูกเขียนลงในที่เก็บเวกเตอร์พร้อมเมทาดาตาการแยก จากนั้นจะถูกกรองในเวลาที่ดึงข้อมูล:

  • session
    • ความจำจากกลุ่ม A จะไม่ถูกเรียกคืนในกลุ่ม B
    • ความจำจากแชทส่วนตัวหนึ่งจะไม่ถูกเรียกคืนในอีกแชทหนึ่ง
  • bot
    • ทุกเซสชันภายใต้บอทเดียวกันจะแชร์พื้นที่ความจำเหตุการณ์เดียวกัน
    • มีประโยชน์เมื่อคุณต้องการแชร์ประสบการณ์ระยะยาวข้ามเซสชัน

เมื่อมี sender_id ปลั๊กอินยังสามารถเลือกความจำที่เกี่ยวข้องกับผู้พูดก่อนที่จะขยายไปยังขอบเขตที่กว้างขึ้น

ทำไมการแยก L1 และ L2 จึงไม่เหมือนกันทุกประการ

นั่นเป็นความตั้งใจ:

  • L1 ทำตัวเหมือนสถานะโปรไฟล์ที่เสถียร ดังนั้นการจัดเก็บเซสชัน / ผู้พูดที่แม่นยำจึงเหมาะสม
  • L2 ทำตัวเหมือนฐานประสบการณ์ที่ดึงข้อมูลได้ ดังนั้นการกรองตามเมทาดาตาจึงเป็นโมเดลที่ปรับขนาดได้ดีกว่า
  • สิ่งนี้ทำให้ L1 แม่นยำและ L2 ยืดหยุ่น

วิธีใช้

  1. ติดตั้งและเปิดใช้งานปลั๊กอิน
  2. สร้างฐานความรู้ความจำหนึ่งรายการด้วย KnowledgeEngine ของปลั๊กอินนี้
  3. กำหนดค่า:
    • embedding_model_uuid
    • isolation
    • (ตัวเลือก) recency_half_life_days
    • (ตัวเลือก) auto_recall_top_k
  4. ให้เอเจนต์ใช้:
    • remember สำหรับเหตุการณ์, แผนการ และข้อเท็จจริงเหตุการณ์
    • recall_memory สำหรับการค้นหาความจำเชิงรุกเมื่อการเรียกคืนอัตโนมัติไม่เพียงพอ
    • update_profile สำหรับความชอบที่เสถียรและข้อมูลโปรไฟล์
    • forget เพื่อลบความจำเหตุการณ์ที่ระบุโดย ID
  5. ใช้ !memory, !memory profile, !memory search <query>, !memory list [page], !memory forget <id> และ !memory export เพื่อตรวจสอบพฤติกรรม

การแชร์บริบทสำหรับปลั๊กอินอื่น

LongTermMemory จะเขียนสรุปบริบทที่มีโครงสร้างไปยังตัวแปรคำถาม _ltm_context ในทุกเหตุการณ์ PromptPreProcessing ปลั๊กอินอื่นสามารถอ่านตัวแปรนี้เพื่อทำการตัดสินใจตามโปรแกรมตามความจำของผู้ใช้ โดยไม่ต้องนำเข้าหรืออ้างอิง LongTermMemory ในทางใดทางหนึ่ง

คีย์ตัวแปร

_ltm_context

โครงสร้าง (Schema)

{
    "speaker": {
        "id": "user_123",           # sender_id, อาจเป็นสตริงว่าง
        "name": "Alice",            # sender_name, อาจเป็นสตริงว่าง
    },
    "session_profile": {            # มีอยู่เสมอ, ฟิลด์อาจว่างเปล่า
        "name": "",
        "traits": ["creative", "analytical"],
        "preferences": ["prefers detailed explanations"],
        "notes": "",
        "updated_at": "2025-03-16T12:00:00Z",
    },
    "speaker_profile": {            # เป็น null เมื่อไม่มี sender_id
        "name": "Alice",
        "traits": ["extroverted"],
        "preferences": ["likes humor"],
        "notes": "",
        "updated_at": "2025-03-16T12:00:00Z",
    },
    "episodes": [                   # ความจำเหตุการณ์ L2 ที่เรียกคืนอัตโนมัติ, อาจว่างเปล่า
        {"content": "User mentioned a trip to Beijing last week"},
    ],
}

ตัวอย่างการใช้งาน

from langbot_plugin.api.definition.components.common.event_listener import EventListener
from langbot_plugin.api.entities import events, context
from langbot_plugin.api.entities.builtin.provider.message import Message


class PersonalityCustomizer(EventListener):
    def __init__(self):
        super().__init__()

        @self.handler(events.PromptPreProcessing)
        async def on_prompt(event_ctx: context.EventContext):
            ltm = await event_ctx.get_query_var("_ltm_context")
            if not ltm:
                # ไม่ได้ติดตั้ง LongTermMemory หรือไม่ได้ใช้งาน — ใช้ค่าเริ่มต้น
                return

            profile = ltm.get("speaker_profile") or ltm.get("session_profile") or {}
            traits = profile.get("traits", [])

            if "ชอบความตลก" in traits:
                style = "Use a humorous and playful tone."
            elif "ชอบความกระชับ" in traits:
                style = "Be concise and direct."
            else:
                return

            event_ctx.event.default_prompt.append(
                Message(role="system", content=style)
            )

บันทึกการออกแบบ

  • หากไม่ได้ติดตั้ง LongTermMemory _ltm_context จะไม่มีอยู่ ปลั๊กอินที่เรียกใช้ควรจัดการกับ None เป็นปกติและกลับไปใช้พฤติกรรมเริ่มต้น
  • หาก LongTermMemory เปิดใช้งานอยู่แต่ยังไม่มีการจัดเก็บข้อมูลโปรไฟล์ ตัวแปรจะมีอยู่แต่ฟิลด์จะว่างเปล่า สิ่งนี้ช่วยให้ปลั๊กอินที่เรียกใช้สามารถแยกแยะระหว่าง "ไม่มีปลั๊กอินความจำ" กับ "ปลั๊กอินความจำทำงานอยู่แต่ยังไม่มีข้อมูล"
  • ทั้งสองฝ่ายพึ่งพาเพียงคีย์ตัวแปรและข้อกำหนดโครงสร้างเท่านั้น ไม่พึ่งพาโค้ดของกันและกัน หาก LongTermMemory ถูกแทนที่ด้วยปลั๊กอินความจำอื่นที่เขียนคีย์เดียวกันด้วยโครงสร้างเดียวกัน ปลั๊กอินที่เรียกใช้จะยังคงทำงานได้
  • LongTermMemory ต้องรันก่อนปลั๊กอินที่เรียกใช้ในลำดับการจัดส่งเหตุการณ์ ในทางปฏิบัติสิ่งนี้ขึ้นอยู่กับลำดับการติดตั้งปลั๊กอิน

การนำเข้า / ส่งออก

  • การส่งออก (โปรไฟล์ L1): ใช้ !memory export เพื่อส่งออกโปรไฟล์เซสชันและผู้พูดของขอบเขตปัจจุบันเป็น JSON จะไม่มีการส่งออกข้อมูลจากเซสชันหรือขอบเขตอื่น
  • การนำเข้า (ความจำเหตุการณ์ L2): อัปโหลดไฟล์ JSON ผ่าน UI ฐานความรู้ของ LangBot เพื่อนำเข้าความจำเหตุการณ์จำนวนมาก
  • ความจำเหตุการณ์ L2 สามารถเรียกดูได้ ผ่าน !memory list [page] และสามารถลบเหตุการณ์แต่ละรายการได้ผ่าน !memory forget <id> การส่งออกจำนวนมากทั้งหมดยังไม่ได้รับการพัฒนา

คำถามทางเทคนิคที่สำคัญ

Q1. ทำไมจึงแบ่งความจำเป็น L1 และ L2 แทนที่จะเก็บทุกอย่างไว้ในฐานข้อมูลเวกเตอร์?

เพราะรูปแบบการเข้าถึงนั้นแตกต่างกัน:

  • L1 ประกอบด้วยข้อเท็จจริงที่เสถียรและควรถูกฉีดอย่างสม่ำเสมอ
  • L2 ประกอบด้วยความจำประเภทเหตุการณ์และควรถูกดึงข้อมูลตามความต้องการ

การใส่ทั้งคู่ลงในที่เก็บเวกเตอร์จะทำให้การเรียกคืนโปรไฟล์ที่เสถียรมีความน่าเชื่อถือน้อยลง และทำให้การอัปเดตความจำยุ่งเหยิงในเชิงความหมาย

Q2. ทำไม L2 จึงถูกดึงข้อมูลแทนที่จะฉีดเข้าไปทั้งหมดในทุกเทิร์น?

เพราะ L2 เติบโตตามเวลา การฉีดเข้าไปทั้งหมดจะทำให้เกิด:

  • prompt บวม
  • เสียงรบกวนที่ไม่เกี่ยวข้องมากเกินไป
  • ความจำเก่าไปเบียดบริบทที่เกี่ยวข้องจริงๆ

กลยุทธ์ปัจจุบันคือการดึงชุดย่อยที่เกี่ยวข้องขนาดเล็กโดยอัตโนมัติ จากนั้นให้เอเจนต์ใช้ recall_memory หากต้องการข้อมูลเพิ่มเติม

Q3. ความจำ L2 จะเสื่อมลงตามเวลาหรือไม่?

ใช่

การจัดอันดับ L2 ไม่ได้ขึ้นอยู่กับความคล้ายคลึงของเวกเตอร์เพียงอย่างเดียว นอกจากนี้ยังมีการใช้การเสื่อมถอยตามเวลา (time decay) เพื่อให้ความจำที่ใหม่กว่ามีแนวโน้มที่จะได้อันดับสูงกว่าความจำเก่า

การใช้งานปัจจุบันใช้วิธีแบบครึ่งชีวิต (half-life style):

  • เมื่อความจำถึง half_life_days น้ำหนักตามเวลาจะลดลงเหลือประมาณ 50%
  • ความจำใหม่จะได้รับความสำคัญในการจัดอันดับ
  • ความจำเก่าจะไม่ถูกลบโดยอัตโนมัติ เพียงแต่เสียเปรียบในการจัดอันดับเท่านั้น

สิ่งนี้มีไว้เพื่อให้ความสำคัญกับบริบทล่าสุด ไม่ใช่การลบอดีตทิ้งอย่างถาวร

Q4. ความจำเก่าจะหายไปอย่างสมบูรณ์ในที่สุดหรือไม่?

ไม่หายไปโดยอัตโนมัติ

การเสื่อมตามเวลาส่งผลต่อการจัดอันดับ ไม่ใช่การลบข้อมูลถาวร ความจำเก่าจะยังคงถูกเรียกคืนได้หากยังคงมีความเกี่ยวข้องเพียงพอ

Q5. ฉันควรเลือกระหว่างการแยกแบบ session และ bot อย่างไร?

ในทางปฏิบัติ:

  • เลือก session
    • เมื่อแต่ละกลุ่มแชท / แชทส่วนตัวควรเก็บความจำที่เป็นอิสระต่อกัน
    • เมื่อคุณต้องการความเสี่ยงที่ต่ำกว่าของการรั่วไหลข้ามเซสชัน
  • เลือก bot
    • เมื่อบอทควรแชร์ประสบการณ์ระยะยาวข้ามเซสชัน
    • เมื่อการเรียกคืนในวงกว้างมีความสำคัญมากกว่าการแยกที่เข้มงวด

หากคุณไม่แน่ใจ ให้เริ่มต้นด้วย session

Q6. ทำไม !memory export จึงส่งออกเฉพาะขอบเขตปัจจุบัน?

นั่นเป็นขอบเขตความปลอดภัยที่ตั้งใจไว้

การอนุญาตให้ส่งออกทุกโปรไฟล์ L1 ในอินสแตนซ์ปลั๊กอินจะทำให้ข้อมูลรั่วไหลข้ามเซสชันได้ง่ายขึ้นมาก การจำกัดการส่งออกไว้ที่ขอบเขตปัจจุบันเป็นไปตามหลักการเปิดเผยข้อมูลให้น้อยที่สุด (minimum-exposure principle)

Q7. จะเกิดอะไรขึ้นหากรันไทม์ไม่ได้เปิดเผย _knowledge_base_uuids ในตัวแปรคำถาม?

การฉีดความจำอัตโนมัติจะยังคงทำงานได้ แต่ปลั๊กอินจะไม่สามารถนำ KB ความจำของตัวเองออกจากการประมวลผล RAG แบบทั่วไปได้

นั่นอาจนำไปสู่การเรียกคืนความจำที่ซ้ำซ้อน:

  • สำเนาหนึ่งถูกฉีดโดย LongTermMemory เอง
  • อีกสำเนาถูกเรียกคืนอีกครั้งโดยโฟลว KB ทั่วไปของรันไทม์

ดังนั้นนี่ไม่ใช่ความล้มเหลวอย่างสิ้นเชิง แต่จะทำให้เสียบริบทและทำให้ prompt มีเสียงรบกวนมากขึ้น

Q8. ทำไมจึงยังไม่รองรับการส่งออก L2?

ตอนนี้ SDK มี API vector_list สำหรับการระบุรายการเนื้อหาในที่เก็บเวกเตอร์แบบแบ่งหน้าแล้ว ความจำเหตุการณ์ L2 สามารถเรียกดูได้ผ่าน !memory list [page] และลบเป็นรายบุคคลได้ผ่าน !memory forget <episode_id> หรือเครื่องมือ forget

การส่งออกจำนวนมากทั้งหมดยังไม่ได้รับการพัฒนา แต่ส่วนประกอบพื้นฐานพร้อมแล้ว

Q9. LongTermMemory และ AgenticRAG จะทำให้เกิดการเรียกคืนที่ซ้ำซ้อนหรือไม่เมื่อเปิดใช้งานทั้งคู่?

ไม่ การซ้ำซ้อนนั้นเป็นสิ่งที่การออกแบบปัจจุบันหลีกเลี่ยง:

  • LongTermMemory จะนำการประมวลผล RAG แบบทั่วไปของตัวเองออก
  • การเรียกคืน L2 อัตโนมัติถูกจัดการโดย LongTermMemory
  • การดึงข้อมูลแบบเฉพาะกิจที่ลึกซึ้งยิ่งขึ้นยังคงสามารถผ่าน AgenticRAG ได้

คอนโซลความจำ (Memory Console)

นอกเหนือจากคำสั่ง !memory ปลั๊กอินยังมาพร้อมหน้าเว็บ คอนโซลความจำ (ลงทะเบียนเป็นคอมโพเนนต์ Page ของปลั๊กอิน) สำหรับการตรวจสอบแบบเห็นภาพและการบำรุงรักษา มันสื่อสารกับปลั๊กอินผ่าน endpoint ของหน้าที่จำกัดขอบเขต และจะไม่ออกนอกพื้นที่ความจำที่เลือกไว้

คอนโซลจัดวางเป็นแถบด้านข้างของพื้นที่ พร้อมแผงแบบแท็บ:

  • แถบด้านข้างพื้นที่ความจำ —— เลือกขอบเขตที่มีอยู่ หรือกรอก bot_uuid / session_name / isolation แล้วคลิก Derive เพื่อคำนวณ scope_key และ user_key ที่ตรงกัน พื้นที่ Advanced / Raw จะแสดงคีย์ภายใน
  • โปรไฟล์ —— ดูโปรไฟล์เซสชันและโปรไฟล์ผู้พูดปัจจุบันของขอบเขตที่เลือก
  • ความจำเหตุการณ์ —— แสดงรายการ (แบ่งหน้า) หรือค้นหาเชิงความหมายของความจำเหตุการณ์ L2 สำหรับ user_key ปัจจุบัน กรองตามสถานะวงจรชีวิต; เก็บถาวร กู้คืน หรือลบแต่ละรายการได้ทันที ทุกการแก้ไขจะเขียนรายการตรวจสอบตามขอบเขต เช่นเดียวกับคำสั่ง !memory ที่เทียบเท่ากัน
  • การฉีด (Injection) —— โหลด สแน็ปช็อตการฉีดความจำล่าสุด ของขอบเขต: รอบล่าสุดมีการฉีดอะไรหรือไม่ กี่บล็อกและกี่อักขระ ผู้พูดคนใด โปรไฟล์ที่ใช้ และความจำเหตุการณ์ L2 ที่ถูกเรียกคืนอัตโนมัติ ตอบคำถาม "บอตจำอะไรได้ในรอบที่แล้ว?" โดยไม่ต้องไล่ดู log
  • การตรวจสอบ (Audit) —— เรียกดู (แบ่งหน้า) และส่งออกบันทึกการตรวจสอบตามขอบเขต
  • ส่งออก —— ส่งออกโปรไฟล์ L1 และความจำเหตุการณ์ L2 ของขอบเขตปัจจุบันเป็น JSON
  • การวินิจฉัย —— Run Health Check จะรันโพรบการแยก metadata เดียวกันกับ !memory health โดยตรงจาก UI (การตั้งค่า KB, โมเดล embedding และการแยกตัวกรอง user_key บน search / list / delete) แสดงเป็นการ์ดสถานะสีแดง/เหลือง/เขียว หมายเหตุ: คอนโซลไม่สามารถยืนยันได้ว่า KB ถูกแนบกับ pipeline ใดโดยเฉพาะ ให้รัน !memory health ภายใน pipeline เพื่อยืนยันส่วนนั้น

คอนโซลรองรับ i18n (en_US, zh_Hans) และปรับตามธีมสว่าง/มืดของ LangBot โฮสต์

ส่วนประกอบ

ช่องว่างในปัจจุบัน

ตอนนี้ README ครอบคลุมการออกแบบหลัก, กฎการแยก, ขอบเขตการส่งออก และส่วนประกอบหลักแล้ว

สิ่งที่ควรเพิ่มในภายหลัง:

  • การอัปเดตเอกสารที่แปลแล้วให้ตรงกัน
  • ตัวอย่างการนำเข้า JSON ที่เป็นรูปธรรม
  • ตัวอย่างแนวทางปฏิบัติที่ดีที่สุดสำหรับ remember, recall_memory และ update_profile

การบันทึก日誌 (Logging)

ตอนนี้ปลั๊กอินจะพ่น log ที่จุดสำคัญของวงจรความจำ เพื่อให้คุณสามารถสังเกตการทำงานของความจำระยะยาวระหว่างรันไทม์ได้

คุณจะเห็น log สำหรับ:

  • การเริ่มทำงานของปลั๊กอินและบริบทความจำที่ตรวจสอบได้
  • การเรียกใช้เครื่องมือ remember, recall_memory และ update_profile
  • การฉีดโปรไฟล์ก่อนการเรียกใช้โมเดล
  • การดึงข้อมูลความจำ L2 อัตโนมัติใน KnowledgeEngine
  • การเขียนเวกเตอร์ความจำเหตุการณ์, การค้นหา, การนำเข้าเป็นกลุ่ม และการลบ

ข้อความ log ทั่วไปมีลักษณะดังนี้:

[LongTermMemory] remember called: query_id=123 params_keys=['content', 'importance', 'tags']
[LongTermMemory] memory injection ready: query_id=123 kb_id=kb-1 scope_key=bot:xxx:group_123 sender_id=u1 block_count=2 prompt_chars=280
[LongTermMemory] engine retrieve called: collection_id=kb-1 top_k=5 session_name=group_123 sender_id=u1 bot_uuid=bot-1 query='user asked about travel plan'
[LongTermMemory] search_episodes completed: collection_id=kb-1 result_count=3 filters={'user_key': 'bot:bot-1:group_123'}