這場示範裡最關鍵的一行,是 Go 那一端——一個用標準函式庫寫的 HTTP server,沒有任何 AI framework 依賴,卻能跟一個 Python ADK agent 同隊跑完整套合規驗證。它們從不曾共用一行程式碼,只是在 HTTP 上講同一套協定。
用 A2A 讓跨語言 agent 同隊協作
A2A(Agent2Agent)想解決的問題只有一句話:讓不同團隊、不同語言、不同 vendor 寫的 agent 能彼此委派任務,而且互不需要知道對方怎麼實作。Google 這篇示範把賭注攤在桌上——一個用 ADK(Agent Development Kit)寫的 Python agent,跟一個純用標準函式庫、不帶任何 AI framework 依賴的 Go service 協作完成合約合規檢查。Go 那端對 LLM、對 Python、對 ADK 一無所知,它只暴露幾個 JSON-RPC endpoint。能讓這兩端拼起來的,不是某個 multi-agent SDK,而是一份 wire-level 的協定規格。這篇要拆的就是這份規格本身:discovery 靠什麼、一次互動的資料怎麼裝、任務在哪些狀態間轉移、為什麼「語言中立」這件事值得當成第一公民來設計——以及它今天到底成熟到哪。
把它跟既有的做法擺在一起,輪廓會更清楚。多數 multi-agent 框架的「協作」其實是 in-process 的:所有 agent 跑在同一個 Python runtime 裡,彼此是物件呼叫,共用記憶體、共用依賴版本。這在單一團隊、單一語言時很順,但一旦你想接進別人寫的 agent——別的部門、別的公司、別的語言——in-process 模型就崩了:對方得用你的框架、你的語言、你的依賴樹重寫。A2A 把協作的邊界從「同一個 process」推到「HTTP 上的兩端」。代價是多了一層 wire 與序列化,換到的是兩端可以各自演化、各自佈署、各自選語言。下面四個小節的順序不是時間軸,是結構——每一層都假設前一層已經到位。
Agent Card:把能力宣告成可被發現的 JSON
一切從 discovery 開始。A2A 的 discovery 不是服務註冊中心,而是一份檔案:Agent Card。規格的權威定義是「a JSON metadata document published by an A2A Server, describing its identity, capabilities, skills, service endpoint, and authentication requirements」——一份由 A2A server 發布的 JSON,描述它的身分、能力、技能、服務端點與認證需求。示範裡這份檔案放在慣例路徑 /.well-known/agent.json,任何 client 先抓這份卡,再決定要不要、能不能把任務交過去。
這個設計的重點在「在不讀對方原始碼的前提下做決策」。呼叫端讀到的最小集合,依示範的說法是「the agent's name, URL, version, skills, and supported input/output formats」。其中 skills 是陣列,每個 skill 帶 id、name、description、tags 與使用範例;input/output 宣告對方收發什麼 media type(示範裡是 application/json)。security scheme 則告訴你要帶什麼憑證。換句話說,Agent Card 把「我會做什麼、怎麼跟我說、怎麼通過我的門禁」全部前置成可機器讀的宣告——這正是「黑箱交接」能成立的前提:你信任卡上的宣告,不要求看內部。下面這張圖把卡片拆成五個欄位群,點任一群看它各自回答什麼問題。
值得停下來想一個對比:這跟 OpenAPI 或 gRPC 的 service descriptor 有什麼不一樣?表面上都是「機器可讀的介面宣告」,但 Agent Card 多了一個維度——skills 不是函式簽章,而是帶 description、tags、使用範例的「能力描述」,刻意留給呼叫端用語意去比對,而不是用嚴格型別去綁定。一個傳統 RPC client 是「我知道要呼叫 ValidateContract(req) -> resp」;一個 A2A 呼叫端則是「我要找一個會做合規驗證的 agent,讓我讀讀有哪張卡的 skill 對得上」。這個鬆綁是刻意的:agent 之間的協作往往不是預先寫死的,而是執行時才決定要把任務交給誰。示範裡的 Go service 卡上 name 是「Security Compliance Validator」、version 是「1.0.0」、skill 帶 tags 與 examples,Python 端讀完就能判斷「對,這就是我要找的那段」。
click any field group to read what it declares · 5 groups
click a field group above
identity · 它是誰
name、description、version、url。示範裡的 Go service 卡上是 name=「Security Compliance Validator」、version=「1.0.0」。version 讓呼叫端能對協定/服務版本做相容判斷。
不回答:它內部用什麼語言、什麼框架實作——這正是 opaque 交接刻意不暴露的部分。
interfaces · 怎麼連到它
service endpoint URL、支援的 protocol binding(示範宣告 JSONRPC)、protocol version。一張卡可宣告多個 supported interface,讓呼叫端挑一個雙方都支援的 transport。
不回答:endpoint 背後是單機還是叢集、怎麼 scale——對呼叫端不可見。
skills[] · 它會做什麼
技能陣列,每項帶 id、name、description、tags 與使用範例。discovery 端就是靠這一段做能力比對:要找「合規驗證」這件事,就看哪張卡的 skill 宣告吻合。
不回答:技能的內部演算法。skill 是介面層的承諾,不是實作描述。
security scheme · 怎麼通過門禁
宣告 authentication requirements。呼叫端據此知道要帶什麼憑證才能把任務送進去——把「能不能呼叫」前置成卡片上的明文宣告,而非試了被拒才知道。
不回答:授權的細部 policy 由 server 自己執行。
capabilities · 它支援哪些選配
input/output modes(支援的 media type)與選配能力旗標——例如是否支援 streaming、是否提供 extended agent card。呼叫端據此決定要不要走串流、要不要拉延伸卡。
不回答:能力是宣告值;真正能不能撐住負載,要靠 Task 跑下去才知道。
這種「opaque 交接」值得拆開看,因為它同時是賣點也是限制。賣點面:規格把設計原則寫得很白——「Agents collaborate based on declared capabilities and exchanged information, without needing to share their internal thoughts, plans, or tool implementations」,並補一句「without needing access to each other's internal state, memory, or tools」。你把任務交出去,對方怎麼做、用了什麼內部邏輯與工具,呼叫端看不到也不需要看到。這對「不同 vendor 的 agent 要協作但又不想互相暴露內部」的場景剛好對味:兩邊的 agent 可以協作完成一件事,而不必把各自的內部實作攤給對方。限制面則是同一枚硬幣的反面:既然彼此不存取內部狀態,呼叫端對對方的內部行為就沒有可見性,只能靠 Agent Card 上宣告的 capability 去推斷「它會不會做、做得對不對」。能力是宣告值,宣告不等於保證——真正能不能撐住負載、結果正不正確,得等 Task 實際跑下去、看它走到 completed 還是 failed 才知道。Agent Card 把信任前置,但把驗證留到執行期。
Task:八個狀態的生命週期
讀完卡、決定委派之後,互動的基本單位是 Task。規格說它是「the fundamental unit of work managed by A2A, identified by a unique ID」——由 server 產生唯一 id,用 contextId 把相關的多次互動串成一條脈絡,status 帶當前狀態與時間戳,artifacts 收結果,history 留往返紀錄。這裡有個落差:Google 示範文裡只點名了四個狀態(submitted、working、completed、failed),但規格本身定義的是八個。這個差距本身就有意義:示範用得到的是一條 happy path,而協定要涵蓋的是含中斷、含拒絕、含取消的完整狀態空間。
把八個狀態分成三類最好懂。submitted、working 是「進行中」;completed、failed、canceled、rejected 是 terminal——走到這四個之一,這個 Task 就結束了;input-required、auth-required 則是「中斷」——任務沒失敗,只是停下來等使用者補資料或補認證,補齊後可以再往前走。這個區分對寫 client 很實際:terminal 狀態要收尾,interrupted 狀態要保留 context 等續傳,而不是當成錯誤重試。把 input-required 誤當 failed 重試,是寫 A2A client 最容易踩的坑——它不是錯,是協定在跟你要東西。下面這張狀態機讓你親手把一個 Task 推過去——點任一狀態節點,看它從哪裡來、能往哪裡去、屬於哪一類。
為什麼一個「把任務交給另一個 agent」的協定需要這麼厚的狀態機?因為它要涵蓋的不是一次 request/response,而是一段可能很長、可能中斷、可能要人介入的協作。對比一個普通的 HTTP 呼叫:你發出去、等一個 response、結束,兩個狀態就夠了。但 agent 委派的任務可能跑幾秒到幾分鐘,過程中對方可能發現缺資訊(input-required)、可能需要你補認證(auth-required)、可能跑到一半被你喊停(canceled)、也可能一開始就回絕(rejected)。規格把這些都顯式化成狀態,client 才有辦法寫出正確的等待與恢復邏輯——而不是把所有非成功都塞進一個 error。auth-required 這個狀態尤其能說明設計意圖:它把 Agent Card 上宣告的 security scheme 從「discovery 階段的靜態宣告」延伸到「執行階段的動態觸發」。卡上說要認證是一回事,任務跑到一半才發現某個子操作需要升級權限是另一回事——協定給了後者一個明確的停靠點。
click a state node to read its role and legal transitions · 8 states
click a state node above
submitted
Task 已被 server 受理、產生唯一 id,但還沒開始處理。這是 message/send 立即回傳 Task 物件、處理在背景啟動的起點。
可往:working(開始處理)、rejected(agent 直接回絕)。
Message 與 Part:一次互動實際傳了什麼
Task 是容器,真正流動的內容裝在 Message 裡。規格把 Message 定義成「one unit of communication between client and server」——一次往返通訊單位,帶 messageId、role(ROLE_USER 或 ROLE_AGENT)、關聯用的 contextId/taskId,以及 parts 陣列。Part 才是內容載體,規格說它是「a container for a section of communication content」,支援四種:text(字串)、data(結構化 JSON)、檔案則可走 raw(base64 內嵌)或 url(外部參照),並可帶 filename 與 mediaType。
這個分型在跨語言場景特別關鍵。Google 示範的講法是「Data travels inside typed Message Parts: TextParts for natural language and DataParts for structured JSON」——自然語言走 TextPart,結構化資料走 DataPart。Python ADK agent 抽完合約欄位,把結構化結果塞進 DataPart 送給 Go validator;Go 端不需要做 prompt 解析,它收到的是型別清楚的 JSON。這一點對「決定性的服務」尤其重要:一個合規驗證器要的是乾淨的欄位,不是要它再去解析一段自然語言。把「需要 LLM 理解的東西」放 TextPart、「結構化、可被一般程式直接處理的東西」放 DataPart,等於在協定層就把「哪些 part 需要智慧、哪些不需要」分了開來——Go 端收到 DataPart 直接 unmarshal 進 struct,完全不必碰任何模型。
任務的最終輸出則收斂成 Artifact——規格定義成「an output generated by agents, composed of Parts」,同樣由 Part 組成,但語意上是「結果」而非「往返訊息」。Message 是對話,Artifact 是交付物,這個區分讓 client 知道哪些東西該存進 history、哪些該當成最終產物收下。一個 Task 跑下來,history 裡可能有好幾輪 Message 往返(包含 input-required 時的補件),但 artifacts 才是你最後要拿去用的東西。把兩者分開,也讓「續傳」變乾淨:中斷後重來,你補的是 Message,不會弄髒已經產出的 Artifact。下表把資料模型的成員、三種 transport binding 與核心 RPC 方法並排。
| 成員 / 項目 | 類別 | 它是什麼 |
|---|---|---|
| AgentCard | discovery | identity / capabilities / skills / endpoint / auth 的 JSON 宣告,慣例放 /.well-known/agent.json。 |
| Task | unit of work | 由 server 產 id、用 contextId 串脈絡,在八個狀態間轉移;artifacts 收結果、history 留紀錄。 |
| Message | 往返單位 | 一次 client↔server 通訊,帶 role、parts、關聯 id。 |
| Part | 內容載體 | text / data(JSON)/ file(raw 或 url);可帶 filename、mediaType。 |
| Artifact | 交付物 | agent 產生的輸出、同由 Part 組成,語意上是 Task 的結果。 |
| JSON-RPC 2.0 | transport | over HTTP、method-based;示範採用的 binding(單一 endpoint)。 |
| gRPC | transport | service RPC 形式的 binding。 |
| HTTP+JSON / REST | transport | RESTful endpoint 形式的 binding。三者須「functionally equivalent」。 |
| message/send | RPC method | SendMessage:發起任務,operation 立即回傳 Task 或 Message。 |
| message/stream | RPC method | SendStreamingMessage:透過 SSE 串流推送增量更新。 |
| tasks/get | RPC method | GetTask:查詢既有 Task 的當前狀態(polling 路徑)。 |
| tasks/cancel | RPC method | CancelTask:請求取消,把 Task 帶往 canceled。 |
更新的傳遞也有三條路。規格強調非同步:「operations return immediately with either Task objects or Message objects, and when a Task is returned, processing continues in the background」——operation 立即回傳,處理在背景跑。要追蹤進度,client 可以 polling(反覆 GetTask)、可以走 SSE 串流(SendStreamingMessage / SubscribeToTask),也可以收 push notification。核心 RPC 方法就是這幾個:SendMessage 發起、SendStreamingMessage 串流、GetTask 查狀態、CancelTask 取消、SubscribeToTask 對既有 task 補建串流。這組方法名直接對映到 wire 上的 message/send、message/stream、tasks/get、tasks/cancel。
三條更新路徑對應三種佈署現實。短任務、簡單 client,polling 就夠——發出去、隔一陣子 GetTask 看看好了沒。要即時把增量結果推給使用者(像把 LLM 的 token 一邊生一邊吐出來),就走 SSE 串流。而當 client 本身是個無法一直掛著連線的環境(serverless function、行動端 app),push notification 讓 server 在任務完成時主動回呼。三種機制共享同一個 Task 模型,差別只在「誰主動、多頻繁」。這也是為什麼規格要把三種 transport binding 定義成「functionally equivalent representations」——JSON-RPC、gRPC、REST 是三種講同一件事的方式,資料模型不變,你可以用 JSON-RPC 跟一端講話、用 gRPC 跟另一端,中間的 Task、Message、Part、Artifact 在語意上完全一致。等價性是互通的前提:少了它,「跨語言」就退化成「只要大家都用同一個 binding」。
跨語言委派:RemoteA2aAgent 把遠端包成本地
把前三節組起來,就能看懂示範那條跨語言 pipeline 為什麼成立。ADK 提供一個叫 RemoteA2aAgent 的抽象,把遠端的 Go A2A service 包成 Python 端可以當「本地 sub-agent」使用的物件。於是 Python 那條 extraction → compliance validation → reporting 的 sequential pipeline 裡,中間那段「compliance validation」表面上像呼叫一個本地 agent,底下其實是一次 A2A Task:讀 Go 端 Agent Card → 發 message/send 帶 DataPart → 等 Task 走到 completed → 收 Artifact。Python 端完全不知道對面是什麼語言寫的——它只知道對方的 Agent Card 宣告了它要的 skill。這個包裝是整套設計的收束點:對 ADK 的編排器而言,一個遠端 A2A agent 跟一個本地定義的 agent 介面一致,都是「丟任務、收結果」的 sub-agent。差異被協定吃掉了。
這正是 language-neutral 協定相對於 framework-coupled SDK 的差別所在。示範把這點講得最直白的一句是「The Python agent doesn't import Go packages. The Go agent doesn't run Python code」——兩端不共用 runtime、不共用依賴,只在 wire 上講協定。如果 Go 那段邏輯要接進一個只認 Python plugin 的 multi-agent 框架,你得把它重寫成那個框架的擴充點;A2A 讓它維持原樣——一個 HTTP server。另一面是 context 的解耦:規格說 agent 協作「without needing access to each other's internal state, memory, or tools」。Go validator 背它自己的 Go module、Python agent 背它自己的 torch 與 SDK,兩端不互相存取內部狀態,也互不綁定版本。第三點,被呼叫的一端不只是被動執行:規格替 rejected 狀態定義「the agent has decided to not perform the task」、可在判定「it can't or won't proceed」時觸發——換句話說,它能回絕,而不是有求必應。委派一個 A2A peer 是發起協作,不是呼叫一個 stateless function。下面這張序列圖把一次委派實際在 HTTP 上傳的東西畫出來。
示範裡還埋了一個值得學的工程細節:resilience。當 Go validator 不可用時,Python agent 的做法是「saves the current state checkpoint and waits for manual approval」——存下當前的 state checkpoint、把無法解決的 case route 到 MANUAL_REVIEW,而不是靜默失敗或把整條 pipeline 炸掉。這在概念上就是把 Task 的 input-required 思路用在 orchestration 層:對方掛了不代表任務失敗,它代表任務需要人介入。能這樣寫,前提正是協定把「中斷」當成一等狀態而非錯誤——你的編排邏輯因此有地方掛載「等人」這件事,而不必自己發明一套 retry-or-die 的土法。
誠實看成熟度,這一段比任何 hero number 都重要。規格頁面標的版本是 Version 1.0.0、託管在 a2a-protocol.org、原始碼在 GitHub 的 a2aproject/A2A。合理的推測是治理已交給中立基金會(Linux Foundation 是公開資訊),但本文引用的兩個來源沒有逐字背書該治理聲明,所以這點我只當推斷,不寫成事實。能直接讀到的事實是協定面:資料模型、八狀態機、三種 transport binding 已經寫進一份自稱 1.0 的規格,成形到值得照規格實作。至於各語言 SDK 各自走到哪一步、誰 GA 誰還在 beta,本文的兩個來源沒有給出可逐字引用的成熟度數字,這裡就不替它斷言——要做 go/no-go,請去官方 SDK 的 repo 看當下的 release 標籤,而不是信任某篇貼文的一句話。示範挑 Python+Go 兩端來搭,至少說明這兩個的官方實作已經穩到可以做端對端 demo。
那麼今天該怎麼用它?如果你手上已經有一個非 Python 的決定性服務——一個 Go 寫的合規驗證器、一個既有的 Java 規則引擎、一段 C# 的計算核心——A2A 給了你一條不必把它改寫成某個 Python multi-agent SDK plugin 的路:給它一張 Agent Card、讓它講 A2A,它就能平等地坐進 agent pipeline。前提是你願意確認自己語言的 SDK 現況、或願意自己照規格實作一端(規格夠完整,這條路是通的)。反過來說,如果你的協作全都在同一個 Python runtime 裡、也沒有跨團隊或跨 vendor 的需求,那 in-process 的框架仍然更省事——A2A 多出來的那層 wire 與序列化是為了「跨」而付的稅,沒有跨的需求就別先繳。
What this enables:把「不同語言、不同 vendor 的 agent 互相委派」從各家 SDK 的私有擴充點,收斂成一份共用的 wire 契約——Agent Card 宣告能力、Task 管生命週期、Part 與 Artifact 裝資料與交付物;一個沒有 AI 框架、純標準函式庫的服務,因此能平等地坐進 agent 團隊裡,只要它願意講這份協定。