Google Search no LangChain depois do fim do Custom Search

Troque o GoogleSearchAPIWrapper antes de 1º de janeiro de 2027: uma tool do LangChain testada que mantém title, link e snippet. Alternativa ao Custom Search.

O GoogleSearchAPIWrapper e as tools google_search construídas sobre ele chamam a Custom Search JSON API do Google, que foi fechada para novos clientes e termina em 1º de janeiro de 2027. Nada no wrapper consegue contornar isso, então a solução é trocar o backend. Abaixo está uma tool do LangChain de 79 linhas, executada em 19 de outubro de 2026, que mantém os mesmos nomes de tool e os mesmos campos title, link e snippet, então suas chains e prompts de agente existentes continuam funcionando. Cada busca custa 5 créditos do FastAF.

O que exatamente para de funcionar no LangChain em 1º de janeiro de 2027?

A visão geral da Custom Search JSON API do Google diz que a API “está fechada para novos clientes” e que os clientes existentes “têm até 1º de janeiro de 2027 para migrar para uma solução alternativa” (conferido em 19 de outubro de 2026). A única alternativa que ela cita, o Vertex AI Search, é descrita como “uma alternativa favorável para buscar em até 50 domínios”, ou seja, busca dentro de um site, não busca na web.

No langchain-google-community 5.1.1 (instalado hoje em um venv limpo, com langchain 1.4.6 e langchain-core 1.6.12), o GoogleSearchAPIWrapper monta um cliente customsearch v1 a partir de GOOGLE_API_KEY e de um GOOGLE_CSE_ID, e toda chamada passa por cse().list(). A cópia mais antiga em langchain_community já está marcada como obsoleta. A página do Google Search no LangChain ainda documenta o wrapper sem nenhum aviso de encerramento, e o índice de tools lista dezenas de outras integrações de busca. Não consegui testar o comportamento depois da data de encerramento, então essa parte é uma declaração do Google, não uma medição minha.

Para o panorama maior, veja a API de busca oficial do Google contra as SERP APIs e busca na web para agentes de IA depois do Bing. Este post trata só da troca no LangChain.

Como substituir o GoogleSearchAPIWrapper por uma tool compatível?

O wrapper antigo tem duas chamadas das quais as chains dependem: run(query), que junta os snippets em uma única string, e results(query, num_results, search_params), que devolve dicts com title, link e snippet, ou [{"Result": "No good Google Search Result was found"}] quando vazio. As tools google_search e google_search_results_json ficam por cima. Este módulo reproduz tudo isso, síncrono e assíncrono, em cima do endpoint /v1/serp:

import os, time, httpx
from langchain_core.tools import StructuredTool, ToolException

BASE = "https://api.fastaf.co/v1"
NOTHING = [{"Result": "No good Google Search Result was found"}]
COST = 5  # credits per search

