AI

[LLM] LangGraph에서 GraphRecursionError 해결하기: 올바른 상태 관리의 중요성

DandyNow 2025. 5. 5. 01:05
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
반응형