3번의 LangGraph 재작성: 프로덕션 체크포인팅(Checkpointing)이 내게 가르쳐준 것들
3주 동안, 우리의 온보딩 에이전트는 저장되어야 할 모든 작업을 놓쳤습니다. 로그에는 아무것도 나타나지 않았습니다. 에러도, 경고도 없었습니다. 체크포인터는 성공했다고 보고했지만, 재개(resume)할 때 상태(state)가 사라져 버렸습니다.
진실을 깨닫기 전까지 저는 동일한 LangGraph 파이프라인을 세 번이나 다시 작성했습니다. 프레임워크는 제가 지시한 대로 정확히 동작하고 있었습니다. 제가 잘못된 지시를 내리고 있었을 뿐이었습니다.
프로덕션을 위해 상태 유지(stateful) 에이전트를 구축한다면, 기본 설정(defaults)이 당신을 구해줄 것이라고 가정하지 마세요. 무엇이 문제였고 어떻게 해결했는지 공유합니다.
첫 번째 문제: 소리 없는 상태 손실 (Silent State Loss)
제 에이전트는 5단계의 온보딩 흐름을 처리했습니다. 사용자가 나중에 재개할 수 있도록 Postgres를 사용하여 진행 상황을 저장했습니다. 하지만 재개할 때마다 항상 1단계부터 시작되었습니다.
원인은 상태 스키마(state schema)에 있었습니다. LangGraph에서 모든 노드는 상태에 병합(merge)되는 업데이트를 반환합니다. 병합 방식을 지정하지 않으면 기본값은 덮어쓰기(overwrite)입니다.
저는 메시지 리스트가 추가(append)될 것이라고 생각했습니다. 하지만 실제로는 새로운 노드가 생길 때마다 전체 히스토리가 단 하나의 메시지로 교체되었습니다. 체크포인트는 잘못된 데이터를 완벽하게 저장하고 있었던 것입니다.
해결책: 명시적인 리듀서(reducer)와 함께 Annotated 필드를 사용하세요.
• 메시지 리스트에는 add 연산자를 사용하세요.
• 딕셔너리에는 커스텀 병합 함수를 사용하세요.
• "step"과 같은 단일 값에만 기본 덮어쓰기를 사용하세요.
두 번째 문제: 역직렬화(Deserialization)와 동시성(Concurrency)
두 번째 재작성에서는 두 가지 새로운 문제에 직면했습니다:
- 손상된 행(Corrupt rows): 상태에 커스텀 객체를 저장했습니다. 직렬화 도구(serializer)가 이를 처리할 수 없었습니다. 이로 인해 행은 존재하지만 사용할 수 없는 상태가 되었습니다.
- 중복 키(Duplicate keys): WhatsApp은 응답이 빠르지 않으면 웹훅(webhook)을 재시도합니다. 두 메시지가 동시에 도착하면, 두 개의 그래프 실행이 동일한 스레드에 쓰기를 시도하게 됩니다. 이는 데이터베이스 충돌을 일으켰습니다.
해결책: • 커스텀 객체를 제거하세요. 일반 딕셔너리와 표준 LangChain 타입만 사용하세요. • 웹훅은 그래프 외부에서 처리하세요. 큐(queue)와 멱등성 키(idempotency key)를 사용하여 중복을 제거하세요. • 데이터베이스 락(lock)을 추가하세요. 한 번에 하나의 스레드에서 하나의 실행만 이루어지도록 보장해야 합니다.
세 번째 재작성: 안정적인 패턴
최종 버전은 세 가지 원칙에 집중했습니다:
• Small graphs: I broke one massive graph into three smaller subgraphs. This reduced the blast radius of bugs. • Explicit checkpoints: I stopped checkpointing after every single node. I only checkpoint at meaningful resume points. This cut database writes by 60%. • Idempotent nodes: This is vital. Every node must produce the same result if it runs twice. If a node sees that a task is already done in the state, it should return immediately. This prevents double-charging for expensive model calls.
𝗟𝗲𝘀𝘀𝗼𝗻𝘀 𝗳𝗼𝗿 𝘆𝗼𝘂:
- Read reducer semantics before you write code.
- Do not store custom objects in state.
- Move concurrency control outside the graph.
- Make every node idempotent.
The framework did not fail. My assumptions did.
Optional learning community: https://t.me/GyaanSetuAi