Interrompre un agent qui se répète
Certains agents restent bloqués à appeler sans cesse le même outil. Demandez à un agent de suivi de colis de « continuer à vérifier jusqu'à la livraison du colis », et il interroge le transporteur en boucle, alors que la réponse ne changera pas avant qu'il ne vous réponde.
Ce guide pratique ajoute un hook qui compte le nombre d'appels à chaque outil au cours d'un tour :
- Les deux premiers appels à un outil s'exécutent normalement.
- À partir du troisième appel, le hook bloque l'appel et indique au modèle de s'arrêter et de répondre.
- Si le modèle ignore trois de ces messages, le hook met fin au tour.
Le paramètre max_iterations d'un agent limite aussi la durée d'un tour, mais il
compte toutes les étapes et termine le tour avec stop_reason="iteration_limit". Ce hook limite les appels à chaque outil
séparément et explique pourquoi au modèle, ce qui permet au tour de se terminer par une réponse.
Le hook conserve ses compteurs dans des champs. Le SDK enregistre les champs entre les appels, les compteurs sont donc conservés.
Fonctionnalités présentées : hooks avec état,
pre_tool_call et
agents.skip, et interruption d'un tour depuis
pre_llm_call.
Exécution : placez les blocs de code des étapes 1 à 4 dans un même fichier, loop_breaker.py. Une fois le SDK
installé et MISTRAL_API_KEY définie, exécutez
python loop_breaker.py.
Fonctionnement
turn 1: track_parcel → runs calls = 1
track_parcel → runs calls = 2
track_parcel → blocked, with a message refusals = 1
... two more blocked calls ... refusals = 3
next model call → blocked: the turn ends with the hook's message
turn 2: track_parcel → runs: each turn starts a new count1. Simuler le transporteur
import asyncio
from pydantic import Field
from mistralai import agents
# --- The carrier, faked -------------------------------------------------------------------------
# In production, track_parcel calls the carrier's API. Here the parcel stays in transit, and
# TRACKED records every call that reached the carrier.
TRACKED: list[str] = []
@agents.tool
def track_parcel(tracking_number: str) -> dict[str, str]:
"""The carrier's latest scan of a parcel."""
print(f"carrier: tracked {tracking_number}")
TRACKED.append(tracking_number)
return {"tracking_number": tracking_number, "status": "in transit", "last_scan": "Lyon hub, 06:12"}2. Écrire le hook d'interruption des boucles
class StopRepeatedCalls(agents.Hook):
"""Refuse a tool once the turn has called it too often; end the turn if the model keeps trying."""
max_calls: int = 2 # calls allowed to each tool in one turn
max_refusals: int = 3 # refusals in one turn before the hook ends it
calls: dict[str, int] = Field(default_factory=dict) # this turn: tool id -> times called
refusals: int = 0 # this turn: calls refused
async def pre_agent_turn(self, ctx, request):
self.calls = {}
self.refusals = 0
async def pre_tool_call(self, ctx, call):
self.calls[call.tool_id] = self.calls.get(call.tool_id, 0) + 1
if self.calls[call.tool_id] <= self.max_calls:
return None
self.refusals += 1
print(f"loop breaker: refused {call.tool_id}, call {self.calls[call.tool_id]} of this turn")
return agents.skip(
f"You already called {call.tool_name} {self.max_calls} times in this turn, and its result will not "
"change before the turn ends. Do not call it again: answer with what you know."
)
async def pre_llm_call(self, ctx, invocation):
if self.refusals < self.max_refusals:
return None
print("loop breaker: ended the turn")
return agents.skip("I stopped checking: the status did not change. Ask me again later.")pre_agent_turns'exécute au début d'un tour. Il remet les compteurs à zéro, car une nouvelle question peut recevoir une nouvelle réponse.pre_tool_calls'exécute avant chaque appel d'outil. Il compte l'appel et le bloque dès que l'outil a été appelémax_callsfois. Le modèle reçoit le message à la place d'un résultat.pre_llm_calls'exécute avant chaque appel au modèle. Dès que le modèle a ignorémax_refusalsmessages, il met fin au tour. L'exécution renvoieCompletedavecstop_reason="skipped".
Les compteurs sont des champs, et non de simples attributs. Quand l'agent est déployé, chaque appel du hook peut s'exécuter dans un processus différent, et le SDK ne transmet que les champs d'un appel à l'autre.
3. Définir l'agent
tracker = agents.Agent(
model="mistral-large-4",
name="parcel-tracker",
instructions=(
"You track parcels with track_parcel. Answer in at most two sentences. You cannot check again later: say so."
),
tools=[track_parcel],
hooks=[StopRepeatedCalls()],
)4. Exécuter deux tours
def show(state: agents.TurnState[object]) -> None:
if isinstance(state, agents.Completed):
print(f"agent ({state.stop_reason or 'answered'}): {state.text}")
else:
print(f"agent: {type(state).__name__}")
async def main() -> None:
async with agents.local_runtime(): # at exit: every local session of the event loop stops
session = await tracker.start_session()
print("--- turn 1")
show(await session.run("Parcel FR-7731 is due today. Keep checking until it is delivered, then tell me."))
print("--- turn 2")
show(await session.run("Check FR-7731 once more."))
await session.cancel()
print(f"calls that reached the carrier: {len(TRACKED)}")
if __name__ == "__main__":
asyncio.run(main())Une exécution réelle a affiché ceci :
--- turn 1
carrier: tracked FR-7731
carrier: tracked FR-7731
loop breaker: refused client.track_parcel, call 3 of this turn
agent (answered): I can't keep checking — the tracking result for this turn won't change and I have no way to check again later. As of now, parcel FR-7731 is **in transit**, with its last scan at the Lyon hub at 06:12; it has not yet been delivered.
--- turn 2
carrier: tracked FR-7731
agent (answered): Parcel FR-7731 is still **in transit** — last scan at the Lyon hub at 06:12, not yet delivered.
calls that reached the carrier: 3Le hook a bloqué le troisième appel, et le modèle a répondu avec les informations disponibles. Le deuxième tour a commencé avec un compteur remis à zéro, son appel a donc pu s'exécuter.
Et après
agents.Hookdans la Référence API : champs, attributs privés et limites des valeurs enregistrées.- Progression en direct : un hook qui transmet chaque étape à votre application.