𝟯 การเขียน LangGraph ใหม่: สิ่งที่การทำ Checkpointing ในระดับ Production สอนผม

เป็นเวลาสามสัปดาห์ที่ onboarding agent ของเราทำข้อมูลงานที่ควรจะบันทึกไว้หล่นหายไปทั้งหมด ใน log ไม่แสดงอะไรเลย ไม่มี error ไม่มี warning ตัว checkpointer รายงานว่าสำเร็จ แต่ state กลับหายไปเมื่อเริ่มทำงานใหม่ (resume)

ผมเขียน pipeline ของ LangGraph เดิมซ้ำถึงสามครั้งก่อนจะเข้าใจความจริง เฟรมเวิร์กทำตามสิ่งที่ผมสั่งทุกประการ เพียงแต่ผมให้คำสั่งที่ผิดไปเอง

หากคุณกำลังสร้าง stateful agents สำหรับใช้งานจริง (production) อย่าคิดไปเองว่าค่าเริ่มต้น (defaults) จะช่วยคุณได้ นี่คือสิ่งที่พังและวิธีที่ผมแก้ไขมัน

𝗧𝗵𝗲 𝗙𝗶𝗿𝘀𝘁 𝗕𝗿𝗲𝗮𝗸: การสูญเสีย State แบบเงียบๆ (Silent State Loss)

Agent ของผมจัดการ flow การ onboarding แบบห้าขั้นตอน ผมใช้ Postgres เพื่อบันทึกความคืบหน้าเพื่อให้ผู้ใช้สามารถกลับมาทำต่อได้ในภายหลัง แต่การกลับมาทำต่อทุกครั้งกลับเริ่มจากขั้นตอนแรกเสมอ

สาเหตุมาจาก state schema ของผม ใน LangGraph ทุกๆ node จะคืนค่าการอัปเดตที่จะนำไปรวม (merge) เข้ากับ state หากคุณไม่ได้ระบุวิธีการ merge ค่าเริ่มต้นจะเป็นการเขียนทับ (overwrite)

ผมคิดว่ารายการข้อความ (message list) ของผมจะถูกเพิ่มต่อท้าย (append) แต่กลายเป็นว่าทุกๆ node ใหม่จะแทนที่ประวัติทั้งหมดด้วยข้อความเพียงข้อความเดียว ตัว checkpoint บันทึกข้อมูลที่ผิดพลาดได้อย่างสมบูรณ์แบบ

วิธีแก้ไข: ใช้ Annotated fields พร้อมกับ explicit reducers

• ใช้ operator add สำหรับ message lists • ใช้ custom merge function สำหรับ dictionaries • ใช้การ overwrite แบบเริ่มต้นเฉพาะกับค่าเดี่ยวๆ เช่น "step"

𝗧𝗵𝗲 𝗦𝗲𝗰𝗼𝗻𝗱 𝗕𝗿𝗲𝗮𝗸: Deserialization และ Concurrency

การเขียนใหม่ครั้งที่สองต้องเผชิญกับปัญหาใหม่สองประการ:

  1. แถวข้อมูลเสียหาย (Corrupt rows): ผมเก็บ custom objects ไว้ใน state ซึ่ง serializer ไม่สามารถจัดการได้ ทำให้เกิดแถวข้อมูลที่มีอยู่จริงแต่ไม่สามารถนำมาใช้งานได้
  2. คีย์ซ้ำ (Duplicate keys): WhatsApp จะพยายามส่ง webhook ซ้ำหากคุณไม่ตอบสนองอย่างรวดเร็ว หากมีข้อความสองข้อความมาถึงพร้อมกัน การรัน graph สองครั้งจะพยายามเขียนข้อมูลลงใน thread เดียวกัน ซึ่งทำให้เกิดการชนกันของข้อมูลในฐานข้อมูล (database collisions)

วิธีแก้ไข: • ตัด custom objects ออก ใช้เพียง plain dicts และประเภทข้อมูลมาตรฐานของ LangChain เท่านั้น • จัดการ webhooks นอก graph โดยใช้ queue และ idempotency key เพื่อคัดกรองข้อมูลที่ซ้ำกันออก • เพิ่ม database lock เพื่อให้แน่ใจว่ามีการรันเพียงครั้งเดียวต่อหนึ่ง thread ในแต่ละช่วงเวลา

𝗧𝗵𝗲 𝗧𝗵𝗶𝗿𝗱 𝗥𝗲𝘄𝗿𝗶𝘁𝗲: รูปแบบที่เสถียร (The Stable Pattern)

เวอร์ชันสุดท้ายมุ่งเน้นไปที่หลักการสามประการ:

• กราฟขนาดเล็ก: ฉันแบ่งกราฟขนาดใหญ่หนึ่งอันออกเป็นกราฟย่อย (subgraphs) สามอัน ซึ่งช่วยลดขอบเขตความเสียหาย (blast radius) ของบั๊ก • Checkpoint ที่ชัดเจน: ฉันหยุดทำ checkpoint หลังจบทุกๆ node และจะทำ checkpoint เฉพาะที่จุดเริ่มทำงานใหม่ (resume points) ที่สำคัญเท่านั้น วิธีนี้ช่วยลดการเขียนข้อมูลลงฐานข้อมูลได้ถึง 60% • Node ที่เป็น Idempotent: เรื่องนี้สำคัญมาก ทุก node ต้องให้ผลลัพธ์เดิมเสมอหากรันซ้ำสองครั้ง หาก node ตรวจพบว่างานนั้นเสร็จสิ้นไปแล้วใน state มันควรจะ return กลับทันที วิธีนี้จะช่วยป้องกันการเสียค่าใช้จ่ายซ้ำซ้อนจากการเรียกใช้ model ที่มีราคาแพง

บทเรียนสำหรับคุณ:

  • อ่าน reducer semantics ให้เข้าใจก่อนเริ่มเขียนโค้ด
  • อย่าเก็บ custom objects ไว้ใน state
  • ย้ายการควบคุม concurrency ออกไปนอกกราฟ
  • ทำให้ทุก node เป็น idempotent

Framework ไม่ได้ล้มเหลว แต่เป็นสมมติฐานของฉันเองที่ผิดพลาด

ที่มา: https://dev.to/elenarevicheva/three-langgraph-rewrites-what-production-checkpointing-actually-taught-me-ok9

ชุมชนแห่งการเรียนรู้ (ไม่บังคับ): https://t.me/GyaanSetuAi