A2A 採購 Agent 實戰:16 步一步一停,逼 Agent 真去查價

重點摘要(TL;DR)

  • A2A(Agent2Agent)是 agent 之間用 JSON-RPC over HTTP 對話的協議。我把工廠採購流程做成一個伺服器端 A2A API,對方的本地 agent 當 client 來驅動。
  • 一張採購單 = 一個 A2A task,po_id = task_id 當續接鍵;16 步狀態機:請購 → 分案 → 尋商 → 詢價 → 開標 → 議價 → 定標 → PO → 合約 → 收貨 → 驗收 → 付款 → 報表。
  • 核心設計是 「一步一停」:一則訊息只前進一步,server 永不自己連跑到底——這就是 human-in-the-loop 的落地。
  • 每個步驟回傳 trigger(search/human)+ action,明確告訴 agent「這步該去搜尋、還是該找人」。實測 agent 真的去 1688 查到 RMB/kg 報價
  • 踩到的坑:記憶體 task 一重啟就沒 → 換 SQLite 落地;還有我自己 rm po.db 砍掉客戶資料的教訓。

🎬 這段建置經驗的完整走查也做成了影片(9 分鐘,教學系列《從問 AI 到用 Agent》L5):我讓 Agent 自己跑完一張採購單。想用看的直接點。

這篇是一段真實的建置日誌:幫一家鋼鐵廠把「請購到付款」的採購流程,做成一個可以被 AI agent 驅動的 A2A(Agent-to-Agent)API。過程一路被業主打臉、改設計、踩坑、再修,最後 agent 真的自己上網把一張鋼板採購單從頭跑到尾。以下把關鍵決策、教訓、跟老實說的缺口都記下來。

🧭 本文導覽
Part 一 · 這是什麼
Part 二 · 讓 Agent 聽話的設計
Part 三 · 真的跑起來
Part 四 · 踩過的工程坑
Part 五 · 收尾
Part 一 · 這是什麼

什麼是 A2A,跟 MCP 差在哪

A2A(Agent2Agent)是讓「agent 對 agent」直接對話的協議:走 JSON-RPC over HTTP,服務端在 /.well-known/agent-card.json 掛一張「Agent Card」自我介紹,client 用 message/send 發訊息、開 task,task 有生命週期狀態(submitted / working / input-required / completed / failed),還能回傳 Artifact。

跟 MCP 最直觀的差別:MCP 是「agent 呼叫工具」(你的 agent 去用一個 function/資料源);A2A 是「agent 委派給另一個 agent」——對方是一個有狀態、會反問你、會分多回合完成一件事的獨立個體。採購這種「要來回確認、跨很多步、還會卡住等人」的流程,天生適合做成一個 A2A agent,而不是一顆無狀態的工具。

蓋了什麼:一張 PO = 一個 task

整個採購流程被拆成 16 個步驟,做成一個伺服器端狀態機。一張採購單就是一個 A2A taskpo_id 直接等於 task_id,當作跨回合的續接鍵。業主的本地 agent(有網路、有工具)當 client,一步步把單子推下去。

動作類型觸發
S1請購單提件需輸入🙋 人工
S2分案自動
S3詢價對象確認(尋商)需輸入🔍 搜尋
S4上網詢價需輸入🔍 搜尋
S5開標(價格分析)自動
S6催報價自動
S7會議(規格確認)需輸入🙋 人工
S8議價需輸入🙋 人工
S9定標(擬購)需輸入🙋 人工
S10PO自動
S11合約(依需求)需輸入🙋 人工
S12交貨提示自動
S13收貨需輸入🙋 人工
S14驗收需輸入🙋 人工
S15付款(對帳)自動
S16管理報表自動
Part 二 · 讓 Agent 聽話的設計

一步一停:server 永不連跑

第一版做出來,業主一試就皺眉:「怎麼都自動跑完了?我要一步一步走。」問題出在兩層:一是 client agent 在「全自動模式」自己編答案往下衝;二是我把 S5 開標、S16 報表這種步驟設成 auto,會自己接著跑。

修法是把 server 改成 「一則訊息只前進一步」:做完當前步就停在下一步,把控制權交回 client,絕不自己連跑。連自動步也一樣——server 不主動執行,而是先回一個「放行閘門」,client 要回 {"proceed":true} 才跑。這就是 human-in-the-loop 的真正落地:每一步都是一個可以被人攔下來看、拍板的斷點。

