iT邦幫忙

2026 iThome 鐵人賽

DAY 26
0
AI Engineering

地端 AI 建築學系列 第 26 篇

26 案例四:Hermes Agent (2)訓練自訂技能

  • 分享至 

  • xImage
  •  

Hermes Agent 本身的安裝、設定,還有接上 Telegram 當作日常助理的部分,官方文件其實寫得很完整了,這裡就不重複描述。這篇要 focus 在一件事:怎麼讓 Hermes Agent 學會一個它原本不會的操作流程,並且把這個能力固化成一個可以重複呼叫的技能。這也是 Hermes Agent 跟一般 Agent 框架比較不一樣的地方,它把「自我學習」這件事做成了內建機制。

接下來會分四段描述:先搞懂官方的 Skill 系統怎麼運作,再用我們自己整理的一套訓練準則,實際玩一個電商操作技能的訓練案例跟驗證。


Hermes Agent 的 Skill 系統

官方文件在 Skills System 這頁講得很清楚:

Skill 是什麼?

用一句話講:Skill 就是 Agent 的「隨需知識文件」。平常不會佔用 context,只有當 Agent 判斷「這個任務需要用到」的時候才會載入進來。這個設計叫做 progressive disclosure(漸進式揭露),分三層:

  • Level 0:skills_list(),只列出技能的名字、描述、分類,非常省 token

  • Level 1:skill_view(name),真的要用了才把整份 SKILL.md 讀進來

  • Level 2:skill_view(name, path),如果技能底下還有 references/ 的補充文件,需要用到哪份才讀哪份

Skill 放在哪裡、長什麼樣子

所有技能預設都放在 ~/.hermes/skills/,目錄結構大概是這樣分類:

~/.hermes/skills/
├── ecommerce/
│   └── ec-checkout/
│       ├── SKILL.md          # 主要指令內容(必要)
│       ├── references/       # 補充文件
│       ├── templates/        # 輸出格式範本
│       ├── scripts/          # 可被技能呼叫的輔助腳本
│       └── examples/         # 參考範例輸出

一份 SKILL.md 的標準格式長這樣,YAML frontmatter 加上固定的段落順序:

---
name: my-skill
description: 這個技能在做什麼(簡短)
version: 1.0.0
metadata:
  hermes:
    tags: [browser, ecommerce]
    category: automation
---

# Skill Title

## When to Use
什麼情境該觸發這個技能

## Procedure
1. 步驟一
2. 步驟二

## Pitfalls
已知的失敗模式跟對應的修法

## Verification
怎麼確認這件事真的做對了

裝好的技能會自動變成一個 /skill-name 的 slash command,也可以一次疊加多個(最多 5 個)在同一則訊息開頭,Agent 會把它們全部載入再執行後面接的指令。

/learn:讓 Agent「學」一個技能,而不是你手寫

這是這整套系統裡我覺得最實用的指令。你不用自己去兜一份 SKILL.md,而是指給它任何形式的材料,讓 Agent 自己去讀、去整理、寫成一份符合規範的技能文件:

/learn 我剛剛帶你走過一遍的那個部署流程
/learn https://docs.example.com/api/quickstart
/learn 請款流程:打開後台 > New > Expense > 附上收據 > 送出

如果丟進去的材料很龐大(一整本書、一份規格書),Agent 不會硬塞成一個檔案,而是拆成一份精簡的 SKILL.md 加上一堆按主題分類的 references/ 檔案,做成一個知識庫型的技能,需要哪塊再讀哪塊。

技能是 Agent 自己管理的

Hermes Agent 透過內建的 skill_manage 工具自己建立、修改、刪除技能,這其實就是它的「程序記憶(procedural memory)」——跟存放零碎事實的 memory 系統是分工的:memory 放的是每次對話都該記得的小事實,skill 放的是「該怎麼做一件複雜的事」的完整流程。

官方文件對「什麼樣的內容才該寫進技能」有個很值得參考的原則:要記的是「教訓」,不是「日誌」。也就是說,一個 pitfall 應該寫成「一條可以通用的規則 + 一句話講清楚背後原因」,而不是寫成「上次在哪個 PR、哪一天發生了什麼事」這種流水帳——因為事件敘述沒有上下文就看不懂,但一條規則本身要能夠獨立成立。

