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 ยืดหยุ่น
วิธีใช้
- ติดตั้งและเปิดใช้งานปลั๊กอิน
- สร้างฐานความรู้ความจำหนึ่งรายการด้วย KnowledgeEngine ของปลั๊กอินนี้
- กำหนดค่า:
embedding_model_uuidisolation- (ตัวเลือก)
recency_half_life_days - (ตัวเลือก)
auto_recall_top_k
- ให้เอเจนต์ใช้:
rememberสำหรับเหตุการณ์, แผนการ และข้อเท็จจริงเหตุการณ์recall_memoryสำหรับการค้นหาความจำเชิงรุกเมื่อการเรียกคืนอัตโนมัติไม่เพียงพอupdate_profileสำหรับความชอบที่เสถียรและข้อมูลโปรไฟล์forgetเพื่อลบความจำเหตุการณ์ที่ระบุโดย ID
- ใช้
!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 โฮสต์
ส่วนประกอบ
- KnowledgeEngine: memory_engine.py
- EventListener: memory_injector.py
- เครื่องมือ: remember.py, recall_memory.py, update_profile.py, forget.py
- คำสั่ง: memory.py
- Page: memory_console.py, index.html
ช่องว่างในปัจจุบัน
ตอนนี้ 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'}