iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0
AI Engineering

打造具備記憶與執行能力的常駐 AI Agent:Hermes Agent × Gemini × MCP 的 Harness 設計系列 第 22 篇

【Day 22】已受理但還沒完成:長時操作在無狀態協定下怎麼做

  • 分享至 

  • xImage
  •  

昨天那組工具全部是同步的,送出請求、收到 API 的回應、回傳給模型。控制實體設備時這個形狀不夠用,指令送出到感測值反映出來之間有一段時間,回一句「成功」講的只是 API 收到了。今天把這段差距補上,做法照規範走,並量出規範與手上的 SDK 之間差了什麼。


同步回應表達不了的那個狀態

一次冷氣調溫實際經過四段,前三段在幾百毫秒內完成,第四段要幾秒到幾十秒:

  1. 工具送出 HTTP 請求
  2. API 回 200
  3. 指令傳到設備
  4. 設備動作,感測端點的讀值開始反映新狀態

同步工具在第二段就回傳了。它能報的只有「請求送達」,而使用者問的是「溫度設好了沒」。兩者之間可能出現的落差有三種:

  • 指令收下但設備沒動:API 回 200,設備離線或忽略
  • 設備動了但值不對:送出的值與實際生效的值不同時 API 照樣回 200
  • 設備正在動:回讀到的仍是舊值,並非失敗

三種情況在同步回應裡長得一模一樣。 要分得出來,回傳裡得有一個位置放「目前進行到哪」。


規範把這件事移進 extension

長時任務原本是核心協定裡的實驗功能,2026-07-28 這一版移進官方 extension io.modelcontextprotocol/tasks。changelog 列的改動有四項:

改動 內容
取得結果的方式 阻塞式的 tasks/result 換成輪詢式的 tasks/get
新增方法 tasks/update,讓 client 在任務執行中把輸入送進去
移除方法 tasks/list
觸發方式 server 可以主動回傳 task handle,不需要逐個請求先徵求同意

同一版另外在 ClientCapabilities 與 ServerCapabilities 加了 extensions 欄位,核心以外的功能從此有正式的宣告位置。


SDK 手上有的是另一組

裝起來的是 MCP Python SDK 2.2.0 與 FastMCP 4.0.10。SDK 的 LATEST_PROTOCOL_VERSION 已經寫 2026-07-28,型別那一側對得上的程度是:

規範這一版 SDK 2.2.0 的型別 對得上嗎
tasks/get 輪詢 GetTaskRequest,method 是 tasks/get 有
tasks/result 已被取代 GetTaskPayloadRequest,method 仍是 tasks/result 還在
tasks/list 已移除 ListTasksRequest,method 是 tasks/list 還在
新增 tasks/update 整個套件搜不到這個字串 沒有

TaskStatus 的列舉是五個值,working、input_required、completed、failed、cancelled。Task 這個型別帶七個欄位,task_id、status、status_message、created_at、last_updated_at、ttl、poll_interval。

規範描述的形狀與 SDK 提供的形狀差一個世代。 照 extension 的介面寫下去,tasks/update 那條路現在沒有對應的型別可以用。


規範對「沒有 session 時怎麼保存狀態」另有一條

同一份 changelog 的第一條處理的就是這個問題。協定層的 session 與 Mcp-Session-Id 標頭都被移除之後,需要跨呼叫狀態的 server 走的是另一條路:由 server 鑄造明確的 handle,當成一般的工具參數傳遞。

這條路不需要等 extension 的型別補齊,用的全是既有的工具機制。照它實作出來的形狀是三個工具:

工具 做什麼
set_ac_temp_async 判定通過後送出指令,立刻回傳 handle 與初始狀態
get_control_task 拿 handle 查目前狀態
cancel_control_task 停止追蹤一個尚未確認的任務

狀態值沿用 TaskStatus 那五個字面值,欄位沿用 Task 那七個。介面走工具,語彙跟著規範走,等 SDK 補上 extension 的型別之後,要換的是介面而非狀態機。

背景那一段做的是送出指令後反覆回讀,讀到預期的值就把狀態推到 completed,次數用盡則推到 failed 並在訊息裡寫明送出成功但沒看到預期狀態。


能做非同步的前提是那個值讀得回來

判定完成與否靠的是回讀,因此只有感測端點會回報的量做得成非同步工具。確認的方式是直接打那支端點,把回傳的欄位攤開:

量 感測端點的欄位 做得成非同步工具
冷氣開關與設定溫度 Status、Temperature 做得成
各房間的溫濕度與空氣品質 四項讀值 本來就是讀取,不需要
風扇吹向 每台設備兩個欄位,值是 IN 或 OUT 做得成

