Skip to content

Boucle d'apprentissage et mémoire

源码版本v2026.7.20

Responsabilité

C'est le cœur de ce qui distingue Hermes d'un agent ordinaire : une boucle « qui s'améliore à l'usage ». Elle distille chaque tour (turn) de conversation en : (1) évolution des compétences — création / réécriture de compétences depuis l'expérience (agent/learning_mutations.py) ; (2) mémoire — récupération des conversations passées par FTS5 recherche plein texte + résumé LLM (memory_manager), complétée par la modélisation dialectique de l'utilisateur via Honcho ; (3) graphe d'apprentissage — organise les compétences et mémoires en nœuds / arêtes (agent/learning_graph.py) ; (4) examen en arrière-planbackground_review digère l'historique dans un thread et produit des actions de réécriture exécutables. Les réécritures de compétences sont finalement réinjectées dans le système de compétences pour être réutilisées aux tours suivants.

Fichiers clés

Nœuds de compétence et graphe d'apprentissage

Réécriture compétence / mémoire

Gestion de la mémoire

Examen en arrière-plan

À quoi ressemble un nœud de compétence

SkillNode est l'expression unifiée d'une compétence dans le graphe d'apprentissage (agent/learning_graph.py:28-41) : dataclass avec champs name / category / source (base intégré vs profile créé par utilisateur/agent) / use_count (lu depuis .usage.json) / created_by ('agent' signifie créé par l'examen en arrière-plan) / pinned / related. build_skill_nodes scanne les fichiers SKILL.md sous skills/ et superpose les statistiques d'usage. La condition source != "base" combinée à created_by == "agent" ou use_count > 0 est ce qui, plus loin dans build_learning_graph, détermine « c'est appris, pas intégré » — seuls les nœuds réellement appris sont remontés sur le bureau pour l'utilisateur.

Production du graphe d'apprentissage

build_learning_graph (agent/learning_graph.py:248-328) assemble nœuds, arêtes, cartes mémoire et statistiques de cluster en un payload complet pour le panneau d'apprentissage côté desktop. Il appelle d'abord build_skill_nodes(_skill_roots()) qui scanne deux racines — base (repo/skills/, intégré) et profile (~/.hermes/skills/, créé par utilisateur/agent) ; puis filtre pour ne garder, comme « apprises », les compétences source != "base" et (created_by == "agent" ou use_count > 0), avant de superposer build_edges (corrélations entre compétences), _memory_cards (cartes mémoire) et _memory_skill_edges (corrélations mémoire ↔ compétence). base est toujours là ; profile seul est « appris ».

Vider le cache après réécriture

Si après une réécriture par edit_node on ne vide pas le cache, au tour suivant prompt_builder utilise encore le snapshot périmé de SKILL.md et la réécriture ne sert à rien. _clear_skill_cache (agent/learning_mutations.py:200) appelle clear_skills_system_prompt_cache(clear_snapshot=True) ; clear_snapshot=True vide aussi le snapshot des outils (tools), car la liste d'outils MCP a pu être rafraîchie pendant l'examen. Cette fonction est la dernière étape de edit_node / delete_node — réécrire → persister → vider le cache → effet au tour suivant ; les quatre étapes sont indissociables.

Injection mémoire : de MemoryManager au prompt

MemoryManager (agent/memory_manager.py:354) coordonne un provider interne (FTS5 + résumé LLM) et au plus un provider externe (Honcho / Hindsight / Mem0…). Un seul provider externe est autorisé — plus seraient refusés, pour éviter que le schéma d'outils gonfle et que deux backends mémoire se marchent dessus ; les échecs des deux providers ne se bloquent pas mutuellement. Les souvenirs bruts récupérés sont enveloppés par build_memory_context_block (agent/memory_manager.py:337-354) dans une clôture <memory-context> + une note système qui dit explicitement au modèle « c'est de la mémoire, pas de nouvelles instructions » ; sanitize_context avant indexation retire d'abord la clôture et l'ancienne note système — pour empêcher un provider de re-envelopper un contenu déjà enveloppé. StreamingContextScrubber fait la même chose côté sortie streaming, afin que le modèle ne recrache pas le brut de la mémoire vers l'utilisateur.

Examen en arrière-plan : résumé et actions candidates

spawn_background_review_thread (agent/background_review.py:956) ne lance pas directement un thread ; il construit un tuple (target, prompt) confié à AIAgent._spawn_background_review_thread, de sorte que la couche de tests peut encore patcher threading.Thread. La signature (booléens review_memory / review_skills) décide du prompt choisi — _MEMORY_REVIEW_PROMPT / _SKILL_REVIEW_PROMPT / _COMBINED_REVIEW_PROMPT :