class FastAFSearch:
    """Same surface as GoogleSearchAPIWrapper: run(), results(), arun(), aresults()."""

    def __init__(self, k=10, max_credits=None, ttl=300, **defaults):
        self.k, self.max_credits, self.ttl, self.defaults = k, max_credits, ttl, defaults
        self.spent, self._cache = 0, {}
        self._headers = {"Authorization": f"Bearer {os.environ['FASTAF_API_KEY']}"}

    def _hit(self, key):
        hit = self._cache.get(key)
        return hit[1] if hit and time.time() - hit[0] < self.ttl else None

    def _params(self, query, extra):
        if self.max_credits is not None and self.spent + COST > self.max_credits:
            raise ToolException(f"Search budget of {self.max_credits} credits is used up.")
        self.spent += COST  # reserve before the request, so parallel async calls cannot both pass
        return {"query": query, **self.defaults, **(extra or {})}

    def _parse(self, key, resp, num):
        # An API error is never charged: give the reservation back. A 200 reports its real cost.
        self.spent += (int(resp.headers["X-FastAF-Credits-Used"]) if resp.status_code == 200 else 0) - COST
        if resp.status_code != 200:
            raise ToolException(f"Search failed: HTTP {resp.status_code}")
        out = [{"title": r["title"], "link": r["link"],
                "snippet": r.get("inline_snippet") or r.get("snippet", "")}
               for r in resp.json()["organic_results"][:min(num, 10)]]
        self._cache[key] = (time.time(), out or NOTHING)
        return out or NOTHING

    def results(self, query, num_results, search_params=None):
        key = (query, num_results, str(search_params))
        if (hit := self._hit(key)) is not None:
            return hit
        resp = httpx.get(f"{BASE}/serp", params=self._params(query, search_params),
                         headers=self._headers, timeout=60)
        return self._parse(key, resp, num_results)

    async def aresults(self, query, num_results, search_params=None):
        key = (query, num_results, str(search_params))
        if (hit := self._hit(key)) is not None:
            return hit
        async with httpx.AsyncClient(timeout=60) as c:
            resp = await c.get(f"{BASE}/serp", params=self._params(query, search_params),
                               headers=self._headers)
        return self._parse(key, resp, num_results)

    @staticmethod
    def _join(rs):
        return " ".join(r["snippet"] for r in rs if "snippet" in r) or NOTHING[0]["Result"]

    def run(self, query: str) -> str:
        return self._join(self.results(query, self.k))

    async def arun(self, query: str) -> str:
        return self._join(await self.aresults(query, self.k))

def fastaf_search_tools(api: FastAFSearch, num_results: int = 4):
    """Two tools with the names and descriptions of GoogleSearchRun / GoogleSearchResults."""
    def as_json(query: str) -> str:
        return str(api.results(query, num_results))
    async def aas_json(query: str) -> str:
        return str(await api.aresults(query, num_results))
    desc = ("A wrapper around Google Search. Useful for when you need to answer questions "
            "about current events. Input should be a search query.")
    return [
        StructuredTool.from_function(func=api.run, coroutine=api.arun, name="google_search",
                                     description=desc, handle_tool_error=True),
        StructuredTool.from_function(func=as_json, coroutine=aas_json,
                                     name="google_search_results_json",
                                     description=desc + " Output is a JSON array of the query results",
                                     handle_tool_error=True),
    ]

Use onde você usava o par antigo:

from fastaf_search_tools import FastAFSearch, fastaf_search_tools

api = FastAFSearch(country="us", max_credits=50)
tools = fastaf_search_tools(api, num_results=5)   # drop into create_agent(model, tools=tools)

Um detalhe é medido, não suposto. Em 20 consultas variadas (192 resultados, 100 créditos, 19 de outubro de 2026), 38 resultados (20%) traziam um inline_snippet. Em 36 deles o campo snippet continha apenas rótulos de sitelinks, como “More results from www.reddit.com”, e a descrição de verdade estava em inline_snippet. O módulo prefere inline_snippet, que é o que o antigo snippet parecia. Toda consulta retornou 9 ou 10 resultados orgânicos.

O que eu de fato executei, e quanto custou?

Eu não tinha chave de API de LLM neste ambiente, então nenhum modelo real está envolvido. Executei as tools diretamente (invoke e ainvoke) e depois dentro de um loop create_agent conduzido por um modelo de chat falso com roteiro, que repete três chamadas de tool. Isso prova a ligação no runtime do LangChain, não o comportamento de um modelo.

Contra https://api.fastaf.co/v1 em 19 de outubro de 2026:

Execução Latência Créditos Resultado
google_search_results_json, síncrono 3,20 s 5 5 resultados, chaves link, snippet, title
Mesma consulta de novo 0,0002 s 0 servida pelo cache
google_search, assíncrono 4,74 s 5 1.642 caracteres de snippets unidos
Agente com roteiro, 3 chamadas de tool, orçamento 10 5,8 s 10 terceira chamada recusada com uma mensagem

