[ blog · tutorial ]9 min read

Crie um agente de research com Web Search + URL Extract (tool use do Claude, de ponta a ponta)

Sarah ChoyPublicado em 3 de maio de 20269 min de leitura
Crie um agente de research com Web Search + URL Extract (tool use do Claude, de ponta a ponta)

A maioria dos tutoriais de 'agente de research com IA' para em 'aqui está uma definição de ferramenta'. Este entrega um agente funcional: entra uma pergunta, sai uma resposta com citações. Buscar, extrair, raciocinar, citar — tudo em menos de 120 linhas de Python.

TL;DR

  • O menor loop de agente de research que realmente serve é: buscar → escolher links → extrair os corpos → pedir ao modelo que responda com citações inline.
  • Duas ferramentas — Web Search (15 créditos) e URL Extract (2 créditos por URL) — cobrem 95% dos casos de resposta fundamentada.
  • O tool use do Claude cuida da orquestração; você só transporta blocos tool_use → chamadas de API → blocos tool_result até o modelo parar.
  • Custo de ponta a ponta: ~25 créditos + ~$0.02 em tokens do LLM por pergunta em profundidade típica.

O que vamos construir

Um agente de research que recebe uma entrada — uma pergunta em linguagem natural — e devolve uma resposta com fontes. Arquiteturalmente:

question → [Claude]
              ↓ tool_use(search)
            [API Pick Web Search] → ranked URLs
              ↓ tool_use(extract)
            [API Pick URL Extract] → cleaned bodies
              ↓ Claude reads, decides if more is needed
              ↓ end_turn
          answer with inline [source: URL] citations

Cerca de 120 linhas de Python. Duas ferramentas — search e extract — e um loop de agente que lida com o que quer que o Claude decida fazer. Vamos construí-lo do zero em 4 passos.

1Puxe os schemas de ferramenta (sem JSON na mão)

Ambos os endpoints publicam uma rota de tool-schema que retorna uma definição de ferramenta do Claude no formato exato que messages.create espera.

import requests

API_KEY = "pk_yourkey"

def fetch_tool(tool_path: str) -> dict:
    """Fetch a Claude tool definition from API Pick's tool-schema endpoint."""
    schema = requests.get(f"https://www.apipick.com{tool_path}/tool-schema").json()
    return schema["claude"]

WEB_SEARCH_TOOL = fetch_tool("/api/search/web")
URL_EXTRACT_TOOL = fetch_tool("/api/extract")

# Cache these once at module load. They don't change between requests.

Duas coisas importantes que isso te dá de graça: o schema de parâmetros (para que o Claude saiba que você aceita query, country_code, start_date, etc.) e uma descrição clara e amigável ao modelo do que a ferramenta faz.

2Escreva o handler da ferramenta

Quando o Claude retorna um bloco tool_use, seu trabalho é chamar a API real e retornar um bloco tool_result. Uma função por ferramenta, um dispatcher que roteia com base no nome que o Claude escolheu.

def call_tool(tool_use_block) -> dict:
    """Execute the tool Claude asked for and return a tool_result block."""
    name = tool_use_block.name
    args = tool_use_block.input

    if name == "web_search":
        endpoint = "https://www.apipick.com/api/search/web"
    elif name == "extract_urls":
        endpoint = "https://www.apipick.com/api/extract"
    else:
        return {
            "type": "tool_result",
            "tool_use_id": tool_use_block.id,
            "content": f"Unknown tool: {name}",
            "is_error": True,
        }

    resp = requests.post(
        endpoint,
        headers={"x-api-key": API_KEY},
        json=args,
        timeout=30,
    )

    if resp.status_code != 200:
        return {
            "type": "tool_result",
            "tool_use_id": tool_use_block.id,
            "content": f"HTTP {resp.status_code}: {resp.text[:500]}",
            "is_error": True,
        }

    return {
        "type": "tool_result",
        "tool_use_id": tool_use_block.id,
        "content": resp.text,
    }

Três coisas que vale destacar. tool_use_id é como o Claude correlaciona o resultado com sua chamada anterior — você precisa devolvê-lo tal como veio. is_error: True diz ao Claude para se recuperar com elegância (geralmente tentando outra consulta). E passamos o texto da resposta cru — o Claude se sente à vontade com o formato JSON que o API Pick retorna, então não é preciso transformação.

3O loop do agente

O loop é curto: envie a conversa, olhe cada bloco da resposta, execute as ferramentas, anexe à conversa tanto o tool_use do Claude quanto o seu tool_result, e repita até que stop_reason seja end_turn.

import anthropic

client = anthropic.Anthropic()

SYSTEM_PROMPT = """You are a research assistant. When the user asks a question:

1. Use web_search to find relevant sources. Prefer recent (set start_date) for time-sensitive queries.
2. Use extract_urls to read the body of the top 3-5 most relevant URLs from the search results.
3. Answer the question concisely. After every factual claim, include an inline citation in the form [source: URL].
4. If you don't have enough information, say so — don't fabricate.

Be concise. Aim for 3-5 sentence answers unless the user asks for depth."""

def research(question: str) -> str:
    messages = [{"role": "user", "content": question}]

    while True:
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=2048,
            system=SYSTEM_PROMPT,
            tools=[WEB_SEARCH_TOOL, URL_EXTRACT_TOOL],
            messages=messages,
        )

        # Append the assistant's response (may contain tool_use blocks)
        messages.append({"role": "assistant", "content": response.content})

        if response.stop_reason == "end_turn":
            # Pull out the text answer
            return "\n".join(b.text for b in response.content if b.type == "text")

        if response.stop_reason == "tool_use":
            # Run every tool the model asked for in this turn
            tool_results = [
                call_tool(b)
                for b in response.content if b.type == "tool_use"
            ]
            messages.append({"role": "user", "content": tool_results})
            continue

        raise RuntimeError(f"Unexpected stop_reason: {response.stop_reason}")

