📚 Agentic AI - 기업용 자율 에이전트 개발 4장 · 에이전트 ② 웹검색 Agentic AI란

검색 도구 붙이기 — 키가 필요 없는 DuckDuckGo

한 줄 요약

ddgs 라이브러리로 DuckDuckGo 검색을 씁니다. API 키가 필요 없습니다. 검색 결과를 모델이 읽을 수 있는 문자열로 다듬는 것까지가 우리 일입니다.


1. 왜 DuckDuckGo인가

웹검색을 코드에서 쓰려면 보통 검색 API가 필요합니다.

서비스 API 키 비용
Google Custom Search 필요 하루 100건 무료, 이후 유료
Bing Search API 필요 유료
Serper, Tavily 등 필요 무료 티어 있음
DuckDuckGo (ddgs) 불필요 무료

이 과정은 키를 하나만 다루기로 했습니다(Gemini). 그래서 키가 필요 없는 DuckDuckGo를 씁니다.

대신 단점이 있습니다. 공식 API가 아니라 호출이 몰리면 일시적으로 막힙니다. 짧은 시간에 여러 번 연달아 실행하면 실패할 수 있습니다. 이 문제는 9절에서 따로 다룹니다.

하나를 얻는 대신 다른 하나를 포기하는 관계입니다 — 키를 안 다뤄도 되는 대신 안정성을 일부 포기했습니다. 이런 관계를 트레이드오프(trade-off) 라고 부릅니다. 이 교재에서는 맞바꾸기라는 한국어로도 씁니다. 두 말은 같은 뜻입니다. 에이전트를 설계할 때 계속 마주치는 판단이라, 앞으로 "무엇을 얻고 무엇을 내줬나"를 매번 짚습니다.

실제 서비스를 만든다면 유료 검색 API를 쓰는 편이 안정적입니다. 다만 바꾸는 건 도구 함수 하나를 갈아 끼우는 일입니다. 에이전트 구조는 그대로입니다. 그게 도구로 분리해 둔 이점입니다.


2. ddgs 써 보기

requirements.txt 로 이미 깔려 있습니다. 먼저 에이전트와 무관하게 그냥 써 봅시다.

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

from ddgs import DDGS

results = DDGS().text("스마트워치 배터리 지속시간 비교", max_results=3, region="kr-kr")
for r in results:
    print(r["title"])
    print(r["href"])
    print(r["body"][:80])
    print()
애플워치 배터리 타임 타사 대비 너무 짧습니다. - 클리앙
https://www.clien.net/service/board/cm_iphonien/18807688
Sep 23, 2024 ... 차이가 나도 너무 나는 것 같습니다. 배터리 용량 자체도 별로 ...

애플워치의 배터리 사용시간이 짧을 수밖에 없는 이유 - 네이버 블로그
https://m.blog.naver.com/footballit/222104062528
Oct 1, 2020 ... 애플은 공식적인 애플워치의 배터리 사용 시간을 항상 18시간이라고 ...

인자 세 개

인자
첫 번째 검색어
max_results 몇 개까지 가져올지
region 어느 지역 기준으로 ("kr-kr" = 한국)

max_results 를 크게 잡지 마세요. 결과가 많아질수록 모델에게 보내는 토큰이 늘어납니다. 이 과정에서는 4개로 씁니다.

결과의 모양

각 결과는 키 세 개짜리 딕셔너리입니다.

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

{"title": "제목", "href": "주소", "body": "본문 요약"}

3. 도구로 감싸기

ddgs 결과를 그대로 모델에게 줄 수는 없습니다. 딕셔너리 목록이라서요. 문자열로 다듬는 것이 우리 일입니다.

아래는 3절에서 만들 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(f"- {r.get('title','')}: {r.get('body','')[:180]}"
                         for r in results)
    except Exception as e:
        # [중요] 검색은 외부 서비스라 언제든 실패한다.
        # 예외를 그대로 터뜨리면 에이전트가 멈추므로, 상황을 알리는 문장을 돌려준다.
        return (f"검색에 실패했습니다({type(e).__name__}). "
                "네트워크 상태를 확인하거나 잠시 후 다시 시도해 주세요.")

세 가지 결정

이 짧은 함수 안에 사람의 판단이 셋 들어 있습니다.

① 제목과 본문만, 주소는 뺀다

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

f"- {r.get('title','')}: {r.get('body','')[:180]}"

