검색 도구 붙이기 — 키가 필요 없는 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딕셔너리 목록입니다.- 결과를 문자열로 다듬는 것이 우리 일입니다. 무엇을 넣고 무엇을 자를지가 사람의 판단입니다.
- 결과 없음과 검색 실패를 구분해 다른 문장을 돌려줍니다. 모델의 다음 행동이 달라집니다.
- 독스트링에 "사내 데이터에 없는 내용에 쓴다" 는 경계선을 적어 둡니다.
- 검색어는 모델이 만듭니다. 우리는 그 코드를 짜지 않습니다.