Dandy Now!
  • [LLM] LangGraph에서 GraphRecursionError 해결하기: 올바른 상태 관리의 중요성
    2025년 05월 05일 01시 05분 33초에 업로드 된 글입니다.
    작성자: DandyNow
    728x90
    반응형

    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 애플리케이션을 구축할 수 있다.

    728x90
    반응형
    댓글