# 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 ` เพื่อลบเหตุการณ์ที่ระบุ - มีคำสั่ง `!memory search ` เพื่อค้นหาเหตุการณ์ (ผลลัพธ์รวมถึง 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 `, `!memory list [page]`, `!memory forget ` และ `!memory export` เพื่อตรวจสอบพฤติกรรม ## การแชร์บริบทสำหรับปลั๊กอินอื่น LongTermMemory จะเขียนสรุปบริบทที่มีโครงสร้างไปยังตัวแปรคำถาม `_ltm_context` ในทุกเหตุการณ์ `PromptPreProcessing` ปลั๊กอินอื่นสามารถอ่านตัวแปรนี้เพื่อทำการตัดสินใจตามโปรแกรมตามความจำของผู้ใช้ โดยไม่ต้องนำเข้าหรืออ้างอิง LongTermMemory ในทางใดทางหนึ่ง ### คีย์ตัวแปร `_ltm_context` ### โครงสร้าง (Schema) ```python { "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"}, ], } ``` ### ตัวอย่างการใช้งาน ```python 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 ` การส่งออกจำนวนมากทั้งหมดยังไม่ได้รับการพัฒนา ## คำถามทางเทคนิคที่สำคัญ ### 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 ` หรือเครื่องมือ `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](components/knowledge_engine/memory_engine.py) - EventListener: [memory_injector.py](components/event_listener/memory_injector.py) - เครื่องมือ: [remember.py](components/tools/remember.py), [recall_memory.py](components/tools/recall_memory.py), [update_profile.py](components/tools/update_profile.py), [forget.py](components/tools/forget.py) - คำสั่ง: [memory.py](components/commands/memory.py) - Page: [memory_console.py](components/pages/memory_console/memory_console.py), [index.html](components/pages/memory_console/index.html) ## ช่องว่างในปัจจุบัน ตอนนี้ README ครอบคลุมการออกแบบหลัก, กฎการแยก, ขอบเขตการส่งออก และส่วนประกอบหลักแล้ว สิ่งที่ควรเพิ่มในภายหลัง: - การอัปเดตเอกสารที่แปลแล้วให้ตรงกัน - ตัวอย่างการนำเข้า JSON ที่เป็นรูปธรรม - ตัวอย่างแนวทางปฏิบัติที่ดีที่สุดสำหรับ `remember`, `recall_memory` และ `update_profile` ## การบันทึก日誌 (Logging) ตอนนี้ปลั๊กอินจะพ่น log ที่จุดสำคัญของวงจรความจำ เพื่อให้คุณสามารถสังเกตการทำงานของความจำระยะยาวระหว่างรันไทม์ได้ คุณจะเห็น log สำหรับ: - การเริ่มทำงานของปลั๊กอินและบริบทความจำที่ตรวจสอบได้ - การเรียกใช้เครื่องมือ `remember`, `recall_memory` และ `update_profile` - การฉีดโปรไฟล์ก่อนการเรียกใช้โมเดล - การดึงข้อมูลความจำ L2 อัตโนมัติใน KnowledgeEngine - การเขียนเวกเตอร์ความจำเหตุการณ์, การค้นหา, การนำเข้าเป็นกลุ่ม และการลบ ข้อความ log ทั่วไปมีลักษณะดังนี้: ```text [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'} ```