ในที่สุดเอเจนต์ของ LangGraph ก็มีวิธีที่เชื่อถือได้ในการรักษา state ของตนเอง หลังจากต้องเผชิญกับปัญหาข้อมูลสูญหายอย่างเงียบเชียบมานานหลายสัปดาห์ หลังจากลองใช้เทคนิคการทำ checkpointing ที่ล้มเหลวไปถึงสามครั้ง—ทั้ง SQLite, raw object storage และเวอร์ชันที่พังของทั้งสองอย่าง—ผู้เขียนก็ได้พบกับรูปแบบการอัปเดตแบบ atomic ที่ช่วยหยุดไม่ให้เอเจนต์ต้องเริ่มต้นใหม่ตั้งแต่ต้นทุกครั้งที่มีคำขอส่งเข้ามา

ทำไม checkpointing ถึงสำคัญสำหรับ LangGraph

LangGraph ช่วยให้นักพัฒนาสามารถเชื่อมต่อการเรียกใช้งาน LLM เข้าด้วยกันเป็น "เอเจนต์" ที่สามารถนำกลับมาใช้ใหม่ได้ และสามารถจดจำสิ่งที่เกิดขึ้นก่อนหน้านี้ในการสนทนาได้ เอเจนต์เหล่านี้จะแบ่งคำขอของผู้ใช้เป็นงานย่อยๆ (sub-tasks) จัดเก็บผลลัพธ์ระหว่างทาง และทำงานต่อจากจุดเดิมในการเรียกใช้งานครั้งถัดไป หาก state ที่จัดเก็บไว้หายไป เอเจนต์จะต้องคำนวณทุกอย่างใหม่ ซึ่งเป็นการสิ้นเปลืองทรัพยากรการคำนวณ เพิ่มความหน่วง (latency) และทำให้ประสบการณ์ของผู้ใช้แย่ลง ในกรณีของบอทที่ใช้งานจริง (production) ซึ่งจัดการข้อความ Telegram การสูญหายของข้อมูลนี้ได้ลบประวัติการสนทนาที่สะสมมาหลายสัปดาห์ทิ้งไปทั้งหมด

วิธีแก้ครั้งแรก: SQLite saver

SqliteSaver ที่มากับตัวระบบทำงานได้ดีเมื่อมีอินสแตนซ์เดียวที่รันเอเจนต์ โดยมันจะเขียนแต่ละ checkpoint เป็น JSON blob ลงในไฟล์ SQLite ในเครื่อง ปัญหาเริ่มเกิดขึ้นเมื่อนักพัฒนาเพิ่มฟิลด์ใหม่เข้าไปในประเภท AgentState และทำการ redeploy checkpoint เดิมที่มีอยู่ซึ่งสร้างขึ้นก่อนการเปลี่ยนแปลง schema จะไม่มีฟิลด์ใหม่นี้ และเนื่องจาก SqliteSaver ไม่เคยรันการทำ migration เลย LangGraph จึงโหลด JSON ที่ไม่สมบูรณ์เข้ามา ทิ้งข้อมูลที่ขาดหายไป และเอเจนต์ก็ต้องเริ่มต้นใหม่ตั้งแต่ต้น

ประเด็นสำคัญ: การจัดเก็บด้วย SQLite เป็นเพียงเครื่องมือสำหรับสาธิต (demo tool) ไม่ใช่โซลูชันที่พร้อมใช้งานจริง (production-ready) เมื่อจำเป็นต้องมีการพัฒนา schema

วิธีแก้ครั้งที่สอง: Object storage

เพื่อให้สามารถควบคุมรูปแบบการทำ serialization ได้ ผู้เขียนจึงเขียน saver แบบกำหนดเองที่อัปโหลด JSON checkpoint ไปยัง Oracle Cloud Object Storage การเปลี่ยนมาใช้วิธีนี้ช่วยให้มีความยืดหยุ่นในการจัดการเวอร์ชันของ schema ด้วยตนเอง แต่ก็นำไปสู่รูปแบบความล้มเหลวใหม่ เมื่อมีสองคำขอส่งมายังเธรดการสนทนาเดียวกันพร้อมกัน ทั้งคู่จะพยายามเขียนทับออบเจกต์เดียวกัน บริการ object storage ถูกปรับแต่งมาเพื่อรูปแบบการเขียนครั้งเดียวและอ่านหลายครั้ง (write-once, read-many) แต่ไม่ได้รองรับการเขียนทับแบบ atomic (atomic overwrite semantics) สภาวะการแข่งขัน (race condition) นี้ทำให้เกิดไฟล์ JSON ที่ผิดรูปแบบหรือถูกตัดตอน และเอเจนต์ก็สูญเสียบริบท (context) ไปอีกครั้ง

ประเด็นสำคัญ: การเขียนทับแบบธรรมดาใน object storage ไม่ปลอดภัยเมื่อมี worker หลายตัวสามารถเข้าถึง key เดียวกันได้ในเวลาเดียวกัน

วิธีแก้ครั้งที่สาม: การอัปเดตแบบ atomic พร้อมการทำ versioning

การออกแบบขั้นสุดท้ายที่เสถียรนั้นเป็นการรวมสองแนวคิดเข้าด้วยกัน: การระบุเลขเวอร์ชันอย่างชัดเจน และการเขียนแบบมีเงื่อนไข (conditional writes) โดยอิงจาก ETag ของออบเจกต์ (ซึ่งเป็นตัวระบุ checksum ของบริการจัดเก็บข้อมูล)

  1. อ่าน (Read) checkpoint ปัจจุบันและเก็บค่า ETag ไว้
  2. เพิ่มค่า (Increment) ฟิลด์เวอร์ชันภายในซองหุ้ม (envelope) ของ checkpoint
  3. เขียน (Write) checkpoint ที่อัปเดตแล้วโดยใช้คำขอแบบมีเงื่อนไข ซึ่งจะสำเร็จก็ต่อเมื่อ ETag ตรงกับค่าที่อ่านมาในตอนแรกเท่านั้น
  4. ลองใหม่ (Retry) ลูปการอ่าน-เพิ่มค่า-เขียน ทั้งหมด หากการเขียนแบบมีเงื่อนไขล้มเหลวเนื่องจากมีกระบวนการอื่นเปลี่ยนออบเจกต์ไปก่อนหน้า

เนื่องจากการเขียนจะสำเร็จก็ต่อเมื่อไม่มีกระบวนการอื่นแก้ไขไฟล์เท่านั้น จึงมี worker เพียงตัวเดียวที่สามารถ commit state ใหม่ได้ในแต่ละครั้ง นอกจากนี้ ฟิลด์เวอร์ชันยังช่วยให้ตรวจพบ checkpoint ที่ล้าสมัยได้ง่าย และช่วยให้สามารถทำ migration ไปข้างหน้าได้เมื่อมีการเปลี่ยนแปลง schema

รูปแบบนี้ใช้งานได้กับ object storage ที่รองรับการเขียนแบบมีเงื่อนไขโดยอิงจาก ETag

บทเรียนสำหรับ AI engineers

  • ใช้ SQLite สำหรับการทำ prototype เท่านั้น เอเจนต์ในระดับ production จำเป็นต้องมีที่จัดเก็บที่สามารถจัดการกับการเปลี่ยนแปลง schema และการเขียนข้อมูลพร้อมกัน (concurrent writes) ได้
  • วางแผนการทำ schema migrations ด้วยตนเอง แม้ว่า Typed dictionaries จะช่วยอธิบายโครงสร้างสำหรับการวิเคราะห์แบบ static (static analysis) แต่ไม่ได้บังคับใช้โครงสร้างในขณะรันไทม์ (runtime structure)
  • มองว่า state คือทรัพยากรที่ใช้ร่วมกัน บั๊กที่เกิดจากการทำงานพร้อมกัน (concurrency bugs) มักแสดงออกมาในรูปแบบของการสูญหายของข้อมูลอย่างเงียบเชียบ ซึ่งตรวจหาได้ยากกว่าการเกิด exception โดยตรง
  • ใช้ cloud primitives การเขียนแบบมีเงื่อนไขโดยอิงจาก ETag ช่วยให้ทำ optimistic locking ได้ในราคาประหยัด โดยไม่ต้องใช้บริการ lock service แยกต่างหาก
  • บันทึก log ทุกขั้นตอน ความล้มเหลวที่เกิดขึ้นอย่างเงียบเชียบ เช่น ฟิลด์ที่หายไปซึ่ง LangGraph มองข้ามไป เป็นสิ่งที่ติดตามหาต้นตอได้ยากที่สุด

ก้าวต่อไปสำหรับการทำ LangGraph checkpointing คืออะไร?

สำหรับทีมที่เคยเจอกับอุปสรรคแบบเดียวกัน สูตรการอัปเดตแบบ atomic นี้เป็นวิธีแก้ไขที่รวดเร็วและมีต้นทุนต่ำ มันแสดงให้เห็นว่า pipeline ในระดับ production ที่เชื่อถือได้ไม่จำเป็นต้องใช้ระบบจัดเก็บสถานะขนาดใหญ่ (heavyweight state store) เพียงแค่ต้องจัดการเรื่อง concurrency และ versioning อย่างระมัดระวังเท่านั้น

บทสรุป: การใช้ซองหุ้มที่มีเวอร์ชัน (versioned envelope) แบบง่ายๆ ร่วมกับการเขียนแบบมีเงื่อนไข สามารถเปลี่ยนระบบที่เปราะบางให้กลายเป็นระบบที่เชื่อถือได้ ช่วยให้ AI engineers สามารถมุ่งเน้นไปที่ตรรกะของเอเจนต์ แทนที่จะต้องมานั่งไล่แก้บั๊กเรื่องข้อมูลสูญหายไม่จบไม่สิ้น