如果團隊比較 care 安全性,也可以開 skills.write_approval: true,這樣 Agent 每次要新增/修改技能都會先暫存,等你手動 approve 才會真的落地,不會學了什麼就直接寫進系統。


我們的自訂技能訓練準則:一個 PDCA 循環

官方文件告訴你機制怎麼運作,但「什麼時候該訓練、怎麼訓練才不會學出一堆垃圾技能」還是得自己抓節奏。我們內部把訓練流程收斂成一個 PDCA 循環,附圖是整套準則:

https://ithelp.ithome.com.tw/upload/images/20261006/20181345l9Z8HlzmNk.png

Plan(拆解需求描述)

先不要一股腦把「幫我做完整個電商下單流程」丟給 Agent。把大目標拆成幾個可以獨立驗證的小 prompt,一步一步餵給 Agent,讓它先把每一段子任務跑順、跑對。這一步的重點是先把「正確答案」跑出來一次,還不急著談效率或封裝。

Do(包裝自訂流程)

等分段的 prompt(包含中途補充、修正的 prompt)都執行完畢、結果也驗證無誤之後,請 Agent 把整個過程包裝成一份自訂 skill。這裡有個容易被忽略但很關鍵的細節:要請 Agent 同時把「這個 skill 對應什麼情境」記錄到 memory——不然下次同樣的情境出現,Agent 可能想不起來自己已經有這把工具可以用,等於白訓練。

Check(驗證 Skill)

先下 /new 把對話 session 完全清空,確保接下來的測試是「乾淨的 Agent + 已存在的 skill」,而不是靠著上下文殘留的記憶蒙混過關。接著直接對它下最初那個「大目標」的 prompt,看它能不能單靠 skill 本身一次到位跑完,不再需要人工分段介入。

Act(優化收斂 Skill)

Check 階段一定會冒出新問題——可能是某個步驟的 timing 沒抓好、某個選擇器換了位置。這時候請 Agent 根據這次 session 裡「最新發現的問題」回頭更新 skill 的內容,同時把重複或已經不需要的舊內容拿掉,避免 SKILL.md 隨著一次次訓練越滾越肥、充滿過時資訊。

Act-Check 反覆迴圈

Act 完不是結束,而是會不停回到 Check 重新驗證,直到大目標的執行結果穩定、問題大致排除為止。


案例:訓練本機模擬電商頁面操作技能

實際拿一個任務來跑一次這套流程:讓 Hermes Agent 學會在一個本機模擬的電商網站 shuttle.ec.com:8081 上,完成從選商品到送出訂單的完整結帳流程。

按照 Plan 階段的做法,我們沒有直接丟「幫我在 Nexus Store 下單」這種模糊指令,而是拆成一串子任務分批帶著 Agent 跑:提問打開頁面、提問選商品、提問選規格、提問填表單、提問送出、提問確認結果 — 每一段都先跑通驗證對了,再進下一段。

跑完整段之後進入 Do 階段,提問請 Agent 把整個流程包裝成技能,整段訓練好的技能內容如下:

# Nexus Store Checkout — shuttle.ec.com:8081

## PREREQUISITES
1. Load **browser-automation** skill
2. Always use `http://` not `https://`

## CORE JS EXECUTION — CDP ALWAYS FIRST

### Get Page Target (every session)
```bash
browser_cdp(method='Target.getTargets', params={})
# -> Find target with url 'http://shuttle.ec.com:8081/' — that's your target_id
```

### The Golden Rule: Never assume JS injection succeeded
Both `browser_console(expression=...)` and `browser_cdp(Runtime.evaluate)` can silently hit `chrome-untrusted` DOM instead of the page — returning `null` **with no error**.

**Always verify JS execution before acting:**
```python
result = browser_cdp(method='Runtime.evaluate', params={"expression": "1+1"}, target_id='<pageTargetId>')
# result['result']['result']['value'] should be 2, NOT null/undefined
```

