- Published on
LangChain 에이전트 무한루프 7가지 원인과 차단법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
LangChain 에이전트는 생각(Reasoning)과 행동(Action)을 반복하며 목표에 도달합니다. 문제는 이 반복이 종료 조건 없이 계속되거나, 같은 툴 호출/같은 검색/같은 플랜을 되풀이하면서 비용과 시간을 태워버리는 형태로 터진다는 점입니다. 특히 ReAct 스타일(툴 사용) 에이전트는 한 번 루프가 걸리면 토큰·API 호출·외부 시스템 부하가 동시에 증가합니다.
이 글은 LangChain 기반 에이전트에서 무한루프가 발생하는 대표 패턴 7가지를 원인별로 분해하고, 코드 레벨에서 차단하는 방법을 정리합니다. AutoGPT 계열에서의 종료조건/메모리 설계 관점도 유사하니 함께 참고하면 좋습니다: AutoGPT 에이전트 무한루프 방지 - 종료조건·메모리
무한루프를 “정의”부터 명확히 하기
현장에서 무한루프는 보통 아래 중 하나로 관측됩니다.
- Step 루프: 동일한
Thought/Action패턴이 N회 이상 반복 - Tool 루프: 같은 입력으로 같은 툴을 계속 호출(예: 검색 쿼리 고정)
- Planner 루프: 계획을 계속 수정만 하고 실행을 못 함
- Recover 루프: 예외 처리/리트라이 경로에서 동일 실패를 반복
따라서 차단도 “한 방”이 아니라, (1) 상한선, (2) 반복 감지, (3) 실패 예산, (4) 종료 신호를 함께 둬야 합니다.
1) 종료 조건 부재: max_iterations와 stop 신호가 없다
가장 흔한 원인은 단순합니다. 반복 상한이 없거나, 상한이 있어도 상한 도달 시 종료 전략이 부실합니다.
차단 전략
- 에이전트 실행에 반드시 반복 상한을 둡니다.
- 상한 도달 시
partial answer를 반환하거나, 사용자에게 추가 정보 요청으로 종료합니다.
코드 예시(파이썬, LangChain)
from langchain.agents import AgentExecutor, create_react_agent
from langchain_openai import ChatOpenAI
from langchain.tools import tool
@tool
def ping(x: str) -> str:
return f"pong: {x}"
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
agent = create_react_agent(llm, tools=[ping])
executor = AgentExecutor(
agent=agent,
tools=[ping],
verbose=True,
max_iterations=8,
early_stopping_method="generate", # 상한 도달 시 요약/답 생성 시도
)
result = executor.invoke({"input": "ping을 한 번만 호출하고 끝내"})
print(result["output"])
max_iterations는 최소 안전장치입니다. 하지만 이것만으로는 “8번까지는 계속 돈다”이므로, 아래 원인들과 함께 막아야 합니다.
2) 툴 출력이 비결정적이거나 장황해서 “다시 확인” 루프가 생긴다
검색/브라우징/스크레이핑/DB 조회 같은 툴이 다음 조건을 만족하면 루프를 유발합니다.
- 결과가 매번 다름(시간/랭킹/랜덤)
- 결과가 너무 길어 핵심이 안 잡힘
- 실패 시 에러 메시지가 애매해 재시도를 부름
에이전트는 “확신”을 얻기 위해 같은 툴을 반복 호출하는 경향이 있습니다.
차단 전략
- 툴 출력은 요약된 고정 스키마로 반환
- 타임아웃/실패 시 명시적 실패 타입 반환
- 동일 입력 반복 호출을 감지해 차단
코드 예시: 툴 출력 스키마 고정 + 길이 제한
import json
from langchain.tools import tool
@tool
def web_search(query: str) -> str:
# 실제로는 검색 API 호출
docs = [
{"title": "DocA", "url": "https://example.com/a", "snippet": "..."},
{"title": "DocB", "url": "https://example.com/b", "snippet": "..."},
]
payload = {
"query": query,
"top_k": 2,
"results": docs,
"note": "결과는 요약 스니펫만 포함하며 원문은 직접 열람 필요",
}
text = json.dumps(payload, ensure_ascii=False)
return text[:2000] # 과도한 길이 차단
툴이 “짧고, 구조화되고, 실패가 명확한” 출력을 주면 에이전트의 재호출 충동이 크게 줄어듭니다.
3) 메모리/상태 설계가 잘못되어 같은 결론을 반복한다
무한루프는 종종 모델이 멍청해서가 아니라, 상태가 누락되어서 발생합니다.
- 이미 시도한 액션을 기억하지 못함
- “무엇을 했고 무엇이 실패했는지”가 프롬프트에 남지 않음
- 반대로 메모리가 너무 길어 핵심이 묻혀 동일 패턴으로 회귀
차단 전략
시도 목록(Attempt log)을 짧게 유지하며 시스템 메시지에 포함- 실패 원인과 다음 액션 금지 규칙을 명시
- 일정 길이 이상 대화는 요약 메모리로 축약
코드 예시: 시도 로그를 별도 상태로 유지
from dataclasses import dataclass, field
@dataclass
class AttemptState:
attempts: list[str] = field(default_factory=list)
def add(self, key: str) -> None:
self.attempts.append(key)
self.attempts = self.attempts[-10:] # 최근 10개만 유지
def seen(self, key: str) -> bool:
return key in self.attempts
이 상태를 툴 래퍼나 실행 루프에서 사용하면, 같은 입력으로 같은 툴을 반복 호출하는 것을 손쉽게 막을 수 있습니다.
4) 관찰(Observation) 해석 실패: 툴 결과를 이해 못해 “다시 해보기”로 도망
툴이 반환하는 데이터가 애매하거나, 에이전트 프롬프트가 결과 해석을 강제하지 않으면 다음이 발생합니다.
Observation을 읽고도 다음 결정을 못 내림- “확인 필요”라는 말로 같은 툴을 재호출
차단 전략
- 프롬프트에 관찰 결과를 반드시 구조화해 요약하도록 강제
- “재호출 조건”을 명확히 제한
프롬프트 패턴(핵심만)
- 관찰을 받으면 아래를 반드시 출력
핵심 사실 3개결론추가 호출이 필요하면 이유 1줄과 필요한 입력
- 동일 툴을 같은 입력으로 2회 이상 호출 금지
LangChain에서는 시스템 메시지/에이전트 프롬프트 템플릿에 이 규칙을 넣고, 아래 5번의 반복 감지와 결합하는 방식이 효과적입니다.
5) 반복 감지(Loop detection)가 없다: 같은 액션 시그니처를 계속 탄다
실전에서 가장 즉효가 큰 차단은 반복 감지입니다.
tool_name + normalized_input을 시그니처로 만들고- 동일 시그니처가 N회 이상이면 종료 또는 다른 전략으로 전환
코드 예시: 툴 호출 반복 차단 래퍼
import re
from collections import Counter
class ToolCallGuard:
def __init__(self, max_same_call: int = 2):
self.max_same_call = max_same_call
self.counter = Counter()
def _norm(self, s: str) -> str:
s = s.strip().lower()
s = re.sub(r"\s+", " ", s)
return s[:500]
def check_and_count(self, tool_name: str, tool_input: str) -> None:
sig = f"{tool_name}:{self._norm(tool_input)}"
self.counter[sig] += 1
if self.counter[sig] > self.max_same_call:
raise RuntimeError(f"loop_detected: repeated tool call: {sig}")
이 가드를 각 툴 실행 직전에 넣으면, “검색을 같은 쿼리로 계속” 같은 전형적 루프를 즉시 차단할 수 있습니다.
6) 예외 처리/리트라이 정책이 공격적이다: 실패가 루프를 만든다
에이전트 시스템은 보통 외부 API를 많이 호출합니다. 네트워크 오류, 429, 5xx, 타임아웃이 생기면 리트라이가 걸리는데, 아래 조합이 위험합니다.
- 무제한 리트라이
- 짧은 고정 딜레이
- 실패 원인을 프롬프트에 전달하지 않음
이러면 “같은 실패를 같은 방식으로 반복”하는 복구 루프가 됩니다.
차단 전략
- 리트라이는 지수 백오프 + 최대 횟수
- 실패 유형을
fatal과retryable로 구분 - 실패가 누적되면 에이전트 종료 또는 대체 경로로 전환
코드 예시: 단순 백오프 리트라이(의사 코드)
import time
def call_with_retry(fn, max_retry: int = 3, base_sleep: float = 0.5):
last_err = None
for i in range(max_retry + 1):
try:
return fn()
except Exception as e:
last_err = e
if i == max_retry:
break
time.sleep(base_sleep * (2 ** i))
raise RuntimeError(f"tool_failed_after_retries: {last_err}")
운영 환경에서는 관측성(로그/메트릭)과 결합해 “특정 툴의 실패율 급등”을 감지해야 합니다. 무한루프는 애플리케이션 레벨 장애로 확대되기 쉽습니다.
7) 목표/성공 기준이 모호하다: 완료 판정이 없어 계속 ‘개선’한다
에이전트에게 “최대한 좋은 답”을 요구하면, 모델은 끝없이 개선하려고 합니다. 특히 아래 형태가 위험합니다.
- “완벽하게”, “가능한 많이”, “모든 경우를” 같은 지시
- 성공 조건이 수치나 체크리스트로 표현되지 않음
- 출력 형식이 불명확
차단 전략
- 성공 기준을 체크리스트로 명시
- “충족 시 즉시 종료” 규칙을 시스템 메시지에 넣기
- 출력 형식을 JSON 스키마 등으로 제한(가능하면)
예시: 성공 기준 체크리스트
- 필수 항목 5개를 채우면 종료
- 불확실한 항목은
unknown으로 표기하고 추가 조사 없이 종료
이 패턴은 RAG에서도 동일하게 중요합니다. 검색을 무한히 늘리는 대신, “충분 조건”을 정의해야 정확도와 비용이 안정됩니다. 관련 튜닝 관점은 RAG 정확도 폭락? Milvus HNSW 튜닝 7가지도 함께 보면 좋습니다.
실전용: 무한루프 차단을 한 번에 넣는 실행 래퍼
아래는 “상한선 + 반복 감지 + 실패 예산”을 묶은 최소 실행 래퍼 예시입니다. LangChain 내부 콜백을 깊게 파지 않더라도, 바깥에서 실행을 감싸는 것만으로도 사고를 크게 줄일 수 있습니다.
from time import monotonic
class AgentRunLimiter:
def __init__(
self,
max_seconds: float = 30.0,
max_tool_errors: int = 3,
max_same_tool_call: int = 2,
):
self.max_seconds = max_seconds
self.max_tool_errors = max_tool_errors
self.guard = ToolCallGuard(max_same_call=max_same_tool_call)
self.tool_errors = 0
self.started = None
def start(self):
self.started = monotonic()
def check_time(self):
if self.started is None:
return
if monotonic() - self.started > self.max_seconds:
raise TimeoutError("agent_timeout")
def on_tool_call(self, tool_name: str, tool_input: str):
self.check_time()
self.guard.check_and_count(tool_name, tool_input)
def on_tool_error(self, err: Exception):
self.tool_errors += 1
if self.tool_errors > self.max_tool_errors:
raise RuntimeError(f"tool_error_budget_exceeded: {err}")
핵심은 “에이전트가 똑똑해지길 기다리는 것”이 아니라, 시스템이 안전하게 멈추게 만드는 것입니다.
디버깅 체크리스트: 어디서 루프가 도는지 빠르게 찾기
운영에서 루프를 잡을 때는 원인 규명이 먼저입니다.
- 동일
tool_name이 연속 호출되는가 - 동일 입력(정규화 기준)이 반복되는가
Observation이 너무 길거나, 에러가 애매한가max_iterations에 도달해도 왜 답이 생성되지 않는가- 실패가 특정 외부 시스템(검색, DB, HTTP)에서 집중되는가
에이전트 시스템도 결국 분산 시스템처럼 장애가 전파됩니다. 특히 외부 인증/권한 오류가 반복되면 “복구 루프”로 보이기 쉬운데, 이런 류의 문제는 원인을 정확히 깎아내야 합니다. 예를 들어 클라우드 권한/토큰 만료는 무한 재시도를 부르기 쉬우니, 유사한 트러블슈팅 글인 EKS IRSA 웹아이덴티티 토큰 만료·403 해결 같은 접근(원인 분류, 재현, 차단)을 참고하면 도움이 됩니다.
결론: 무한루프는 모델 문제가 아니라 “가드레일 부재” 문제다
LangChain 에이전트 무한루프는 대부분 아래 4가지 가드레일이 빠져서 발생합니다.
- 반복 상한(
max_iterations,timeout) - 반복 감지(같은 툴/같은 입력 시그니처)
- 실패 예산(리트라이/에러 누적 제한)
- 명확한 성공 기준(체크리스트, 출력 스키마)
위 7가지 원인을 기준으로 자신의 에이전트를 점검하고, 최소한 상한선 + 반복 감지부터 적용해도 비용 폭주와 장애 전파를 크게 줄일 수 있습니다.