Boucle principale de l'agent
Responsabilité
run_conversation est le cœur d'Hermes : recevoir un message utilisateur, appeler le modèle et les outils (tools) en boucle jusqu'à épuisement du budget (budget) d'itération (iteration) ou achèvement de la tâche. Elle coordonne également la construction du contexte (context), la distribution (dispatch) des outils, l'évaluation des conditions d'arrêt et le repli (fallback) en cas d'échec.
Le fichier compte environ 5800 lignes, à forte densité logique — cette page ne couvre que la ligne principale, les détails vont dans les sous-pages.
Fichiers clés
entrée run_conversation:588-720— signature, initialisation et préparation avant boucleboucle principale while:721-760— conditionapi_call_count < agent.max_iterations and agent.iteration_budget.remaining > 0, inclut le grace callsous-boucle de retry API:1227-1280—while retry_count < max_retriesbuild_turn_context— assemblage du contexte de chaque tour (turn)prompt_builder— assemblage du prompt systèmemoa_loop— chemin d'agrégation Mixture-of-Agentscontext_compressor— compression (compression) de l'historique au seuilIterationBudget— cœur du comptage de budget
Flux de données
run_agent.py AIAgentest un transmetteur léger ; l'injection réelle des dépendances se fait dansinit_agent.- L'appelant passe un
user_message, etrun_conversationpasse d'abord parbuild_turn_contextpour assembler le contexte de ce tour (api_messages). - Entrée dans la boucle
whileprincipale : check d'interruption → décrément du budget (iteration_budget.consume()) → construction/nettoyage desapi_messages→ pré-compression → drain du texte/steer→ agrégation MoA → entrée dans la sous-boucle de retry d'appel API. - Au retour du modèle : validation de la réponse et traitement du
finish_reason; en cas d'échec, appel àagent._try_activate_fallback(). - Au seuil de compression, passage par
context_compressorpour récupérer de l'espace. - À la sortie de la boucle (budget épuisé / tâche achevée / interruption), renvoie le message final, délivré par la couche passerelle (gateway) (voir Couche passerelle).
L'aspect réel de la boucle principale est dans agent/conversation_loop.py:721-740 :
while (api_call_count < agent.max_iterations and agent.iteration_budget.remaining > 0) or agent._budget_grace_call:
agent._checkpoint_mgr.new_turn()
if agent._interrupt_requested:
interrupted = True
_turn_exit_reason = "interrupted_by_user"
break
api_call_count += 1
if agent._budget_grace_call:
agent._budget_grace_call = False
elif not agent.iteration_budget.consume():
_turn_exit_reason = "budget_exhausted"
breakLe or agent._budget_grace_call greffé sur la condition de boucle est le point d'appui « une fois le budget à zéro, accorder une dernière chance » : en entrant dans ce tour, on clearing le flag immédiatement, donc « une chance » ne fait vraiment qu'un.
Le retry d'appel API n'est pas récursif, c'est un while retry_count < max_retries intérieur. Voici le début dans agent/conversation_loop.py:1227-1245 :
while retry_count < max_retries:
if agent.provider == "nous":
try:
from agent.nous_rate_guard import nous_rate_limit_remaining
if nous_rate_limit_remaining() is not None and nous_rate_limit_remaining() > 0:
if agent._try_activate_fallback():
active_system_prompt = _sync_failover_system_message(
agent, api_messages, active_system_prompt)
retry_count = 0
continue
return {"final_response": "⏳ rate-limited, no fallback.",
"messages": messages, "completed": False, "failed": True}
except Exception:
pass # Never let rate guard break the agent loopAprès le changement de provider, retry_count = 0 repart d'un état propre pour réessayer. Le except Exception: pass au niveau le plus externe est délibéré : le guard est une optimisation, il ne doit jamais en retourter casser la boucle principale.
Mot de conception
Pourquoi le retry en sous-boucle plutôt qu'en récursion ? La récursion en Python mange la pile d'appels (profondeur limitée), et l'état d'un échec s'éparpille sur plusieurs frames de pile, difficile à nettoyer uniformément. Avec while retry_count < max_retries, tout l'état de retry (retry_count, compression_attempts) est local à une seule frame ; au bascule de fallback, un seul retry_count = 0 réinitialise tout.
Pourquoi un grace call ? Le budget est un plafond dur, mais le modèle est parfois à un ou deux appels d'outils de finir, et le bloquer net perd le travail déjà fait. _budget_grace_call est positionné par la couche interne quand elle juge « presque fini » ; le or externe laisse ce tour courir, mais le flag est clearing dès l'entrée — « une chance » ne fait vraiment qu'un.
Pourquoi un objet iteration_budget plutôt qu'un simple compteur ? Le budget est accédé depuis plusieurs threads (sous-agent, refund de compression) ; IterationBudget enveloppe consume / refund d'un lock, garantissant la sécurité concurrente tout en laissant le compresseur appeler refund() pour rendre des itérations après avoir retiré des anciens messages. Un simple entier ne fait ni l'un ni l'autre.
Limites et échecs
- Budget épuisé en cours de route : avant de sortir,
_persist_session(messages, conversation_history)persiste la session (session) courante ;_turn_exit_reasonest marqué"budget_exhausted"pour que la couche supérieure distingue fin normale et troncature. - Exception d'appel d'outil : une erreur côté outil est attrapée par la couche interne de retry ; après
max_retries, appel àagent._try_activate_fallback(); si la chaîne de fallback est vide, on renvoie un dict résultatfailed: Trueplutôt que de lever — la passerelle supérieure ne voit qu'un échec structuré. - Le guard de rate-limit lui-même plante : un import
nous_rate_guardqui échoue ou une exception à l'appel est attrapé parexcept Exception; le guard est dégradé en « inopérant », la boucle principale continue son chemin normal. Le commentaire est explicite : "Never let rate guard break the agent loop". - Refresh infini du pool OAuth :
_auth_pool_refresh_countsest réinitialisé en début de tour, pour empêcher un pool OAuth à entrée unique de « réussir » indéfiniment viatry_refresh_current()face à des 401 continus (filet de#26080).
Résumé
La boucle principale ne fait qu'orchestrer : pilotage par tour, gestion du budget, retry et repli. La vraie capacité vient des outils de la couche capacités et des compétences, la livraison multiplateforme est déléguée à la couche passerelle, et l'auto-évolution est portée par la boucle d'apprentissage.
Pour une introduction officielle en chinois, voir README.zh-CN et website/i18n/zh-Hans/.