-
LangGraph 자기 수정 패턴의 State 설계 — 루프를 위한 5가지 필드IT 2026. 6. 27. 23:00
LLM 기반 코드를 짜다 보면 어느 순간 "한 번 생성해서 끝내는 게 아니라, 결과물을 검토하고 부족하면 다시 고치는 루프"가 필요해진다. 글쓰기라면 초고를 쓰고 → 편집자가 피드백을 주고 → 그 피드백을 반영해 고쳐 쓰는 과정이다. LangGraph에서 이 구조를 자기 수정 패턴(self-correction loop)이라 부른다.
문제는 이 루프를 코드로 옮길 때다. 단순히
while을 돌리면 되는 게 아닌가 싶지만, LangGraph는 그래프 기반 실행 엔진이라 루프의 상태를 State라는 공유 객체에 담아서 노드 간에 넘겨야 한다. 그래서 "어떤 필드를 State에 두어야 루프가 제대로 작동하는가"가 설계의 핵심이 된다. 이 글은 그 5가지 필드를 역할별로 정리한다.자기 수정 패턴이 무엇인가
먼저 패턴의 전체 흐름부터 본다.
생성 노드(generator)가 초안을 만들고, 검증 노드(validator)가 그 초안을 평가한다. 평가 결과가 "충분하다"거나 "최대 시도 횟수를 넘었다"면 흐름이 끝나고, 그렇지 않으면 피드백을 State에 담아 다시 생성 노드로 돌아간다. 루프의 핵심은 두 노드 사이에 오가는 State에 무엇이 담겨 있느냐다.
State 5가지 필드와 역할 분류
아래는 이 패턴을 구현하는 State의 최소 구성이다.
from typing import Annotated from typing_extensions import TypedDict from langgraph.graph.message import add_messages from langchain_core.messages import BaseMessage class WriterState(TypedDict): # 대화 이력 — add_messages 리듀서로 덮어쓰지 않고 누적 messages: Annotated[list[BaseMessage], add_messages] # 입력: 루프 내내 변하지 않는 원래 요청 topic: str # 현재 결과물: 매 루프마다 새 초안으로 갱신 draft: str # 루프 간 통신: 검증 노드가 생성 노드에게 보내는 피드백 feedback: str # 루프 카운터: 몇 번 시도했는지 추적 retry_count: int # 종료 신호: True가 되면 루프를 빠져나옴 is_approved: bool필드가 6개지만
messages는 일반적인 LangChain 대화 이력이고, 자기 수정 루프를 구동하는 핵심은 나머지 5개다. 이 5개를 역할별로 분류하면 패턴이 보인다.이 다이어그램은 6개 필드를 4가지 역할 버킷으로 나눈 것이다. 입력(topic)은 루프 내내 바뀌지 않는 원본 요청이다. 현재 결과물(draft)은 매 루프마다 덮어써지는 최신 생성본이다. 루프 간 통신(feedback)은 검증 노드가 생성 노드에게 "무엇을 고쳐야 하는지"를 전달하는 메신저다. 루프 제어(retry_count, is_approved)는 루프를 계속할지 끝낼지를 결정하는 두 개의 게이트다. 놓치기 쉬운 함정은 이 네 역할 중 하나라도 빠지면 루프가 깨진다는 점이다. 특히 feedback 없이 draft만 갱신하면 생성 노드는 "무엇이 부족했는지"를 모른 채 똑같은 초안을 반복해서 내놓는다.
루프 3회 시뮬레이션 — State 값이 어떻게 변하는가
각 필드가 실제로 어떻게 변하는지는 루프를 한 단계씩 따라가 보는 게 가장 빠르다.
흐름을 읽는 방법은 간단하다. 각 박스는 생성 또는 검증이 끝난 직후의 State 스냅샷이다. 1차 시도에서 draft는 짧은 두 문장으로 채워지고, 검증 노드는 "3문장 이상 필요"라는 피드백을 feedback 필드에 기록하며 approved를 False로 유지한다. retry_count가 1로 올라가고 루프는 다시 생성 노드로 향한다. 2차 시도에서는 생성 노드가 feedback을 읽고 세 문단짜리 글로 draft를 갱신한다. 검증 노드는 이번에 "구체적 예시 부족"을 잡아낸다. 3차에서 드디어 is_approved가 True가 되고 루프가 멈춘다. 놓치기 쉬운 함정은 draft가 누적되지 않고 매번 덮어씌워진다는 점이다. 이전 초안이 필요하다면 messages 같은 별도 누적 필드에 보관해야 한다.
retry_count와 is_approved가 협업하는 방식
루프 종료 조건은 두 필드가 함께 만든다.
이 결정 트리가 하는 일은 OR 조건이다. 품질 기준을 달성(is_approved=True)하거나, 최대 시도 횟수를 초과(retry_count >= MAX_RETRIES)하거나, 둘 중 하나만 만족해도 루프는 END로 빠진다. 두 조건이 모두 필요한 이유가 있다. is_approved만 있으면 검증 노드가 영원히 False를 돌려줄 때 루프가 멈추지 않는다. retry_count만 있으면 품질 기준에 도달했는데도 카운터가 다 찰 때까지 쓸데없는 생성을 반복한다. 놓치기 쉬운 함정은 MAX_RETRIES 값이다. 너무 낮으면 괜찮은 결과를 얻기 전에 잘려버리고, 너무 높으면 API 비용이 선형으로 증가한다. 대부분의 경우 3~5 사이가 적정선이다.
코드로 표현하면 조건 분기는 이렇게 생긴다.
MAX_RETRIES = 3 def should_continue(state: WriterState) -> str: # 승인됐거나 횟수를 다 썼으면 종료 if state["is_approved"] or state["retry_count"] >= MAX_RETRIES: return "end" # 아직 여유가 있으면 다시 생성 return "generate"LangGraph의 조건부 엣지(conditional edge)에 이 함수를 연결하면 그래프가 스스로 루프 여부를 판단한다. 생성 노드와 검증 노드는 각자 State를 갱신하는 데만 집중하면 되고, 제어 흐름은 이 함수 하나가 담당한다.
이 패턴이 적용되는 곳
자기 수정 루프의 State 구조는 생성 대상이 무엇이든 거의 그대로 재사용된다.
- 코드 생성 루프: draft에 코드를 담고, 검증 노드가 테스트 실행 결과를 feedback으로 돌려준다. is_approved는 테스트 전체 통과 시 True.
- 영화 추천 사유 루프: draft에 추천 사유를 담고, 검증 노드가 사용자 취향과의 부합도를 LLM 평가로 feedback에 넣는다.
- SQL 생성 루프: draft에 SQL 쿼리를 담고, 실제 DB 실행 결과(오류 메시지 또는 빈 결과셋)를 feedback으로 전달한다.
세 경우 모두 공통 구조는 동일하다: 생성 노드 + 검증 노드 + 루프를 제어하는 State 필드 5개. 바뀌는 건 draft에 담기는 내용과 feedback을 생성하는 로직뿐이다. 이 공통 뼈대를 먼저 TypedDict로 정의해두면, 새 루프가 필요할 때 State 상속이나 컴포지션으로 빠르게 확장할 수 있다.
마무리
LangGraph 자기 수정 루프의 핵심은 그래프 구조가 아니라 State 설계다. 노드를 아무리 정교하게 만들어도 State에 역할이 잘못 배분되어 있으면 루프는 제대로 작동하지 않는다. 입력(topic)은 변하지 않아야 하고, 결과물(draft)은 매 루프마다 갱신되어야 하고, 피드백(feedback)은 노드 간 메신저 역할을 해야 하고, 제어 필드(retry_count, is_approved)는 두 개가 함께 OR 조건을 만들어야 한다. 이 네 역할이 모두 갖춰졌을 때 루프는 품질 기준에 도달하거나 한계에 부딪혀서야 멈추게 된다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
LangGraph가 Annotated를 쓰는 이유 — 덮어쓰기 문제와 리듀서의 등장 (0) 2026.06.29 RAG 에이전트 완전 조립 — create_agent부터 동작 추적까지 (1) 2026.06.29 검색 결과를 에이전트 도구로 — build_context와 @tool 패턴 (0) 2026.06.28 RAG의 배경과 make_retriever — LLM이 모르는 문서를 검색하는 방법 (0) 2026.06.28 생성과 검증의 분리 — generator 노드와 validator 노드 설계 (0) 2026.06.28 LangGraph 조건부 라우팅 — 상태 값으로 다음 노드를 결정하는 방법 (0) 2026.06.27 LLM을 분류기로 쓰기 — SystemMessage와 HumanMessage로 Classifier Node 만들기 (0) 2026.06.27 LangGraph 상태에 메시지 외 필드 추가하기 — RouterState 설계 (0) 2026.06.26 graph.invoke vs graph.stream — 에이전트 실행의 두 가지 방식 (1) 2026.06.26 bind_tools — LLM이 도구를 인식하는 방법 (0) 2026.06.26