動 Skill 自動進化)
1. 從一次 K8s 部署翻車說起Hermes Agent 的 Self-Improving 到底在解決什么如果你用過 OpenClaw 這類 Agent 框架大概率經(jīng)歷過這樣的場景第一次讓它部署一個 Flask 應(yīng)用到 K8s它踩了 ImagePullBackOff 的坑你手動糾正了第二次換個項目再部署它把同樣的坑又踩了一遍。Skill 是手寫的 Markdown 文件你不寫它就不會你寫了它也不會自己更新。Hermes Agent 在 OpenRouter 排行榜上增速 204%Top Coding Agents 排第一GitHub 從 0 到 106k Star靠的不是功能堆疊而是設(shè)計哲學(xué)上的分水嶺——它讓 Agent 干完活之后自動把踩坑經(jīng)驗提煉成可復(fù)用的 Skill下次遇到同類問題直接調(diào)用。用得越久能力越強。這篇文章拆解 Hermes Agent 源碼中 Self-Improving 與 Skill 模塊的協(xié)作鏈路給出可復(fù)制的源碼閱讀路徑和關(guān)鍵函數(shù)調(diào)用鏈并演示一次完整的 Skill 迭代驗證動作。適合想理解 Agent 自進化原理的開發(fā)者也適合正在選型 Agent 框架的技術(shù)負(fù)責(zé)人。三個子系統(tǒng)撐起一個閉環(huán)Memory 是助手隨身帶的小本子記著老板喜歡喝美式這些事實Skill 是助手積累的操作手冊——部署 K8s 第 2 步一定要先推鏡像Nudge Engine 是定時響的鬧鐘提醒助手回頭想想有沒有什么值得記的。三者協(xié)作構(gòu)成一個完整的反饋回路。源碼倉庫地址github.com/NousResearch/hermes-agent。下面按模塊拆解每一步都給出文件路徑和行號區(qū)間你可以直接對照閱讀。2. Memory 系統(tǒng)源碼拆解兩個文件如何定義 Agent 對你的全部認(rèn)知Memory 系統(tǒng)設(shè)計得很克制——兩個純文本文件用§分隔條目~/.hermes/memories/ ├── MEMORY.md # Agent 的個人筆記環(huán)境事實、項目約定、工具怪癖 └── USER.md # Agent 對用戶的認(rèn)知偏好、溝通風(fēng)格、工作習(xí)慣字符上限故意設(shè)得很緊MEMORY 限 2200 charsUSER 限 1375 chars。容量有限就迫使 Agent 挑重要的記不重要的自然被擠掉。對比 OpenClaw——它的 MEMORY.md 是純追加模式用幾個月就膨脹成幾萬行的怪獸文件找?guī)讉€月前的一句話只能笨拙地通讀全文。Hermes 的做法反過來容量有限就倒逼 Agent 做信息壓縮過時的自然被擠掉留下的都是高密度事實。具體實現(xiàn)上MemoryStore維護兩組平行狀態(tài)——實時可寫的條目列表和會話開始時凍結(jié)的快照# tools/memory_tool.py:116-122 class MemoryStore: def __init__(self, memory_char_limit2200, user_char_limit1375): self.memory_entries: List[str] [] self.user_entries: List[str] [] self.memory_char_limit memory_char_limit self.user_char_limit user_char_limit self._system_prompt_snapshot: Dict[str, str] {memory: , user: }但設(shè)了上限只是第一步關(guān)鍵是超限之后怎么處理。Hermes 不會靜默丟棄舊條目也不會自動壓縮——它選擇讓add直接失敗然后把當(dāng)前所有條目返回給模型# tools/memory_tool.py:248-259 if new_total limit: current self._char_count(target) return { success: False, error: ( fMemory at {current:,}/{limit:,} chars. fAdding this entry ({len(content)} chars) would exceed the limit. fReplace or remove existing entries first. ), current_entries: entries, usage: f{current:,}/{limit:,}, }錯誤信息里一句 Replace or remove existing entries first 就把模型引導(dǎo)到了replace和remove操作上。同時返回current_entries讓模型能看到現(xiàn)有的所有條目自己決定哪些過時了該刪、哪些可以合并壓縮。模型不是被動地執(zhí)行淘汰規(guī)則而是主動做信息整理——這本身就是一次自我反思。凍結(jié)快照機制也值得注意。每次會話啟動時Memory 加載后立刻捕獲一份快照之后系統(tǒng)提示詞里用的都是這份快照# tools/memory_tool.py:124-140 def load_from_disk(self): mem_dir get_memory_dir() self.memory_entries self._read_file(mem_dir / MEMORY.md) self.user_entries self._read_file(mem_dir / USER.md) # 會話開始時凍結(jié)快照之后不再變動 self._system_prompt_snapshot { memory: self._render_block(memory, self.memory_entries), user: self._render_block(user, self.user_entries), }快照注入系統(tǒng)提示詞后Agent 還沒看到用戶消息就已經(jīng)知道你的環(huán)境和偏好了。為什么凍結(jié)而不是實時更新因為系統(tǒng)提示詞會話內(nèi)不變就能共享前綴緩存Prefix Cache省掉重復(fù)計費。新寫入的內(nèi)容只改磁盤下一個會話才刷新進來。提示詞引導(dǎo)方面系統(tǒng)提示詞中的MEMORY_GUIDANCE明確了什么該記、什么不該記# agent/prompt_builder.py:144-162 MEMORY_GUIDANCE ( You have persistent memory across sessions. Save durable facts using the memory tool: user preferences, environment details, tool quirks, and stable conventions.\n Prioritize what reduces future user steering — the most valuable memory is one that prevents the user from having to correct or remind you again.\n Write memories as declarative facts, not instructions to yourself. User prefers concise responses ? — Always respond concisely ?. Project uses pytest with xdist ? — Run tests with pytest -n 4 ?. )注意這里的區(qū)別Memory 要求寫成聲明式事實User prefers concise responses而不是命令式指令A(yù)lways respond concisely。前者是偏好可以被當(dāng)前上下文覆蓋后者是死命令會限制 Agent 的靈活性。Tool Schema 里還有一句關(guān)鍵的邊界規(guī)則If youve discovered a new way to do something, save it as a skill. —— Memory 不存操作步驟操作步驟歸 Skill 管。這一句話把兩個系統(tǒng)的分工畫清了。3. Skill 模塊源碼拆解從 SKILL.md 結(jié)構(gòu)到自動創(chuàng)建觸發(fā)條件Memory 是我知道什么Skill 是我會做什么。每個 Skill 是一個目錄核心是 SKILL.md 文件~/.hermes/skills/ ├── devops/ │ └── flask-k8s-deploy/ │ ├── SKILL.md # 主指令 │ ├── references/ # 參考文檔 │ └── templates/ # 模板文件 └── software-development/ └── fix-pytest-fixtures/ └── SKILL.md一個典型的 SKILL.md--- name: flask-k8s-deploy description: Deploy a Flask app to Kubernetes with health checks version: 1.0.0 --- # Flask K8s Deployment ## When to use - User wants to deploy a Flask/Python app to Kubernetes - User mentions K8s, kubectl, or container deployment ## Steps 1. Create Dockerfile with gunicorn (not dev server) 2. Build and push image to registry BEFORE creating deployment 3. Write deployment.yaml with livenessProbe pointing to /health 4. Write service.yaml with correct port mapping 5. kubectl apply both files 6. Verify with kubectl get pods and kubectl logs ## Pitfalls - MUST push image to registry before kubectl apply, otherwise ImagePullBackOff - Flask 默認(rèn)沒有 /health 端點需要手動添加 - Django 需要額外設(shè)置 ALLOWED_HOSTS 環(huán)境變量 - livenessProbe path 必須返回 200不能用需要認(rèn)證的路徑Pitfalls 這一節(jié)不是預(yù)先寫好的而是 Agent 踩坑后追加的——這就是 Skill 層面的self-improving。Agent 不需要用戶說幫我創(chuàng)建一個 Skill。驅(qū)動力來自skill_manage工具的 schema# tools/skill_manager_tool.py:681-701 SKILL_MANAGE_SCHEMA { name: skill_manage, description: ( Manage skills (create, update, delete). Skills are your procedural memory — reusable approaches for recurring task types.\n\n Create when: complex task succeeded (5 calls), errors overcome, user-corrected approach worked, non-trivial workflow discovered, or user asks you to remember a procedure.\n Update when: instructions stale/wrong, OS-specific failures, missing steps or pitfalls found during use. If you used a skill and hit issues not covered by it, patch it immediately with skill_manage(actionpatch) — dont wait to be asked.\n\n After difficult/iterative tasks, offer to save as a skill. Skip for simple one-offs. ), }創(chuàng)建的門檻設(shè)得比較清晰工具調(diào)用超過 5 次才值得創(chuàng)建簡單任務(wù)不記、踩過坑再修復(fù)的經(jīng)驗才有價值、用戶糾正過的做法要銘記。OpenClaw 也有 Skill 系統(tǒng)也是 SKILL.md YAML frontmatter但 Skill 要么是你手寫的要么是從社區(qū)裝的。手寫的成本高懶得維護社區(qū)裝的不是針對你的環(huán)境。關(guān)鍵問題是Agent 本身不會從工作中學(xué)到任何東西——干了一百次部署第一百零一次犯的錯跟第一次一模一樣。HN 上有個帖子叫 Data Is the Final Moat——當(dāng)模型智能被商品化、Agent 框架被開源真正的護城河是 Agent 在工作中積累的領(lǐng)域知識。OpenClaw 的 Skill 是手寫的配置文件用了一年還是那份手寫的配置文件Hermes 的 Skill 是越用越厚的經(jīng)驗資產(chǎn)——每一次踩坑都在加固護城河。這不是 OpenClaw 團隊不想做而是它的架構(gòu)沒有為Agent 自主學(xué)習(xí)預(yù)留通路——沒有創(chuàng)建觸發(fā)、沒有 patch 機制、沒有 review agent。要補這一課是要重寫核心架構(gòu)。Hermes 這邊Agent 踩了坑、修了 bug、用了 12 次工具調(diào)用才搞定一個部署——這些經(jīng)驗被自動提煉成 Skill下次再遇到同類任務(wù)就是 6 次調(diào)用零錯誤。系統(tǒng)提示詞里還有一句 Skills that arent maintained become liabilities——通過提示詞給 Agent 灌輸責(zé)任感防止它只管創(chuàng)建不管維護。當(dāng) Agent 按照已有 Skill 執(zhí)行但中途發(fā)現(xiàn)步驟有遺漏或者踩了新坑時它會在完成任務(wù)后回頭修補 Skill。不是全量重寫而是做精確的局部 patch# tools/skill_manager_tool.py:397-485 def _patch_skill(name, old_string, new_string, file_pathNone, replace_allFalse): Targeted find-and-replace within a skill file. from tools.fuzzy_match import fuzzy_find_and_replace new_content, match_count, _strategy, match_error fuzzy_find_and_replace( content, old_string, new_string, replace_all ) if match_error: return {success: False, error: match_error, file_preview: content[:500]} # ...省略 _validate_content_size、_validate_frontmatter 等校驗 # 修改前備份原內(nèi)容 original_content content _atomic_write_text(target, new_content) # 修改后重新做安全掃描 scan_error _security_scan_skill(skill_dir) if scan_error: _atomic_write_text(target, original_content) # 不通過就回滾 return {success: False, error: scan_error}這里用了fuzzy_find_and_replace做模糊匹配——Agent 給出的old_string可能跟原文有格式差異模糊匹配能容忍這些差異。每次修改后還要跑一遍_security_scan_skill()不通過就自動回滾。Agent 在踩完坑的當(dāng)場就把 Pitfalls 補上了下次同事遇到同樣的場景直接繞過去。Skill 多了以后不能全塞進系統(tǒng)提示詞——這也是 OpenClaw 的一個痛點它采用全量背包模式每次會話把 SOUL.md、IDENTITY.md 和各種設(shè)定一股腦塞進上下文設(shè)定越多背包越沉Token 浪費嚴(yán)重模型注意力也被稀釋。Hermes 更像一座動態(tài)圖書館默認(rèn)上下文極其輕量只放一個輕量索引——每個 Skill 的名字和一句描述Available skills: devops: - flask-k8s-deploy: Deploy a Flask app to Kubernetes with health checks - nginx-reverse-proxy: Configure Nginx reverse proxy with SSL software-development: - fix-pytest-fixtures: Debug and fix pytest fixture scope issuesAgent 判斷某個 Skill 跟當(dāng)前任務(wù)相關(guān)時才通過skill_view加載完整內(nèi)容。先看目錄再翻全文按需加載。4. Nudge Engine 與后臺 fork反饋回路如何自動觸發(fā) Skill 迭代Memory 和 Skill 都是存儲系統(tǒng)寫入需要有人觸發(fā)。Nudge Engine 就是這個觸發(fā)器——運行時維護兩個計數(shù)器定時提醒 Agent 該停下來想想了。兩個計數(shù)器兩種粒度# run_agent.py:1328-1331 — Memory 計數(shù)器 self._memory_nudge_interval 10 # 每 10 個用戶回合觸發(fā)一次 self._turns_since_memory 0 # run_agent.py:1428-1431 — Skill 計數(shù)器從配置讀取默認(rèn) 10 self._skill_nudge_interval int(skills_config.get(creation_nudge_interval, 10)) self._iters_since_skill 0粒度不同是有道理的Memory 的信息來自用戶輸入按回合計Skill 的經(jīng)驗來自工具使用過程按迭代計。計數(shù)器到閾值就觸發(fā)審查Agent 主動調(diào)用了 memory 或 skill_manage 則重置——已經(jīng)在做了就不用催。Nudge 觸發(fā)后怎么處理它不會在主對話中插一條讓我想想有沒有什么該記的——那樣太打擾用戶了。而是在后臺 fork 一個獨立的 Agent 實例拿著主對話的快照去做審查# run_agent.py:2665-2711 def _spawn_background_review(self, messages_snapshot, review_memoryFalse, review_skillsFalse): def _run_review(): with open(os.devnull, w) as _devnull, \ contextlib.redirect_stdout(_devnull), \ contextlib.redirect_stderr(_devnull): review_agent AIAgent( modelself.model, max_iterations8, quiet_modeTrue, ) review_agent._memory_store self._memory_store review_agent._memory_enabled self._memory_enabled review_agent._user_profile_enabled self._user_profile_enabled # 禁用 review agent 自身的 nudge否則會無限遞歸 review_agent._memory_nudge_interval 0 review_agent._skill_nudge_interval 0 review_agent.run_conversation( user_messageprompt, conversation_historymessages_snapshot, ) thread threading.Thread(target_run_review, daemonTrue) thread.start()幾個細(xì)節(jié)輸出重定向到/dev/null用戶完全無感知最多 8 次工具調(diào)用不會無限消耗 APIreview agent 自身的 nudge 被禁用避免無限遞歸和主 agent 共享同一份 Memory寫入直接生效。干活和反思拆成兩個實例互不干擾。Review Agent 靠兩套審查提示詞決定做什么Memory Review 關(guān)注用戶偏好和個人信息Skill Review 關(guān)注非平凡的解題過程。每個 prompt 都以 If nothing is worth saving, just say Nothing to save. and stop. 收尾——防止 review agent 每次都往里塞東西來交差。審查在響應(yīng)發(fā)送給用戶之前才觸發(fā)用戶收到回復(fù)后該干嘛干嘛Agent 在后臺默默復(fù)盤。完整案例從不會到精通的三次會話。用一個 K8s 部署場景串一下三個子系統(tǒng)的協(xié)同。第 1 次會話冷啟動。用戶說幫我把這個 Flask 應(yīng)用部署到 K8s 集群。Memory 和 Skills 都是空的Agent 靠基座知識摸索12 次工具調(diào)用踩了兩個坑iter1: terminal(kubectl version) → 確認(rèn)集群版本 iter2: read_file(app.py) → 讀取應(yīng)用代碼 iter3: write_file(Dockerfile) → 創(chuàng)建 Dockerfile iter4: terminal(docker build -t myapp .) → 構(gòu)建鏡像 iter5: write_file(deployment.yaml) → 編寫 K8s 部署文件 iter6: terminal(kubectl apply -f deployment.yaml) → ImagePullBackOff忘記推鏡像到 registry iter7: terminal(docker push myregistry.azurecr.io/myapp) iter8: terminal(kubectl apply -f deployment.yaml) → 重新部署 iter9: write_file(service.yaml) → 編寫 Service iter10: terminal(kubectl apply -f service.yaml) iter11: terminal(kubectl get pods) → CrashLoopBackOfflivenessProbe 路徑不對 iter12: 修改 deployment.yaml → 重新部署 → 成功12 次迭代觸發(fā) Skill ReviewReview Agent 看到兩次報錯和修復(fù)過程創(chuàng)建了一個 SkillReview Agent 執(zhí)行: → skill_manage(actioncreate, nameflask-k8s-deploy, categorydevops, content --- name: flask-k8s-deploy description: Deploy a Flask app to Kubernetes with health checks --- ## Steps 1. Create Dockerfile with gunicorn 2. Build and push image to registry BEFORE kubectl apply 3. Write deployment.yaml with livenessProbe → /health ... ## Pitfalls - MUST push image to registry first, otherwise ImagePullBackOff - Flask 默認(rèn)沒有 /health 端點需手動添加 - livenessProbe path 必須返回 200 )安全掃描通過后寫入磁盤用戶對這一切毫不知情。第 2 次會話Skill 復(fù)用 自我修補。用戶說幫我再部署一個 Django 應(yīng)用到 K8s。系統(tǒng)提示詞里多了 Skills 索引Agent 加載flask-k8s-deploy后照著步驟做iter1: skill_view(flask-k8s-deploy) → 加載完整 Skill iter2: read_file(manage.py) → 確認(rèn) Django 項目結(jié)構(gòu) iter3: write_file(Dockerfile) → 用 gunicornSkill 指示 iter4: 添加 /health 端點Skill Pitfalls 提醒 iter5: terminal(docker build docker push) → 先 push 再 applySkill Steps 第2步 iter6: write_file(deployment.yaml) → livenessProbe → /health iter7: terminal(kubectl apply) → DisallowedHost 錯誤Django 特有的問題Skill 沒覆蓋 iter8: 修改 deployment.yaml 添加 ALLOWED_HOSTS env iter9: terminal(kubectl apply) → 成功從 12 次調(diào)用降到 9 次已知坑被繞過但遇到 Django 特有的新坑。Review Agent 一口氣做了三件事寫入用戶畫像、記住 registry 地址、patch Skill 補上 ALLOWED_HOSTS 坑。第 3 次會話零錯誤一次搞定。用戶說幫我部署一個新的 FastAPI 微服務(wù)。Agent 已經(jīng)知道你是誰、registry 在哪、集群在哪Skill 里也包含了 ALLOWED_HOSTS 的坑——6 次調(diào)用零錯誤。三次對比維度會話 1 (冷啟動)會話 2 (Skill 復(fù)用)會話 3 (全自愈)工具調(diào)用12 次9 次6 次錯誤數(shù)210Memory無觸發(fā)寫入系統(tǒng)提示詞注入Skill觸發(fā)創(chuàng)建復(fù)用 自我修補復(fù)用已修補版本在開源 Hermes 中這些經(jīng)驗積累在單個用戶的~/.hermes/目錄下。RDSHermes 把 Skill 存儲從本地磁盤搬到了云端——一個 DBA 踩過的坑團隊里所有人的 Agent 都能繞過。自我進化不再是單點的而是組織級的。5. 常見報錯排查401、local proxy failed、reading choices 與 OAuth 失效在跑通 Hermes Agent 的 Self-Improving 鏈路之前接入層的問題往往先卡住你。下面按真實報錯對照排查。401 Unauthorized / invalid_api_key。最常見的原因是 Base URL 和 Key 不匹配。如果你用的是 TaoToken 的 API 網(wǎng)關(guān)Base URL 必須寫成https://taotoken.net/apiKey 從控制臺的 API Keys 頁面生成。檢查配置文件里base_url和api_key是否成對出現(xiàn)以及 Key 是否帶了多余空格。用 curl 快速驗證curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d {model:claude-sonnet-4-20250514,messages:[{role:user,content:ping}]}返回 200 且 choices 數(shù)組非空說明 Key 和 Base URL 都沒問題。local proxy failed / connection refused。這個報錯通常出現(xiàn)在你配置了本地代理端口但代理進程沒起來。檢查config.yaml里是否殘留了proxy: http://127.0.0.1:xxxx這類配置。如果你不需要本地代理直接刪掉這一行。Hermes 默認(rèn)走直連不需要額外代理層。reading choices 報錯 / KeyError: choices。這通常意味著上游返回的不是標(biāo)準(zhǔn) OpenAI 格式的響應(yīng)??赡茉蚰P?ID 寫錯了網(wǎng)關(guān)返回了錯誤 JSON或者 Base URL 少了/v1路徑。確認(rèn)你的 Model ID 跟網(wǎng)關(guān)支持的列表一致比如claude-sonnet-4-20250514、gpt-4o這類。用模型對話頁面先手動測一次確認(rèn)模型可用再寫進配置。OAuth token expired / Claude Code 認(rèn)證失敗。如果你在用 Claude Code 接入認(rèn)證走的是 Anthropic 的 OAuth 流程。token 過期后需要重新走一遍授權(quán)。檢查~/.claude/下的憑證文件是否還在有效期內(nèi)。如果頻繁過期考慮改用 API Key 方式接入穩(wěn)定性更好。CC Switch / Cline MCP / Codex auth.json 三件套配置。如果你在 Hermes 里掛 MCP Server 或者用 CC Switch 管理多套配置記住每個接入點都需要三樣?xùn)|西Base URL、API Key、Model ID。以 Codex 的auth.json為例{ base_url: https://taotoken.net/api, api_key: sk-xxxxxxxx, model: claude-sonnet-4-20250514 }Cline 的 MCP 配置在settings.json里{ mcpServers: { hermes: { command: npx, args: [-y, hermes/mcp-server], env: { HERMES_BASE_URL: https://taotoken.net/api, HERMES_API_KEY: sk-xxxxxxxx, HERMES_MODEL: claude-sonnet-4-20250514 } } } }CC Switch 的配置文件里同樣需要這三項對齊。任何一項缺失或?qū)戝e都會導(dǎo)致 401 或 reading choices 報錯。Skill 創(chuàng)建后不生效。檢查~/.hermes/skills/目錄下是否有對應(yīng)的 SKILL.md 文件以及 YAML frontmatter 里的name和description是否完整。如果安全掃描沒通過文件會被回滾磁盤上不會留下痕跡。查看日志里有沒有Security scan blocked this skill的輸出。Nudge 不觸發(fā)。確認(rèn)config.yaml里creation_nudge_interval的值默認(rèn)是 10。如果你把值設(shè)得太大比如 100短時間內(nèi)不會觸發(fā)審查。另外如果 Agent 在會話中已經(jīng)主動調(diào)用了skill_manage計數(shù)器會重置不會重復(fù)觸發(fā)。6. 把 Self-Improving 能力接進你的工作流Hermes Agent 的 Self-Improving 就是三件事的配合Memory 記住你是誰Skill 記住怎么做Nudge Engine 保證這個循環(huán)不停轉(zhuǎn)。用得越久Agent 幫你干活就越快、踩坑就越少。如果你想在自己的項目里復(fù)現(xiàn)這套機制源碼閱讀路徑建議按這個順序先看tools/memory_tool.py理解存儲和上限控制再看tools/skill_manager_tool.py理解創(chuàng)建和 patch 流程最后看run_agent.py里的 nudge 計數(shù)器和后臺 fork 邏輯。三個文件加起來不到 2000 行一個下午能讀完。接入層用 TaoToken 的 API 網(wǎng)關(guān)Base URL 寫https://taotoken.net/apiKey 在控制臺的 API Keys 頁面生成。模型 ID 根據(jù)你的任務(wù)選編碼類任務(wù)用claude-sonnet-4-20250514通用任務(wù)用gpt-4o。配置寫好后先用模型對話頁面手動驗證一次確認(rèn)返回正常再寫進 Hermes 的config.yaml。如果你需要長期跑編碼 Agent 或者多 Agent 協(xié)作Coding Plan 的額度更劃算適合持續(xù)迭代的場景。接入文檔里有完整的配置示例和排錯指南遇到 401 或 reading choices 報錯先對照文檔檢查 Base URL、Key、Model ID 三件套是否對齊。自我進化不是魔法是一套設(shè)計良好的反饋回路在持續(xù)運轉(zhuǎn)。理解了這個回路你就能在自己的 Agent 項目里復(fù)現(xiàn)它。