Lesson 3 — Construire l'agent pas à pas

Sources citées et rapport structuré

5 min read

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

Sources citées et rapport structuré

Le problème des citations

Le texte renvoyé par un modèle ancré ne contient pas de liens : les sources sont dans grounding_metadata.grounding_chunks. Si on laisse le modèle « citer de mémoire », il peut se tromper de numéro ou inventer une URL. On fait donc circuler les numéros dans le texte et les URL dans l'état.

flowchart LR
  G["Réponse Gemini + grounding_chunks"] --> CB["memoriser_sources (after_model_callback)"]
  CB -->|"dédoublonne, numérote"| ST[("state['sources']")]
  CB -->|"ajoute 'Sources : [1] a.org ; [2] b.org'"| TX["Texte renvoyé à l'agent principal"]
  TX --> P["analyste_recherche cite [1][2]"]
  ST --> B["bibliographie() dans run.py"]
  P --> R["rapport.md"]
  B --> R

Le callback memoriser_sources

Un after_model_callback reçoit la réponse du modèle avant qu'ADK ne la transforme en événement. Il peut la lire, la modifier et la renvoyer.

files/agent.py
def memoriser_sources(callback_context, llm_response):
    gm = llm_response.grounding_metadata
    if not gm or not gm.grounding_chunks or not llm_response.content:
        return None
    sources = list(callback_context.state.get("sources", []))
    lignes = []
    for chunk in gm.grounding_chunks:
        if not chunk.web or not chunk.web.uri:
            continue
        source = {"titre": chunk.web.title or chunk.web.domain or "source", "url": chunk.web.uri}
        if source not in sources:
            sources.append(source)
        lignes.append(f"[{sources.index(source) + 1}] {source['titre']}")
    callback_context.state["sources"] = sources
    llm_response.content.parts.append(types.Part(text="\n\nSources : " + " ; ".join(dict.fromkeys(lignes))))
    return llm_response

Trois détails importants :

DétailPourquoi
if source not in sourcesune même page trouvée par deux recherches garde le même numéro
dict.fromkeys(lignes)dédoublonne en gardant l'ordre (une page peut apparaître plusieurs fois dans les chunks)
callback_context.state["sources"] = ...l'écriture passe par le state_delta du sous-agent, qu'AgentTool recopie dans la session principale

Deux recherches successives trouvent b.org. Quel numéro b.org porte-t-il dans la deuxième recherche ?

Le rapport structuré

L'instruction de l'agent principal impose les étapes et le format :

files/agent.py
INSTRUCTION = """Tu es un analyste de veille. Tu réponds en français.
1. Appelle date_du_jour.
2. Découpe la question de l'utilisateur en 2 ou 3 sous-questions précises.
3. Appelle chercheur_web une fois par sous-question.
4. Rédige un rapport Markdown avec exactement ces sections :
   ## Synthèse (3 phrases)
   ## Points clés (puces ; chaque puce finit par le ou les numéros de source, ex. [1][3])
   ## Ce qui reste incertain
N'utilise QUE les faits renvoyés par chercheur_web et ne cite QUE les numéros [n] qu'il donne.
Ne recopie pas la liste des sources : elle est ajoutée automatiquement."""

Puis run.py assemble le fichier final : titre, texte de l'agent, et la bibliographie construite par le code depuis l'état.

files/run.py
s = await runner.session_service.get_session(app_name="veille", user_id="moi", session_id=s.id)
sources = s.state.get("sources", [])
texte = f"# {question}\n\n{rapport.strip()}\n\n## Sources\n{bibliographie(sources)}\n"
open("rapport.md", "w", encoding="utf-8").write(texte)

Qui écrit les URL de la section « Sources » du rapport ?

Exercice : signaler les citations orphelines

Écrivez une fonction citations_invalides(rapport, sources) qui renvoie les numéros cités dans le rapport ([7]) qui n'existent pas dans sources. Où l'appelez-vous ?

Voir la solution
import re

def citations_invalides(rapport: str, sources: list) -> list[int]:
    cites = {int(n) for n in re.findall(r"\[(\d+)\]", rapport)}
    return sorted(n for n in cites if not 1 <= n <= len(sources))

Appelez-la dans run.py juste avant d'écrire rapport.md ; si la liste n'est pas vide, affichez un avertissement ou relancez la rédaction. Ajoutez un test : citations_invalides("A [1] B [4]", [s1, s2]) == [4].

Résultat obtenu

Rapport réel produit par python run.py (gemini-3.5-flash, 2026-10-15), extrait. Les faits viennent des pages trouvées ce jour-là ; relisez-les avant tout usage.

## Points clés
* **Moteur d'exécution par graphes (Workflow Runtime)** : La publication d'ADK Python v2.0.0 en
  mai 2026, suivie de Go v2.0.0 en juin et TypeScript v2.0.0 en août 2026, marque la transition
  d'une exécution hiérarchique vers le *Workflow Runtime* [...] [3].
* **Écosystème multi-langage et Java v1.0.0** : ADK prend en charge Python, Java, Go, TypeScript
  et Kotlin [...] [1][2][3].

## Sources
1. [googleblog.com](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQF28L...)
2. [googleblog.com](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGLLs...)
3. [google.com](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQE4qD...)
[...] 8 sources au total

Je sais faire

  • Lire grounding_metadata dans un after_model_callback
  • Numéroter des sources de façon stable entre plusieurs recherches
  • Faire remonter l'état d'un sous-agent appelé par AgentTool
  • Imposer un format de rapport et séparer texte du modèle et bibliographie du code
Next