Lesson 2 — Construire pas à pas

La recherche web et les sources vérifiables

5 min read

This content is not translated yet. Showing the original version.

La recherche web et les sources vérifiables

1. Le sous-agent de recherche

files/veille.py
chercheur = Agent(
    name="chercheur_web",
    model=MODEL,
    description="Recherche sur le web les actualités récentes d'une entreprise et renvoie des faits datés.",
    instruction="Tu fais une recherche Google sur la demande reçue. Réponds par 3 à 6 puces factuelles, "
                "chacune avec sa date (AAAA-MM-JJ si connue). Uniquement des faits trouvés, aucune opinion.",
    tools=[google_search],
)

La description compte : c'est elle que le veilleur lit pour savoir à quoi sert l'outil chercheur_web.

2. L'emballer dans un AgentTool

files/veille.py
tools=[AgentTool(chercheur_agent, propagate_grounding_metadata=True), enregistrer_signal],
after_tool_callback=ajouter_sources,

Quand le veilleur appelle chercheur_web(request=...), AgentTool lance une exécution séparée du sous-agent et renvoie seulement le texte de sa dernière réponse. Avec propagate_grounding_metadata=True, il range en plus les métadonnées d'ancrage de cette réponse dans l'état, sous la clé temp:_adk_grounding_metadata (nom relevé dans le code d'ADK 2.10 ; le préfixe temp: signifie « pour cette invocation seulement »).

Paramètre d'AgentToolEffet
skip_summarizationFalse par défaut : le veilleur relit le résultat et continue ; True : le résultat de l'outil clôt le tour, sans nouvel appel au modèle du veilleur
propagate_grounding_metadatacopie les métadonnées d'ancrage du sous-agent dans l'état temporaire
include_pluginsapplique au sous-agent les plugins du Runner parent

3. Les métadonnées d'ancrage

Une réponse ancrée contient notamment :

  • grounding_chunks : la liste des pages consultées, chacune avec web.uri et web.title ;
  • grounding_supports : des passages du texte (segment.text) et les indices des pages qui les justifient (grounding_chunk_indices).
flowchart LR
  T["Texte : « Scaleway ouvre une GPU Zone à Copenhague. »"] --> S["grounding_supports[0] : chunks 3 et 4"]
  S --> C3["grounding_chunks[3] : larevuedudigital.com"]
  S --> C4["grounding_chunks[4] : scaleway.com"]
  C3 --> ID["sources globales n° 4 et 5"]
  C4 --> ID

4. Le callback qui fabrique les sources

Un callback ADK est une fonction appelée à un moment précis du cycle de l'agent. after_tool_callback est appelé après chaque outil ; s'il renvoie un dictionnaire, celui-ci remplace le résultat de l'outil vu par le modèle.

files/veille.py
def ajouter_sources(tool, args, tool_context: ToolContext, tool_response):
    """after_tool_callback : après chaque recherche, numérote les sources et relie chaque fait à ses sources."""
    if tool.name != chercheur.name:
        return None
    meta = tool_context.state.get("temp:_adk_grounding_metadata")
    tool_context.state["temp:_adk_grounding_metadata"] = None  # ne pas réattribuer à la recherche suivante
    sources = list(tool_context.state.get("sources", []))
    id_de = {}  # index du chunk dans cette recherche -> identifiant global de la source
    for i, chunk in enumerate(getattr(meta, "grounding_chunks", None) or []):
        if not (chunk.web and chunk.web.uri):
            continue
        url = url_finale(chunk.web.uri)
        existant = next((s for s in sources if s["url"] == url), None)
        if existant is None:
            existant = {"id": len(sources) + 1, "titre": chunk.web.title or chunk.web.domain or url, "url": url}
            sources.append(existant)
        id_de[i] = existant["id"]
    tool_context.state["sources"] = sources
    faits = [{"texte": s.segment.text, "sources": sorted({id_de[j] for j in s.grounding_chunk_indices or [] if j in id_de})}
             for s in (getattr(meta, "grounding_supports", None) or []) if s.segment and s.segment.text]
    return {"faits_sources": faits, "texte_complet": tool_response,
            "sources": [{"id": s["id"], "titre": s["titre"]} for s in sources if s["id"] in id_de.values()]}

Points clés :

  • les identifiants sont globaux à toute la veille (la source n° 7 reste la n° 7 d'une recherche à l'autre) et dédoublonnés par URL ;
  • le modèle ne reçoit que id et titre : il n'a aucune URL à recopier, donc aucune à inventer ;
  • on vide la clé temporaire après lecture : si la recherche suivante ne renvoyait aucune métadonnée, on ne lui attribuerait pas celles de la précédente.
files/veille.py
def url_finale(url: str) -> str:
    """Les URL d'ancrage sont des redirections Google : on garde la vraie page (repli : l'URL reçue)."""
    if "grounding-api-redirect" not in url:
        return url
    try:
        return httpx.head(url, follow_redirects=False, timeout=10).headers.get("location", url)
    except httpx.HTTPError:
        return url

Que voit le veilleur comme résultat de chercheur_web, une fois le callback appliqué ?

Pourquoi relier chaque fait à ses sources (faits_sources) plutôt que donner toutes les sources de la recherche ?

Exercice : filtrer les domaines

La direction ne veut pas de sources issues de sites de cours de bourse grand public. Modifiez le callback pour ignorer les pages dont le domaine figure dans une liste DOMAINES_EXCLUS.

Voir la solution
from urllib.parse import urlparse

DOMAINES_EXCLUS = {"exemple-bourse.test"}

# dans la boucle, après url = url_finale(chunk.web.uri) :
if urlparse(url).hostname and urlparse(url).hostname.removeprefix("www.") in DOMAINES_EXCLUS:
    continue

Le chunk ignoré n'entre pas dans id_de : les faits qui ne s'appuyaient que sur lui auront une liste de sources vide, et enregistrer_signal refusera de les enregistrer. Le filtre se fait après url_finale, car avant la redirection tous les domaines sont vertexaisearch.cloud.google.com.

Résultat obtenu

Section « Sources » du rapport réel du 2026-10-15 (extrait) : les URL sont les pages finales, plus les redirections.

1. [scaleway.com](https://www.scaleway.com/news/scaleway-scales-up-to-power-europes-ai-ambitions/)
4. [larevuedudigital.com](https://www.larevuedudigital.com/scaleway-de-xavier-niel-renforce-ses-capacites-de-calcul-pour-une-ia-hebergee-en-europe/)
5. [scaleway.com](https://www.scaleway.com/en/news/)
13. [hetzner.com](https://status.hetzner.com/incident/0bd2f9f1-d3fd-420d-80e3-0259cbd9a095)

Seules les sources citées par au moins un signal figurent dans le rapport : 13 sur les 19 collectées.

Je sais faire

  • Isoler google_search dans un sous-agent appelé via AgentTool
  • Récupérer les métadonnées d'ancrage avec propagate_grounding_metadata
  • Écrire un after_tool_callback qui remplace le résultat d'un outil
  • Résoudre les URL de redirection d'ancrage
Next