อยู่ช่วงหนึ่ง agent ของเราเหมือนคนความจำเสื่อม เปิด session ใหม่ทีไร มันทำเหมือนไม่เคยเจองานเก่ามาก่อนทุกที ทั้งที่โค้ดฝั่งบันทึกความทรงจำก็รันผ่านทุกรอบ ไม่มี error โผล่มาให้เห็นแม้แต่ตัวเดียว
พอเปิดเข้าไปดูในกราฟจริง ๆ ถึงได้เจอของ มีความทรงจำกองอยู่ 43 episode กับ 185 fact เขียนเข้าไปครบทุกก้อน แต่ฝั่งอ่านดึงกลับมาได้ 0
บั๊กนี้โพสต์ก่อนหน้าเราเปรยไว้แค่บรรทัดเดียว ในฐานะที่มาของเรื่อง สถาปัตยกรรม ความทรงจำที่เชื่อถือได้ คราวนี้คือเรื่องเต็มของมัน กับ memory stack จริงที่อยู่รอบ ๆ ทั้งเครื่องมือแต่ละตัวที่เราใช้ประกอบกันขึ้นมา และจุดที่มันเงียบ ๆ พังโดยไม่บอกใคร
ช่วงที่ 1ลิ้นชักที่อ่านผิดใบ
ต้นเหตุของอาการ "เขียนได้ อ่านไม่ได้" อยู่ตรงรอยต่อเล็ก ๆ ที่มองข้ามง่ายมาก
กราฟ database ที่เราใช้ มีค่าตั้งต้นตัวหนึ่งที่ระบุว่า "จะเก็บข้อมูลไว้ที่ไหน" ปัญหาคือค่านี้ทำหน้าที่เป็นกุญแจของกราฟไปด้วยในตัว ฝั่งเขียนความทรงจำเรายิงเข้ากราฟที่ผูกกับตัวตนของ agent ส่วนฝั่งอ่านดันชี้ไปที่กราฟ default ที่ว่างเปล่า ทั้งคู่ดู "ทำงานปกติ" แยกกัน แต่คนละลิ้นชัก ความทรงจำเลยอยู่ครบ ส่วนการเรียกคืนก็ว่างเปล่าอยู่ดี
แก้จริงคือแก้บรรทัดเดียว ชี้ฝั่งอ่านให้ตรงกับฝั่งเขียน แค่นั้น recall ก็กลับมาเห็นทั้ง 43 episode ทันที
บั๊กแบบนี้น่ากลัวกว่าบั๊กที่ crash เสียอีก เพราะมันไม่ส่งเสียงร้อง ทุกอย่างเขียว ไม่มี error และระบบก็ดู "จำได้" จากภายนอก กว่าจะจับได้คือตอนที่นั่งเปิดกราฟดูเองกับตา
ช่วงที่ 2stack จริง ๆ ที่อยู่ใต้คำว่า "ความทรงจำ"
พอเข้าใจว่าความทรงจำมันแยกเป็นฝั่งเขียนกับฝั่งอ่านแบบนี้ ก็เห็นภาพง่ายขึ้นว่า stack ประกอบจากอะไรบ้าง แต่ละชั้นทำหน้าที่ต่างกันชัดเจน
- Graphiti เป็นตัวจัดการความทรงจำแบบ episodic คือรับงานเข้ามาทีละก้อน (เรียกว่า episode) แล้วสกัดออกมาเป็น fact กับความสัมพันธ์ เก็บลงกราฟตามเวลา
- FalkorDB คือกราฟ database ที่อยู่ข้างใต้ Graphiti อีกที สิ่งของและคนกลายเป็น node ความสัมพันธ์ระหว่างกันกลายเป็น edge
- bge-m3 ที่รันผ่าน Ollama ทำหน้าที่ embedder คือแปลงข้อความเป็น vector เพื่อให้ค้นด้วยความหมายได้ ไม่ใช่แค่ match คำตรง ๆ
- extraction LLM ตัวเล็ก ๆ อีกตัว คอยอ่าน episode ดิบ แล้วตัดสินว่าอะไรคือ fact อะไรคือ entity ที่ควรเก็บ
- CLI บาง ๆ ของเราเอง เป็นตัวคุมจังหวะ record กับ recall ให้เป็น loop เดียวกัน
จะเห็นว่าคำว่า "ให้ AI มีความจำ" ที่ฟังดูเป็นปุ่มเดียว จริง ๆ แล้วมีของต่อกันอยู่ 5 ชั้น และแต่ละรอยต่อระหว่างชั้นคือจุดที่พังเงียบได้ทั้งนั้น
ช่วงที่ 3ทำไมต้องมีทั้งกราฟและ embedder
คำถามที่เจอบ่อยคือ ทำไมไม่จบที่ vector database ตัวเดียว ที่นิยมกันอยู่แล้ว
เพราะ vector เก่งเรื่องเดียวคือ "หาของที่ความหมายใกล้กัน" ถ้าถามว่าเคยคุยเรื่องคล้าย ๆ นี้ไหม vector ตอบได้ดี แต่พอถามว่า "ใครเกี่ยวกับเรื่องนี้บ้าง แล้วเชื่อมกับเคสไหนเมื่อไหร่" คำถามแบบนี้ต้องการความสัมพันธ์กับลำดับเวลา ซึ่งเป็นงานของกราฟ
เราเลยใช้สองอย่างคู่กัน embedder ช่วยให้เจอก้อนที่เกี่ยวข้องด้วยความหมาย ส่วนกราฟเก็บว่าก้อนพวกนั้นโยงถึงกันยังไงและเกิดก่อนหลังกันแบบไหน ขาดตัวใดตัวหนึ่งไป ความทรงจำก็จะตอบได้แค่ครึ่งเดียว
ช่วงที่ 4ทำไมบันทึกแบบประหยัด ไม่ใช่บันทึกทุกอย่าง
ทุกครั้งที่บันทึก 1 episode extraction LLM ต้องอ่านแล้วสกัด fact ออกมา งานนี้กินโควต้า ตอนที่เรารันบน free tier โควต้าสกัดอยู่ราว 20 ครั้งต่อวัน ตีเป็นความทรงจำได้ราว 2 ก้อนต่อวันเท่านั้น
ข้อจำกัดนี้บังคับให้ออกแบบดี ๆ แทนที่จะบันทึกทุก session อัตโนมัติ เราบันทึกเฉพาะตอนจบงานที่มีสาระ ผ่าน ritual ที่เขียนสรุปอยู่แล้ว แล้วตัดความยาวแต่ละก้อนให้เหลือราว 1,000 ตัวอักษร เก็บเฉพาะ fact ที่อยู่ทน ไม่เก็บการบรรยายยืดยาว
กลายเป็นว่าข้อจำกัดเรื่องเงินดันทำให้ความทรงจำดีขึ้น เพราะมันคัดมาแล้วว่าอะไรควรค่าแก่การจำ ไม่ใช่กองทุกอย่างทิ้งไว้ให้รก
อีกเรื่องที่ตัดสินใจตั้งแต่ต้นคือ ความทรงจำทั้งหมดอยู่ในกุญแจกลุ่มเดียวกัน งานจาก session พัฒนา กับงานที่คุยผ่านแชต ใช้กราฟเดียวกัน เวลาเรียกคืนเลยข้ามร่างกันได้ จำเรื่องที่เกิดอีกที่หนึ่งได้ด้วย
ช่วงที่ 5ตอนที่ "ระบบล่ม" แต่จริง ๆ ไม่ได้ล่ม
อีกวันหนึ่ง recall เด้ง timeout ขึ้นมา agent ตอบแบบกลวง ๆ เหมือนนึกอะไรไม่ออก สัญชาตญาณแรกคือ "tunnel ที่ต่อไปหา database คงตาย" เพราะ status ขึ้น 255
แต่ก่อนจะลงมือแก้ เราลอง verify ก่อน ยิง PING เข้าไปที่ port ตรง ๆ มันตอบ PONG กลับมา ตัว database เองก็ขึ้นว่า up มา 6 วันแล้ว แปลว่า tunnel ไม่ได้ตาย เลข 255 นั่นเป็นค่าค้างจากครั้งก่อนที่เน็ตสะดุดต่างหาก status code บอกผลครั้งสุดท้าย ไม่ได้บอกสถานะตอนนี้
ของจริงคือ timeout ตั้งไว้ 5 วินาที ซึ่งต่ำกว่าที่ cold path ต้องใช้ ตอนระบบเพิ่งตื่น แค่ embedder รอบแรกก็กิน 3.66 วินาทีแล้ว พอบวกกราฟกับการต่อข้ามภูมิภาค รวมแล้วแตะ ~10 วินาที recall ที่ทำงานครบจริง ๆ จับเวลาได้ 9.7 วินาที เลยชนเพดาน 5 วินาทีตลอด
แก้คือขยับเพดานเป็น 12 ถึง 15 วินาที จุดสำคัญคือ timeout เป็นเพดาน ไม่ใช่เวลารอตายตัว ครั้งที่ระบบอุ่นแล้ว recall ก็ยังกลับมาใน 1 ถึง 2 วินาทีเหมือนเดิม ขยับเพดานขึ้นเลยมีแต่ได้กับได้ มีแต่ cold path เท่านั้นที่ได้ประโยชน์
ช่วงที่ 6ถ้าจะต่อ memory stack ของตัวเอง
ทั้งสองเรื่องข้างบนมาจากบั๊กคนละแบบ แต่สอนเรื่องเดียวกัน คือความทรงจำของ agent พังแบบเงียบ ๆ ได้เสมอ ทั้งที่ log ขึ้นเขียว ฉะนั้นก่อนจะเชื่อว่ามันจำได้จริง ลองสองอย่างนี้
อย่างแรก พิสูจน์ round-trip ให้เห็นกับตา เขียนความทรงจำลงไป 1 ก้อน แล้วสั่งดึงกลับมาให้เห็นว่าได้ก้อนเดิม อย่าเชื่อแค่ว่า "บันทึกผ่าน ไม่มี error" เพราะฝั่งเขียนกับฝั่งอ่านอาจชี้คนละลิ้นชักอยู่ก็ได้
อย่างที่สอง จับเวลา cold path จริง คือครั้งแรกหลังระบบเพิ่งตื่น ไม่ใช่ครั้งที่อุ่นแล้ว แล้วค่อยตั้ง timeout จากตัวเลขนั้น ไม่ใช่จากที่เดาเอาว่า "ค้นน่าจะเร็ว"
ความจำที่เชื่อถือได้ ไม่ได้วัดกันที่ตอนทุกอย่างปกติ แต่วัดกันตอนที่มันเงียบ ๆ ไม่ทำงาน แล้วคุณจับได้ทันหรือเปล่า
- ทุกตัวเลขในบทความนี้ (43 episode / 185 fact / cold embed 3.66 วินาที / recall ครบวง 9.7 วินาที / timeout 5 วินาที ขยับเป็น 12 ถึง 15 วินาที) มาจากการรันและ log ของระบบที่เราต่อ stack นี้ขึ้นมาใช้จริงบน fleet ของเราเอง (มิ.ย. 2026) วัดเอง ไม่ได้หยิบมาจากที่อื่น