· 문서버전 1.0

웹 스크래핑 에이전트 만들기 — 문서를 마크다운으로

링크 너머의 내용은 모델이 못 봅니다. Firecrawl 스크래핑 도구를 하나 붙인 LangGraph 에이전트를 사용하여 페이지를 마크다운으로 가져와 읽는 과정을 직접 만들어 봅니다.

웹 스크래핑 에이전트 — 문서를 마크다운으로.

관련 개념

도구 개념의 스크래핑·크롤링에서 “문서 페이지를 마크다운으로”를 예로 들었습니다. 검색이 어디를 볼지 찾는 일이라면, 스크래핑은 그 페이지를 통째로 읽어 오는 일입니다. 이 글에서는 그 예시를 실제로 돌아가는 에이전트로 만들어 봅니다.

무엇을 만드나

URL이 담긴 작업을 받으면 모델이 Firecrawl로 그 페이지를 마크다운으로 가져오고, 내용을 읽어 요약·답변하는 에이전트입니다.

flowchart LR
  q["작업 — 이 문서 읽고 요약"] --> model["모델"]
  model -- URL --> tool["scrape · Firecrawl"]
  tool --> md[("마크다운")]
  md --> model
  model --> ans["요약 · 답변"]
  class model roleModel
  class tool roleTool
  class md roleSource

검색이 출처를 가리키는 데 그친다면, 스크래핑은 그 페이지의 본문 자체를 모델 앞에 펼쳐 놓습니다.

코드 뜯어보기

전체 구조

app.py의 전체 흐름은 함수 셋으로 나뉩니다 — main()이 모델과 에이전트를 짜고, 모델이 부르는 도구가 scrape(), 그리고 마지막 답을 다듬는 게 message_text()입니다.

flowchart TB
  q["작업 (sys.argv)"] --> build["main() — 모델·에이전트 구성"]
  build --> react{"ReAct 루프"}
  react -- 추론 --> model["모델 (ChatLiteLLM)"]
  model -- 도구 호출 --> tool["scrape(url)"]
  tool --> fc[("Firecrawl.scrape_url")]
  fc -- markdown --> tool
  tool -- 관찰 --> react
  react -- 종료 --> msg["message_text(content)"]
  msg --> out["print → stdout"]
  class model roleModel
  class tool roleTool
  class fc roleSource

세부 구조

scrape(url) — 도구

  • @tool로 감싼 함수 하나 — 모델은 docstring을 보고 언제 페이지를 읽을지 판단
  • _firecrawl.scrape_url(url, formats=["markdown"])로 Firecrawl 호출
  • 반환이 객체든 딕셔너리든 markdown을 꺼내 사용 (SDK 버전 차이 흡수)
  • 긴 페이지가 프롬프트를 넘치지 않게 앞부분 6,000자로 자름
@tool
def scrape(url: str) -> str:
    """Fetch a web page and return its content as clean Markdown.

    Use this to read what's actually behind a URL — docs, articles, anything the
    model can't see on its own. Pass a single absolute URL.
    """
    res = _firecrawl.scrape_url(url, formats=["markdown"])
    md = getattr(res, "markdown", None)
    if md is None and isinstance(res, dict):  # older SDKs return a dict
        md = res.get("markdown") or res.get("data", {}).get("markdown")
    # Cap the length so a long page doesn't blow up the prompt.
    return (md or "")[:6000] or "No content."

message_text(content) — 출력 다듬기

  • 모델 응답의 content는 모양이 제각각 — 클라우드는 문자열, 일부 로컬 모델은 [{type: "text", …}, …] 블록 리스트
  • 리스트면 type == "text" 블록의 텍스트만 이어붙임
  • 문자열이면 그대로 둠
  • 그래서 어느 제공자든 깔끔한 한 줄로 출력
def message_text(content) -> str:
    """Flatten an assistant message's content to plain text (cloud models return a
    string; some local models return a list of blocks)."""
    if isinstance(content, list):
        return "".join(
            part.get("text", "")
            for part in content
            if isinstance(part, dict) and part.get("type") == "text"
        )
    return content

main() — 조립

  • MODELChatLiteLLM을 만들고 create_agent(model, tools=[scrape])로 ReAct 루프 구성
  • ChatLiteLLMLiteLLM을 LangChain 모델로 감싼 어댑터 — 라우팅(어느 API를 부를지)은 LiteLLM, 인터페이스 연결은 ChatLiteLLM
  • agent.invoke({"messages": […]})가 추론→도구 호출→관찰을 돌림
  • 끝나면 마지막 메시지의 contentmessage_text()로 다듬어 출력
def main() -> None:
    question = " ".join(sys.argv[1:]) or "https://docs.firecrawl.dev 를 읽고 핵심 기능 3가지만 요약해줘"

    # MODEL chooses the provider (claude-opus-4-8 / gpt-4o / gemini/gemini-2.5-flash).
    model = ChatLiteLLM(model=os.environ.get("MODEL", "claude-opus-4-8"), temperature=0)
    agent = create_agent(model, tools=[scrape])

    result = agent.invoke({"messages": [{"role": "user", "content": question}]})
    print(message_text(result["messages"][-1].content))

임포트는 langchain이지만 create_agent가 돌려주는 것은 LangGraph 그래프입니다.
이 접착 계층 없이 LiteLLM + LangGraph만으로 직접 구성하면 어떻게 되는지는 별도 비교 글에서 다룹니다.

구현

LangGraphReAct 루프에 scrape 도구 하나만 붙였습니다. 모델은 LiteLLM을 통해 라우팅되므로 같은 코드가 Claude·OpenAI·Gemini에서 모두 동작합니다.

관련 샘플Firecrawl 웹 스크래핑 에이전트 — 문서를 마크다운으로Firecrawl 기반 scrape 도구 하나를 가진 약 45줄짜리 LangGraph ReAct 에이전트입니다. "이 문서 페이지를 읽고 요약해줘"처럼 페이지 내용이 필요한 작업을 던지면, 그 페이지를 깔끔한 마크다운으로 가져와 읽고 답합니다. 모델은 LiteLLM을 통해 라우팅되므로 같은 코드가 Anthropic Claude, OpenAI, Google AI Studio (Gemini) 에서 모두 동작합니다 — 코드는 그대로 두고 .env의 MODEL만 바꾸면 됩니다.samples/firecrawl_12026년 6월 28일

핵심만 짚으면

  • 도구는 함수다@tool로 감싼 scrape(url) 하나가 페이지를 읽는 전부입니다.
  • 검색과 스크래핑은 다르다 — 검색은 어디를 볼지 찾고, 스크래핑은 그 페이지를 읽어 옵니다.
  • 결과는 근거가 된다 — Firecrawl이 돌려준 마크다운이 추론에 들어가, 링크 너머의 실제 내용으로 답합니다.
  • 제공자는 갈아끼운다.envMODEL만 바꾸면 같은 코드로 다른 모델을 씁니다.

같은 루프에서 도구만 검색(Tavily)으로 바꾸면 오늘의 환율 묻기가 되고, 브라우저 자동화로 바꾸면 또 다른 “지금” 데이터를 끌어올 수 있습니다.
도구의 갈래는 도구 개념에 정리해 두었습니다.

관련 도구

관련 글