LangGraph supervisor 멀티에이전트가 GraphRecursionError(25)로 죽습니다 — recursion_limit를 올리는 게 맞나요?
LangGraph로 supervisor + 워커 3개(researcher / writer / reviewer) 구조를 짰습니다. 한국어 리서치 태스크를 돌리면 정상 종료가 안 되고 약 25스텝쯤에서 이렇게 죽습니다.
langgraph.errors.GraphRecursionError: Recursion limit of 25 reached without hitting a stop condition.
LangSmith 트레이스를 보면 supervisor → reviewer → supervisor → researcher → supervisor → reviewer ... 식으로 supervisor와 워커 사이를 계속 왕복하다가 한도에 걸립니다. 결과물 자체는 3~4스텝이면 충분한데도요.
graph.invoke(..., {"recursion_limit": 100})으로 한도를 올리면 에러는 안 나지만 LLM 호출이 그만큼 늘어서 토큰/비용만 폭증하고, 가끔은 100까지 돌고도 답을 못 냅니다. recursion_limit를 올리는 게 정답인가요, 아니면 구조 문제인가요?
답변 1개
- 채택된 답변에디터 검증
결론: recursion_limit를 올리는 건 증상 가리기다.
GraphRecursionError: Recursion limit of 25(LangGraph 기본값 25 supersteps)는 거의 항상 supervisor가 "끝났다"를 판단하지 못해 워커와 무한 핑퐁하는 구조 문제다. 한도를 100으로 올리면 같은 루프를 4배 더 비싸게 돌 뿐이다. 고칠 곳은 세 군데다: supervisor에 명시적 종료 분기, 워커→supervisor 단방향 강제, state에 라운드 카운터.왜 핑퐁이 생기나
supervisor 패턴에서 워커가 끝나면 보통 supervisor로 엣지가 돌아온다. supervisor의 라우팅 프롬프트가 "다음에 누구에게 보낼까"만 묻고 "이제 끝낼까"를 동등한 선택지로 주지 않으면, LLM은 항상 누군가를 더 호출하는 쪽으로 기운다. reviewer가 "문제없음"이라고 답해도 supervisor는 그걸 종료 신호가 아니라 또 다른 워커 호출 트리거로 읽는다. 여기에 양방향 자유 핸드오프(워커끼리도 서로 부를 수 있게)까지 열어두면 사이클이 폭발한다.
고치는 법
1) 종료를 1급 라우팅 옵션으로. supervisor가 고를 수 있는 목적지에
FINISH/END를 반드시 넣고, reviewer가 승인하면 그 경로로만 가게 한다.from typing import Literal, TypedDict from pydantic import BaseModel from langgraph.graph import StateGraph, START, END from langgraph.types import Command class State(TypedDict): messages: list round: int # 의미 있는 작업 반복 카운터 approved: bool # reviewer 승인 플래그 MEMBERS = ["researcher", "writer", "reviewer"] class Router(BaseModel): next: Literal["researcher", "writer", "reviewer", "FINISH"] def supervisor(state: State) -> Command[Literal["researcher", "writer", "reviewer", "__end__"]]: # 하드 가드: 승인됐거나 라운드 초과면 LLM에 묻지도 말고 종료 if state.get("approved") or state["round"] >= 6: return Command(goto=END) decision = llm.with_structured_output(Router).invoke( [SYS_PROMPT] + state["messages"] # SYS_PROMPT에 "reviewer가 approve하면 반드시 FINISH" 명시 ) nxt = decision.next return Command( goto=END if nxt == "FINISH" else nxt, update={"round": state["round"] + 1}, )2) 워커는 supervisor로만 복귀(단방향). 워커끼리 직접 핸드오프하는 엣지를 만들지 마라.
researcher → supervisor,writer → supervisor처럼 워커의 out-edge를 supervisor 하나로 고정하면 사이클 토폴로지 자체가 줄어든다. reviewer는 평가 결과를 state의approved불리언으로 써서 supervisor의 하드 가드가 읽게 한다(reviewer가 직접 다음 워커를 호출하는 self-approval 루프는 금지).3) 라운드 카운터로 fail-safe. 위 코드의
state["round"] >= 6처럼 state 안에 카운터를 두고 임계치에서 END로 보낸다. 이건 recursion_limit(전역 supersteps)와 다르다. recursion_limit는 그래프의 모든 스텝을 세는 안전망이고, 라운드 카운터는 의미 있는 작업 반복만 센다. 둘 다 있어야 한다. recursion_limit는 5~6라운드에 맞춰 넉넉히(예: 30~40) 잡고, 실제 종료는 라운드 카운터가 책임진다.한국어 작업에서 더 위험한 이유 두 가지
- 한글 토큰 비용. 대부분의 BPE 토크나이저에서 한글은 영어보다 토큰이 더 잘게 쪼개진다. supervisor 라우팅 프롬프트가 전체 한국어 대화 히스토리를 매 스텝 통째로 본다면, 25스텝 핑퐁만으로 입력 토큰이 같은 영어 시나리오보다 눈에 띄게 더 청구된다. supervisor에는 전체 메시지가 아니라 마지막 워커 산출 요약 + 라우팅에 필요한 필드만 넘겨라.
- structured output 라우터가 한국어에서 잘 깨진다.
with_structured_output으로next필드를 강제하지 않고 자유 텍스트로 "reviewer에게 보낼게요"를 파싱하면, 한국어 응답이 enum 밖 값을 내서 라우팅이 supervisor로 폴백되고 무한 루프가 된다. 목적지는 반드시Literal[...]enum으로 못 박아라.
recursion_limit를 올려도 되는 유일한 경우
워커가 진짜로 많은 단계를 거치는 정당한 장기 작업(예: 50개 파일을 순회 처리)이고, 트레이스상 같은 노드 쌍의 반복이 아니라 서로 다른 작업이 누적되는 경우뿐이다. LangSmith에서
A→B→A→B반복이 보이면 그건 한도 문제가 아니라 종료조건 부재다.Sources: GRAPH_RECURSION_LIMIT - LangChain Docs, LangGraph supervisor infinite loop issue #6731