Integrando LLMs com Resultados de Busca em Tempo Real via SERP API

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:

  • 1001 UNAUTHORIZED: Chave de API inválida. Retentativas inúteis.
  • 1004 NOT_FOUND: Recurso inexistente. Retentativas inúteis.
  • 1029 RATE_LIMITED: Limite de taxa atingido. Reduzir concorrência e retentar.
  • 1500 INTERNAL_ERROR: Bug na plataforma. Retentativas com ID de rastreamento.
  • 1502 UPSTREAM_FAILED: Falha no serviço upstream. Ideal para retentativas.
  • 1503 SERVICE_UNAVAILABLE: Serviço temporariamente indisponível. Retentar.
  • 1504 UPSTREAM_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:

  1. Resultados Orgânicos: Manter os 5 primeiros, extraindo apenas title, link e snippet.
  2. PAA: Manter as 4 primeiras perguntas, extraindo apenas question e answer.
  3. Knowledge Graph: Incluir description e 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 / cn para chinês, en / us para 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_id junto com status, elapsed_ms e credits_charged para 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.

Tags: LLM SERP API Google Search web scraping API Integration

Publicado em 10-24 23:59