Cours
GLM-5-Turbo est spécialement conçu pour les workflows de type agent, où le modèle orchestre des exécutions en plusieurs étapes. Il est optimisé pour comprendre des consignes complexes, appeler des outils de manière fiable et produire des sorties structurées facilement exploitables par des systèmes.
Dans ce tutoriel, nous allons créer un agent de navigation en temps réel qui parcourt le web, extrait les informations pertinentes et génère un résumé clair pour l’utilisateur. À la fin, vous disposerez d’une architecture réutilisable que vous pourrez étendre, au-delà des vols, à toute tâche de recherche et d’extraction dans le monde réel.
Vous pouvez voir notre projet GLM-5-Turbo en action dans la vidéo ci-dessous :
Qu’est-ce que GLM-5-Turbo ?
GLM-5-Turbo est conçu pour prendre en charge des workflows d’agent concrets. Il propose plusieurs modes de raisonnement, permettant d’arbitrer entre rapidité de réponse et profondeur d’analyse selon la tâche.
Pour les applications interactives, il gère aussi la diffusion en continu (streaming) des sorties, afin de livrer les réponses jeton par jeton et d’améliorer la réactivité de l’interface.
L’un de ses points forts est l’appel de fonctions, qui permet d’invoquer des outils externes de manière fiable. Le modèle prend également en charge des sorties structurées comme le JSON, ce qui facilite l’intégration avec les systèmes en aval et les couches UI.
Pour les workflows plus longs, GLM-5-Turbo inclut une mise en cache du contexte, utile pour préserver les performances lors d’interactions étendues sans retraiter les mêmes informations. Il prend aussi en charge l’intégration à des systèmes externes via MCP (Model Context Protocol), afin de se connecter à d’autres outils et sources de données.
Conçu pour les workflows d’agent
GLM-5-Turbo est fortement optimisé pour des environnements de type agent, notamment des scénarios comme OpenClaw, où les tâches impliquent plusieurs étapes, outils et longues chaînes d’exécution.
Cette optimisation améliore plusieurs aspects clés :
- Fiabilité des appels d’outils : garantit que le modèle appelle systématiquement les bonnes fonctions.
- Décomposition des instructions : permet de scinder des tâches complexes en étapes plus petites.
- Stabilité des exécutions longues : essentielle pour des agents multi-étapes.
En pratique, GLM-5-Turbo est suffisamment rapide pour des systèmes interactifs tout en restant fiable dans des scénarios multi-étapes avec appels d’outils : exactement ce qu’il faut pour construire des agents de navigation.
Projet exemple GLM-5-Turbo : créer un agent de navigation en temps réel
Dans ce tutoriel, nous allons créer une application Streamlit qui se comporte comme un assistant de voyage léger. L’utilisateur saisit une requête du type : « Trouvez les vols les moins chers de SFO à JFK du vendredi au lundi. » Le système va ensuite :
- rechercher sur le web des pages de résultats de vols pertinentes ;
- ouvrir et analyser ces pages ;
- extraire des informations utiles (prix, durée, escales) ;
- résumer les meilleures options dans un format lisible.
À un niveau élevé, le système repose sur quatre composants qui collaborent.
- Le premier est la couche LLM (
llm_client.py), qui envoie des requêtes de complétion de chat à Z.AI ou OpenRouter. - La deuxième est la couche d’outils de navigation (
browser_tools.py), qui expose des actions comme rechercher, ouvrir des pages et extraire du contenu. - La troisième est la boucle d’agent (
agent.py), qui relie le modèle et les outils dans un workflow multi-étapes. - Enfin, l’UI Streamlit (
app.py) fournit l’interface d’interaction et de visualisation.
Code source complet : https://github.com/AashiDutt/GLM5_TURBO
Étape 1 : prérequis
Avant de construire l’application, assurez-vous d’avoir :
- Python 3.9+
- Une clé API GLM-5-Turbo (Z.AI ou OpenRouter)
- Playwright installé pour l’automatisation du navigateur
Installez les bibliothèques requises :
pip install streamlit pandas python-dotenv requests playwright
playwright install chromium
Le fichier app.py utilise Streamlit, pandas, dotenv et des modules personnalisés. Tandis que browser_tools.py s’appuie sur requests et Playwright. Les appels d’API sont effectués dans llm_client.py via requests.
Exécutez maintenant la commande suivante pour créer un fichier .env qui contiendra votre (vos) clé(s) API :
cp .env.example .env
Sélectionnez votre point de terminaison API préféré et renseignez la clé API :
.env
PROVIDER=zai
# API directe Z.AI
ZAI_API_KEY=Your Z.AI API key
ZAI_BASE_URL=https://api.z.ai/api/paas/v4/chat/completions
ZAI_MODEL=glm-5-turbo
# OpenRouter
OPENROUTER_API_KEY=Your OpenRouter API key
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1/chat/completions
OPENROUTER_MODEL=z-ai/glm-5-turbo
# Navigateur
HEADLESS=true
MAX_STEPS=10
De même, le navigateur Playwright est lancé depuis ce constructeur dans browser_tools.py :
class BrowserTools:
def __init__(self, headless: bool = True) -> None:
self.playwright = sync_playwright().start()
self.browser = self.playwright.chromium.launch(headless=headless)
self.context = self.browser.new_context(viewport={"width": 1440, "height": 1100})
self.page = self.context.new_page()
Ce bloc initialise un environnement de navigation entièrement contrôlé via Playwright, que l’agent utilise pour interagir avec le web.
Il démarre le moteur Playwright, lance un navigateur Chromium, crée une session isolée et ouvre une nouvelle page où toutes les actions auront lieu.
Cette configuration permet à l’agent de parcourir de vrais sites, d’observer du contenu en direct et de prendre des décisions en fonction de ce qu’il voit.
Étape 2 : configuration du client LLM
Nous allons ensuite configurer notre LLMClient, compatible à la fois avec Z.AI en direct et OpenRouter via la même interface. Le point de terminaison spécifique au fournisseur est sélectionné dans le constructeur. Les tarifs étant comparables, vous pouvez utiliser l’une ou l’autre API pour cette démo selon votre préférence.
Voici la configuration clé :
class LLMClient:
def __init__(
self,
provider: str,
api_key: str,
model: str,
app_name: str = "GLM-5 Browser Agent Demo",
app_url: str = "http://localhost:8501",
) -> None:
self.provider = provider.lower().strip()
self.api_key = api_key
self.model = model
if self.provider == "zai":
self.url = os.getenv("ZAI_BASE_URL", "https://api.z.ai/api/paas/v4/chat/completions")
elif self.provider == "openrouter":
self.url = os.getenv("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1/chat/completions")
Cet initialiseur configure le client LLM en sélectionnant l’endpoint approprié (Z.AI ou OpenRouter) tout en conservant une interface homogène.
Il stocke la clé API, le modèle et l’URL de l’endpoint, permettant au reste du système d’appeler le modèle sans se soucier des différences entre fournisseurs.
Ensuite, create_chat_completion() construit la charge utile, attache des outils si nécessaire et analyse les éventuels appels d’outils renvoyés.
payload: Dict[str, Any] = {
"model": self.model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
"stream": False,
}
if tools:
payload["tools"] = tools
payload["tool_choice"] = "auto"
La charge utile ci-dessus définit la requête envoyée au modèle, incluant le contexte de conversation et les paramètres de génération. En joignant les tools et en activant tool_choice="auto", on laisse le modèle décider quand invoquer des fonctions externes.
Étape 3 : définir les outils de navigation
Dans agent.py, nous définissons un schéma d’outils appelables par le modèle. Ils incluent search_web, open_url, open_search_result, extract_page, extract_flight_cards, click, type_text et wait_for_text.
Voici un exemple de définition d’outil :
{
"type": "function",
"function": {
"name": "search_web",
"description": "Search the public web for flight result pages or travel aggregators.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string"},
"max_results": {"type": "integer", "default": 8},
},
"required": ["query"],
},
},
}
Ce schéma indique au modèle ce que fait l’outil, les entrées attendues et la manière de l’appeler. En définissant les outils de cette façon, on permet au modèle de décider dynamiquement quand lancer une recherche web, au lieu de figer le flux.
Côté exécution, ces outils sont implémentés dans browser_tools.py. Pour cette démo, nous avons utilisé l’endpoint HTML de DuckDuckGo pour stocker les liens de résultats courants en vue d’une navigation ultérieure.
def search_web(self, query: str, max_results: int = 8) -> Dict[str, Any]:
url = f"https://html.duckduckgo.com/html/?q={quote_plus(query)}"
headers = {"User-Agent": "Mozilla/5.0"}
resp = requests.get(url, headers=headers, timeout=30)
resp.raise_for_status()
html = resp.text
La fonction search_web() ci-dessus effectue une recherche légère en récupérant le HTML brut et en le parcourant, sans recourir à une API de recherche payante. Elle conserve aussi les résultats en interne pour que l’agent puisse y naviguer ensuite.
Maintenant que nous avons défini comment l’agent interagit avec le web, voyons comment il utilise ces outils pour extraire les informations de vol via la recherche.
Étape 4 : extraction des informations de vol
Une fois qu’une source prometteuse est trouvée, il faut l’inspecter. Nous pouvons utiliser les outils de navigation (de l’étape 3) pour extraire des instantanés bruts de page.
Étape 4.1 : ouverture et extraction des pages
Nous allons utiliser l’outil open_url() pour charger la page web et renvoyer un instantané avec le titre, l’URL et le texte visible.
def open_url(self, url: str) -> Dict[str, Any]:
self.page.goto(url, wait_until="domcontentloaded", timeout=45000)
self.page.wait_for_timeout(1200)
return self._page_snapshot()
Pour une extraction plus large, nous utiliserons l’outil extract_page() ci-dessous, qui renvoie l’instantané de page et un ensemble de liens collectés depuis la page courante.
def extract_page(self, max_chars: int = 7000, link_limit: int = 20) -> Dict[str, Any]:
snapshot = self._page_snapshot(max_chars=max_chars)
snapshot["links"] = self._collect_links(limit=link_limit)
self.current_links = snapshot["links"]
return snapshot
C’est utile car le modèle n’a pas toujours besoin de cliquer à l’aveugle. Il peut d’abord inspecter le texte et les liens disponibles, puis choisir la prochaine action.
Étape 4.2 : extraction heuristique des vols
Un élément clé de la démo est la fonction extract_flight_cards() qui prend le texte visible de la page et utilise des expressions régulières pour détecter les prix, horaires, durées et escales.
Voici le cœur de cette logique :
money_pattern = re.compile(r"(?:\$|USD\s?)(\d{2,5})")
time_pattern = re.compile(r"\b\d{1,2}:\d{2}\s?(?:AM|PM|am|pm)?\b")
duration_pattern = re.compile(r"\b\d{1,2}\s?h\s?\d{0,2}\s?m?\b|\b\d{1,2}\s?hr\s?\d{0,2}\s?min?\b", re.I)
stop_pattern = re.compile(r"nonstop|1 stop|2 stops|3 stops", re.I)
Ces expressions régulières agissent comme des détecteurs de signaux courants liés aux vols. money_pattern capture des prix au format $299 ou USD 299, tandis que time_pattern identifie les heures de départ et d’arrivée.
De même, duration_pattern extrait la durée du trajet, et stop_pattern détecte si le vol est direct ou avec escales. Vous pouvez créer des motifs similaires pour d’autres cas d’usage.
Une fois ces signaux définis, la fonction balaie de petits blocs de texte et recherche des combinaisons pertinentes. Lorsqu’un ensemble correspond, elle construit une « carte » de vol structurée :
card = {
"snippet": joined[:350],
"price_usd": int(price_match.group(1)),
"times": times[:2],
"duration": duration.group(0) if duration else None,
"stops": stops.group(0) if stops else None,
}
Ce bloc transforme le texte brut en une représentation structurée d’une option de vol. Il extrait le prix, limite les horaires aux plus pertinents et gère les champs manquants. Le contexte d’origine est préservé, ce qui est utile pour le débogage.
Avec cette extraction en place, l’agent sait convertir du contenu non structuré en données exploitables. Voyons maintenant comment ces données circulent dans la boucle d’agent pour piloter la prise de décision multi-étapes.
Étape 5 : boucle d’agent
Le comportement de l’agent se trouve dans la classe BrowserAgent (dans agent.py). Cette classe envoie l’invite et le schéma d’outils au modèle, vérifie s’il souhaite appeler un outil, exécute cet outil, ajoute le résultat dans la conversation, puis continue jusqu’à produire une réponse finale.
Voici la boucle principale :
for step in range(1, self.max_steps + 1):
assistant = self.llm.create_chat_completion(
messages=messages,
tools=TOOLS,
temperature=0.2,
max_tokens=2200,
enable_thinking=False,
)
if assistant.tool_calls:
...
result = self.tool_runner(tc.name, tc.arguments)
...
continue
final_content = assistant.content.strip()
if final_content:
yield {"type": "final", "step": step, "content": final_content}
return
Cette boucle est le cœur de la démo.
À chaque étape, le modèle reçoit l’état courant de la conversation et décide de l’action suivante. S’il renvoie des appels d’outils, ceux-ci sont exécutés via tool_runner, puis les résultats sont réinjectés dans la boucle pour l’itération suivante.
Le processus continue jusqu’à ce que le modèle produise une réponse finale au lieu d’appeler un outil.
La boucle impose également une limite maximale d’étapes pour éviter les exécutions infinies et garder le processus maîtrisé et efficace.
Étape 6 : connecter les outils à l’application
Dans app.py, nous associons les noms d’outils demandés par le modèle aux méthodes réelles du navigateur. Cette correspondance est implémentée via la fonction make_tool_runner() :
def make_tool_runner(browser: BrowserTools):
def run(name: str, args: Dict[str, Any]):
if name == "search_web":
return browser.search_web(**args)
if name == "open_url":
return browser.open_url(**args)
if name == "open_search_result":
return browser.open_search_result(**args)
if name == "extract_page":
return browser.extract_page(**args)
if name == "extract_flight_cards":
return browser.extract_flight_cards(**args)
Cette fonction joue le rôle d’aiguillage entre le modèle et le navigateur. Le modèle renvoie uniquement le nom de l’outil et ses arguments, et run() traduit cela en appel de fonction sur l’instance BrowserTools.
Maintenant que l’agent peut appeler et exécuter des outils, il reste à construire l’interface utilisateur qui relie le tout.
Étape 7 : construire l’UI Streamlit
L’UI Streamlit transforme l’agent de navigation en application interactive. Elle recueille les informations de voyage, lance l’agent, diffuse les étapes intermédiaires et, au final, affiche un résumé ainsi qu’un tableau structuré des meilleures options de vol. Tout ce flux UI est implémenté dans app.py.
st.set_page_config(page_title="GLM-5 Browser Agent", page_icon="", layout="wide")
st.title("GLM-5-Turbo Real-Time Browser Agent")
st.subheader("Trip Request")
origin = st.text_input("Origin", value="SFO")
destination = st.text_input("Destination", value="JFK")
depart_date = st.date_input("Depart date")
return_date = st.date_input("Return date")
adults = st.number_input("Adults", min_value=1, max_value=9, value=1)
natural_request = st.text_area(
"Optional natural language request",
value="Find the cheapest reasonable flight options and summarize the best ones.",
height=120,
)
user_query = (
f"Find flight options from {origin} to {destination} for {adults} adult(s). "
f"Departure date: {depart_date}. Return date: {return_date}. "
f"User preference: {natural_request}"
)
run = st.button("Run browser agent", type="primary", use_container_width=True)
if run:
if not api_key:
st.error("Please enter an API key in the sidebar.")
st.stop()
llm = LLMClient(provider=provider, api_key=api_key, model=model)
event_box = st.container(border=True)
result_box = st.container(border=True)
all_events = []
with managed_browser(headless=headless) as browser:
agent = BrowserAgent(llm=llm, tool_runner=make_tool_runner(browser), max_steps=max_steps)
for event in agent.stream(user_query):
all_events.append(event)
with event_box:
if event["type"] == "status":
st.write(f"**Step {event['step']}** — {event['content']}")
elif event["type"] == "tool_call":
with st.expander(f"Step {event['step']} • Tool call: {event['tool']}", expanded=False):
st.json(event["arguments"])
elif event["type"] == "tool_result":
with st.expander(f"Step {event['step']} • Tool result: {event['tool']}", expanded=False):
st.json(event["result"])
elif event["type"] == "final":
st.success("Agent finished.")
final_event = next((e for e in reversed(all_events) if e["type"] == "final"), None)
final_text = final_event["content"] if final_event else ""
with result_box:
st.subheader("Final Answer")
parsed = parse_final_json(final_text)
if parsed:
st.markdown(render_final_markdown(parsed))
options_df = build_options_dataframe(parsed)
if not options_df.empty:
st.subheader("Top options table")
st.dataframe(
options_df,
use_container_width=True,
column_config={
"Link": st.column_config.LinkColumn("Link", display_text="Open source"),
},
)
else:
st.markdown(final_text)
Voici comment l’UI Streamlit réunit tous les composants :
- Les appels
st.set_page_config()etst.title()définissent la mise en page et le titre de l’application, tandis que les widgets d’entrée commest.text_input(),st.date_input(),st.number_input()etst.text_area()collectent les informations de voyage. - Lorsque l’utilisateur clique sur le bouton
Run browser agent, l’app vérifie d’abord la présence d’une clé API, puis initialise leLLMClient. Elle crée ensuite des conteneurs distincts pour les journaux intermédiaires de l’agent et les résultats finaux, et ouvre une session Playwright gérée. Dans cette session, leBrowserAgentcommence à diffuser des événements, et l’UI affiche chaque statut, appel d’outil et résultat d’outil. - Une fois l’agent terminé, l’app extrait l’événement final, analyse le JSON renvoyé et l’affiche. Le résumé est rendu via
render_final_markdown(), tandis quebuild_options_dataframe()convertit les options de vol structurées en un tableau avec des liens sources cliquables.
Avec cette étape, vous pouvez enregistrer le tout sous app.py et lancer l’app avec :
streamlit run app.py

