Hermes-agent:從原始碼看一個為 Production 設計的 Agent 系統
---
---
讓 Agent 跑起來不難——把 LLM API 包一層、加幾個工具、跑個 while loop,大概一個下午就能做出一個能用的東西。真正麻煩的問題,通常是後來才浮現的:token 費用怎麼控制?context 滿了怎麼辦?記憶系統怎麼讓 Agent 下次還記得上次的事?這些問題不在 demo 裡出現,但在真實使用裡是逃不掉的。
讀 hermes-agent 的原始碼,我發現這些問題幾乎每一個都有對應的設計——而且不是臨時補上去的,是從一開始就長在架構裡的。12155 行程式碼、40 個工具、8 種記憶體 Provider、13 個 Plugin 注入點,這些數字本身說明了一件事:hermes-agent 的作者在意的問題,比「讓 Agent 回答問題」要更難一層。
這篇我想拆開幾個關鍵的設計機制,試著解釋它的選擇背後在想什麼。
讀完這篇,你會理解:
- hermes-agent 的 AIAgent 作為 Harness 是怎麼組織的
- 為什麼 System Prompt 的「穩定性」是一個工程問題,不只是設計偏好
- Memory Fencing 是什麼,為什麼記憶召回之後不能直接放進 Context
- Context 壓縮是怎麼運作的——以及為什麼這是長期 Agent 的必要機制
- Skill 系統 + RL 訓練迴路:hermes-agent 最特別的設計
AIAgent:一個 Harness,不是一個 Chatbot
hermes-agent 的核心是一個叫做 AIAgent 的類別。它不是一個「對話機器人」,而是一個 Harness——它的職責是把所有東西包在一起,讓 Agent 能夠執行。
AIAgent 大概負責七件事:管理系統提示、維護對話狀態、呼叫 LLM(透過 Provider Layer)、執行工具、處理記憶體、管理 Context 壓縮、還有處理中斷訊號。這些事情放在一起,核心是一個 run_conversation() 方法,裡面跑一個 while loop。
每一輪迭代大概是這樣:組合這輪要送給 LLM 的訊息(含 cache marker)→ 呼叫 API(內建 retry 機制,出錯時分類後 rotate/retry)→ 把回應 normalize 成統一格式 → 有工具呼叫的話執行工具 → 沒有工具呼叫的話,這一輪結束,回傳最終回應。
工具執行這一塊,hermes-agent 把工具分成三類:絕對不能平行的、可以安全平行的、還有路徑相依的(例如同時修改同一個檔案會有問題)。這個分類本身不複雜,但它是明確的設計決策,而不是「先跑跑看」——最多 8 個 ThreadPoolExecutor worker,分類決定哪些可以同時跑。
Credential Pool 也在這一層管理:4 種策略(fill_first、round_robin、random、least_used),遇到 429 會進 cooldown,OAuth token 到期會自動 refresh。這些細節加在一起,是「讓 Agent 能夠 24/7 跑著」的基礎設施。
System Prompt 的穩定性問題
這個地方是我讀 hermes-agent 時覺得最有意思的設計決策之一。
大多數框架在每一輪對話都會重新組建 system prompt——工具清單、使用者偏好、目前任務狀態,塞進去,送出去。這樣做很直覺,但有一個代價:Anthropic 的 API 有一個叫做 prefix cache 的機制,意思是如果你送出去的 prompt 前綴和上一次完全一樣,API 就不需要重新處理那部分——可以省下 70-80% 的 input token 費用。只要 system prompt 每輪都不一樣,這個 cache 就完全沒用。
hermes-agent 的做法是:System Prompt 在 session 開始時 build 一次,存進資料庫,之後每一輪都直接讀出來用,永遠不重建。
這帶出了一個問題:如果不能改 system prompt,需要在對話過程中動態注入資訊(比如「這個使用者偏好用繁體中文」、「這個 session 有特殊的 tool policy」),要怎麼做?
答案是 Plugin Hooks。hermes-agent 有 13 個注入點分佈在 Agent lifecycle 的不同位置。其中 pre_llm_call 這個 hook 允許 Plugin 在每次呼叫 LLM 之前,把內容注入進使用者訊息裡——而不是 system prompt。位置的差異很重要:注入 user message 不會破壞 system prompt 的 prefix cache,注入 system prompt 會。
Memory 的三層結構與 Memory Fencing
hermes-agent 的記憶體系統分三層,從輕到重:
最輕的一層是 MEMORY.md,一個存在檔案系統上的 Markdown 文件,裡面是結構化的長期記憶,每次 session 開始會直接載入。第二層是 SQLite FTS5 全文搜尋,用來搜尋歷史對話,支援 BM25 排名,查不到的 CJK 場合 fallback 到 LIKE。第三層是外部 MemoryProvider——一個 ABC,允許接不同的 backend,每個 session 只能有一個外部 Provider。粗略分工:MEMORY.md 是「常駐的基本認識」,FTS5 是「可搜尋的事件記憶」,外部 Provider 是「特定應用需要的向量記憶或其他結構」。
這個三層架構本身不難理解,但有一個設計細節值得特別說:Memory Fencing。
當 Agent 從 memory 系統召回記憶,準備放進這一輪的 context 時,hermes-agent 會用一個 XML 標籤把召回的內容包起來:
[召回的記憶內容]
這個 fence 是語義上的隔離——告訴模型:這塊內容是「被召回的背景資訊」,不是使用者現在說的話。
舉個具體的情境:假設上次對話裡使用者說「我偏好簡短的回答」,這條記憶在這一輪被召回之後進入 context。如果沒有 fence,模型可能把它和目前訊息混淆,把舊的偏好當成「使用者剛才說的話」來解讀。有了 fence,模型能分清楚「這是我記得的事」和「這是使用者現在說的事」——兩件在語義上完全不同的事。
比較反直覺的是,這個帶著 fence 的 memory context 不會被 persist 到對話歷史裡。每次 session 都是重新召回、重新 fence、重新放進 context。原因有兩個:一是 fence 的內容如果被存進去,下一次 context 裡就會出現「我上次召回了哪些記憶」這件事,這不是真正的對話,累積多輪之後 context 裡會有很多這種召回殘留,開始影響模型的判斷;二是 fence 內容一旦改變,system prompt 的 prefix cache 就會失效——和 hermes-agent 整個 caching 策略是衝突的。
所以記憶這件事,hermes-agent 的態度是:召回、隔離、用完即走。
Context 壓縮:長期 Agent 的必要設計
Agent 跑得夠久,context window 一定會滿。hermes-agent 的 ContextEngine 是一個 ABC,策略可以替換,但預設實作是一個 4-phase 壓縮演算法,在 context 使用量到達 75% 時觸發。
壓縮的四個步驟大致是:先把 tool call 的結果截短(這些通常最冗長、保留價值最低)→ 找到一個合適的壓縮邊界(不要在對話中間切斷)→ 把要壓縮的段落送給 LLM 做摘要 → 用摘要替換原始內容,重新組合 context。
壓縮後的 session 會記錄一個 parent_session_id,指向壓縮前的 session。這讓 hermes-agent 可以追溯完整的對話歷史鏈,即使 context 已經被壓縮過很多次。
Skill 系統 + RL 訓練迴路
這個是 hermes-agent 和其他兩個框架差異最大的地方。
hermes-agent 有一個 Skill 系統:每個 skill 是一個 Markdown 文件(SKILL.md),包含 frontmatter(name、description、trigger 條件)和技能內容。這些 skill 存在一個 Skills Hub 裡(GitHub 來源),Agent 可以在對話過程中動態載入。Agent 有一個 skill nudge daemon,每 10 次工具呼叫會提示一次,看看有沒有適合當前任務的 skill 可以使用。
這個機制本身還算正常。真正不同的是下一層:Trajectory → RL 訓練迴路。
hermes-agent 會把對話軌跡(每一輪的輸入、工具呼叫、輸出、使用者反饋)存成 ShareGPT 格式。這些軌跡可以被 batch_runner 批次處理,然後送進 Atropos RL 訓練 pipeline,用來微調模型。
這個設計的意涵是:hermes-agent 不只是一個讓 Agent 完成任務的框架,它還是一個資料收集系統。每一次使用,都在累積未來改善模型的訓練資料。Skill 系統決定了 Agent 現在能做什麼,RL 迴路決定了 Agent 未來會變成什麼。
這些設計在說什麼
拆開這幾個機制之後,hermes-agent 的設計哲學大概是這樣:系統的每一層都在為「長期穩定運作」和「持續改善」服務。System Prompt 穩定性是為了讓 token cost 可控;Memory Fencing 是為了讓記憶系統不污染 context;Context 壓縮是為了讓長對話不崩潰;RL 迴路是為了讓 Agent 越用越好。
這些選擇放在一起,讀起來更像是一個有一定 production 運作經驗的人,把踩過的坑轉成了架構上的防線。
> 結語:hermes-agent 最讓我印象深刻的不是某個單一機制,而是這些機制加在一起,形成了一種一致的設計意圖——讓 Agent 不只是能跑,而是跑得久、跑得穩、然後越跑越好。