검색은 실패한다 — 끊겨도 멈추지 않게
한 줄 요약
웹검색은 자주 실패합니다. 특히 짧은 시간에 여러 번 연달아 실행하면 실패하기 쉽습니다. 이건 사고가 아니라 예상하고 대비해야 할 상황입니다. 실패를 예외가 아니라 문장으로 처리하는 것이 이 절의 핵심입니다.
1. 실제로 보게 될 화면
==============================================================
[1] 도구 두 개를 따로 확인
승승 스마트워치 Fit 5 | 카테고리 전자기기 | 판매가 159,000원 | 카탈로그 평점 4.8
검색에 실패했습니다(RatelimitException). 네트워크 상태를 확인하거나 잠시 후 ...
여러분 코드가 틀린 게 아닙니다. 이런 이유들로 실패합니다.
| 원인 | 언제 |
|---|---|
요청 제한 (RatelimitException) |
짧은 시간에 검색을 여러 번 연달아 실행 |
응답 없음 (TimeoutError) |
네트워크가 느리거나 불안정 |
| 연결 실패 | 인터넷이 끊김, 사내망 차단 |
| 결과 없음 | 검색어가 너무 특이함 |
DuckDuckGo는 키가 필요 없는 대신 이런 제약이 있습니다. 2절에서 말한 그 맞바꾸기(트레이드오프) 입니다.
2. 실패를 어떻게 처리했나
아래는 code/ch04_web_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.
def search_web(query: str) -> str:
"""인터넷에서 최신 정보를 검색한다. ..."""
try:
results = DDGS().text(query, max_results=4, region="kr-kr")
if not results:
return "검색 결과가 없습니다. 다른 검색어로 시도해 보세요."
return "\n".join(...)
except Exception as e:
return (f"검색에 실패했습니다({type(e).__name__}). "
"네트워크 상태를 확인하거나 잠시 후 다시 시도해 주세요.")
try / except 로 감싸고, 예외 대신 문장을 돌려줍니다.
만약 감싸지 않았다면
아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.
def search_web(query: str) -> str:
results = DDGS().text(query, ...) # 실패하면 여기서 예외
return "\n".join(...)
[1단계] search_web({'query': '스마트워치 시장 가격대'})
Traceback (most recent call last):
...
ddgs.exceptions.RatelimitException: ...
프로그램이 통째로 멈춥니다. 1단계에서 얻어 둔 상품 가격도 날아가고, 사용자는 아무 답도 못 받습니다.
3. 문장으로 돌려주면 무슨 일이 생기나
에이전트가 계속 동작합니다.
[1단계] get_product_info({'product_name': '승승 스마트워치 Fit 5'})
└ 승승 스마트워치 Fit 5 | 전자기기 | 판매가 159,000원 ...
[2단계] search_web({'query': '스마트워치 시장 가격대'})
└ 검색에 실패했습니다(RatelimitException). ...
[3단계] 도구 호출 없음 → 최종 답변
[답변]
승승 스마트워치 Fit 5의 판매가는 159,000원입니다.
다만 시장 가격대 정보는 검색에 실패하여 확인하지 못했습니다.
세 가지가 일어났습니다.
- 1단계에서 얻은 정보는 살아남았습니다
- 모델이 실패 문장을 읽고 이해했습니다
- 아는 것만 답하고, 모르는 부분은 모른다고 했습니다
세 번째가 특히 중요합니다. 실패했다는 사실도 정보이기 때문에 모델이 지어내지 않았습니다. 3장 7절에서 본 원칙 그대로입니다 — 빈칸을 채워 주면 지어내지 않는다.
만약 빈 문자열을 돌려줬다면 모델은 검색이 실패한 줄 모르고, "시장 가격대는 대략 10만~30만원대입니다" 같은 걸 자기 지식으로 지어낼 수 있습니다. 이게 1장 반품 기간 사례와 정확히 같은 구조입니다.
4. 예외 종류를 알려 주는 이유
아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.
f"검색에 실패했습니다({type(e).__name__}). ..."
type(e).__name__ 은 예외의 종류 이름입니다 — RatelimitException, TimeoutError 등.
모델과 우리 모두에게 도움이 됩니다.
- 모델에게 — 어떤 종류의 실패인지에 따라 다시 시도할지 말지 판단할 수 있습니다
- 우리에게 — 화면만 보고 "아, 요청 제한이구나 / 네트워크구나"를 압니다
예외 메시지 전체(
str(e))를 넣지 않은 이유는, 라이브러리 예외 메시지가 길고 영어이고 URL이 잔뜩 들어 있어 토큰만 쓰기 때문입니다. 종류 이름 하나면 충분합니다.
5. 결과 없음과 실패를 나눈 이유 — 다시
아래는 code/ch04_web_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.
if not results:
return "검색 결과가 없습니다. 다른 검색어로 시도해 보세요."
아래는 code/ch04_web_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.
except Exception as e:
return f"검색에 실패했습니다({type(e).__name__}). ..."
돌려주는 문장이 모델의 다음 행동을 결정합니다.
| 문장 | 모델의 반응 |
|---|---|
| "검색 결과가 없습니다. 다른 검색어로 시도해 보세요" | 검색어를 바꿔 재시도 |
| "검색에 실패했습니다. 잠시 후 다시 시도해 주세요" | 재시도해도 소용없다고 판단 → 아는 것으로 답 |
도구의 반환 문자열이 곧 모델에게 주는 지시입니다. 3장 3절에서 "반환값도 설명"이라고 한 것이 여기서 실제로 적용됩니다.
6. 검색이 계속 실패할 때
지금 당장
- 1~2분 기다렸다가 다시 실행하세요. 요청 제한은 시간이 지나면 풀립니다.
[1]의search_web만 실패하고get_product_info는 되나요? 그러면 코드는 정상입니다.
그래도 안 되면 — 검색 없이 진행하기
search_web 을 잠시 가짜 결과를 돌려주는 함수로 바꿔 두면, 검색 없이도 루프 구조를 실습할 수 있습니다.
직접 고쳐 보기 —
agentic_ai/code/ch04_web_agent.py를 열어 아래처럼 바꾸거나 덧붙인 뒤 다시 실행하세요. (검색이 다시 되면 원래대로 되돌리세요.)
def search_web(query: str) -> str:
"""인터넷에서 최신 정보를 검색한다. 시장 동향·경쟁 제품처럼 사내 데이터에 없는 내용에 쓴다."""
# [임시] 검색이 막혔을 때 실습을 이어 가기 위한 대체 결과
return ("- 스마트워치 시장 가격대: 5만 원대 가성비 모델부터 30만 원대 프리미엄까지 형성\n"
"- 주요 경쟁 요소: 배터리 지속시간, 건강 측정 기능, 스마트폰 호환성")
독스트링은 그대로 두세요. 모델은 독스트링만 보므로, 도구 선택 실습에는 아무 지장이 없습니다.
이것도 배워 둘 만한 방법입니다. 도구 안이 진짜 검색이든 고정 문자열이든 에이전트 구조는 하나도 안 바뀝니다. 도구로 분리해 둔 덕분입니다. 실제로 개발할 때도 외부 서비스를 가짜(mock)로 바꿔 끼우고 나머지를 먼저 만듭니다.
7. 원칙 — 도구는 실패할 것을 전제로 만든다
이 과정의 모든 도구에 같은 원칙이 적용됩니다.
| 장 | 도구 | 실패할 때 |
|---|---|---|
| 3장 | get_order_status |
"주문번호를 찾을 수 없습니다" |
| 4장 | search_web |
"검색에 실패했습니다(...)" |
| 5장 | run_pandas |
"실행 오류: KeyError: ..." |
| 6장 | policy_search |
관련 조각이 없어도 가장 가까운 것을 돌려준다 → 그래서 "문서에 없으면 모른다고 하라"를 프롬프트로 지시한다 (6장 6절) |
| 8장 | 담당자(워커) 전체 | 한 명이 실패해도 나머지 보고로 답한다 |
"예외를 던지지 말고 상황을 설명하는 문장을 돌려준다" — 이 한 줄이 에이전트를 적은 비용으로 안정적으로 만드는 방법입니다.
스스로 해 보기
search_web의try/except를 일부러 지우고 인터넷을 끊은 채 실행해 보세요. 어떻게 되나요?except블록의 반환값을return ""으로 바꾸고 실행해 보세요. 모델의 최종 답변이 어떻게 달라지나요? 위험한가요?- 위의 가짜
search_web으로 바꿔 실행해 보세요. 단계 구조가 똑같은지 확인하세요. except문장에서type(e).__name__을 빼면 무엇이 불편해질까요?
핵심 정리
- 웹검색은 자주 실패합니다. 짧은 시간에 여러 번 실행하면 실패할 수 있습니다. 정상입니다.
- 예외를 던지면 프로그램이 멈춥니다. 그때까지 얻은 정보도 활용하지 못합니다.
try/except로 감싸고 문장을 돌려주면 에이전트가 계속 동작하고, 아는 것만 답합니다.- 실패했다는 사실도 정보입니다. 그래서 모델이 지어내지 않습니다.
- 결과 없음과 실패를 나눠 다른 문장을 돌려줍니다. 모델의 다음 행동이 달라집니다.
- 막혔을 때는 가짜 결과를 돌려주는 도구로 바꿔 끼우면 실습을 이어 갈 수 있습니다. 에이전트 구조는 안 바뀝니다.
- 도구는 실패할 것을 전제로 만듭니다. 이 과정의 모든 도구가 그렇습니다.