## WORKFLOW

### Step 1: Navigate
```bash
browser_navigate('http://shuttle.ec.com:8081')
# -> browser_snapshot -> verify page loaded, step is 01
```

### Step 2: Select Product
```bash
browser_snapshot -> find product card refs
browser_click(ref='@eN') on desired product
# -> Step 02 auto-unlocks with spec options
```

### Step 3: Select Specifications
**⚠️ CRITICAL: Cascade Timing** — the first spec click triggers a one-shot cascade that auto-selects ALL remaining specs at their system defaults. For specific non-default specs, batch-click desired specs in a SINGLE tool-use block before the cascade fires.

### Step 4: Fill Delivery Form
```bash
browser_type(ref='@<name_ref>', text='<name>')
browser_type(ref='@<phone_ref>', text='<phone>')
browser_type(ref='@<email_ref>', text='<email>')
browser_type(ref='@<zip_ref>', text='<zip>')
browser_type(ref='@<address_ref>', text='<address>')
```

**City dropdown** — use JS (never `browser_click` on dropdown options):
```python
browser_cdp(method='Runtime.evaluate', params={
  "expression": "(function(){ var f=document.getElementById('fc') || document.querySelector('select#fc'); if(f){ f.value='New York, NY'; f.dispatchEvent(new Event('change',{bubbles:true})); return 'SET:' + f.value; } return 'FIND_FAILED'; })()"
}, target_id='<pageTargetId>')
```

### Step 5: Submit Order
**Credit Card** is pre-selected by default. No payment action needed.

```bash
# 1. VERIFY:
browser_cdp(method='Runtime.evaluate', params={
  "expression": "tryUnlockSubmit(); document.getElementById('btnSubmit')?.disabled"
}, target_id='<pageTargetId>')
# -> disabled should be false

# 2. SUBMIT:
browser_cdp(method='Runtime.evaluate', params={"expression": "submitOrder()"}, target_id='<pageTargetId>')
```

### Step 6: Verify Confirmation
Look for **"Continue Shopping"** + **"Track Order"** buttons (NOT "PLACE ORDER").

## PRICING REFERENCE

| Product | Type | Base Price |
|---|---|---|
| NEXUS-X PRO | Computer | NT$ 45,900 |
| VORTEX PHONE | Phone | NT$ 24,900-36,900 |
| PULSE AUDIO X | Audio | NT$ 8,900 |
| MATRIX WATCH | Watch | NT$ 9,500-17,500 |

## COMMON PITFALLS
1. JS injection can fail silently or with CDP error -32601. Always verify with `1+1` test.
2. `return` in arrow function body of Runtime.evaluate fails silently (SyntaxError). Use IIFE or expression-style.
3. City dropdown only has US cities. Use "New York, NY" placeholder.
4. Submit requires `tryUnlockSubmit()` — button may appear enabled but JS state gates it.
5. Confirmation is a PAGE CHANGE — don't assume submit succeeded from snapshot alone.

接著講一下這份技能反映出哪些訓練過程中踩過的坑,並對照技能內容:

開場先立下「黃金原則」

技能一開頭就寫了一條規矩:永遠不要假設 JS 注入成功了。這是因為 Agent 在測試階段發現,透過瀏覽器工具送出的 JS 表達式,偶爾會靜默地打到瀏覽器自己的內部 UI(chrome-untrusted)而不是頁面本身,而且不會噴錯誤,只會回傳 null。所以技能裡明訂了這樣一個驗證慣例,每次真正動作前一律先跑一次:

result = browser_cdp(method='Runtime.evaluate',
                      params={"expression": "1+1"},
                      target_id='<pageTargetId>')
# result 應該是 2,不是 null / undefined

確認回傳值真的是 2,再放心往下做。這就是很典型的「教訓」寫法 — 一條可以在任何情境套用的規則,加一句話講清楚為什麼會踩雷。

規格選擇的「Cascade Timing」警告

技能裡特別標了一個 ⚠️,內容是這樣寫的:

第一次點選規格時會觸發一次性連鎖反應,系統自動把其餘規格設成預設值。如果要的不是預設值,必須在連鎖反應觸發前,把所有目標規格包在同一個工具呼叫裡批次點完。

換句話說,如果你要的不是預設規格,就得在連鎖反應觸發前把所有想要的規格選項一次性包進同一個工具呼叫裡批次點完。這種時序型的坑,如果不是真的跑過幾次失敗案例,是很難憑空寫出來的 — 這正是「先跑、再收斂成 skill」這套訓練順序存在的意義。

表單這段,文字欄位跟下拉選單分開處理

姓名、電話、Email、地址這種文字欄位直接用一般的輸入工具打字就好,但城市這個下拉選單技能特別交代不要用點擊模擬,改用 JS 直接設定 value 再手動觸發 change 事件:

browser_cdp(method='Runtime.evaluate', params={
  "expression": "(function(){ var f=document.getElementById('fc'); "
                 "if(f){ f.value='New York, NY'; "
                 "f.dispatchEvent(new Event('change',{bubbles:true})); "
                 "return 'SET:' + f.value; } return 'FIND_FAILED'; })()"
}, target_id='<pageTargetId>')

這也是訓練過程中撞出來的坑 — 下拉選單用模擬點擊常常因為渲染時機對不上而點不中,乾脆記錄下「這個欄位就是要走 JS 這條路」。

送出訂單前有一道防呆閘門

送出前技能要求分成兩個步驟:

# 1. 先解鎖確認
browser_cdp(method='Runtime.evaluate', params={
  "expression": "tryUnlockSubmit(); document.getElementById('btnSubmit')?.disabled"
}, target_id='<pageTargetId>')
# -> 應該回傳 false

# 2. 才真的送出
browser_cdp(method='Runtime.evaluate',
            params={"expression": "submitOrder()"},
            target_id='<pageTargetId>')

先呼叫頁面內建的 tryUnlockSubmit() 確認 btnSubmit 的 disabled 狀態真的變成 false,才呼叫 submitOrder()。技能裡也提醒了:畫面上按鈕看起來可以點,不代表底層的 JS 狀態真的解鎖了,這也是靠實測才抓出來的細節。而且送出後要看到「Continue Shopping」+「Track Order」兩顆按鈕(不是「PLACE ORDER」)才算真的確認成功 — 技能特別點名確認畫面其實是換頁,不能只靠 snapshot 就假設送出成功。

整份自訂技能內容看下來,你會發現它幾乎沒有任何「事件敘述」,沒有寫「第幾次測試撞到什麼」,也沒有時間戳>記或流水帳,全部都是「規則 + 做法」,這正是 Act 階段「去掉日誌、只留教訓」收斂出來的結果。


驗證模擬電商頁面操作技能

驗證流程

https://ithelp.ithome.com.tw/upload/images/20261006/20181345MhZJquB8Ey.png

對照 Check 階段的做法:

  1. 啟動模擬站

  2. 對話 session 下 /new,清空掉先前訓練階段殘留的任何上下文記憶

  3. 直接丟出最初的大目標 prompt,例如「幫我選擇預算內的耳機,使用信用卡付費」

  4. 觀察 Agent 是不是單靠技能,不需要人工中途補充指令,就能一路跑完選商品、選規格、填表單、通過防呆閘門、送出訂單,最後停在「ORDER CONFIRMED」這個畫面

如果中途卡關,比如某個規格連鎖反應又搶拍了,或是某個下拉選單又點不中,這些新發現的問題就會被撈回 Act 階段,更新回技能後,再跑一次 Check — 通常這個迴圈跑個幾輪後,技能就會穩定下來。


小結

這篇的核心概念其實很簡單:Skill不是一次性寫死的文件,而是要透過收斂迭代流程,讓 Agent 在真實嘗試中撞出教訓,最後蒸餾成一份越用越穩定的流程筆記。


上一篇
25 案例四:Hermes Agent (1)簡介與安裝
下一篇
27 案例四:Hermes Agent (3)與自建 MCP Agent 協作
系列文
地端 AI 建築學 共 28 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言