Ce projet fonctionne bien en démonstration réelle car il utilise de vrais résultats de recherche et des appels d’outils. Cela reste toutefois un prototype. Les sites de vols sont dynamiques, les pages peuvent être bruyantes et certaines plateformes emploient des protections anti-bot.
La logique d’extraction actuelle est volontairement heuristique, ce qui garde le système simple et facile à comprendre.
Surtout, cette configuration ne se limite pas aux vols. Avec quelques ajustements — principalement des invites et de la logique d’extraction — vous pouvez réutiliser la même architecture pour créer des agents de shopping, de recherche, d’agrégation d’actualités, ou toute tâche nécessitant de parcourir et de résumer de l’information en direct.
Conclusion
Dans ce tutoriel, nous avons construit un agent de navigation en temps réel avec GLM-5-Turbo, Playwright et Streamlit. Le modèle joue le rôle de planificateur, les outils de navigation constituent la couche d’exécution, et l’UI transforme les résultats structurés en un format accessible.
À retenir : la valeur ne vient pas du modèle seul. Elle naît de la combinaison d’un modèle, d’une couche d’outils maîtrisée et d’une boucle d’agent.
Vous pouvez prolonger ce projet de plusieurs manières : améliorer l’extraction avec des parseurs spécifiques par site, étendre le système aux hôtels ou aux itinéraires, ou intégrer des préférences utilisateur comme les vols directs ou des compagnies privilégiées. De quoi transformer cette démo en véritable extension de navigateur.
FAQ sur GLM-5-Turbo
Ai-je besoin d’un GPU pour exécuter ce projet GLM-5-Turbo ?
Non. Ce projet ne nécessite pas de GPU car le modèle est accessible via API (Z.AI ou OpenRouter). Tous les calculs lourds s’exécutent sur l’infrastructure du fournisseur, tandis que votre machine locale gère uniquement l’UI et l’automatisation du navigateur.
Pourquoi utiliser un agent de navigation plutôt qu’une API de vols ?
Les API sont plus fiables et structurées, mais elles se limitent à des données prédéfinies et sont souvent payantes. Un agent de navigation, en revanche, peut :
- explorer plusieurs sources de manière dynamique ;
- s’adapter à des requêtes variées ;
- fonctionner sans API préintégrées.
Cependant, pour la production, une approche hybride (agent et API) est généralement la plus efficace.
Pourquoi l’agent échoue-t-il parfois sur certains sites ?
De nombreux sites de voyage utilisent :
- un rendu JavaScript dynamique ;
- des protections anti-bot ;
- des mises en page changeant fréquemment.
Comme cette démo repose sur une automatisation générique du navigateur et une extraction heuristique, elle peut ne pas fonctionner de manière constante sur tous les sites.
Pourquoi éviter de renvoyer du JSON comme sortie finale ?
Le JSON est utile en interne pour le débogage, le traitement structuré, le tri et le filtrage. En revanche, les utilisateurs finaux préfèrent des résumés lisibles. C’est pourquoi le système convertit les sorties structurées en un format convivial.
Je suis experte Google Developers en ML (Gen AI), triple experte Kaggle et ambassadrice Women Techmakers, avec plus de trois ans d’expérience dans la tech. J’ai cofondé une startup dans le domaine de la santé en 2020 et je poursuis actuellement un master en informatique à Georgia Tech, avec une spécialisation en apprentissage automatique.


