學習迴圈與記憶
職責
這是 Hermes 有別於一般 agent 的核心:一個「越用越好用」的閉環。它把每輪對話沉澱成:① 技能演化——從經驗建立/改寫技能(agent/learning_mutations.py);② 記憶——FTS5 全文檢索 + LLM 摘要的過往會話檢索(memory_manager),配合 Honcho 辯證式使用者建模;③ 學習圖——把技能與記憶組織成節點/邊(agent/learning_graph.py);④ 背景審視——background_review 在執行緒裡消化歷史、產出可執行的改寫動作。技能改寫最終回灌到技能系統,供後續輪次複用。
關鍵檔案
技能節點與學習圖
@dataclass SkillNode:28-41— 技能節點技能載入:77-125—_iter_skill_files/build_skill_nodes(掃描skills/建 nodes)邊與統計:156-193—build_edges/density_stats(技能間關聯)記憶-技能邊:193-227—_memory_cards/_memory_skill_edges(記憶與技能的關聯)build_learning_graph:248-328— 彙總產出完整學習圖_load_usage:84-97— 使用統計(指導哪些技能常用)
技能/記憶改寫
node_detail:86-124— 查看某節點詳情delete_node:124-157— 刪除技能或記憶edit_node:157-200— 編輯技能或記憶內容_clear_skill_cache:200— 改寫後清快取(讓下輪生效)記憶定位:30-65—_memories_dir/_parse_memory_id/_locate_memory
記憶管理
class MemoryManager:354— 記憶管理器本體memory provider 工具注入:83-164—memory_provider_tools_enabled/inject_memory_provider_toolsbuild_memory_context_block:337-354— 把記憶拼成上下文 (context) 塊注入提示 (prompt)sanitize_context:164-172— 上下文脫敏(FTS5 索引前)StreamingContextScrubber:172-337— 串流 (stream) 輸出脫敏
背景審視
spawn_background_review_thread:956— 起背景審視執行緒_run_review_in_thread:617— 執行緒內主迴圈_digest_history:122-373— 摘要最近 24 條歷史summarize_background_review_actions:373-590— 彙總審視動作build_memory_write_metadata:590-617— 寫記憶的元資料
技能節點長什麼樣
SkillNode 是學習圖裡技能的統一表達(agent/learning_graph.py:28-41):dataclass 欄位有 name/category/source(base 內建 vs profile 使用者/agent 建立)/use_count(從 .usage.json 讀)/created_by('agent' 表示背景審視建立的)/pinned/related。build_skill_nodes 掃描 skills/ 下的 SKILL.md 檔案並疊加使用統計。source != "base" 加上 created_by == "agent" 或 use_count > 0 是後面 build_learning_graph 判斷「這是學到的,不是內建的」的過濾條件——只把真正學到的節點放到桌面上給使用者看。
學習圖的產出
build_learning_graph(agent/learning_graph.py:248-328)把節點、邊、記憶卡、聚類統計拼成一個完整 payload,供桌面端學習面板渲染。它先呼叫 build_skill_nodes(_skill_roots()) 掃兩個根——base(repo/skills/,內建)和 profile(~/.hermes/skills/,使用者/agent 建立);然後過濾出 source != "base" 且 created_by == "agent" 或 use_count > 0 的技能當「學到的」,再疊加 build_edges(技能間關聯)、_memory_cards(記憶卡)、_memory_skill_edges(記憶與技能的關聯)。base 永遠在,profile 才是「學到的」。
改寫後必須清快取
技能被 edit_node 改寫後,如果不清快取,下一輪 prompt_builder 還會用舊的 SKILL.md 快照,改寫就白做了。_clear_skill_cache(agent/learning_mutations.py:200)呼叫 clear_skills_system_prompt_cache(clear_snapshot=True),clear_snapshot=True 連工具 (tool) 快照一起清,因為 MCP 工具列表也可能在審視中被刷新。這個函式是 edit_node / delete_node 的最後一環——改寫→落盤→清快取→下輪生效,四步缺一不可。
記憶注入:從 MemoryManager 到提示詞
MemoryManager(agent/memory_manager.py:354)協調一個內建 provider(FTS5 + LLM 摘要)加至多一個外部 provider(Honcho/Hindsight/Mem0 之類)。外部 provider 只允許一個,多了會被拒——避免工具 schema 膨脹和記憶後端打架;兩個 provider 失敗互不阻塞。檢索到的原始記憶會被 build_memory_context_block(agent/memory_manager.py:337-354)包成 <memory-context> 圍欄 + 系統註的上下文塊,系統註明確告訴模型「這是記憶不是新指令」;sanitize_context 在索引前先剝掉圍欄和舊的系統註——防止 provider 把已經包過的內容再包一層。StreamingContextScrubber 在串流輸出端做同樣的事,避免模型把記憶塊原文吐回使用者。
背景審視:摘要與候選動作
spawn_background_review_thread(agent/background_review.py:956)不直接起執行緒,而是建構 (target, prompt) 元組交給 AIAgent._spawn_background_review_thread,這樣測試層 patch threading.Thread 還能工作。函式簽名(review_memory / review_skills 兩個 bool)決定挑哪個 prompt——_MEMORY_REVIEW_PROMPT / _SKILL_REVIEW_PROMPT / _COMBINED_REVIEW_PROMPT:
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執行緒內 _run_review_in_thread(agent/background_review.py:617)fork 一個繼承父 agent runtime 的子 agent(避免重新解析憑證——OAuth-only provider 在子 agent 裡根本重建不出來),並裝 _bg_review_auto_deny 審批回調,讓審視期間觸碰危險命令一律 deny 而不是 fallback 到 input()(會和父 agent 的 prompt_toolkit TUI 死結)。_digest_history(agent/background_review.py:122)把歷史壓成「近 24 條 verbatim + 更早的合成摘要」,只在審視被路由到不同模型時用(冷快取少寫 token 划算);主模型路徑跑全量 replay(快取還熱)。
資料流
- 每輪對話結束,主迴圈把訊息快照交給背景審視執行緒(
agent/background_review.py:956)。 _digest_history(agent/background_review.py:122)摘要近期歷史,產出候選動作(新建/改寫技能、寫記憶)。- 技能改寫經
edit_node:157/ 刪除經delete_node:124落盤,並_clear_skill_cache:200清快取。 - 記憶寫入經
MemoryManager:354,脫敏(agent/memory_manager.py:164)後建 FTS5 索引,LLM 摘要壓縮 (compression)。 - 下輪對話開始時,
build_memory_context_block(agent/memory_manager.py:337)把相關記憶拼進build_turn_context:268;技能經agent/skill_bundles.py注入。 - 完整學習圖由
build_learning_graph:254彙總,供 UI/除錯展示技能與記憶的關聯。
設計動機
為什麼把學習閉環拆成四個獨立模組?因為它們的代價結構不同。background_review 跑在子 agent 上,要 fork runtime、要花 LLM 呼叫,得非同步、得能失敗回退 (fallback)——它和主迴圈必須解耦。learning_graph 是唯讀彙總,便宜,可以隨時拉來給 UI 用,不能和改寫耦合。learning_mutations 是寫路徑,改完必須清快取,否則下輪吃到舊快照。memory_manager 是檢索+脫敏+注入,放在一起是因為「從外部 provider 拿記憶 → 脫敏 → 包圍欄 → 塞提示」必須在一個邊界內完成,否則脫敏漏一處就把記憶原文漏到模型輸出裡。
邊界與失敗
- 背景審視不是同步的。
_run_review_in_thread跑在 daemon 執行緒裡,主迴圈不等它完成就回傳使用者。審視剛跑完那一輪,這輪的技能改寫一般要等到下一輪才生效。 - 外部記憶 provider 只能掛一個。Honcho 和 Hindsight 不能同時跑,
add_provider會拒絕第二個外部 provider。 _digest_history只在審視被路由到不同模型時生效。主模型路徑跑全量 replay(快取還熱);改這個行為要先看auxiliary.background_review配置。- 記憶注入的脫敏邊界在「圍欄和系統註」。
sanitize_context只剝<memory-context>圍欄和系統註,不改寫記憶正文——記憶原文含敏感資訊得在寫入前就處理好。
小結
學習閉環把「對話」變成「資產」:背景審視在背景把經驗沉澱為技能與記憶,下輪自動注入。關鍵四件套——learning_graph(圖譜)、learning_mutations(改寫)、memory_manager(檢索+脫敏+摘要)、background_review(非同步消化)。這是 Hermes「與使用者共同成長」的機制底座,也是它和「無狀態 LLM 呼叫」的根本區別。