종료 조건 — 무한 반복과 비용 폭탄 막기
한 줄 요약
에이전트는 스스로 멈출 줄 압니다. 다만 못 멈추는 경우가 있고, 그때는 요금이 계속 나갑니다. MAX_STEPS 한 줄이 그 사고를 막습니다.
1. 종료는 두 가지
아래는 code/ch04_web_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.
for step in range(1, MAX_STEPS + 1):
...
if not resp.function_calls:
return resp.text.strip() # ① 정상 종료
...
return "(최대 단계에 도달해 중단했습니다)" # ② 강제 종료
| 언제 | 결과 | |
|---|---|---|
| ① 정상 종료 | 모델이 "이제 답할 수 있다"고 판단 | 답변 반환 |
| ② 강제 종료 | MAX_STEPS 번을 다 썼는데도 안 끝남 |
중단 메시지 |
대부분은 ①입니다. 앞 절의 실행에서도 3단계 만에 스스로 끝났습니다.
2. 못 멈추는 경우
②가 필요한 이유는 이런 상황들 때문입니다.
① 도구가 계속 실패한다
[1단계] search_web("스마트워치 시장") → "검색에 실패했습니다(TimeoutError)"
[2단계] search_web("스마트워치 가격") → "검색에 실패했습니다(TimeoutError)"
[3단계] search_web("smartwatch price") → "검색에 실패했습니다(TimeoutError)"
[4단계] ...
모델은 "검색어가 나빴나?" 하고 바꿔 가며 계속 시도합니다. 나쁜 판단은 아닙니다 — 원인이 네트워크라는 걸 모르니까요. 하지만 몇 번을 해도 안 됩니다.
② 답할 수 없는 것을 계속 찾는다
질문: "승승장구몰의 2027년 매출 전망을 알려주세요"
[1단계] search_web("승승장구몰 매출 전망") → 결과 없음
[2단계] search_web("승승장구몰 2027") → 결과 없음
[3단계] get_product_info("승승장구몰") → 찾을 수 없음
[4단계] ...
애초에 어디에도 없는 정보입니다. 그런데 모델은 "다른 도구로 찾아보자"를 반복합니다.
③ 두 도구를 오간다
[1단계] get_product_info("스마트워치") → 159,000원
[2단계] search_web("스마트워치 가격") → 시장 정보
[3단계] get_product_info("스마트워치") → 159,000원 ← 아까 한 것
[4단계] ...
드물지만 일어납니다. 이미 얻은 정보를 다시 가져오는 것이죠.
3. 왜 이게 사고인가
LLM 호출 한 번에 요금이 나갑니다. 그리고 history 는 계속 자랍니다.
[1단계] 입력 500 토큰
[2단계] 입력 1,200 토큰 (1단계 것이 다 들어감)
[3단계] 입력 2,000 토큰
[4단계] 입력 3,100 토큰
...
[50단계] 입력 60,000 토큰
단계가 늘수록 한 단계의 비용도 커집니다. 2장에서 본 그 특성이 여기서 더 두드러집니다.
while True: 로 짜고 이 상황이 되면 — 프로그램은 안 멈추고 요금은 계속 나갑니다. 밤새 돌면 어떻게 될지 상상해 보세요.
이걸 왜 알아야 하나 — 이걸 모르면 에이전트를 서버에 올려놓고 "왜 요금이 이렇게 나오지?" 할 때, 원인이 루프가 안 끝나는 것이라는 걸 떠올리지 못합니다. 그리고 대책(반복 횟수 상한, 로그, 반복 감지)도 생각해 내지 못합니다.
4. MAX_STEPS — 가장 단순한 안전장치
아래는 code/ch04_web_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.
MAX_STEPS = 5 # 안전장치: 아무리 꼬여도 5번 안에 멈춘다
아래는 code/ch04_web_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.
for step in range(1, MAX_STEPS + 1):
while 이 아니라 for + 상한을 쓴 이유입니다. 못 멈추는 상황이 와도 반드시 끝납니다.
몇으로 잡나
| 값 | 언제 |
|---|---|
| 3 | 도구 1~2개, 단순 조회 |
| 5 | 이 과정의 기본값. 도구 2~3개 |
| 10 | 도구가 많고 여러 번 오가야 할 때 |
| 20+ | 코드 작성처럼 시행착오가 필요한 작업 |
너무 작으면 정상적인 작업도 중간에 잘립니다. 너무 크면 안전장치 역할을 못 합니다.
감을 잡는 법 — 정상적인 질문 몇 개를 돌려 보고 보통 몇 단계에 끝나는지 확인한 뒤, 그 두 배쯤으로 잡으면 무난합니다. 우리 예제는 3단계에 끝났으니 5는 적절합니다.
5. 중단됐을 때 무엇을 돌려주나
아래는 code/ch04_web_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.
return "(최대 단계에 도달해 중단했습니다)"
빈 문자열이나 None 을 돌려주면 안 됩니다. 사용자에게는 아무 일도 안 일어난 것처럼 보이니까요.
실제 서비스라면 이렇게 하는 편이 낫습니다.
아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.
return ("죄송합니다. 요청하신 내용을 확인하는 데 실패했습니다. "
"잠시 후 다시 시도하거나 상담원에게 문의해 주세요.")
그리고 로그에는 자세히 남깁니다 — 몇 단계까지 갔는지, 어떤 도구를 반복했는지. 그래야 나중에 원인을 찾습니다.
6. 더 나은 안전장치들 (참고)
이 과정에서는 MAX_STEPS 하나로 충분하지만, 실제로는 이런 것들도 씁니다.
| 장치 | 무엇을 막나 |
|---|---|
| 최대 단계 수 | 무한 반복 (우리가 쓴 것) |
| 누적 토큰 상한 | 비용 폭탄 |
| 시간 제한 | 응답이 안 오는 상황 |
| 같은 호출 반복 감지 | 같은 도구를 같은 인자로 두 번 부르면 중단 |
| 도구별 호출 횟수 제한 | 검색만 20번 하는 상황 |
세 번째까지는 만들기 쉽습니다. 예를 들어 같은 호출을 감지하려면:
아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.
seen_calls = set()
...
key = (fc.name, str(dict(fc.args)))
if key in seen_calls:
result = "이미 같은 조회를 했습니다. 다른 방법을 시도하거나 지금까지의 정보로 답하세요."
else:
seen_calls.add(key)
result = TOOLS[fc.name](**dict(fc.args))
결과 문자열로 모델에게 알려 주는 것을 눈여겨보세요. 3장에서 배운 원칙 그대로입니다 — 실패했을 때도 상황을 설명하는 문장을 돌려준다.
7. 5장에서는
LangChain에도 같은 개념이 있습니다. 이름과 방식이 다를 뿐입니다.
아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.
agent.invoke({...}, {"recursion_limit": 10})
이름이 달라도 하는 일은 같습니다 — 몇 번까지 돌지 정하는 것. 여기서 손으로 MAX_STEPS 를 걸어 봤기 때문에, 라이브러리에서 이 설정을 찾아 쓸 수 있습니다.
스스로 해 보기
MAX_STEPS = 1로 바꾸고 실행해 보세요. 무엇이 출력되나요?- 답할 수 없는 질문을 던져 보세요.
python answer = run_agent("승승장구몰의 2027년 매출 전망은 어떻게 되나요?")
몇 단계까지 가나요? 어떤 도구를 반복하나요? - 매 단계
history길이를 찍어 보세요. 단계가 늘수록 얼마나 커지나요? - 위의 같은 호출 반복 감지를 직접 넣어 보세요.
핵심 정리
- 종료는 두 가지 — ① 모델이 스스로 멈춤(정상) 과 ②
MAX_STEPS로 강제 중단. - 못 멈추는 경우가 있습니다 — 도구가 계속 실패 · 없는 정보를 계속 찾음 · 같은 조회 반복.
while True:는 위험합니다. 단계가 늘수록history가 커져 한 단계의 비용도 커집니다.for+MAX_STEPS가 가장 단순하고 확실한 안전장치입니다. 이 과정의 기본값은 5.- 값을 정하는 법 — 정상적인 질문이 몇 단계에 끝나는지 보고 그 두 배쯤.
- 중단됐을 때 사용자에게 보여 줄 문장을 돌려주고, 로그에는 자세히 남깁니다.
- 5장 LangChain에도 같은 개념이
recursion_limit이라는 이름으로 있습니다.