安全判定那個模組的 docstring 寫的是感測端點不回目前的吹向,因而無法偵測方向的切換。實際打端點拿回來的內容裡,兩台設備各帶兩個吹向欄位,值就是控制端點吃的那組列舉。註解的說法與端點的實際回傳對不上,判定依據取端點。

這件事往下帶出兩個結果:

  • 風扇這一組也做得成非同步工具,回讀的對象就是那兩個欄位
  • 那條「設定任一具體吹向都要確認」的規則,它寫下的理由已經不成立,規則本身要不要留是另一個決定

回讀比對還有一個細節要處理。控制端點吃的是 ON 與 OFF,感測端點回的是 On,直接拿字串相等去比會一路判定成未完成,比對前得先正規化。

一個量讀不讀得回來,去打那支端點確認,程式碼註解不作為依據


實測一次完整的生命週期

以替身 API 執行,那支 API 設定成指令受理後 4 秒才讓讀值改變。呼叫端每秒查一次:

經過秒數 status status_message
0.0 working 已受理,尚未確認實際狀態
1.0 working 已受理,尚未確認實際狀態
2.0 working 已受理,尚未確認實際狀態
3.1 working 已受理,尚未確認實際狀態
4.1 working 已受理,尚未確認實際狀態
5.1 completed 第 5 次回讀確認到預期狀態

任務自己記的兩個時間戳相差 5.05 秒,第五次回讀命中。背景迴圈設定的間隔是 1 秒,每輪多出來的時間來自一次 HTTP 往返。

完成時的 result 欄位帶兩份資料,sent 是 API 對控制指令的回應,observed 是最後一次回讀到的值,兩者都是 26。判定依據是回讀到的那個值,不是 API 的回應。

確認閘門在送出之前就判定,非同步這條路繞不過去:

呼叫 回傳
set_ac_temp_async(26) 判定通過,回 handle
set_ac_temp_async(20) needs_confirmation: true,沒有建立任務
set_ac_temp_async(35) error: true,沒有建立任務

另外兩條路徑:

  • 取消:狀態立刻變 cancelled,六秒後再查仍是 cancelled,背景迴圈看到狀態已經改掉就停止推進
  • 未知的 handle:回 ok: false 與一行說明,查無此 task_id

取消的範圍限於追蹤這一段。它停的是追蹤,已經送到設備的那道指令不會被收回,狀態變成 cancelled 表示的是呼叫端不再等這個結果。


代價有兩項

第一項是 schema。三個工具加起來 1,608 byte:

工具 schema byte
set_ac_temp_async 679
get_control_task 505
cancel_control_task 424

原本五個同步工具是 3,474 byte,加上這三個是 5,082,增加 46%。調溫這件事現在有兩種做法,模型每一輪都看得到兩套。

第二項是 handle 的壽命。任務表是行程裡的一個字典,ttl 預設 300 秒,超過就把狀態推成 failed。這帶來兩個限制:

  • 行程重啟,全部的 handle 一起消失,模型手上那個 task_id 查不到對應的任務
  • 多個副本各有各的表,同一個 handle 只在鑄造它的那個副本上查得到

協定層拿掉 session 之後,狀態沒有消失,它移到實作裡由自己管。 要跨重啟就得寫進儲存體,要跨副本就得共用一份。

無狀態講的是協定這一層,系統的狀態移到實作裡由自己管


心得

寫之前先去翻 SDK,想照 extension 的介面實作,翻到一半發現 tasks/update 整個套件裡搜不到。那一刻要決定的是等它補上,還是換一條路。

換路的理由來自同一份 changelog。移除 session 那一條後面接著寫了替代做法,由 server 鑄造 handle、當成一般工具參數傳遞。這句話原本讀起來像是給「需要跨呼叫狀態」的 server 的一般性建議,實際上它就是長時任務要的那個東西。

規範文件裡寫得最有用的段落,常常是移除某個東西時附帶的那一句替代做法。刪掉 session 那一條佔的篇幅只有兩行,它決定的事情比整個 tasks extension 還多。

一項功能還沒有實作可用時,規範多半已經把它的職責分給了別的地方


明天

改接一個別人寫的 MCP server,OAuth 的 client 這次不是 agent 自己,組態裡因此少掉兩個區塊。


上一篇
【Day 21】把 REST API 包成 MCP server:5 個工具 3,347 byte,描述佔五成
系列文
打造具備記憶與執行能力的常駐 AI Agent:Hermes Agent × Gemini × MCP 的 Harness 設計 共 22 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言