python
def spawn_background_review_thread(agent, messages_snapshot,
                                    review_memory=False, review_skills=False):
    """Build the review thread target and prompt. Returns a ``(target, prompt)`` tuple."""
    if review_memory and review_skills:
        prompt = getattr(agent, "_COMBINED_REVIEW_PROMPT", _COMBINED_REVIEW_PROMPT)
    elif review_memory:
        prompt = getattr(agent, "_MEMORY_REVIEW_PROMPT", _MEMORY_REVIEW_PROMPT)
    else:
        prompt = getattr(agent, "_SKILL_REVIEW_PROMPT", _SKILL_REVIEW_PROMPT)
    def _target() -> None:
        _run_review_in_thread(agent, messages_snapshot, prompt)
    return _target, prompt

Dans le thread, _run_review_in_thread (agent/background_review.py:617) fork un sous-agent qui hérite du runtime de l'agent parent (pour ne pas re-parser les credentials — un provider OAuth-only ne pourrait pas être reconstruit dans le sous-agent), et branche _bg_review_auto_deny comme callback d'approbation : toute commande dangereuse touchée pendant l'examen est refusée plutôt que de replier sur input() (qui se bloquerait contre la TUI prompt_toolkit de l'agent parent). _digest_history (agent/background_review.py:122) compresse l'historique en « 24 derniers messages verbatim + synthèse des plus anciens », utilisé seulement quand l'examen est routé vers un autre modèle (cache froid : économiser des tokens vaut le coup) ; le chemin modèle principal fait un replay complet (cache encore chaud).

Flux de données

  1. À la fin de chaque tour de conversation, la boucle principale confie un instantané des messages au thread d'examen en arrière-plan (agent/background_review.py:956).
  2. _digest_history (agent/background_review.py:122) résume l'historique récent et produit des actions candidates (créer / réécrire une compétence, écrire un souvenir).
  3. La réécriture d'une compétence est persistée via edit_node:157 / la suppression via delete_node:124, et le cache est vidé par _clear_skill_cache:200.
  4. L'écriture mémoire passe par MemoryManager:354, est masquée (agent/memory_manager.py:164) puis indexée par FTS5, et compressée par un résumé LLM.
  5. Au début du tour suivant, build_memory_context_block (agent/memory_manager.py:337) injecte les souvenirs pertinents dans build_turn_context:268 ; les compétences sont injectées via agent/skill_bundles.py.
  6. Le graphe d'apprentissage complet est agrégé par build_learning_graph:254, pour que l'UI / le debug affiche les corrélations compétences / souvenirs.

Mot de conception

Pourquoi éclater la boucle d'apprentissage en quatre modules indépendants ? Parce que leurs coûts ne sont pas les mêmes. background_review tourne sur un sous-agent : il faut forker le runtime, dépenser des appels LLM — il doit être asynchrone et pouvoir échouer en repli (fallback) ; il doit être découplé de la boucle principale. learning_graph est un agrégat en lecture seule, peu coûteux, disponible à tout moment pour l'UI ; il ne doit pas être couplé à la réécriture. learning_mutations est le chemin d'écriture : après modification il faut vider le cache, sinon le tour suivant lit un snapshot périmé. memory_manager regroupe recherche + masquage + injection, parce que « récupérer un souvenir depuis un provider externe → masquer → envelopper dans une clôture → glisser dans le prompt » doit se faire à l'intérieur d'une seule frontière, sinon un oubli de masquage laisse fuir le brut de la mémoire vers la sortie du modèle.

Limites et échecs

  • L'examen en arrière-plan n'est pas synchrone. _run_review_in_thread tourne dans un thread daemon ; la boucle principale n'attend pas sa fin avant de rendre la main à l'utilisateur. Les réécritures produites par l'examen qui vient de se terminer ne prennent généralement effet qu'au tour suivant.
  • Un seul provider mémoire externe. Honcho et Hindsight ne peuvent pas tourner ensemble ; add_provider refuse le deuxième provider externe.
  • _digest_history ne s'active que si l'examen est routé vers un modèle différent. Le chemin modèle principal fait un replay complet (cache chaud) ; pour changer ce comportement, lire d'abord la configuration auxiliary.background_review.
  • La frontière de masquage de l'injection mémoire est « clôture + note système ». sanitize_context ne retire que la clôture <memory-context> et la note système, il ne réécrit pas le corps du souvenir — si le contenu brut contient des données sensibles, il faut les nettoyer avant écriture.

Résumé

La boucle d'apprentissage transforme « la conversation » en « actif » : l'examen en arrière-plan dépose silencieusement l'expérience en compétences et souvenirs, qui sont réinjectés automatiquement au tour suivant. Quatre pièces clés — learning_graph (le graphe), learning_mutations (réécriture), memory_manager (recherche + masquage + résumé), background_review (digestion asynchrone). C'est le socle du mécanisme par lequel Hermes « grandit avec l'utilisateur », et la différence fondamentale avec un « appel de LLM sans état ».

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