Execuções anteriores do mesmo código em produção levaram 6,9 e 8,1 s por busca, então conte com algo entre 3 e 8 segundos, não milissegundos. Na execução de 20 consultas com concorrência 4, a mediana foi 4,0 s e o máximo 12,4 s. Requisições que falham não são cobradas. A checagem do orçamento foi reforçada depois dessas execuções (agora reserva o custo antes da requisição) e repetida apenas com um stub: com max_credits=6, duas chamadas paralelas a aresults deixam uma passar e recusam a outra.

Como os parâmetros antigos se mapeiam para a nova tool?

GoogleSearchAPIWrapper Esta tool
GOOGLE_API_KEY + GOOGLE_CSE_ID só FASTAF_API_KEY, sem ID de mecanismo
k, num_results mesmos nomes; limitado a 10 por chamada, como o num do Google
search_params={"gl": "de"} {"country": "de"}
{"hl": "de"} {"lang": "de"}
{"lr": ...}, {"cr": ...}, {"safe": "active"} mesmos nomes
{"start": 11} (base 1) {"page": 1} (base zero; a página 1 retornou resultados diferentes da página 0)
siterestrict=True, siteSearch coloque site:example.com na consulta (testado)
exactTerms, excludeTerms aspas e -termo na consulta
dateRestrict="w1" {"tbs": "qdr:w"}, uma string de filtro do Google (aceita; não verifiquei o filtro de datas)
campos image, thumbnail não retornados
searchType="image" não coberto por este endpoint

A diferença de comportamento é a maior. O Custom Search consultava um mecanismo que você configurava. Esta tool devolve o que o google.com devolveu para a consulta, então os rankings vão diferir dos seus resultados antigos. Reveja qualquer conjunto de avaliação fixado em URLs específicas. O parâmetro results da API não reduziu o tamanho da página nos meus testes (results=3 e results=20 retornaram 9), então o módulo corta no cliente.

Como fazer cache e limitar o gasto por execução?

Os dois estão no módulo. O ttl faz cache por consulta, quantidade e parâmetros, então um agente que repete uma busca paga uma vez. O max_credits reserva os 5 créditos de uma busca antes da requisição, troca-os pelo header X-FastAF-Credits-Used da resposta (um erro da API os devolve) e lança ToolException quando a próxima busca não caberia mais; com handle_tool_error=True, o agente recebe “Search budget of 10 credits is used up.” como um resultado normal de tool e pode raciocinar sobre isso, como na tabela acima. O wrapper não guarda estado entre processos, e o cache fica em memória.

O saldo da conta é o limite externo: um saldo esgotado devolve 402 e para tudo. Dê a um agente de IA busca na web ao vivo com um teto de gastos rígido cobre as camadas ao redor disso, e SERP APIs para RAG cobre o que fazer com os resultados.

Posso usar MCP em vez de uma tool customizada?

Sim. O FastAF roda um servidor MCP hospedado com uma tool search_web. Carreguei com langchain-mcp-adapters 0.3.2 (MultiServerMCPClient, transporte streamable_http, header Bearer), recebi as 15 tools, e search_web devolveu 3 resultados por 5 créditos em 4,9 s. Duas ressalvas: ele devolve url em vez de link, e repassa o snippet sem alteração, então você recebe os rótulos de sitelinks descritos acima. Para chains existentes que leem link, mantenha a tool customizada. Para um agente novo que também deva abrir páginas, use MCP.

Resumindo

O Google Custom Search termina em 1º de janeiro de 2027 e o GoogleSearchAPIWrapper para junto. Copie o módulo, mantenha os nomes das tools, confira os testes sensíveis a ranking e defina max_credits para que um agente em loop não gere uma conta alta. Uma busca custa 5 créditos, de $1,33 a $2,50 por 1.000 conforme o pacote, e o plano gratuito é de 500 créditos, cerca de 100 buscas. Pegue uma chave grátis e teste com as suas próprias consultas.