Skip to content

Boucle principale de l'agent

源码版本v2026.7.20

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

Flux de données

  1. run_agent.py AIAgent est un transmetteur léger ; l'injection réelle des dépendances se fait dans init_agent.
  2. L'appelant passe un user_message, et run_conversation passe d'abord par build_turn_context pour assembler le contexte de ce tour (api_messages).
  3. Entrée dans la boucle while principale : check d'interruption → décrément du budget (iteration_budget.consume()) → construction/nettoyage des api_messages → pré-compression → drain du texte /steer → agrégation MoA → entrée dans la sous-boucle de retry d'appel API.
  4. Au retour du modèle : validation de la réponse et traitement du finish_reason ; en cas d'échec, appel à agent._try_activate_fallback().
  5. Au seuil de compression, passage par context_compressor pour récupérer de l'espace.
  6. À 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 :

python
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"
        break

Le 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 :

python
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 loop

Aprè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_reason est 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ésultat failed: True plutô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_guard qui échoue ou une exception à l'appel est attrapé par except 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_counts est réinitialisé en début de tour, pour empêcher un pool OAuth à entrée unique de « réussir » indéfiniment via try_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/.

Site d'apprentissage communautaire non officiel. Basé sur le code source de NousResearch/hermes-agent (licence MIT).