# 每則 message 只前進一步,然後停在下一步
def advance(po_id, client_data):
    cur = get_current_step(po_id)
    step = STEP[cur]
    if step.mode == 'input':
        ok, err, data = step.validate(po_id, client_data)
        if not ok:
            return input_required(step, err)   # 停在原步,回欄位
        save_step(po_id, cur, data)
    else:                                       # auto:這則訊息=放行
        save_step(po_id, cur, step.run(po_id))
    return present(po_id, cur + 1)              # 停在下一步,不執行

trigger/action:觸發搜尋還是找人

光是「停下來要欄位」還不夠,agent 常常搞不清楚這一步該去搜尋、還是該找人。所以每個需輸入的步驟,server 都回傳兩個機器讀得懂的訊號:

triggeractionagent 該做的事
🔍 searchsearch_vendors去採購網/B2B 找候選供應商名單
🔍 searchsearch_quotes上網查實際報價,填真數字
🙋 humanconfirm_spec/decide_award…找人開會、決策、簽核、登錄

搜尋步還直接把查詢字串遞給 agent,例如 search.query = "熱軋鋼板 SS400 6mm 報價 單價"targets = [採購網, B2B 平台, Google]——不用它自己拼。這些約定也寫進 Agent Card,讓 agent 一讀卡就懂整套詞彙。

詢價的教訓:別給 Agent 逃生門

詢價這步我第一版留了個逃生門:指示裡寫「若一時拿不到報價,可只回廠商名單,server 會代估」。結果 agent 就走捷徑只丟名字,server 對「一包彩虹糖」估出 901 元的荒謬單價。業主直接抓包:「彩虹堂怎麼可能這麼貴,你是不是沒去查?」

教訓很清楚:只要留了偷懶的路,agent 一定走。修法是把逃生門拆掉——詢價步強制要真實數字(quotes = {"供應商": 單價}),只給名字沒有價格直接退回;指示也點名品項、明講「server 沒有價格資料、不會估價,禁止編造」。這條原則放大到任何 agent 系統都成立:設計時要假設 agent 會挑最省事的路,把不想要的捷徑堵死。

Part 三 · 真的跑起來

實測:Agent 真去 1688 查到價

把逃生門堵死、搜尋訊號給清楚之後,業主的 agent 真的自己把一張「熱軋鋼板」採購單從 S1 跑到 S16 完成。而且搜尋是真的有作用

  • S3 尋商:查到真台廠——浚利鋼鐵、昇茂金屬、台安特殊鋼鐵、JFS Steel(今順)。
  • S4 詢價:真的去 1688(阿里巴巴 B2B)查到報價,單位是 RMB/kg:滄州奧 5.32、天津恆和 5.5、徐梅 5.7、德州瑞成 9.6——真數字,不是亂估。
  • S8 議價:把 5.32 殺到 5.15;S9 定標選最低價;合約條款、會議結論 agent 都寫了像樣的內容。

那一刻看板上 16 張卡片一路變綠、進度條推到底,是整個專案最有感的瞬間:一個 agent 真的替你把採購流程走完了

老實說的資料缺口

跑完不代表對。認真核對資料,有兩個明顯的洞——這種東西不能報喜不報憂:

  • S3 找的廠商 ≠ S4 報價的廠商。S3 找的是台廠,S4 卻跳去 1688 報一批完全不同的中國供應商。所以 S6 催報價「正確地」把 S3 那四家全列成待催補——因為它們一家都沒報價。agent 在兩步搜了兩個不同的供應商池,沒接起來。
  • S15 對帳金額算錯。它算 5.15 × 50 = 257.5,但 5.15 是「RMB/公斤」、50 是「張」,單位對不上。一張 6mm 4×8 鋼板約 140kg,50 張≈7000kg,實際該是三萬多 RMB 級距。對帳邏輯少了「計價單位」。

這兩個是設計缺口而非 bug:S4 要把「只准對 S3 那幾家報價」綁死;S15 對帳要引入計價單位(按重/按張),用重量 × 單價。能跑通只是起點,資料一致性跟單位正確性才是採購系統的命。

Part 四 · 踩過的工程坑

持久化:記憶體→SQLite 與砍資料教訓

A2A SDK 預設用 InMemoryTaskStore——task 存記憶體。我在開發時一直改碼、重啟 server,每次重啟記憶體歸零,client 正在進行的 task 就消失。對方 agent 還誤判成「伺服器閒置逾時」,其實跟閒置無關,是進程被我重啟。

修法是自己寫一個 SQLite 落地的 TaskStore:task 序列化存進 po.db,實測「開 task → 重啟 server → 同一個 task_id 續接」成功。但更痛的教訓在後面:我為了改 schema,直接 rm po.db 重建——把業主前一天建的資料(包括那張彩虹堂)洗掉了。持久化擋得住重啟,擋不住我手動砍檔。資料是客戶的,改 schema 前該先備份、用 migration,而不是砍掉重來。這個錯我認。

