ในที่สุดเอเจนต์ของ 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 ของบริการจัดเก็บข้อมูล)
- อ่าน (Read) checkpoint ปัจจุบันและเก็บค่า ETag ไว้
- เพิ่มค่า (Increment) ฟิลด์เวอร์ชันภายในซองหุ้ม (envelope) ของ checkpoint
- เขียน (Write) checkpoint ที่อัปเดตแล้วโดยใช้คำขอแบบมีเงื่อนไข ซึ่งจะสำเร็จก็ต่อเมื่อ ETag ตรงกับค่าที่อ่านมาในตอนแรกเท่านั้น
- ลองใหม่ (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 สามารถมุ่งเน้นไปที่ตรรกะของเอเจนต์ แทนที่จะต้องมานั่งไล่แก้บั๊กเรื่องข้อมูลสูญหายไม่จบไม่สิ้น
