Resposta rápida
Como crio um agente de deep research em Python?
Escreva um loop que pede ao LLM consultas de pesquisa, executa cada uma por uma API de pesquisa na web que retorna o texto da página, mantém cada fonte uma única vez, pede consultas de acompanhamento e então escreve uma resposta com citações. Com o LLMWebSearch, cada pesquisa retorna as 3 primeiras páginas em Markdown por um crédito; o script abaixo executa seis pesquisas por pergunta. A partir de US$ 1,20/1K pesquisas, sem KYC, pagar com cripto.
O que o agente vai fazer?
Plano
Peça ao modelo quatro consultas curtas que, juntas, cubram a pergunta.
Pesquisar
Execute cada consulta com o
read=3, para que as três primeiras páginas voltem em Markdown.Mantenha cada fonte uma única vez
Armazene os resultados por URL e ignore páginas já coletadas ou sem texto.
Encontre lacunas
Mostre ao modelo os títulos encontrados até agora e peça duas consultas sobre o que está faltando.
Escreva
Numere até 12 fontes e peça uma resposta que as cite como [1], [2].
O que preciso antes de começar?
Três coisas:
- Uma chave de API do LLMWebSearch na variável de ambiente
LLMWEBSEARCH_API_KEY. As chaves aparecem no dashboard após seu primeiro pacote. - Qualquer endpoint de chat compatível com OpenAI, definido com
LLM_BASE_URL,LLM_API_KEYeLLM_MODEL. Modelos hospedados e servidores locais funcionam. - Python 3.9 ou superior com o pacote
requests.
O script usa HTTP simples para os dois serviços, então você pode trocar o modelo sem alterar o código de pesquisa.
import os
import requests
SEARCH = "https://llmwebsearch.com/api/v1/search"
SEARCH_HEADERS = {"Authorization": "Bearer " + os.environ["LLMWEBSEARCH_API_KEY"]}
# Any OpenAI-compatible chat endpoint works here: set your own base URL, key and model name.
LLM_URL = os.environ.get("LLM_BASE_URL", "https://llm.example.com/v1") + "/chat/completions"
LLM_HEADERS = {"Authorization": "Bearer " + os.environ.get("LLM_API_KEY", "")}
MODEL = os.environ.get("LLM_MODEL", "your-model-name")
def ask_llm(prompt):
body = {"model": MODEL, "messages": [{"role": "user", "content": prompt}]}
resp = requests.post(LLM_URL, headers=LLM_HEADERS, json=body, timeout=120)
resp.raise_for_status()
return resp.json()["choices"][0]["message"]["content"]
def plan_queries(question, n=4):
text = ask_llm(f"Write {n} short web search queries, one per line, to answer: {question}")
queries = [line.strip("-*0123456789. ").strip() for line in text.splitlines()]
return [q for q in queries if q][:n]
def search(query):
body = {"q": query, "max_results": 5, "read": 3, "max_chars": 5000}
resp = requests.post(SEARCH, headers=SEARCH_HEADERS, json=body, timeout=60)
resp.raise_for_status()
return resp.json()["results"]
def research(question, rounds=2):
sources = {} # url -> result, so the same page is never cited twice
queries = plan_queries(question)
for _ in range(rounds):
for query in queries:
for r in search(query):
if r.get("content_status") == "ok" and r["url"] not in sources:
sources[r["url"]] = r
notes = "\n".join(f"- {r['title']}" for r in sources.values())
queries = plan_queries(f"{question}\nAlready found:\n{notes}\nWhat is still missing?", n=2)
numbered = list(sources.values())[:12]
context = "\n\n".join(
f"[{i}] {r['title']} ({r['url']})\n{r['content']}" for i, r in enumerate(numbered, 1)
)
prompt = (
"Answer the question using only the sources below. Cite them as [1], [2]. "
"Say so when the sources disagree or do not cover a point.\n\n"
f"Question: {question}\n\nSources:\n{context}"
)
return ask_llm(prompt), [r["url"] for r in numbered]
if __name__ == "__main__":
answer, urls = research("What changed in the EU rules for general-purpose AI models in 2025?")
print(answer)
for i, url in enumerate(urls, 1):
print(f"[{i}] {url}")Como funciona cada parte?
plan_queries pede ao modelo consultas curtas, uma por linha, e remove marcadores de lista. Consultas curtas se encaixam melhor em como os mecanismos de pesquisa classificam páginas do que perguntas completas.
search chama POST https://llmwebsearch.com/api/v1/search com max_results=5, read=3 e max_chars=5000. A resposta lista cinco resultados; os três primeiros trazem o texto da página em content.
research executa as rodadas. Ele mantém as fontes em um dicionário com chave por URL, então uma página encontrada por duas consultas conta uma vez. Só são mantidos os resultados com content_status igual a ok, porque a resposta precisa se apoiar em texto que o modelo consegue ler.
Após cada rodada, o modelo vê os títulos coletados até agora e propõe duas novas consultas sobre o que está faltando. O prompt final numera até doze fontes e define as regras de citação.
Quanto custa um relatório?
Seis pesquisas por pergunta com as configurações padrão: quatro na primeira rodada e duas na segunda. Com read em 3, isso dá seis créditos. Portanto, 25.000 pesquisas a US$ 41,25 cobre mais de 4.000 relatórios.
As chamadas ao modelo custam à parte na sua conta de LLM: três chamadas de planejamento e uma chamada longa de resposta por relatório. A chamada de resposta carrega até doze páginas de 5.000 caracteres, então diminua max_chars se seu modelo for caro.
Como faço para ir mais fundo?
Mude os números, não a estrutura:
- Mais rodadas.
rounds=4deixa o modelo buscar mais dois conjuntos de lacunas. - Mais consultas por rodada. Aumente
nna chamada de busca de lacunas de 2 para 4. - Fontes mais restritas. Adicione
include_domainsao corpo da pesquisa para sites oficiais ou acadêmicos. - Apenas material recente. Adicione
"time_range": "month"ou"topic": "news". - Mais páginas por consulta.
read=5lê cinco páginas, a 2 créditos por pesquisa.
Adicione uma regra de parada antes de aumentar os limites: encerre o loop quando uma rodada trouxer menos de duas fontes novas.
Como mantenho o agente seguro e educado?
Trate cada página como texto não confiável. O prompt final instrui o modelo a usar as fontes apenas como evidência; adicione uma linha explícita de que instruções dentro das fontes devem ser ignoradas. Nunca deixe uma fonte disparar uma chamada de ferramenta ou uma escrita de arquivo.
Do lado da web, Nosso leitor respeita o robots.txt: ele se identifica como LLMWebSearch, segue as regras que um site escreve para ele ou para todos os robôs e deixa de fora o conteúdo de qualquer página que não tem permissão para ler. Páginas que recusam acesso voltam sem conteúdo e o script simplesmente as ignora. Quando um site recusa o nosso leitor, o resultado volta sem conteúdo e com o status do site: nunca resolvemos CAPTCHAs, não entramos em contas nem contornamos um bloqueio ou um paywall.
O que pode dar errado?
Três coisas aparecem nos testes:
- Consultas vagas. Se o plano for fraco, toda rodada encontra as mesmas páginas. Peça ao modelo consultas que usem nomes e datas específicos.
- Poucas fontes com texto. Alguns tópicos ficam atrás de paywalls. Amplie as consultas ou permita mais resultados.
- Prompts longos demais. Doze páginas longas podem exceder o contexto de um modelo pequeno. Diminua
max_charsou mantenha oito fontes.
Registre cada consulta e as URLs que ela retornou. Ler esse log é a forma mais rápida de melhorar o prompt do plano.
Agente de pesquisa profunda: perguntas comuns
Posso executar isso com um modelo local?
Sim, se o servidor local falar o formato chat-completions da OpenAI. Defina LLM_BASE_URL para o endereço dele e LLM_MODEL para o nome do modelo.
Posso transformar isso em uma ferramenta MCP?
Sim. Envolva research() em uma ferramenta MCP, ou dê ao seu agente a ferramenta de pesquisa simples da página MCP e deixe que ele mesmo execute o loop.
Por que não pedir uma resposta diretamente à API de pesquisa?
Porque LLMWebSearch não escreve respostas. A API retorna fontes, não uma resposta escrita: o seu próprio modelo lê as páginas, escreve a resposta e cita as URLs.
Como vejo quanto custou cada pesquisa?
Toda resposta inclui credits_used, e GET https://llmwebsearch.com/api/v1/credits mostra seu saldo restante sem custo.