href 를 안 넣었습니다. URL은 토큰만 쓰고 모델의 판단에는 별 도움이 안 되기 때문입니다. 출처를 보여 줘야 하는 경우라면 넣어야 합니다 — 6장 RAG(문서검색)에서 출처를 붙이는 법과, 그것을 도구로 만들 때 출처가 어떻게 사라지는지를 함께 봅니다. 목적에 따라 다릅니다.

② 본문을 180자로 자른다

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

r.get('body','')[:180]

검색 결과 4개 × 본문 전체를 다 보내면 토큰이 크게 늡니다. 판단에 필요한 만큼만 보냅니다.

.get() 으로 안전하게 꺼낸다

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

r.get('title','')     # r['title'] 이 아니라

키가 없을 수도 있는 외부 데이터를 다룰 때의 습관입니다. r['title'] 은 키가 없으면 예외가 나지만, .get('title','') 은 빈 문자열을 돌려줍니다.


4. 결과 없음과 실패는 다릅니다

아래는 3절에서 만들 code/ch04_web_agent.py 의 일부를 미리 보는 것입니다 — 지금 붙여 넣지 않아도 됩니다.

if not results:
    return "검색 결과가 없습니다. 다른 검색어로 시도해 보세요."

아래는 3절에서 만들 code/ch04_web_agent.py 의 일부를 미리 보는 것입니다 — 지금 붙여 넣지 않아도 됩니다.

except Exception as e:
    return f"검색에 실패했습니다({type(e).__name__}). ..."

둘을 구분한 이유가 있습니다.

상황 모델이 해야 할 일
결과 없음 검색어를 바꿔 다시 시도해 볼 만하다
검색 실패 다시 시도해도 소용없다. 다른 방법을 찾거나 사용자에게 알린다

돌려주는 문장이 모델의 다음 행동을 바꿉니다. 3장에서 배운 원칙 — "실패했을 때도 상황을 설명하는 문장을 돌려준다" — 의 확장판입니다.

type(e).__name__ 은 예외의 종류 이름(TimeoutError, RatelimitException 등)입니다. 이걸 넣어 두면 모델에게도, 화면을 보는 우리에게도 무엇이 문제인지 힌트가 됩니다.


5. 독스트링 다시 보기

아래는 3절에서 만들 code/ch04_web_agent.py 의 일부를 미리 보는 것입니다 — 지금 붙여 넣지 않아도 됩니다.

"""인터넷에서 최신 정보를 검색한다. 시장 동향·경쟁 제품처럼 사내 데이터에 없는 내용에 쓴다."""

"사내 데이터에 없는 내용에 쓴다" — 3장 3절에서 말한 경계선입니다.

이 문장이 없으면 어떻게 될까요? 모델이 "승승 스마트워치 Fit 5 가격" 을 웹에서 찾으려 할 수 있습니다. 가상의 쇼핑몰이라 당연히 안 나오고요.

다음 절에서 도구를 두 개 쥐여 주면 이 문장이 실제로 어떤 역할을 하는지 보게 됩니다.


6. 검색어는 누가 만드나

여기가 이 장의 재미있는 지점입니다.

사용자 질문:  "요즘 스마트워치 시장 가격대와 비교하면 어느 수준인가요?"
                              ↓  (모델이 변환)
실제 검색어:  "스마트워치 시장 가격대"

우리는 검색어를 만드는 코드를 한 줄도 안 짰습니다. 사용자 문장에서 검색에 적합한 짧은 구절을 뽑아내는 것 — 그게 모델이 하는 일입니다.

다음 절에서 이걸 눈으로 확인합니다.


핵심 정리

  • ddgs 는 API 키 없이 쓸 수 있는 DuckDuckGo 검색 라이브러리입니다.
  • DDGS().text(query, max_results=4, region="kr-kr") — 결과는 title·href·body 딕셔너리 목록입니다.
  • 결과를 문자열로 다듬는 것이 우리 일입니다. 무엇을 넣고 무엇을 자를지가 사람의 판단입니다.
  • 결과 없음과 검색 실패를 구분해 다른 문장을 돌려줍니다. 모델의 다음 행동이 달라집니다.
  • 독스트링에 "사내 데이터에 없는 내용에 쓴다" 는 경계선을 적어 둡니다.
  • 검색어는 모델이 만듭니다. 우리는 그 코드를 짜지 않습니다.
← 이전 절01. LLM이 모르는 것 학습이 끝난 시점 이후의 세상다음 절 →03. 따라하기 검색어를 LLM이 직접 만들게 하기