Outil de recherche Google LangChain : remplacer Custom Search
Remplacez GoogleSearchAPIWrapper avant la fin de Google Custom Search le 1er janvier 2027 : un outil LangChain testé qui conserve titre, lien et extrait.

GoogleSearchAPIWrapper et les outils google_search construits dessus appellent la Custom Search JSON API de Google, fermée aux nouveaux clients et arrêtée le 1er janvier 2027. Rien dans le wrapper ne permet de contourner cela : la solution est de changer de backend. Voici un outil LangChain de 79 lignes, exécuté le 19 octobre 2026, qui garde les mêmes noms d’outils et les mêmes champs title, link et snippet, de sorte que vos chaînes et vos prompts d’agent existants continuent de fonctionner. Une recherche coûte 5 crédits FastAF.
Qu’est-ce qui cesse exactement de fonctionner dans LangChain le 1er janvier 2027 ?
La présentation de la Custom Search JSON API de Google indique que l’API « est fermée aux nouveaux clients » et que les clients existants « ont jusqu’au 1er janvier 2027 pour passer à une autre solution » (vérifié le 19 octobre 2026). Sa seule alternative nommée, Vertex AI Search, est décrite comme « une alternative intéressante pour chercher dans 50 domaines au plus », c’est-à-dire de la recherche sur site, pas de la recherche web.
Dans langchain-google-community 5.1.1 (installé aujourd’hui dans un venv neuf, avec langchain 1.4.6 et langchain-core 1.6.12), GoogleSearchAPIWrapper construit un client customsearch v1 à partir de GOOGLE_API_KEY et d’un GOOGLE_CSE_ID, et chaque appel passe par cse().list(). L’ancienne copie dans langchain_community est déjà marquée comme dépréciée. La page Google Search de LangChain documente toujours le wrapper sans mention de l’arrêt, et son index des outils liste des dizaines d’autres intégrations de recherche. Je n’ai pas pu tester le comportement après la date d’arrêt : cette partie est la déclaration de Google, pas ma mesure.
Pour la vue d’ensemble, voir l’API de recherche officielle de Google contre les SERP API et la recherche web pour agents IA après Bing. Cet article ne traite que du remplacement dans LangChain.
Comment remplacer GoogleSearchAPIWrapper par un outil compatible ?
L’ancien wrapper a deux appels dont dépendent les chaînes : run(query), qui assemble les extraits en une seule chaîne, et results(query, num_results, search_params), qui renvoie des dicts avec title, link et snippet, ou [{"Result": "No good Google Search Result was found"}] quand il n’y a rien. Les outils google_search et google_search_results_json reposent dessus. Ce module reproduit le tout, en synchrone et en asynchrone, au-dessus de l’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),
]
Utilisez-le là où vous utilisiez l’ancienne paire :
from fastaf_search_tools import FastAFSearch, fastaf_search_tools
api = FastAFSearch(country="us", max_credits=50)
tools = fastaf_search_tools(api, num_results=5) # à brancher dans create_agent(model, tools=tools)
Un détail est mesuré, non supposé. Sur 20 requêtes variées (192 résultats, 100 crédits, 19 octobre 2026), 38 résultats (20 %) portaient un inline_snippet. Dans 36 d’entre eux, le champ snippet ne contenait que des libellés de sitelinks du type « More results from www.reddit.com », et la vraie description se trouvait dans inline_snippet. Le module préfère inline_snippet, qui ressemble à ce qu’était l’ancien snippet. Chaque requête a renvoyé 9 ou 10 résultats organiques.
Qu’ai-je réellement exécuté, et pour quel coût ?
Je n’avais pas de clé d’API LLM dans cet environnement, donc aucun vrai modèle n’est impliqué. J’ai exécuté les outils directement (invoke et ainvoke), puis dans une boucle create_agent pilotée par un faux modèle de chat scripté qui rejoue trois appels d’outils. Cela prouve le câblage dans le runtime LangChain, pas le comportement d’un modèle.
Contre https://api.fastaf.co/v1 le 19 octobre 2026 :
| Exécution | Latence | Crédits | Résultat |
|---|---|---|---|
google_search_results_json, synchrone |
3,20 s | 5 | 5 résultats, clés link, snippet, title |
| Même requête de nouveau | 0,0002 s | 0 | servie depuis le cache |
google_search, asynchrone |
4,74 s | 5 | 1 642 caractères d’extraits assemblés |
| Agent scripté, 3 appels d’outils, budget 10 | 5,8 s | 10 | troisième appel refusé avec un message |
Des exécutions antérieures du même code en production ont pris 6,9 et 8,1 s par recherche : prévoyez donc environ 3 à 8 secondes, pas des millisecondes. Dans l’exécution de 20 requêtes à concurrence 4, la médiane était de 4,0 s et le maximum de 12,4 s. Les requêtes échouées ne sont pas facturées. Le contrôle du budget a été resserré après ces exécutions (il réserve désormais le coût avant la requête) et rejoué seulement avec un stub : avec max_credits=6, deux appels parallèles à aresults en laissent passer un et refusent l’autre.
Comment les anciens paramètres se traduisent-ils dans le nouvel outil ?
| GoogleSearchAPIWrapper | Cet outil |
|---|---|
GOOGLE_API_KEY + GOOGLE_CSE_ID |
FASTAF_API_KEY seule, pas d’ID de moteur |
k, num_results |
mêmes noms ; plafonné à 10 par appel, comme le num de Google |
search_params={"gl": "de"} |
{"country": "de"} |
{"hl": "de"} |
{"lang": "de"} |
{"lr": ...}, {"cr": ...}, {"safe": "active"} |
mêmes noms |
{"start": 11} (base 1) |
{"page": 1} (base zéro ; la page 1 a renvoyé d’autres résultats que la page 0) |
siterestrict=True, siteSearch |
mettre site:example.com dans la requête (testé) |
exactTerms, excludeTerms |
guillemets et -terme dans la requête |
dateRestrict="w1" |
{"tbs": "qdr:w"}, une chaîne de filtre Google (acceptée ; je n’ai pas vérifié le filtrage par date) |
champs image, thumbnail |
non renvoyés |
searchType="image" |
non couvert par cet endpoint |
La différence de comportement est la plus importante. Custom Search interrogeait un moteur que vous aviez configuré. Ici, vous obtenez ce que google.com a renvoyé pour la requête, donc le classement différera de vos anciens résultats. Revérifiez tout jeu d’évaluation figé sur des URL précises. Le paramètre results de l’API n’a pas réduit la taille de la page dans mes tests (results=3 et results=20 ont tous deux renvoyé 9), donc le module découpe côté client.
Comment mettre en cache et plafonner la dépense par exécution ?
Les deux sont dans le module. ttl met en cache par requête, nombre et paramètres, de sorte qu’un agent qui répète une recherche ne paie qu’une fois. max_credits réserve les 5 crédits d’une recherche avant la requête, les remplace par l’en-tête X-FastAF-Credits-Used de la réponse (une erreur de l’API les rend) et lève ToolException quand la recherche suivante ne tiendrait plus ; avec handle_tool_error=True, l’agent reçoit « Search budget of 10 credits is used up. » comme un résultat d’outil normal et peut raisonner dessus, comme dans le tableau ci-dessus. Le wrapper ne garde aucun état entre processus, et le cache est en mémoire.
Le solde du compte est la limite externe : un solde épuisé renvoie 402 et arrête tout. Donner à un agent IA une recherche web en direct avec un plafond de dépense strict couvre les couches autour de cela, et les SERP API pour le RAG explique quoi faire des résultats.
Puis-je utiliser MCP à la place d’un outil sur mesure ?
Oui. FastAF fait tourner un serveur MCP hébergé avec un outil search_web. Je l’ai chargé avec langchain-mcp-adapters 0.3.2 (MultiServerMCPClient, transport streamable_http, en-tête Bearer), j’ai obtenu les 15 outils, et search_web a renvoyé 3 résultats pour 5 crédits en 4,9 s. Deux réserves : il renvoie url au lieu de link, et il transmet snippet tel quel, donc vous obtenez les libellés de sitelinks décrits plus haut. Pour des chaînes existantes qui lisent link, gardez l’outil sur mesure. Pour un nouvel agent qui doit aussi ouvrir des pages, utilisez MCP.
En bref
Google Custom Search s’arrête le 1er janvier 2027 et GoogleSearchAPIWrapper avec lui. Copiez le module, gardez les noms d’outils, vérifiez vos tests sensibles au classement, et fixez max_credits pour qu’un agent en boucle ne puisse pas faire grimper la facture. Une recherche coûte 5 crédits, soit de 1,33 $ à 2,50 $ pour 1 000 selon le pack, et l’offre gratuite est de 500 crédits, soit environ 100 recherches. Obtenez une clé gratuite et essayez-la sur vos propres requêtes.