
에이전트가 같은 도구만 계속 부르고 멈추지 않는데, 대체 어디서 끊어야 할까요? 답은 모델 호출 루프 안에 종료 조건과 최대 스텝을 코드로 직접 심어두는 것입니다. LLM 자체는 “이제 그만 부르자”는 판단을 스스로 하지 못하는 경우가 많아서, AI 에이전트 무한 루프는 안전장치를 따로 만들지 않으면 언제든 벌어질 수 있는 일입니다.
도구 호출이 멈추지 않는 구조적인 이유
에이전트 루프는 보통 “모델이 도구 호출 여부를 판단 → 도구를 실행 → 결과를 다시 프롬프트에 넣고 모델을 재호출”하는 사이클로 돌아갑니다. 이 사이클 자체에는 원래 종료 신호가 없습니다. 개발자가 별도로 멈추는 조건을 넣지 않으면, 모델은 도구 결과를 계속 새로운 입력으로 받아들이고 또 다른 호출을 시도합니다.
특히 도구가 빈 결과나 애매한 오류 메시지를 돌려줄 때 문제가 커집니다. 모델이 이걸 “실패했으니 다른 방식으로 다시 시도해야 한다”로 해석해서, 인자만 살짝 바꿔가며 같은 도구를 계속 호출하는 패턴이 흔합니다. 서브 에이전트를 가드레일 없이 돌렸다가 API 한도가 순식간에 소진됐다는 사례들도 이런 구조에서 나옵니다.
로그를 보면 무한 루프가 이렇게 찍힙니다
실제로 문제가 생기면 로그에 특정 패턴이 남습니다. 동일한 tool 이름과 거의 같은 인자값이 연속으로 여러 번 찍히고, 모델의 응답 텍스트도 “다시 검색해보겠습니다”, “다른 방법으로 시도하겠습니다” 같은 문구를 반복하는 식입니다.
이때 max_tokens만 걸어두는 방식으로는 근본적인 해결이 안 됩니다. 토큰 상한에 걸릴 때까지 호출은 계속되고, 비용만 소진된 채로 작업은 끝내 완료되지 않습니다. 토큰 제한은 최후의 방어선일 뿐이고, 루프를 끊는 조건은 별도로 필요합니다.
최대 스텝을 코드로 박아넣는 법

프레임워크를 쓰는 경우 최대 스텝 제한이 이미 옵션으로 제공될 때가 많습니다. 예를 들어 LangGraph는 그래프를 실행할 때 config에 recursion_limit 값을 넣을 수 있고, 기본값은 25입니다. 이 값을 넘기면 GraphRecursionError가 발생하면서 실행이 강제로 멈춥니다. 자세한 옵션은 LangGraph 공식 문서에서 확인할 수 있습니다.
프레임워크 없이 직접 루프를 짜는 경우엔 스텝 카운터를 직접 넣으면 됩니다.
MAX_STEPS = 10
step = 0
while step < MAX_STEPS:
response = call_model(messages)
if response.tool_calls:
result = run_tool(response.tool_calls[0])
messages.append(result)
step += 1
else:
break
if step >= MAX_STEPS:
print("최대 스텝 도달 - 강제 종료")
이 코드만으로도 최소한 무한히 도는 상황은 막을 수 있습니다. 다만 스텝 수를 채우기 전까지는 같은 실패를 계속 반복할 수 있다는 한계가 남습니다.
종료 조건은 몇 겹으로 쌓아야 안전한가
최대 스텝 하나만으로는 부족합니다. 스텝 25를 다 채우기 전에 이미 같은 실패를 스무 번 반복하고 있었을 수도 있기 때문입니다. 그래서 실무에서는 아래처럼 서로 다른 성격의 조건을 겹쳐서 씁니다.
| 종료 조건 방식 | 감지 대상 | 장점 | 한계 |
|---|---|---|---|
| 최대 스텝 수 | 누적 호출 횟수 | 구현이 단순하고 반드시 종료를 보장 | 정상 작업도 중간에 끊길 수 있음 |
| 반복 호출 탐지 | 동일 tool + 인자 연속 반복 | 무의미한 반복을 빠르게 차단 | 정상적인 반복 호출과 구분이 어려움 |
| 결과 기반 종료 함수 | 목표 달성 여부 | 가장 정확하게 판단 | 판별 함수 자체를 설계하기 까다로움 |
| 타임아웃 | 경과 시간 | 비용 폭주를 막는 최후 방어선 | 원인 진단에는 도움이 안 됨 |
반복 호출 탐지는 아래처럼 최근 호출들의 시그니처를 비교하는 방식으로 구현할 수 있습니다.

import hashlib
recent_calls = []
MAX_REPEAT = 3
def call_signature(tool_name, args):
raw = f"{tool_name}:{sorted(args.items())}"
return hashlib.sha1(raw.encode()).hexdigest()
def should_stop(tool_name, args):
recent_calls.append(call_signature(tool_name, args))
if len(recent_calls) > MAX_REPEAT:
recent_calls.pop(0)
return len(recent_calls) == MAX_REPEAT and len(set(recent_calls)) == 1
이 함수는 같은 도구 이름과 같은 인자 조합이 3회 연속 나오면 True를 돌려줍니다. 최대 스텝 카운터와 이 함수를 같이 걸어두면, 스텝 한도를 다 채우기 전에도 의미 없는 반복을 조기에 끊을 수 있습니다.
이 설계가 안 통하는 경우도 있습니다
반복 호출 탐지는 정상적인 반복 작업까지 강제로 끊어버릴 위험이 있습니다. 페이지네이션을 순회하며 데이터를 계속 가져오거나, 여러 파일을 하나씩 열어서 리팩토링하는 작업은 겉보기엔 같은 도구를 같은 형태로 계속 호출하는 것처럼 보입니다. 이런 경우 동일 호출 3회 규칙을 그대로 적용하면 정상 작업이 중간에 끊깁니다.
이럴 땐 “인자가 완전히 동일한가”만 볼 게 아니라 “결과가 앞으로 나아가고 있는가”까지 같이 봐야 합니다. 예를 들어 페이지 번호가 매번 바뀌고 있다면 반복이 아니라 진행으로 판단해야 합니다. 다만 이 판별 로직을 정교하게 만들수록 구현 난이도와 유지보수 비용이 같이 올라간다는 점은 감안해야 합니다. 안전성과 작업 완결성 사이에서 어느 쪽에 더 무게를 둘지는 서비스 성격에 따라 달라집니다.
지금 코드에 적용해 볼 순서
가장 먼저 할 일은 지금 돌고 있는 에이전트의 로그에서 tool 호출 시퀀스를 뽑아 반복 패턴이 실제로 있는지 확인하는 것입니다. 그다음 최대 스텝 값을 낮게 잡고 테스트해보면서, 정상 작업이 몇 스텝 안에 끝나는지부터 파악하는 게 순서입니다.
여기에 반복 호출 탐지 로직을 추가하고, 마지막으로 “이 작업이 끝났다고 볼 수 있는 조건”을 명확한 함수로 만들어두면 종료 조건 설계는 대부분 마무리됩니다. 무한 루프는 도구를 잘못 만들어서가 아니라 멈추는 조건을 안 만들어서 생기는 문제인 경우가 많다는 점을 기억해두면 좋습니다.
루프와 재귀의 기본 개념 자체는 위키백과의 무한 루프 문서에서, AI 시스템의 신뢰성·안전성 관리 관점은 미국 NIST의 AI 위험관리 프레임워크에서 참고할 수 있습니다.
