Um desafio comum no desenvolvimento de aplicações com LLMs é a limitação do conhecimento do modelo à data de treinamento. Perguntar sobre eventos recentes pode levar a respostas inventadas, disfarçadas de fatos concretos. A gravidade disso reside no fato de que os LLMs apresentam informações incorretas com a mesma confiança que apresentam informações corretas, dificultando a distinção pelo usuário.
A solução mais comum é equipar o modelo com ferramentas externas para realizar buscas na web. No entanto, alimentar o modelo diretamente com o HTML bruto de uma página de resultados de busca é ineficaz. LLMs necessitam de dados estruturados, não de tags <div> e classes CSS. O HTML contém uma quantidade significativa de conteúdo supérfluo, como estilos, scripts e anúncios, que sobrecarregam e confundem o modelo, mesmo que ele consiga processá-lo.
É aqui que entra uma SERP API: ela converte os resultados brutos da busca do Google em um formato JSON estruturado, pronto para ser consumido pelo LLM. A responsabilidade desta API é "limpar" o HTML e organizar os dados dispersos em campos bem definidos, permitindo que o LLM se concentre em como utilizar essa informação.
Vamos detalhar o processo de implementação, abordando a escolha do protocolo, o design dos campos, o tratamento de erros e a compressão de contexto para o LLM.
API vs. Web Scraping Próprio
A experiência inicial com web scraping, utilizando Playwright e um sistema de agendamento simples, rapidamente apresentou problemas:
- Captchas e Bloqueios de IP: IPs eram bloqueados, e a troca de User-Agents não resolvia o problema.
- Alterações no Google: Atualizações na estrutura do Google invalidavam os parsers, causando falhas inesperadas.
- Variações Regionais de SERP: Resultados de busca variavam por região, com algumas versões sendo "simplificadas", omitindo PAA (People Also Ask), Knowledge Graph ou até mesmo os primeiros resultados orgânicos.
O último ponto é particularmente insidioso: um HTML que parece normal e retorna um código HTTP 200 pode, na verdade, estar incompleto. Ao ser mesclado a um JSON, essa informação faltante leva o modelo a acreditar em dados inexistentes. A depuração revelou que a incompleteza dos SERPs era atribuída ao IP de origem.
Adotar uma SERP API terceiriza o trabalho complexo de scraping, parsing e gerenciamento de IPs. O foco se desloca para a integração dos dados com o LLM. Essa divisão de responsabilidades é crucial: o consumidor da API não deve se preocupar com as mudanças na estrutura do Google ou com a degradação dos resultados de busca por IP. Essas preocupações devem ser tratadas pela API especializada.
Estrutura da Interface da API
Utilizando a API do SerpBase como exemplo, a estrutura básica envolve:
- URL Base:
https://api.serpbase.dev - Autenticação: Cabeçalho
X-API-Key - Corpo da Requisição: JSON
Para a consulta de busca mais comum:
curl -X POST https://api.serpbase.dev/google/search \
-H "Content-Type: application/json" \
-H "X-API-Key: $SERPBASE_API_KEY" \
-d '{"q":"openai realtime api","hl":"en","gl":"us","device":"default"}'
A chamada ao endpoint /google/search consome 1 crédito. Os parâmetros incluem q (termo de busca), hl (idioma), gl (país) e device (automático por padrão, ou toçado para pc/mobile).
A resposta JSON principal contém os seguintes módulos:
organic: Resultados orgânicos.top_stories: Notícias principais.people_also_ask: Perguntas relacionadas.knowledge_graph: Gráfico de conhecimento.related_searches: Buscas relacionadas.ai_overview: Resumo gerado por IA do Google (se disponível).
Todos os endpoints compartilham uma estrutura de resposta comum:
{
"status": 0,
"request_id": "0f3576b2-6e2e-4f1e-bb0e-8cb0d4a60195",
"elapsed_ms": 1432,
"credits_charged": 1,
"search_type": "search"
}
status: 0 indica sucesso. request_id é um identificador único para rastreamento em toda a cadeia de processamento. elapsed_ms mede a latência observada pelo gateway, útil para diagnosticar gargalos. credits_charged informa quantos créditos foram consumidos; geralmente é 0 em caso de falha.
Códigos de Erro: A Camada Essencial para o Cliente
A experiência inicial com APIs SERP mostrou a importância de ir além dos códigos de status HTTP. Um código HTTP 200 com status: 1029 (rate limiting) foi interpretado erroneamente como "sem resultados", levando o modelo a gerar uma resposta completamente fabricada.
A solução envolve duas camadas de códigos de status:
- Códigos HTTP: Para monitoramento de infraestrutura e decisões de roteamento de tráfego.
- Códigos de Status de Negócio (
status): Para o cliente determinar sucesso/falha e a necessidade de retentativas.
Exemplos de códigos de erro do SerpBase:
1001UNAUTHORIZED: Chave de API inválida. Retentativas inúteis.1004NOT_FOUND: Recurso inexistente. Retentativas inúteis.1029RATE_LIMITED: Limite de taxa atingido. Reduzir concorrência e retentar.1500INTERNAL_ERROR: Bug na plataforma. Retentativas com ID de rastreamento.1502UPSTREAM_FAILED: Falha no serviço upstream. Ideal para retentativas.1503SERVICE_UNAVAILABLE: Serviço temporariamente indisponível. Retentar.1504UPSTREAM_TIMEOUT: Timeout no serviço upstream. Retentar.
Um HTTP 200 com status: 1029 é uma resposta válida. O cliente deve focar no campo status e usar o request_id para logs de depuração.
import os
import time
import requests
API_ENDPOINT = "https://api.serpbase.dev"
API_KEY = os.environ["SERPBASE_API_KEY"]
RETRYABLE_STATUS_CODES = {1029, 1502, 1503, 1504}
def perform_search(query: str, max_retries: int = 3):
for attempt in range(max_retries):
try:
response = requests.post(
f"{API_ENDPOINT}/google/search",
headers={"X-API-Key": API_KEY, "Content-Type": "application/json"},
json={"q": query, "hl": "en", "gl": "us"},
timeout=15, # Timeout de 15 segundos
)
response.raise_for_status() # Lança exceção para códigos HTTP ruins
data = response.json()
status_code = data.get("status")
if status_code == 0:
return data # Sucesso
if status_code == 1001:
raise ValueError(f"Unauthorized access: {data}")
if status_code in RETRYABLE_STATUS_CODES:
# Exponential backoff com jitter
sleep_time = (2 ** attempt) + (random.random() * 0.5)
time.sleep(sleep_time)
continue # Tenta novamente
# Erro não retentável
raise RuntimeError(f"Search failed with status {status_code}: {data}")
except requests.exceptions.Timeout:
print(f"Timeout occurred on attempt {attempt + 1}. Retrying...")
time.sleep(2 ** attempt)
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}. Retrying...")
time.sleep(2 ** attempt)
raise RuntimeError(f"Maximum retries ({max_retries}) exhausted for query: {query}")
# Exemplo de uso:
# try:
# results = perform_search("latest advancements in AI")
# print("Search successful:", results)
# except RuntimeError as e:
# print(e)
Detalhes importantes incluem o timeout de 15 segundos, a estratégia de retentativa com backoff exponencial e o limite de retentativas para evitar loops infinitos.
Compressão de Dados para LLM
O JSON completo retornado pela API pode ser volumoso. Uma consulta pode retornar:
- Até 10 resultados orgânicos, cada um com possíveis sitelinks.
- 4-8 perguntas PAA.
- Um Knowledeg Graph extenso com imagens e atributos, totalizando milhares de tokens.
Para evitar exceder o limite de contexto do LLM, aplicam-se regras de compressão:
- Resultados Orgânicos: Manter os 5 primeiros, extraindo apenas
title,linkesnippet. - PAA: Manter as 4 primeiras perguntas, extraindo apenas
questioneanswer. - Knowledge Graph: Incluir
descriptione atributos chave, omitindo imagens.
Esses dados comprimidos são então incluídos no prompt do sistema:
import json
# Supondo que 'compressed_data' seja o dicionário com os dados comprimidos
system_prompt = f"""Use the following search results to answer the user's question.
If the answer is not in the results, say you don't know — do not make things up.
{json.dumps(compressed_data, ensure_ascii=False, indent=2)}
"""
É crucial filtrar snippets irrelevantes, títulos duplicados ou descrições excessivamente curtas antes de alimentar o prompt. Uma otimização adicional envolve injetar seletivamente partes do JSON com base no tipo de pergunta: knowledge_graph para definições, organic + people_also_ask para "como usar", e top_stories + ai_overview para atualizações recentes. Essa seleção pode ser guiada por uma instrução no prompt do sistema, onde o LLM classifica a intenção da pergunta antes de chamar a ferramenta.
Adicionalmente, extrair os links (link) de cada resultado e listá-los separadamente no prompt como "Fontes" aumenta a credibilidade da resposta, permitindo que o usuário verifique a origem da informação.
Modo Avançado: MCP
Para uma integração mais profunda, o Agent pode interagir diretamente com a API via MCP (Machine Communication Protocol). O MCP padroniza a descrição de ferramentas para LLMs, permitindo que o modelo decida autonomamente quando e como usar a busca.
Um exemplo de configuração MCP para SerpBase em clientes como Claude ou Cursor:
{
"mcpServers": {
"serpbase": {
"command": "python",
"args": ["-m", "serpbase_mcp"],
"env": { "SERPBASE_API_KEY": "your_api_key" }
}
}
}
Isso simplifica a gestão de ferramentas em comparação com a definição manual de esquemas. O modelo gerencia a lógica de chamadas, evitando a necessidade de código explícito para sequências de busca. Em conversas multi-turno, o modelo pode reutilizar informações de buscas anteriores sem a necessidade de novas chamadas.
Contudo, o MCP introduz sobrecarga (instanciar um processo para cada sessão) e validação de schema para cada chamada. Para funcionalidades de busca simples, esquemas de ferramentas manuais podem ser mais leves. Para fluxos de trabalho complexos com múltiplas ferramentas, o MCP pode oferecer economia significativa de engenharia.
Cenários com Mapas: Chamada em Duas Etapas
Para buscas locais, a combinação dos endpoints /google/maps/search e /google/maps/detail (consumindo 2 créditos cada) é eficiente. Primeiro, maps/search retorna uma lista de locais, e o feature\_id de cada um é usado para obter detalhes completos via maps/detail.
Os parâmetros lat e lng devem ser fornecidos juntos. O parâmetro zoom (1-21) controla o nível de detalhe; o padrão é 14. Os detalhes incluem informações como telefone, site, horário de funcionamento e avaliações.
É importante notar que o feature\_id tem um formato específico (0x...:0x...) e não deve ser confundido com data\_id ou cid retornados por outros endpoints, um detalhe que pode causar confusão durante a depuração.
Considerações Finais e Armadilhas Comuns
- Parâmetros
hl/gl: Use combinações corretas (ex:zh-CN / cnpara chinês,en / uspara inglês) para evitar resultados inconsistentes. - Paginação: O parâmetro
pageé baseado em 1 (não em 0). - Cache: Implemente cache para consultas frequentes, pois os resultados de busca do Google tendem a ser estáveis em curtos períodos.
- Logging: Sempre registre o
request_idjunto comstatus,elapsed_msecredits_chargedpara facilitar a investigação de problemas. - Créditos em Falhas: Nem todas as falhas resultam em 0 crédito consumido. Códigos como
1029(rate limiting) podem consumir créditos mesmo em caso de erro.
A implementação desses padrões resolve o problema de "tempo real" para LLMs. A documentação completa da API SerpBase está disponível em serpbase.dev/docs.