[LLM] LangGraph에서 GraphRecursionError 해결하기: 올바른 상태 관리의 중요성
LangGraph에서 GraphRecursionError 해결하기: 올바른 상태 관리의 중요성
LangGraph를 사용하여 에이전트 워크플로우를 구축할 때 가장 자주 마주치는 오류 중 하나는 GraphRecursionError
이다. 이 오류는 그래프가 종료 조건에 도달하지 못하고 최대 반복 횟수를 초과할 때 발생한다. 오늘은 이 오류의 주요 원인과 해결 방법을 실제 사례를 통해 살펴보겠다.
문제 상황: 무한 재귀 발생
LangGraph를 사용하여 계획 수립 및 실행 에이전트를 구현하는 중 다음과 같은 오류가 발생했다:
GraphRecursionError: Recursion limit of 10 reached without hitting a stop condition.
You can increase the limit by setting the `recursion_limit` config key.
이 오류는 그래프가 종료 조건에 도달하지 못하고 계속해서 같은 노드를 순환하고 있음을 의미한다. 대부분의 경우, 이는 상태(state) 관리에 문제가 있다는 신호이다.
주요 원인 분석
코드를 분석해보니 다음과 같은 문제점들이 확인되었다:
1. 상태 업데이트 불완전: PlanExecute
타입 정의 문제
문제의 핵심은 PlanExecute
상태 객체와 그 업데이트 방식에 있었다. 원래 코드에서는:
class PlanExecute(TypedDict):
input: str
plan: List[str]
past_steps: List[Tuple[str, str]]
response: str # 타입 힌트만 있고 실제로는 항상 채워지지 않음
이 상태 정의는 response
필드가 필수인 것처럼 보이지만, 실제로는 이 필드가 언제 채워지는지 명확하지 않았다. 더 큰 문제는 종료 조건이 이 필드에 의존한다는 점이었다:
def should_end(state: PlanExecute):
if "response" in state and state["response"]:
return END
else:
return "agent"
2. 반환 값 누락: execute_step
함수 문제
또 다른 중요한 문제는 execute_step
함수가 실행된 계획 단계를 제거하지만, 업데이트된 계획을 반환하지 않는다는 점이었습니다:
async def execute_step(state: PlanExecute):
# ... 코드 생략 ...
return {
"past_steps": [(task, response_content)],
# "plan": remaining_plan, # 이 라인 누락
}
이로 인해 상태의 plan
필드가 갱신되지 않고, 그래프는 계속해서 같은 단계를 수행하려고 시도했다.
3. 애매한 응답 구조: edited_planner
반환 값 문제
마지막으로, edited_planner
가 반환하는 Act
객체의 구조가 명확하지 않아, 종료 조건을 제대로 트리거하지 못했다:
class Act(BaseModel):
action: Union[Response, Plan] = Field(...)
이 구조는 Union 타입을 사용하여 복잡성을 증가시켰고, 타입 체크가 제대로 이루어지지 않았다.
해결 방법: 명확한 상태 관리
이 문제를 해결하기 위한 핵심은 다음과 같다:
1. 타입 정의 개선: Optional
타입 사용
class PlanExecute(TypedDict):
input: str
plan: List[str]
past_steps: List[Tuple[str, str]]
response: Optional[str] # Optional로 명시하여 필수가 아님을 표시
이렇게 함으로써 response
필드가 있을 수도, 없을 수도 있다는 것을 명확히 했다.
2. 상태 업데이트 보장: 모든 필요한 필드 반환
async def execute_step(state: PlanExecute):
# ... 코드 생략 ...
return {
"past_steps": state.get("past_steps", []) + [(task, response_content)],
"plan": remaining_plan, # 남은 계획을 명시적으로 반환
}
이제 execute_step
함수는 실행 후 남은 계획을 명시적으로 반환하므로, 상태가 올바르게 갱신된다.
3. 명확한 응답 구조: Act
클래스 개선
class Act(BaseModel):
action_type: str = Field(description="'response' 또는 'plan'")
response: Optional[str] = Field(default=None)
steps: Optional[List[str]] = Field(default=None)
@property
def action(self):
if self.action_type == "response":
return Response(response=self.response)
else:
return Plan(steps=self.steps)
이렇게 하면 응답 구조가 명확해지고, 종료 조건을 올바르게 트리거할 수 있다.
4. 종료 조건 강화
def should_end(state: PlanExecute):
# response가 있고 빈 문자열이 아닌 경우에만 종료
if "response" in state and state["response"] and state["response"].strip():
return END
# 또는 계획이 비어있으면 종료 (선택적)
elif "plan" in state and len(state.get("plan", [])) == 0:
return END
else:
return "agent"
더 엄격한 종료 조건을 추가하여 그래프가 적절히 종료되도록 했다.
결론
LangGraph를 사용하여 복잡한 에이전트 워크플로우를 구축할 때 GraphRecursionError
는 흔히 발생하는 오류이다. 이 오류를 해결하는 핵심은 상태 객체의 명확한 정의와 완전한 상태 업데이트를 보장하는 것이다. 특히 종료 조건에 사용되는 필드들이 올바르게 설정되고 업데이트되는지 확인하는 것이 중요하다.
올바른 상태 관리만으로도 대부분의 GraphRecursionError
문제를 해결할 수 있으며, 더 안정적이고 예측 가능한 LangGraph 애플리케이션을 구축할 수 있다.