에이전트 시리즈 (23): 웹 에이전트 — 인터넷에서 에이전트에게 실제 눈을 달아주기
요약
LLM의 지식 컷오프 문제를 해결하기 위해 실시간 인터넷 브라우징이 가능한 웹 에이전트 구축 방법을 다룹니다. HTML 클리닝, 토큰 예산 관리, 단계 제한, URL 오류 처리 등 웹 에이전트 설계 시 직면하는 핵심 엔지니어링 과제와 해결책을 제시합니다.
핵심 포인트
- LLM의 지식 컷오프를 극복하기 위한 실시간 웹 브라우징의 필요성
- HTML 태그 제거 및 토큰 예산 관리를 통한 컨텍스트 최적화
- 무한 루프 방지를 위한 명시적인 단계 제한(Step Limit) 설계
- URL 환각 방지를 위한 프롬프트 지침 및 에러 처리 전략
웹 에이전트가 존재하는 이유
LLM (Large Language Models)은 지식 컷오프 (knowledge cutoff)를 가집니다. "LangGraph의 최신 버전은 무엇인가요?"라고 물으면, 모델은 학습 데이터에 포함되어 있던 내용만 말할 수 있습니다. 웹 에이전트 (Web Agents)는 이 문제를 해결합니다. 에이전트가 실제로 인터넷을 브라우징하고 실시간 정보를 반환합니다.
하지만 "인터넷 브라우징"은 생각보다 더 복잡합니다:
- 웹 페이지는 텍스트가 아닌 HTML입니다 — 가공되지 않은 HTML을 컨텍스트 (context)에 쏟아부으면 쓸모없는 태그들로 가득 차게 됩니다.
- 단일 페이지가 수만 개의 토큰 (tokens)이 될 수 있습니다 — 유용한 컨텍스트 밀도를 훨씬 초과하는 수준입니다.
- 에이전트가 무한 루프에 빠질 수 있습니다 — 페이지 A가 B로 연결되고, B가 C로 연결되어 결코 멈추지 않을 수 있습니다.
- URL이 환각 (hallucinated)될 수 있습니다 — LLM은 존재하지 않지만 그럴듯하게 들리는 링크를 만들어낼 수 있습니다.
네 가지 문제에 대한 네 가지 엔지니어링 설계: HTML 클리닝 (HTML cleaning), 토큰 예산 (Token Budget), 단계 제한 (Step Limit), URL 오류 처리 (URL error handling). 이 글은 이들을 모아 작동하는 웹 에이전트를 구성합니다.
아키텍처 (Architecture)
전체적인 구조는 표준적인 LangGraph 2-노드 그래프입니다:
사용자 질문 (User question)
│
▼
...
상태 (State)에는 오직 두 개의 필드만 있습니다:
class WState(TypedDict):
messages: Annotated[list, add_messages] # 누적된 메시지
steps: int # 소모된 단계
steps는 웹 에이전트 전용입니다. 표준 에이전트는 명시적인 단계 카운터가 필요하지 않지만, 웹 에이전트는 페이지 사이를 무한히 건너뛸 수 있습니다. 따라서 엄격한 제한이 필수적입니다.
두 가지 도구 (Two Tools)
web_search: DuckDuckGo 검색
@tool
def web_search(query: str) -> str:
"""
...
DuckDuckGo의 HTML 인터페이스를 사용하며, API 키가 필요하지 않습니다. .result CSS 클래스를 파싱하여 제목, 스니펫 (snippet), URL을 추출하고 구조화된 텍스트를 LLM에 반환합니다.
도구 설명에는 중요한 지침이 포함되어 있습니다: 결과에서 나온 URL을 사용하여 fetch_page를 호출하십시오 — 절대로 URL을 지어내지 마십시오. 이는 URL 환각에 대한 첫 번째 방어선입니다. 프롬프트 (Prompt) 계층에서 모델에게 유효한 URL이 어디서 오는지 지시하는 것입니다.
fetch_page: 페이지 가져오기 + 클리닝
@tool
def fetch_page(url: str) -> str:
"""
...
세 단계가 있습니다:
clean_html: BeautifulSoup을 사용하여 script/style/nav/footer를 제거하고 일반 텍스트 (plain text)를 반환합니다.truncate_to_budget: 토큰 예산 (Token Budget)을 초과하는 모든 내용을 잘라냅니다.- 에러 분류 (Error classification): HTTP 에러, 연결 에러 및 기타 예외 상황은 각각 서로 다른 안전한 문자열을 반환합니다.
requests.HTTPError와 requests.ConnectionError는 두 가지 서로 다른 실패 시나리오를 나타낸다는 점에 유의하세요. 전자는 서버가 응답했음(4xx/5xx)을 의미하며, 후자는 연결 자체가 실패했음(도메인이 존재하지 않거나 네트워크에 도달할 수 없음)을 의미합니다.
세 가지 엔지니어링 가드 (Engineering Guards)
가드 1: URL 에러 처리 (URL Error Handling)
완전히 존재하지 않는 도메인을 테스트하는 경우:
fetch_page(https://totally-made-up-domain-xyz99999.org/docs/n...)
→ Connection error — https://totally-made-up-domain-xyz99999.org/docs/nonexistent may not exist or be unreachable
충돌(crash)이나 예외 전파(exception propagation) 없이 안전한 에러 문자열이 반환됩니다. LLM은 이 문자열을 전달받아 다른 URL을 시도하거나 다른 검색 쿼리를 선택할 수 있습니다.
이것은 핵심적인 가드 설계 원칙입니다: 에러는 예외(exception)가 아니라 반환 값(return value)이어야 합니다. 도구 호출 (tool call) 실패가 에이전트 (Agent) 실행 전체를 중단시켜서는 안 되며, 대신 LLM이 에러 정보를 바탕으로 적응할 수 있도록 해야 합니다.
가드 2: 토큰 예산 절단 (Token Budget Truncation)
PyPI에서 langgraph 페이지를 테스트하는 경우:
fetch_page(pypi.org/project/langgraph/)
→ [Size: 4576 tokens → showing 800 tokens (budget=800)]
원본 페이지는 4,576 토큰입니다. 절단 후에는 800 토큰이 됩니다. 이는 컨텍스트 (context) 사용량이 82% 감소했음을 의미합니다.
절단 구현은 간단합니다:
PAGE_TOKEN_BUDGET = 800 # fetch당 LLM에 전송되는 페이지 텍스트의 최대 토큰 수
def count_tokens(text: str) -> int:
...
count_tokens는 정밀한 토크나이저 (tokenizer)가 아닌 대략적인 추정치(3자 ≈ 1 토큰)를 사용합니다. 절단 목적상으로는 정밀도보다 속도가 더 중요합니다.
가드 3: 단계 제한 (Step Limit)
MAX_STEPS = 8
def router(state: WState) -> str:
...
state["steps"]는 모든 agent_node 실행 시마다 증가합니다:
def agent_node(state: WState) -> dict:
msgs = [SystemMessage(content=SYSTEM_PROMPT)] + state["messages"]
response = bound_llm.invoke(msgs)
...
라우터(router)는 tool_calls를 확인하기 전에 단계(step) 수를 먼저 확인합니다. LLM이 도구 호출(tool calling)을 계속하고 싶어 하더라도, 단계 제한에 도달하면 실행이 종료됩니다. 이는 무한 루프를 방지하기 위한 강력한 경계(hard boundary)입니다.
단계 수는 호출 시점에 초기화됩니다:
state = graph.invoke(
{"messages": [HumanMessage(content=query)], "steps": 0},
config={"recursion_limit": MAX_STEPS * 3},
...
recursion_limit은 LangGraph의 내장 보호 기능이며, steps는 애플리케이션 레벨의 사용자 정의 보호 기능입니다. 두 기능은 독립적으로 작동합니다.
실행 결과 (Run Results)
======================================================================
Web Agent Demo
Model: glm-4-flash | Token budget/page: 800 | Max steps: 8
...
세 가지 가드(guards) 모두 예상대로 작동했습니다.
연구 섹션(Part 1 & 2)에서 DuckDuckGo의 속도 제한(rate limiting)에 걸렸습니다. 검색 결과가 빈 값으로 반환되었으나, 모델은 환각(hallucination)을 일으켜 답을 지어내는 대신 실패를 정확하게 보고했습니다. 이는 가드가 효과적이라는 증거이기도 합니다. 에이전트가 빈 결과에 대해 루프를 돌지 않고, 사용자에게 실패를 명확하게 보고했기 때문입니다.
DuckDuckGo의 한계
DuckDuckGo HTML 인터페이스는 API 키가 필요하지 않지만, 프로덕션(production) 환경에서 사용하기에는 신뢰성이 떨어집니다:
- 빈번한 요청 시 속도 제한(rate limiting)이 걸리거나 빈 결과가 반환됨
- HTML 구조가 언제든 변경될 수 있어 CSS 선택자(CSS selectors)가 깨질 수 있음
- 속도 제한 제어가 불가능하며 차단(block)을 유발하기 쉬움
프로덕션 대안:
| 옵션 | 특징 |
|---|---|
| Tavily API | LLM 에이전트를 위해 설계되었으며, 구조화된 결과를 반환함 |
| ... |
교체 시 web_search 도구 구현부만 바꾸면 되며, 에이전트 그래프 구조는 그대로 유지됩니다.
전체 그래프 코드 (Complete Graph Code)
TOOLS = [web_search, fetch_page]
TOOL_MAP = {t.name: t for t in TOOLS}
bound_llm = llm.bind_tools(TOOLS)
...
컴파일된 그래프는 모듈 레벨의 graph 변수에 할당됩니다. run_research는 graph.invoke()를 직접 호출합니다.
설계 체크리스트 (Design Checklist)
도구 설계 (Tool design)
- HTML 클리닝: script/style/nav/footer 제거, body 텍스트만 유지
- 오류 분류: HTTP 오류 / 연결 오류 / 기타 — 각각 안전한 문자열 반환
- 도구 설명에 URL 출처 규칙 포함:
URL을 절대로 지어내지 않기
엔지니어링 가드 (Engineering guards)
- 토큰 예산 (Token Budget): 페이지 텍스트를 합리적인 한계(800–2000 토큰)로 자르기(truncate)
- 단계 제한 (Step Limit): 라우터가
tool_calls확인 전에 단계 수를 검사함 - 이중 계층 보호: 애플리케이션 레벨의
steps+ LangGraph의recursion_limit
상태 설계 (State design)
-
messages: Annotated[list, add_messages]— 리듀서(reducer)를 사용해야 메시지가 누적됨 -
steps: int— 웹 에이전트 전용 필드; 표준 에이전트는 이를 생략할 수 있음
운영 환경 강화 (Production hardening)
- 검색 도구를 신뢰할 수 있는 API 키 기반 솔루션(Tavily/SerpAPI)으로 대체
- 거부되는 것을 방지하기 위해 User-Agent를 실제 브라우저 UA로 설정
- 요청 시간 초과: 검색 및 페이지 가져오기 모두
timeout=12설정
요약 (Summary)
세 가지 결론:
- 가드는 콘텐츠 품질에 독립적: 도구 실패가 에이전트 실패를 의미하지는 않습니다. 오류를 반환 값으로 처리하면 LLM이 적응할 수 있으며, 충돌하는 대신 실행이 계속됩니다.
- 토큰 예산은 협상 불가: 일반적인 웹 페이지는 4,576 토큰이며, 이를 800 토큰으로 자르면 컨텍스트의 82%를 절약합니다. 대규모로 여러 페이지를 탐색할 때 이 기능이 없다면 몇 단계 만에 컨텍스트가 고갈될 것입니다.
- 단계 제한은 확고한 경계:
steps >= MAX_STEPS → END는 프롬프트(Prompt)가 아닌 라우터(router)에 존재합니다. LLM이 아무리 계속하고 싶어 해도, 카운터가 이를 막습니다. 안전이 중요한 동작에는
_ PrimeSkills를 확인해 보세요 — 실제 기업급 워크플로우에서 검증된 AI 에이전트와 기술(skills)을 엄선하여 제공하는 마켓플레이스입니다. 불필요한 내용은 빼고, 실제로 작동하는 것들만 모았습니다._
_ 저의 홈페이지에서 더 유용한 지식과 흥미로운 제품들을 찾아보세요_
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기