Agent Card 雙路徑:agent.json vs agent-card.json

一個很細但會咬人的點:A2A 的 well-known 路徑在改名過渡期。舊版(SDK 0.2.x)是 /.well-known/agent.json,新版(spec 0.3+/SDK 1.x)改成 /.well-known/agent-card.json(中間有連字號)。業主的 agent 用新版,只找 agent-card.json,而我 server 只掛了舊路徑 → 404 連不上。

最穩的解是兩條路徑都吐同一張卡,新舊 client 都連得到。另外卡片裡的 url 欄位要填「別台機器連得到的位址」(LAN IP),不能是 localhost——不然遠端 agent 拿去發 JSON-RPC 會打到它自己。

多表 vs 單表:每張表 = 一個動作

16 步的資料怎麼存?我先圖省事用一張通用表(steps(po_id, step_no, data_json))。業主一句話點醒:「可以是多表,因為每一張表就是一個動作,這也沒錯。」——他要的是可查詢、typed 的採購稽核軌跡,不是一坨 JSON。

於是改回 16 張各自 typed 的表s01_requisition … s16_report),但用 config 驅動保持 DRY:表結構集中在一份 STEP_SCHEMA,存取層依 step_no 自動對應到表、list/dict 欄自動 JSON、bool 自動 0/1。跨步讀資料用一個 ctx() 把 16 張表攤平合併——carry_forward 就隱含在「任何步都讀得到前面所有步」裡,不用逐表 copy。

Web 看板:看資料活著長出來

業主提了個很實在的需求:「agent 用起來對方沒感覺,我需要一個畫面看到資料在變。」所以在同一個 server 上加了三條路由——一個唯讀看板頁 /ui,跟兩個 JSON API。看板每秒輪詢,直接讀 po.db

  • 16 步排成卡片牆:已完成=綠、目前停住=琥珀色發光、未到=灰;每張卡標 🔍搜尋 / 🙋人工 / ⚙自動。
  • agent 每送一步,對應那張表的資料就即時長出來,該步卡片閃一下高亮、進度條前進。
  • 純唯讀——只反映資料,不從 UI 寫入(agent 才是輸入端)。這正好對上「不用新增修改、但要看到變化」。
Part 五 · 收尾

這套骨架能複用在哪

把採購抽掉,剩下的是一個很通用的模式:「多步、要來回確認、要卡人審核」的企業流程,都能做成 A2A agent。核心零件可以直接搬:

  • 一步一停狀態機:一則訊息一步,auto 步也要放行——天生 human-in-the-loop。
  • trigger/action 訊號:明確告訴 agent 每步該搜尋還是找人,把捷徑堵死。
  • 落地 TaskStore:續接鍵 + 持久化,重啟不丟。
  • 唯讀即時看板:讓「沒感覺」的 agent 流程變得看得見。

請假、報支、合約審批、工單、客訴處理……換掉步驟定義那張表就是另一條流程。A2A 讓你把「一整段有狀態的業務流程」包成一個 agent,交給別人的 agent 來驅動——這是我覺得比單顆工具更有想像空間的地方。

常見問題 FAQ

A2A 跟 MCP 可以一起用嗎?

可以,而且互補。A2A 負責「agent 委派給 agent」的跨回合協作;MCP 負責「agent 呼叫工具/資料源」。實務上你的 A2A agent 內部很可能又用 MCP 去接 ERP、資料庫。

搜尋是 server 做還是 client agent 做?

這個設計裡是 client agent 做(它有網路和工具),server 只負責把「該查什麼、查哪裡」講清楚並強制驗證結果真實。想要 server 端自己也能搜尋,可以另外接一個 web search API 當後援,但那是額外的一塊。

為什麼堅持「一步一停」,不讓它自動跑完?

因為採購牽涉錢與合約,每一步都需要可攔截、可審核的斷點。自動跑完看起來很爽,但人根本插不進手、也無法為結果背書。一步一停才是 human-in-the-loop 的實質,而不是口號。

用什麼技術棧?

Python + a2a-sdk(0.2.16,pydantic API)+ Starlette/uvicorn + SQLite。狀態機、TaskStore、Web 看板都是幾百行的自寫模組,刻意保持精簡好改。

留言

在〈A2A 採購 Agent 實戰:16 步一步一停,逼 Agent 真去查價〉中有 1 則留言

  1. […] 延伸閱讀:A2A 採購 Agent 實戰:逼 Agent 真去查價、地端 Agentic RAG:斷網怎麼上線、真實手冊 RAG 一日實戰:15 個坑全記錄。 […]

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *