故事要從一個濕熱的週六傍晚說起。地點?河灣新落成的「潮汐書市」,旁邊就是市民最愛的環河夜跑步道。時間?晚間六點四十分,天空不巧地飄起了毛毛細雨。原本,這該是兩個互不相干的美好日常,書香與汗水各自安好。
但命運,總喜歡在週六晚上開個大玩笑。
混亂的種子,其實早在幾週前就埋下了。路跑聯盟的活動,是循規蹈矩地在交通局的系統裡登記、審批、排程的「乖寶寶」。而書市的「作者簽名會延長」,則是在活動當天,由一位興高采烈、腎上腺素飆升的社群小編,在臉書粉絲頁上勇敢地按下了「發佈」按鈕。
(是的,一個是寫在石頭上的律法,另一個是寫在沙灘上的情書。在 Codetopia 的數位世界裡,這兩者跟陌生人沒兩樣。)
於是,災難的雞尾酒就這樣調成了。主料是三個互不通話的訊息管道:臉書小編的熱情、交通局電子看板的冷漠、路跑 App 的準時。配料是那場恰到好處的雨。輕輕一搖,倒出來的就是現場那杯名為「人間煉獄」的特調。想帶著戰利品和作者簽名回家的書迷,迎面撞上了正在原地高抬腿熱身的跑者;推著嬰兒車想躲雨的家長,絕望地發現自己卡在了一群穿著螢光緊身衣的移動路障中間。
值班室的電話線燙得能煎蛋,志工的對講機裡,尖叫聲和求救聲糊成一鍋數位濃粥。終於,指揮中心的值班官員,抓著麥克風,發出了那句所有工程師都耳熟能詳的絕望嘶吼:「聽著!我需要你們在二十分鐘內變出一個奇蹟!還有,誰都不准給我動市民 App 的程式碼!」
就在這片數位與現實交織的混亂中,一個冷靜的聲音切入頻道,像冰塊掉進了滾水:「收到。多代理臨時反應小隊,入場。」
Aster,多代理協作總監,來上班了。
MAS (Multi-Agent Systems):多個自主「代理人」透過協作解決單一代理無法處理的複雜問題,包含 DF (黃頁服務) 與 ACL (代理通訊語言)。
Contract-Net:一種任務分配協議,由一方「招標」,多方「投標」,實現去中心化的決策,避免「一神獨裁」。
Critic/Planner/Executor 迴圈:一種決策流程,由規劃者提出方案、批判者審查風險、執行者負責落實,確保每一步都可回放、可審計。
City Event Bus:城市事件總線,所有代理人溝通的唯一訊息入口,確保訊息傳遞的可靠性與一致性。(旁批:這條總線只傳遞「事實 (Events)」,不應把大型查詢結果塞進來!設計鐵律:查詢走同步 API 或快取、指令可走同步 API 或 command topic,事件只承載已發生的事實。)
在 Aster 小隊抵達前,有人曾試圖用「老方法」解決問題。讓我們把時間稍微往前調一點,看看那場華麗的失敗是如何上演的:
一個提示詞餵給神:有人寫了一段三百行的「超級提示詞」給一個全能的 AI 代理,內容包山包海,從疏散路線、天氣預測到安抚民眾情緒的廣播稿。「去吧!解決一切!」... 結果,那個代理產生了幻覺命令,建議大家「原地跳躍以保持溫暖」,還試圖同時呼叫所有交通號誌 API,直接踩爆了系統頻寬。
直接呼叫供應商 SDK:某個代理直接串接了電子看板供應商的 SDK,但該 SDK 回傳的亮度值是 0
到 1
的浮點數;而另一個廣播系統的音量單位卻是 0
到 100
的整數。結果?看板亮到像白天,廣播卻小聲到像蚊子叫,行為完全無法對齊。
無情的重試雪崩:一個指令因為網路抖動而超時,系統很自然地進行了重試。但因為沒有設計冪等性 (idempotency_key
),同一個「封閉步道」的指令被執行了三次。於是,三組不同的柵欄被送到現場,造成了更大的混亂。
消失的決策軌跡:事後複盤時,沒人知道是「誰」在「哪個時間點」根據「哪一版規定」下達了「原地跳躍」的指令。記憶無法稽核,讓每一次災難都成了懸案。
Aster|多代理協作總監抵達後,沒有一絲慌亂。她打開了終端機,召集了她的菁英小隊,一場多代理協作就此展開。
Aster (總監) - 發包!不是發瘋:她立刻使用 Contract-Net (合約網協議) 發出兩個清晰的標案:
標案 A / 動線調度:需求是「不更新市民 App,20 分鐘內解除人潮逆流」,SLA 要求極高。
標案 B / 訊息一致性:需求是「一鍵同步真相 (SSOT) 到現場看板、手持廣播與官方社群」,確保資訊零時差。(旁批:這裡的一致性,是透過共享的語彙庫與模板中心來達成的,確保『延後十分鐘』在三個管道的說法完全一樣,杜絕人工轉述的語意誤差。)
Kian (工具鍛造師) - 穩定的插座,不是混亂的電線:他並未讓代理們直接去碰觸那些脾氣古怪的硬體 SDK。而是開啟了「城市工具庫」三個穩定的 Port (埠):RoutePort
、SignagePort
、BroadcastPort
。所有工具的呼叫,都必須透過這些穩定 Port 進行;對外副作用以同步 API 或 command.*
topic 發出,事件只承載已發生的事實,查詢走同步 API 或快取層(由 Adapter 觸碰 Bus,Domain 不直連)。
_圖說小字:事件 publish/consume 僅在 Adapter/邊界層,Domain 不依賴 Bus 實作。_
他還再三強調:「事件 Schema 的演進,必須遵守只增不改的原則。新增欄位需為可選或具備預設值,任何破壞性變更的 PR 都會被 CI 自動退件,這是為了保障舊的代理不會因為新事件而崩潰!」
Rook (協商仲裁員) - 在安全與體驗間,跳一曲探戈:很快,動線規劃代理和使用者體驗代理吵起來了。「安全第一!應立刻全面封鎖步道!」「不行!書迷還沒走完,會引發恐慌!」Rook|協商仲裁員介入了。他調出 城市幸福指標 (CUPID) 的權重檔,一鍵導出折衷決策:立即開闢一條「安靜帶走線」供離場民眾使用,夜跑活動延後十分鐘,並採分批通行。雙方代理都接受了這個方案。
Sable (安全評審官) - 命令出口的最後一道防火牆:所有即將對外發出的命令,都必須經過 Sable 的審查。她為每個指令都套上了硬停 (hard stop) 與降級計畫 (rollback plan):
若監測到現場人流密度超過 P95 警戒線,自動觸發備援方案:志工立刻舉起離線的 QR Code 手牌引導。
她攔下了一個即將發出的廣播指令,冷靜地說:「『廣播』是不可逆的外部動作,沒有『回滾』這回事。」她要求改為兩階段提交 (Two-phase Publish):先發布一則 broadcast.previewed
內部審計事件,待所有相關方確認無誤後,才發出那則 broadcast.committed
事件。如果真的發錯了,只能發布 broadcast.corrected
來進行覆寫修正。(旁批:這稱為 Forward-Fix,是在不可逆操作上進行補救,而不是真正的『回滾』。)
她還順手駁回了一個計畫,理由是:「冪等鍵和關聯 ID 裡,絕對不准包含任何市民個資 (PII)!這是紅線!」
_圖說小字:corrected 是 forward-fix,不是 rollback(呼應文中的紅線)。_
run_id=BOOKRUN-2210
。所有決策、所有被採納的規則版本 (ordinance_version=v2025-10-12.1
),都被精確地記錄下來,形成了一條可回放、可稽核的時光快照。想知道「回到剛剛」發生了什麼?問 Ember 就對了。✅ 問題複雜且高度動態:當單一演算法或單一「神腦」無法應對環境的快速變化時,如智慧交通、供應鏈調度。
✅ 需要去中心化決策:當任務可以被分解成多個子任務,且子任務間需要協商與合作時,如災難響應、資源分配。
✅ 系統需具備韌性與可擴展性:單一代理的失敗不應導致整個系統崩潰,新增代理應能無縫融入協作網絡。
⛔ 問題簡單且流程固定:如果只是一個固定的 ETL 流程或簡單的 CRUD 操作,引入多代理無疑是殺雞用牛刀,會帶來不必要的複雜性。
⛔ 缺乏清晰的協作協議:如果無法定義代理之間清晰的溝通語言 (ACL) 和互動規則 (Protocol),它們會變成一盤散沙,甚至互相干擾。
⛔ 對即時性要求達到奈秒級:代理間的協商和通訊需要時間,對於高頻交易這類場景,多代理的通訊開銷可能無法接受。
⛔ 治理風險:當表面上是多代理,但所有決策實質上仍由單一代理或中心節點做出時,就形成了「中心化獨裁者 (Centralized Dictator)」的反模式,失去了去中心化的優勢。
導播,鏡頭拉一下!讓我們看看這場精彩救援在不同尺度下的樣貌。
視角 | 觀念/模式 | 在本篇的說法 |
---|---|---|
微觀 (GoF) | Strategy / Observer / State | 動線策略的即時切換;市民對廣播事件的觀察;電子看板與號誌的狀態流轉。 |
中觀 (EIP/EDA) | Topic/Queue / Outbox+Inbox / Saga | command.* 與 events.* 以 partition_key = run_id 投遞,保證局部有序;重試採 backoff+jitter ,達上限則移入 DLQ 告警。 |
宏觀 (MAS) | DF / ACL / Contract-Net | Aster (總監) 透過 DF (黃頁) 招標,各代理以 ACL (通訊語言) 投標,Rook (仲裁員) 進行仲裁,Sable (守門員) 進行風險審查。 |
圖說小字:同一場域以 partition_key=run_id/venueId 投遞,保障局部有序;DLQ 觸發告警與人工介入流程。
事件包裝(最小欄位)
id | topic | type | ts | key(idempotency) | correlationId | causationId | payload |
---|
硬規:Schema 只增不改;新增欄位需可選或具預設值;破壞性變更 CI 自動退件。
(旁批:事件的發布與消費,嚴格限制在六角架構的 Adapter/邊界層,核心領域 (Domain) 不應依賴任何 Bus 的實作細節。)
讓我們看看 Aster 小隊的行動,如果翻譯成更嚴謹的 Python pseudo code,會是什麼樣子。
# --- 訊息結構 (Kian & Ember 的傑作) ---
# 注意:指令(Command)是意圖,事件(Event)是事實
# 兩者都包含完整的追蹤欄位
run_id = "BOOKRUN-2210"
plan_id = "plan-001"
command_id = "cmd-001"
# Conductor 不直接 publish,而是將指令交給 Executor 服務
# 註:以 partition_key=run_id 投遞,確保同一任務的指令與事件局部有序
command = {
"topic": "command.signage.update.v1",
"name": "signage.update",
"payload": {"message": "請改走安靜帶"},
"key": f"{run_id}-cmd-signage", # 冪等鍵
"correlation_id": run_id, # 追蹤整個任務
"causation_id": plan_id # 追蹤此指令的來源
}
# --- Executor Service (內部包含 Outbox/Relay 機制) ---
# 註:所有發布到事件總線的訊息,其消費者都應實作
# 1. 冪等性接收 (透過 key)
# 2. 帶有 backoff+jitter 的重試策略
# 3. 達到重試上限後,將訊息移至 Dead-Letter Queue (DLQ) 並觸發告警
class ExecutorService:
def execute_and_notify(self, command: dict):
print(f"📥 [EXECUTOR] Received command: {command['name']}")
# 1. 原子化寫入 Outbox (此處簡化)
print(f"📦 [OUTBOX] Saving command to outbox...")
# 2. 執行實際動作 (呼叫 Kian 的 Tool Port)
print(f"⚙️ Calling SignagePort with: {command['payload']['message']}")
# 3. 執行成功後,發布「事件」
event = {
"topic": "events.signage.updated.v1",
"type": "updated",
"payload": command["payload"],
"key": f"{run_id}-evt-signage", # 事件的冪等鍵
"correlation_id": command["correlation_id"],
"causation_id": command_id, # 事件的直接原因是該指令
}
print(f"📦 [OUTBOX] Saving event to outbox for relay...")
print(f"📢 [EVENT BUS] Event '{event['topic']}' published.")
# --- Sable 的兩階段廣播 ---
def two_phase_broadcast(payload: dict):
# 階段一:發布預覽事件,僅供內部審計
preview_event = { "topic": "events.broadcast.previewed.v1", ... }
print("👀 [SABLE] Publishing preview event for audit.")
# ... 經過審批流程 ...
user_approval = True # 假設審批通過
# 階段二:發布正式提交事件,對外廣播
if user_approval:
committed_event = { "topic": "events.broadcast.committed.v1", ... }
print("🚀 [SABLE] Publishing committed event to public.")
# 若事後發現錯誤,只能發布更正事件
# corrected_event = { "topic": "events.broadcast.corrected.v1", "supersedes": committed_event_id, ... }
# --- 執行場景 ---
if __name__ == "__main__":
executor = ExecutorService()
executor.execute_and_notify(command)
two_phase_broadcast({"message": "夜跑延後10分鐘"})
現在,換你來當總設計師了!
如果你是當時的英雄:在 3) 笑中帶淚
的失敗場景中,那個「超級提示詞」導致了災難。如果你是 Aster,你會如何將那個巨大的任務,拆解成至少三個可以交給不同專業代理 (例如:交通、資訊、安撫) 的 Contract-Net 標案?請寫下這三個標案的核心需求。
工具庫的擴充:假設明天城市新增了一種「無人機空中廣播」的新工具。身為 Kian (工具鍛造師),你會如何設計一個新的 DronePort
,並確保它和其他 Port (如 BroadcastPort
) 的介面契約是一致的,以便代理可以無痛切換?
反模式紅旗 🚩:除了文章中提到的,「代理間的閒聊 (Chattering Agents)」:代理們花費大量時間在不必要的通訊和協商上,而不是執行任務。你還能想到另一個多代理協作的「反模式」嗎?例如:「中心化獨裁者 (Centralized Dictator)」:表面上是多代理,但所有決策實質上仍由單一代理或中心節點做出,失去了去中心化的韌性與並行優勢。
今天我們看到的,是多代理系統 (MAS) 在城市治理中的一個縮影。但若我們將鏡頭拉得更遠,你會發現,這正是將我們前 29 天所學的一切——從 GoF 的微觀模式到 EDA 的中觀事件流——升維為一個完整「協作語言」的最後一塊拼圖。
六角形架構的 Ports 成了代理們穩定可靠的工具庫;事件總線是它們唯一的溝通渠道;SRP/OCP/LID 等 SOLID 原則,是它們之間簽訂的契約,保證了可替換性;而 Visitor、Memento、Interpreter 這些模式,則變成了代理們進行「觀測、回放、求值」的基礎設施。
圖說小字:對齊你列的 DoD:一致率、P95 延遲、重複率=0、追溯性 100%。
22 點 10 分,Aster 的小隊輕鬆收隊。指揮中心的大螢幕上,各項指標都回到了綠色安全區。Ember 存檔了這次 run_id
的所有紀錄,並輸出了一份可供隔天複盤的報告。這次行動的成功,可以用幾項關鍵指標來衡量:
人流逆向事件:在 20 分鐘內下降了 92%。
三聲道一致率(看板/社群/廣播):以 SSOT 版本比對,達到 99.8%。
分發延遲 P95:三管道訊息同步的端到端延遲,P95 控制在 5 秒內。
重複命令率:透過冪等鍵 (key
) 稽核,為 0。
決策可追溯性:每一次指令與事件,都包含了 correlation_id
與 causation_id
,審計軌跡 100% 完整。
今天的核心:好的系統不是靠一個超級英雄,而是靠一套允許凡人成為英雄的協作協議。
Codetopia 的故事到此畫下一個句點,但我們的創城之旅才剛開始。感謝各位總設計師 30 天的陪伴,這座城市因為有你們的智慧而更加美好。未來,還會有更多的挑戰與冒險,我們後會有期!
為了確保在不支援 Mermaid 渲染的環境中也能正常閱讀,以下提供文中圖表的 ASCII 替代版本:
┌─────────────────────────────────────────────────────────────────┐
│ DOMAIN CORE │
│ ┌──────────────┐ 只使用抽象 Port │
│ │ Business │ 不認識 Event Bus │
│ │ Logic │◄───────────────────────┐ │
│ └──────────────┘ │ │
└─────────────────┬───────────────────────┬─┴─────────────────────┘
│ │
┌──────────▼──────────┐ ┌─────────▼──────────┐
│ PORTS (抽象) │ │ PORTS (抽象) │
│ • RoutePort │ │ • SignagePort │
│ • BroadcastPort │ │ • DronePort (新) │
└──────────┬──────────┘ └─────────┬──────────┘
│ │
┌──────────▼──────────┐ ┌─────────▼──────────┐
│ ADAPTERS │ │ ADAPTERS │
│ • EventPublisher │ │ • VendorSDK │
│ • Port 實作 │ │ Wrappers │
└──────────┬──────────┘ └─────────┬──────────┘
│ │
└──────────┬───────────┘
│
┌────────────▼────────────┐
│ EVENT BUS │
│ (基礎設施層) │
│ • publish() │
│ • subscribe() │
└─────────────────────────┘
Producer Service Event Bus Consumer Service
┌───────────────┐ ┌─────────┐ ┌───────────────┐
│ Command │ │ │ │ Inbox/ │
│ Handler │ │ Message │ │ Idempotency │
│ │ │────────────►│ Broker │────────────►│ Check │
│ ▼ │ │ │ │ │ │
│ ┌───────────┐ │ │ Topic: │ │ ▼ │
│ │ Outbox │ │ │ events. │ │ ┌───────────┐ │
│ │ (原子化) │ │ │ command.│ │ │ Handler │ │
│ └─────┬─────┘ │ │ │ │ │ │ │
│ │ │ └─────────┘ │ │ │ │ │
│ ┌─────▼─────┐ │ │ │ ▼ │ │
│ │ Relay │ │ 成功 │ │ Side │ │
│ │ Process │ │ │ │ │ Effect │ │
│ └───────────┘ │ │ │ └─────┬─────┘ │
└───────────────┘ │ │ │ │
│ │ ▼ │
│ │ ┌───────────┐ │
重試策略 backoff+jitter │ │ Success │ │
│ │ └───────────┘ │
│ │ │
▼ │ 失敗時 │
┌─────────────┐ │ │ │
│ Dead-Letter │◄────────────┤ ▼ │
│ Queue (DLQ) │ │ ┌───────────┐ │
│ + 告警機制 │ │ │ Retry │ │
└─────────────┘ │ │ (上限後DLQ)│ │
│ └───────────┘ │
└───────────────┘
Aster(Conductor) ──┐
│
▼
┌─────────────────┐
│ 發出標案 │
│ (Contract-Net) │
└─────────────────┘
│
┌───────┴───────┐
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Planner │ │ 其他代理 │
│ Agent │ │ 投標 │
│ 提交計畫 │ │ │
└──────┬──────┘ └─────────────┘
│
▼
┌─────────────┐
│ Sable │ ──→ 風險評估
│ (Critic) │ ──→ 兩階段審查
│ 安全審查 │ ──→ Forward-Fix Only
└──────┬──────┘
│ (核可)
▼
┌─────────────┐ ┌──────────────┐
│ Executor │ ────►│ Outbox │
│ Agent │ │ Service │
│ 執行指令 │ │ │
└─────────────┘ └──────┬───────┘
│
┌────────────────────┘
▼
┌─────────────┐
│ City Event │ ──→ partition_key=run_id
│ Bus │ ──→ 局部有序保證
│ │ ──→ 冪等性處理
└─────────────┘
┌─────────────────────┐
│ Aster (總監) │
│ • Contract-Net 招標 │
│ • 任務分派 │
└──────────┬──────────┘
│
┌──────────────────┼──────────────────┐
│ │ │
┌───────▼───────┐ ┌──────▼──────┐ ┌────────▼────────┐
│ Kian (工具師) │ │ Rook (仲裁) │ │ Sable (安全官) │
│ • RoutePort │ │ • 協商衝突 │ │ • 風險審查 │
│ • SignagePort │ │ • CUPID 權重 │ │ • 兩階段提交 │
│ • DronePort │ │ • 折衷決策 │ │ • Forward-Fix │
└───────────────┘ └─────────────┘ └─────────────────┘
│
┌──────────▼──────────┐
│ Ember (記憶管家) │
│ • run_id 追蹤 │
│ • 決策歷史記錄 │
│ • 可稽核快照 │
└─────────────────────┘
│
┌──────────────────┼──────────────────┐
│ │ │
┌───────▼───────┐ ┌──────▼──────┐ ┌────────▼────────┐
│ Planner │ │ Executor │ │ 其他專業代理 │
│ Agent │ │ Agent │ │ • 交通 │
│ • 方案規劃 │ │ • 指令執行 │ │ • 廣播 │
│ • 策略制定 │ │ • 工具呼叫 │ │ • 資訊同步 │
└───────────────┘ └─────────────┘ └─────────────────┘
BOOKRUN-2210 夜間行動時間線 (18:40 - 22:13)
時間 │18:40 19:00 19:20 19:40 20:00 20:20 20:40 21:00 21:20 21:40 22:00│
│ │ │ │ │ │ │ │ │ │ │ │ │
天候 │▓▓▓▓▓▓▓▓▓▓毛毛雨轉中雨▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│
人流 │ ████████████████人流逆向高峰(P95+)████████ │
│ │
協作 │ ■■■■■Aster招標 │
節點 │ ■■■■■■■■■■Rook仲裁(折衷方案)■■■■■■■■■■ │
│ ■■■■■■■■■■Sable兩階段│
│ ■■■SSOT同步│
│ ■收隊 │
圖例:▓ 環境因素 ████ 問題高峰 ■ 協作行動
┌─────────────┬──────────────────┬────────────────────────────────────────┐
│ 欄位 │ 範例值 │ 說明 │
├─────────────┼──────────────────┼────────────────────────────────────────┤
│ id │ uuid-12345 │ 事件唯一識別 │
│ topic │ events.*.v1 │ 主題名稱(含版本) │
│ type │ updated/created │ 事件類型 │
│ ts │ 2025-10-12T19:45 │ 時間戳記 │
│ key │ BOOKRUN-cmd-001 │ 冪等鍵(去重複) │
│ correlationId│ BOOKRUN-2210 │ 追蹤整個任務 │
│ causationId │ plan-001 │ 追蹤直接原因 │
│ payload │ {...} │ 事件承載內容 │
└─────────────┴──────────────────┴────────────────────────────────────────┘
重要約束:
✓ Schema 只增不改 ✗ 破壞性變更會被 CI 退件
✓ 新欄位可選/預設值 ✗ 不可移除既有欄位
✓ 版本語義化命名 ✗ 不可更改已發布的事件結構