iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
AI Engineering

30天拆Agent:從Repo看設計系列 第 16 篇

Day 16|OpenClaw 的 Agent 怎麼組成?從小航一路看到跨 Agent 委派

  • 分享至 

  • xImage
  •  

前幾篇介紹 Codex 和 Claude 時,我們追的是一個 Agent Run,OpenClaw 當然也有。

但如果真的想自己打造一個長期運作的 Agent,更值得看的問題是:

一個 Agent 到底怎麼從幾份 Markdown、Session、Tool,組成一次真正可以執行的工作?

這篇只用一個例子。

假設我先建立一個航空客服 Agent:

agentId = flight
名字 = 小航

之後再跟小航說:

幫我建立一個票規助理,專門檢查退改限制。

系統裡因此多一個:

agentId = fare_rules
名字 = 票規助理

最後 Alice 從 Slack 問:

BR123 可以改到明天下午嗎?

小航先取得訂位資料,再把票規檢查交給 fare_rules,最後整理結果回覆 Alice。

整篇只看四件事:

1. Markdown 怎麼真的進到 Model?
2. 自然語言怎麼真的建立第二個 Agent?
3. A 怎麼把工作交給 B,再拿回結果?
4. Runtime、Tool、Skill、SOP 到底怎麼分工?

1. SOUL.md 寫了「回答簡潔」,Model 為什麼真的看得到?

先假設小航已經存在,它的 Workspace 是:

~/.openclaw/workspace-flight

裡面有:

順序 標準檔案 放進上下文的目的
1 AGENTS.md 工作指示,例如接到改票需求後先收集哪些資料
2 SOUL.md 行為原則與語氣
3 IDENTITY.md 名稱、形象等身分資料
4 USER.md 使用者資訊與偏好
5 BOOTSTRAP.md 首次設定引導,依設定完成狀態過濾
6 MEMORY.md 符合注入條件的長期摘要

例如:

SOUL.md

你叫小航。
回答乘客問題時保持簡潔。
不要一次丟出大量規則。

磁碟上的 Markdown,怎麼變成 Model 的輸入?

Model 本身不會掃 Workspace。

OpenClaw 要先經過一條明確的載入流程。


先讀指定文件,不是掃描所有 Markdown

OpenClaw 執行 flight 時,會先解析它的 Workspace,接著呼叫:

src/agents/workspace.ts

loadWorkspaceBootstrapFiles()

它讀一份固定清單:

src/agents/workspace-bootstrap-policy.ts

AGENTS.md
SOUL.md
IDENTITY.md
USER.md
BOOTSTRAP.md
MEMORY.md

所以:

workspace-flight/
├── AGENTS.md       ← 會自動讀
├── SOUL.md         ← 會自動讀
├── USER.md         ← 會自動讀
└── flight-sop.md  ← 不會因為是 Markdown 就自動讀

loadWorkspaceBootstrapFiles() 讀完後,拿到的大概是:

{
  name: "SOUL.md",
  path: ".../SOUL.md",
  content: "...",
  missing: false
}

這時還只是「檔案資料」,還沒有進 Model。


接著依這次 Session 決定真正能注入哪些內容

下一步會進:

src/agents/bootstrap-files.ts

resolveBootstrapFilesForRun()
resolveBootstrapContextForRun()

處理:

這是哪種 Session?
哪些文件這次可以載入?
Workspace bootstrap 是否完成?
有沒有個人的 USER.md?
Context 是否超過大小限制?

最後buildBootstrapContextForFiles(),會套用 Context Budget,把文件轉成contextFiles


contextFiles 最後才進 System Prompt

接著buildAgentSystemPrompt()會把 Runtime 本身的指示、Tools、Skills,以及 Workspace 的 contextFiles 組在一起。

Workspace 文件真正被放進 Project Context 的邏輯在:

src/agents/system-prompt-context-files.ts

最後,buildAgentSystemPrompt() 會把整理後的 contextFiles 交給 buildProjectContextSection(),產生 # Project Context 區塊,並直接加入完整的 System Prompt。因此 AGENTS.md、SOUL.md 等 Workspace 文件不是由 Model 自己讀取,而是由 OpenClaw Runtime 先載入,再組進 System Prompt。

# Project Context

## /workspace/AGENTS.md

收到改票需求後,
先確認訂位、票規與新航班。

## /workspace/SOUL.md

你叫小航。
回答乘客問題時保持簡潔。

這個Repo會在後續 Turn 重新檢查 Bootstrap 文件,而不是 Agent 建立時只讀一次,所以修改soul.md,會被重新讀取。


2. 「幫我建立票規助理」怎麼真的產生第二個 Agent?

現在小航已經能服務乘客。

接著我對它說:

幫我建立一個票規助理,專門檢查退改限制。

這裡其實包含兩個不同工作:

Model:
理解使用者想建立一個新 Agent

以及:

Host:
真的修改設定、建立 Workspace

OpenClaw 把這兩件事分開。


自然語言先由 Model 理解

官方文件明確支援:

已配置的 Agent 可以透過 openclaw Tool 請 OpenClaw 建立另一個 Agent。

因此大致流程是:

User
 │
 │ 幫我建立票規助理
 ▼
小航 Model
 │
 │ 理解意圖
 ▼
openclaw Tool
 │
 ▼
OpenClaw System Agent

自然語言仍先交給 Model 理解。

Model 再選擇:

create_agent

這個 typed operation。


Model 產生操作,Host 才負責真正執行

例如 Model 最後提出的 Tool Call 可以簡化成:

{
  "action": "create_agent",
  "agentId": "fare_rules",
  "name": "票規助理",
  "purpose": "檢查退票與改票限制"
}

system-agent-tool.ts 會把它轉成:

kind = create-agent

但到這裡 Agent 還沒真的建立。

因為建立 Agent 是 Persistent Operation,會修改:

OpenClaw Config
Workspace
Agent State

所以還必須經過 Host 的:

Permission Policy
Authorization
必要時 Human Approval

真正建立 Agent 的是 createAgent()

通過授權後,才進入:

src/agents/agent-create.ts

createAgent()

如果先忽略大量安全檢查,它最重要的工作可以濃縮成兩件。

第一個:

applyAgentConfig()

把新 Agent 加入設定。

原本:

agents
└── flight

變成:

agents
├── flight
└── fare_rules

第二個:

ensureAgentWorkspace()

建立或準備:

~/.openclaw/workspace-fare-rules

以及必要的 Bootstrap 文件。


建立 Agent 不代表它已經開始工作

這裡是 OpenClaw Multi-Agent 很容易混淆的一點。

完成:

createAgent()

只代表:

fare_rules

這個持久 Agent 已經存在。

它現在有:

自己的 Agent Config
自己的 Workspace
自己的狀態空間

但還沒有在處理 Alice 的票規。

真正讓它開始做一件事情,是後面的:

sessions_spawn

所以兩者可以簡單分成:

createAgent()
→ 建立「這個 Agent 是誰」
sessions_spawn
→ 建立「這次請它做什麼」

這個區分也是接下來理解委派的關鍵。


3. 小航怎麼把 Alice 的工作交給票規助理?

現在系統中有:

flight
→ 小航

fare_rules
→ 票規助理

Alice 從 Slack 問:

BR123 可以改到明天下午嗎?

這次直接跟著資料走。


Alice 先進入小航自己的 Session

Gateway 收到 Slack 訊息後,會先把它 Route 到:

agentId = flight

並找到 Alice 對應的:

sessionKey  #ex. agent:flight:slack:direct:alice

這個 sessionKey 可以直接理解成:

OpenClaw 用來定位一段邏輯對話的地址。

所以 Alice 與 Bob 都使用小航,不代表他們一定共用同一段 Conversation。

實際是否共享,仍取決於 Session Scope 設定。


Conversation 會保存到 Agent 自己的 SQLite

目前版本的 Agent runtime state 預設放在:

~/.openclaw/agents/<agentId>/agent/openclaw-agent.sqlite

因此小航有:

~/.openclaw/agents/flight/agent/openclaw-agent.sqlite

票規助理則有:

~/.openclaw/agents/fare_rules/agent/openclaw-agent.sqlite

Alice 原本的聊天歷史屬於 flight。

票規助理之後建立的 Child Session,則屬於 fare_rules。

兩邊不是共用同一份 Transcript。


小航先取得真正要交辦的資料

例如小航透過訂位 Tool 查到:

PNR = ABC123
Fare Basis = Y26
Ticket Status = OPEN
Requested Date = tomorrow

現在它不自己處理所有票規,而是呼叫:

sessions_spawn

例如:

{
  "agentId": "fare_rules",
  "task": "檢查此訂位是否可改到明天下午。Fare Basis=Y26,Ticket Status=OPEN。請回傳限制與費用,不要執行改票。",
  "context": "isolated",
  "completionTarget": "parent"
}

入口在:

src/agents/tools/sessions-spawn-tool.ts

最後會進:

spawnSubagentDirect()

並建立一個新的 Child Session。


跨 Agent 不會直接複製 Alice 全部 Context

這裡是最值得注意的地方。

OpenClaw 的:

context = "fork"

目前只支援:

Requester Agent
=
Target Agent

如果:

flight
→ fare_rules

是不同 Agent,就必須使用:

context = "isolated"

因此票規助理不會因為和小航在同一個 Gateway,就自動得到:

Alice 全部聊天紀錄
小航所有 Tool Result
小航的 Persona

真正需要的資訊,必須由小航放進:

task

所以這次 B 真正收到的是:

檢查改票限制

Fare Basis = Y26
Ticket Status = OPEN
需求 = 明天下午

不要執行改票

而不是整段 Alice Conversation。

這其實是一個很乾淨的隔離方式:

A 的 Context
≠
B 的 Context

A 要明確決定哪些資料可以交給 B。


Runtime 會保存 Parent / Child 關係

建立 Child Session 時:

createInitialSubagentSession()

除了產生新的 Session Identity,還會記錄:

spawnedBy
parentSessionKey
completionOwnerSessionKey

概念上:

fare_rules child session

spawnedBy
= Alice 的 flight session

parentSessionKey
= Alice 的 flight session

completionOwnerSessionKey
= Alice 的 flight session

這代表系統不需要讓 B 自己記得:

「我做完之後要通知小航。」

Runtime 一開始就已經知道這次工作的 Parent 是誰。

所以完整流程是:

Alice
 ↓
flight Session
 ↓
小航查訂位
 ↓
sessions_spawn
 ↓
建立 fare_rules Child Session
 ↓
B 執行票規檢查
 ↓
完成結果
 ↓
completionOwnerSessionKey
 ↓
flight Session
 ↓
小航整理結果
 ↓
Alice

例如 B 最後得到:

Y26 可以改期
手續費 TWD 1,500
另補 Fare Difference
仍需確認新航班座位

這個結果回到 Parent Session 後,小航才把它整理成:

可以改到明天下午,但目前票規會收取
TWD 1,500 改票費,另外仍需計算票價差額。

我還需要確認明天下午航班的座位後,
才能提供最終金額。

所以 Multi-Agent 的本質不是:

兩個 Model 自己互相聊天

而是:

A 呼叫 sessions_spawn
 ↓
Runtime 建立 B 的 Session
 ↓
保存 Parent / Child 關係
 ↓
B 執行
 ↓
Runtime 將結果交回 A

4. Runtime、Tool、Skill、SOP 到底各負責什麼?

走完前面的 Case,其實就不需要另外背很多定義。

直接看小航需要什麼。


Skill:告訴 Model「這種工作應該怎麼做」

例如改票 Skill 可以寫:

收到改票需求
 ↓
先取得訂位
 ↓
確認 Ticket Status
 ↓
查票規
 ↓
確認新航班
 ↓
試算費用
 ↓
回報乘客

Skill 解決的是:

Agent 應該怎麼做這類工作?

它比較像操作方法。

但 Skill 本身不會真的查訂位,也不會真的修改 Booking。


Tool:真正可以做事情

例如:

get_booking()
search_flights()
quote_change()
update_booking()
sessions_spawn()

Model 可以根據 Skill 決定:

下一步要呼叫 get_booking()

但真正取得資料的是 Tool。

同樣地:

sessions_spawn

也不是一句 Prompt。

它是真的 Runtime Tool,會建立 Child Session。

所以:

Skill
→ 教 Model 怎麼工作

Tool
→ 真的執行工作

SOP:如果「不能跳過」,就不能只寫在 Prompt

假設航空公司的規則是:

未取得乘客確認,不得真的改票。

如果只在 Skill 寫:

請先取得乘客確認,再執行改票。

這仍然只是 Model Instruction。

如果「未取得乘客確認不得改票」是不可違反的業務規則,不需要修改 OpenClaw 核心程式;應在自行串接的 update_booking Tool 或後端 API 加入檢查。OpenClaw 的 Tool Policy 可以限制 Agent 是否能使用該 Tool,若還需要每次呼叫前再次攔截,也可以透過 Plugin 的 before_tool_call Hook 加上額外 Gate。


Runtime:把以上東西組成一次真正的 Agent Run

最後才是 Runtime。

小航執行一次工作時,需要準備:

Workspace Context
Session History
Skills
Tools
Model

然後送進 Agent Loop:

Model
 ↓
Tool
 ↓
Tool Result
 ↓
Model

因此可以把這次航空 Case 收斂成:

需求 放在哪裡
「你叫小航、回答簡潔」 SOUL.md / IDENTITY.md
航空客服的一般工作規則 AGENTS.md
改票 SOP Skill
查訂位、查航班、改票 Tool
將票規工作交給 B sessions_spawn
Alice 與 B 各自的工作紀錄 Session / SQLite
未確認不得改票 Tool / Backend Gate
把 Context、Session、Tool、Model 組起來 Runtime

這張表其實就是 OpenClaw Agent 架構最重要的部分。


References


上一篇
Day 15|Codex vs Claude Commerce Agents:Config、Memory、Tool 與 Sandbox 快速比較
下一篇
Day 17|OpenClaw 的 Tool、Skill、MCP:真正不同的是誰能用、何時觸發,以及怎麼學
系列文
30天拆Agent:從Repo看設計 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言