Repare que o modelo pode disparar várias chamadas de ferramenta no mesmo turno — por exemplo, ele pode emitir uma única chamada a extract_urls com cinco URLs para processá-las em lote. Lidamos com isso reunindo todos os blocos tool_use do turno do assistente e devolvendo todos os blocos tool_result no turno de usuário seguinte.

4Execute

if __name__ == "__main__":
    answer = research("What were the major announcements at OpenAI DevDay this year?")
    print(answer)

Você verá algo como:

OpenAI's DevDay focused on three themes: cheaper inference (a new gpt-mini
tier at roughly half the prior cost) [source: https://openai.com/devday-2026],
agent infrastructure (a managed agent runtime with persistent memory and
tool sandboxing) [source: https://techcrunch.com/...], and developer
tooling (a new Responses API replacing the legacy Assistants API)
[source: https://platform.openai.com/docs/...].

Isso é um agente de research. ~120 linhas, duas ferramentas, saída com fontes.

O que isso custa

Uma chamada de research típica:

  • 1 chamada de busca — 15 créditos ($0.015)
  • 1 chamada de extração cobrindo 3–5 URLs — 6–10 créditos ($0.006–$0.010)
  • ~3,000 tokens de entrada + 800 de saída do Claude — ~$0.02

Número redondo: ~3 centavos por resposta pesquisada. A 1,000 chamadas de research por dia, isso dá ~$30/dia — mais ou menos o orçamento de café matinal de um analista. Faça orçamento para o LLM, não para as APIs de busca.

Três refinamentos para produção

1. Faça cache por pergunta

Se o seu produto faz perguntas semelhantes, calcule o hash de (question, today's date) e faça cache da resposta final por uma hora. A maior parte do research impulsionado por usuários tem taxas de acerto de cauda longa acima de 30%.

2. Restrinja quando a atualidade importa

Para perguntas de 'esta semana' / 'hoje', defina start_date na chamada de busca para a data atual ou ontem. Sem isso, a busca pode retornar artigos do ano passado. A versão mais simples: uma regra de system prompt como 'sempre inclua start_date quando a pergunta contiver hoje, esta semana, recente ou mais recente'.

3. Salvaguardas contra alucinação

A regra de maior alavancagem individual: 'Se você não tiver conteúdo extraído suficiente para responder, retorne: Não consegui encontrar uma fonte confiável para isto. Não adivinhe.' Adicionar isso ao system prompt reduz as respostas inventadas em uma ordem de magnitude em nossos testes.

Para onde levá-lo a seguir

Três extensões naturais:

  • Verticalize: troque Web Search por Academic Search para um agente de revisão de literatura, SEC Filings Search para due diligence, ou Clinical Search para pesquisa médica. O handler não muda — só o system prompt.
  • Adicione saída estruturada: peça ao modelo que retorne JSON com os campos answer e citations[] para que você possa renderizá-los como um card de UI.
  • Faça streaming de tokens: mude para client.messages.stream() para saída incremental — útil quando a resposta é longa. O loop de tool-call é o mesmo.

Perguntas Frequentes

Por que duas ferramentas em vez de um único endpoint 'answer'?

Separar busca e extração dá a você controle sobre quantas URLs ler, quando parar e quais citar. Um único endpoint hospedado de 'answer' esconde isso — você não consegue mudar a estratégia facilmente sem rearquitetar. Com duas ferramentas, o mesmo código vira um agente de briefing matinal, um revisor de literatura ou um scraper de inteligência competitiva apenas ajustando o prompt.

Isso também funciona com OpenAI Assistants / Responses API?

Sim. A arquitetura é idêntica — a única coisa que muda é como você faz o parsing dos blocos de tool-call e envia os resultados. O formato do handler passa a ser submit_tool_outputs(...) em vez de um bloco de conteúdo tool_result, mas o loop do agente e as definições de ferramenta são o mesmo JSON.

Como o agente decide quando parar?

O Claude retorna stop_reason: end_turn quando tem contexto suficiente para responder, e tool_use quando quer chamar uma ferramenta. O loop simplesmente continua até end_turn. Na prática, o modelo costuma fazer de 1 a 3 ciclos de busca/extração antes de responder.

Como garanto que ele realmente cite as fontes?

Duas alavancas. Primeira: no system prompt, exija explicitamente '[source: URL]' inline após cada afirmação. Segunda: a resposta do URL Extract inclui a URL, então quando o modelo a tem no contexto ele tende a citar naturalmente. Se as citações escaparem, adicione um passo final de formatação que rejeite a resposta e peça ao modelo que adicione as citações.

E quanto à latência?

Cada ida e volta de busca leva ~1s; cada lote de extração leva de 1 a 4s dependendo do número de URLs e da renderização de JS. Uma chamada típica de 'pesquise uma pergunta' fica entre 5 e 15s de relógio. Se você precisar mais rápido, paralelize o passo de extração (o extract já recebe uma lista de URLs em uma única chamada) e limite o modelo a uma rodada de ferramentas por pergunta com instruções no system prompt.

APIs usadas neste artigo

Sarah Choy
Escrito por
Sarah Choy
CEO, API Pick

Sarah Choy é a CEO da API Pick. Ela escreve sobre a construção de APIs prontas para produção para agentes de IA e fluxos de trabalho com LLMs.