LOCAL 問:「要幫你整理一則詢問集合地點的訊息嗎?」你回了「好」,活動時間卻剛剛更新。今天讓 Gemini 搭配 Google ADK(Agent Development Kit,用來串起模型、工具與對話流程的 Agent 開發框架)準備確認內容,再由程式核對使用者、內容版本與期限。確認還有效就留下紀錄;資料變了,就帶你看新版。省下的是重新打字,不是省略重要變更。
Day 8 turns a conversational “yes” into confirmation of a specific request. Gemini and ADK prepare the confirmation flow, while application code binds the response to a user, session, request snapshot, catalog version, and expiry. We compare an unchanged request with one whose source changes while the user is deciding. The result is a content-confirmation record; creating the service request follows in Day 9.
這次用 Gemini 搭配 Google ADK 的工具確認(Tool Confirmation),比較「資料沒變」和「等待期間換版」兩個情境。確認回覆由測試程式代替使用者送出,Gemini 與 ADK 則實際執行工具流程;下圖是執行結果的靜態報告,不是已接上後端的網頁按鈕。
| 情境 | 原本待確認的內容 | 送回確認時發生什麼 | 工具結果 |
|---|---|---|---|
| 情境一:正常確認 | 時段 07:30~11:00 的活動時段與集合地點詢問 | 資料版本未變(v-0337e2296139) |
confirmation_recorded(已記錄確認) |
| 情境二:等待中換版 | 同一份舊時段與問題 | 演練目錄已切換至 08:00~11:00(v-62ccd0ef3ca44e6ca8b7ef2d3302ab28) |
version_changed(資料已換版,原確認不接受) |
註:本例的
v-0337e2296139與v-62ccd0ef3ca44e6ca8b7ef2d3302ab28是 Day 8 adapter 指定的演練版本標籤,不是 Day 7 原始歸檔的version_id。這裡比較的是兩份已固定的教學快照是否仍是同一版。
實測中的用量與耗時等實驗數據整理如下表:
實測指標(gemini-3.8-flash) |
情境一:正常確認 | 情境二:等待中換版 |
|---|---|---|
| 模型請求次數 | 3 次 | 3 次 |
| 總 Token 用量 | 2,277 | 2,513 |
| 情境耗時 | 9.13 秒 | 4.76 秒 |
| 模型回覆重點 | 已成功確認草稿內容,下一階段將建立服務單 | 活動資訊已更新,出示變動並請使用者確認新版 |
實測環境為 google-adk 2.9.1 與 gemini-3.8-flash,兩情境總計 6 次模型請求、4,790 Token(預算上限 8 次)。全套 34 項離線回歸測試(25 項核心單元測試 + 9 項 ADK 整合測試)全數通過(PASS,0 外部網路存取)。

圖 1:兩個確認情境的靜態實測報告。上方是程式組成的待確認單,Gemini 負責提出工具呼叫,ADK 串接確認流程;左下為版本未變時的「已記錄確認」,右下為等待中換版後的「資料已換版」。本次確認輸入由測試程式代送,圖中的按鈕只作示意,尚未串接網頁後端。
兩個結果的 execution_allowed 都是 false,表示本篇只處理內容確認。是否確認成功要看各自的狀態與紀錄;真正建立服務請求留到下一篇。
Day 7 把新海報整理進 LOCAL,也讓查詢分得出更新前、待核與採用後。接著換使用者出場:活動查到了,想進一步請服務人員協助,要怎麼接?
這次把前篇花壇場次的活動內容整理成兩份內建教學快照,服務窗口也是測試用的。演練一個需求:「請幫我整理一則詢問,問這場活動的集合地點在哪裡。」
這時 LOCAL 最有用的工作,是把活動與問題整理好,讓你少打一段字。下面是確認畫面的文案示例:
請確認這份詢問內容
活動:卦山大縱走・花壇場次
日期:2026-09-19
活動時段:07:30~11:00
詢問內容:請問這場活動的集合地點在哪裡?
接收對象:LOCAL 教學服務窗口確認內容 修改內容 取消
人看的是活動、日期與問題;程式在背後記住它們對應的版本。今天按「確認內容」會留下確認紀錄,下一篇再讓這份紀錄接到真正的請求建立。
「好」本身沒有問題,問題是系統不知道你對哪一件事說好。 我們先用明確的確認操作保留對象;日後接回 LINE,同樣可以讓自然的對話對應到這份待確認內容。
這篇的 Google 元件是 Gemini 與 ADK 的工具確認流程。
如果你第一次看到這幾個名詞,先抓住三件事就好:
Gemini 根據使用者需求提出工具呼叫,整理想詢問的內容;工具讀取目前採用的活動資料,產生確認摘要。[2] 工具函式中的 tool_context: ToolContext 參數,是 ADK 執行工具時自動提供的環境物件,不是模型或使用者填寫的業務資料。本篇透過它的 request_confirmation() 發出確認請求,收到回覆後再從 tool_confirmation 讀回使用者的選擇。[1][7]
流程可以這樣讀:
使用者說明需求 → Gemini 提出工具呼叫 → 程式組成確認內容 → ADK 等待回覆 → 程式重新核對 → 回傳確認結果。
以下節錄本次版本 adk_bridge.py 的確認流程,省略資料準備與記錄部分;註解補上這幾步在做什麼:
from google.adk.tools import ToolContext
def prepare_handoff_draft(
draft_id: str = "draft-001",
request_text: str = "請問這場活動的集合地點在哪裡?",
tool_context: ToolContext | None = None, # ADK 自動提供,不由模型或使用者填寫
event_id: str = "evt-60d76a55472c503faa4c",
) -> dict:
identity = get_current_identity(tool_context)
confirmation = tool_context.tool_confirmation
if confirmation is None:
# 階段一:建立草稿與待確認單(offer),向使用者出示確認請求
operation = service.get_or_create_operation(
draft_id=draft_id, event_id=event_id, request_text=request_text, owner=identity
)
offer = service.issue_offer(owner=identity, operation=operation)
ev = operation.displayed_event
hint = (
f"【請確認這份詢問內容】\n"
f"活動:{ev.get('name')}({ev.get('area')}・{ev.get('venue')})\n"
f"日期:{ev.get('date')}\n"
f"活動時段:{ev.get('time')}\n"
f"詢問內容:{request_text}\n"
f"接收對象:LOCAL 教學服務窗口"
)
tool_context.request_confirmation(
hint=hint,
payload={
"confirmation_id": offer["confirmation_id"],
"draft_id": draft_id,
"event_id": event_id,
"operation_fingerprint": offer["operation_fingerprint"],
},
)
return {"status": "awaiting_confirmation", "confirmation_id": offer["confirmation_id"]}
# 階段二:使用者已回覆確認,重新核對伺服器最新狀態
confirmation_id = None
if confirmation.payload and isinstance(confirmation.payload, dict):
confirmation_id = confirmation.payload.get("confirmation_id")
if not confirmation_id:
confirmation_id = lookup_confirmation_id_from_session(tool_context)
# 找不到原確認請求時就拒絕,不回退(不改用最新待確認單)
if not confirmation_id:
return {
"status": "not_found",
"execution_allowed": False,
}
return service.validate_and_record(
confirmation_id=confirmation_id,
actor=identity,
draft_id=draft_id,
approved=bool(confirmation.confirmed),
)
lookup_confirmation_id_from_session() 不是找「最新一張」待確認單,而是利用目前工具呼叫的 function_call_id,從 Session 事件找回原本那次確認請求綁定的 confirmation_id;找不到原確認請求時就明確拒絕,不回退(不改用最新待確認單),避免舊回覆誤套到另一份新內容。
這段分成「提出確認」與「收到確認後重新核對」。第一段產生的摘要,必須來自程式保存的同一份操作內容;第二段讀回伺服器紀錄,檢查目前狀態,再決定是否接受。
活動的時間、資料版本與使用者身分,由程式取得;模型負責把問題整理成容易理解的文字。如此一來,模型就算換一種說法,使用者確認的對象仍然明確。
程式把使用者的確認結果包成工具回覆訊息(FunctionResponse),交回 ADK 接續流程。這份回覆的 id 要對上發出 adk_request_confirmation 請求時的 function_call_id,就像用「對號單」找回原本的工具呼叫;LOCAL 的 confirmation_id 則用來找回伺服器保存的那份待確認內容。[1]
只存 confirmed = True 很方便,卻回答不了「誰同意哪一份?」所以程式先建立一張待確認單(本篇程式裡叫 offer),記下這次要確認的內容、版本與對象。
我把確認紀錄分成五個部分:
| 要記住的事 | 本篇的安排 | 為什麼要有它? |
|---|---|---|
| 誰在確認 | 使用者與 Session | 另一個人的回覆不能套到這份內容。 |
| 確認哪件事 | 草稿 ID、操作類型、接收對象 | 區分不同詢問,也避免同意被移到其他用途。 |
| 看過哪份內容 | 草稿修訂版與操作指紋 | 使用者改了問題,也要重新確認。 |
| 根據哪版資料 | 本篇演練目錄的版本標籤與活動 ID | 確認時核對資料是否已換版。 |
| 何時還有效 | 建立時間、到期時間、處理狀態 | 處理久未回覆、取消及重複點擊。 |
圖中的「操作指紋」是把操作內容按固定的 JSON 編碼規則整理後,計算出的 SHA-256。它涵蓋詢問文字、接收對象、草稿修訂版、資料版本與呈現的活動資訊等內容,用來檢查使用者當時看過的那份操作是否仍相同;身分與授權則由應用程式另外核對。[3]
模型可以協助改寫詢問,但最後供人確認的文字與活動資訊,要一起保存。使用者看到「集合地點」,實際記錄的卻是「取消報名」,就算那張單子有確認勾選也沒有用。
因此,這篇由固定摘要用來確認內容,模型回覆另由事件紀錄檢查。這個做法把內容一致性留給程式,也保留模型整理日常用語的方便。
最值得演練的,不是隨便填一個不存在的 ID,而是使用者真的會遇到的等待。
先以內建的 07:30~11:00 快照準備詢問內容。ADK 發出確認請求後,測試程式把演練目錄切換成 08:00~11:00,再送回原請求的確認回覆。
兩件事現在分開了:畫面裡的內容仍是原版,系統已經採用新版。確認入口要讀的是當下的伺服器版本,不是相信按鈕夾帶的舊版本。
在真實實測中,當工具回傳 version_changed 阻斷舊確認後,Gemini 給出的回覆是:
「活動資訊已更新,請留意相關時段與內容有所變動。請確認您是否同意依據最新版本的活動資訊繼續處理此詢問?」
模型捕捉到了變動並主動向使用者詢問,沒有盲目繼續。若進一步配合應用端介面,也可以出示期待的介面文案:
活動時段已更新為 08:00~11:00。我保留了你要詢問集合地點的問題,請看過新版後,再按一次「確認內容」。
這比回一句「版本衝突,操作失敗」有用。問題不用重打,重要變更也能被看見。
同樣地,使用者把「問集合地點」改成「問附近停車位置」,就重新建立確認內容,讓舊的確認識別退出待處理狀態。保存原本的操作脈絡,更新真正改動的部分,這才是確認流程該有的體驗。
這裡開始使用一個很小的規格驅動開發例子。先寫清楚接受條件與反例,再拿它核對 Coding Agent 交付的程式;規格的價值是讓我們知道在測什麼,而非多產生幾頁文件。
本篇核心附在 examples/day08/confirmation.py;以下是其中版本與內容核對的節錄:
if current_catalog_version != record["snapshot"]["catalog_version"]:
record["status"] = "version_changed"
return result("version_changed")
if operation_fingerprint(current_operation) != record["fingerprint"]:
record["status"] = "content_changed"
return result("content_changed")
這幾行是在確認入口重新檢查伺服器目前的資料與操作內容。完整核心也檢查身分、期限、授權,以及取消與重複確認;順利通過才回傳 confirmation_recorded。
從 Repo 根目錄,分開執行純核心邏輯與需要 ADK 的完整離線驗證:
# 只執行不依賴 ADK 的核心測試與示範
python3 examples/day08/test_confirmation.py
python3 examples/day08/demo.py
# 完整離線流程需要已安裝 ADK 的虛擬環境(沿用 Day 5 環境)
PY=examples/day05/.venv/bin/python
$PY examples/day08/verify.py
完整離線測試仍需要 ADK 套件,只是不會使用模型金鑰;這裡沿用 Day 5 的虛擬環境,新環境的準備方式見 README。若需執行真實 Gemini API 實測(需事先設定 GEMINI_API_KEY,嚴格限制單情境最多 4 次、總上限 8 次模型請求):
# 真實模型呼叫(需事先設定 GEMINI_API_KEY)
$PY examples/day08/run.py --live
demo.py 使用合成人物與版本,對照「資料沒變」與「等待中換版」。verify.py 同時驗證核心邏輯與 ADK 事件暫停/接續;而 run.py --live 則使用真實 Gemini 與 ADK 跑完兩種情境並記錄原始 Token 與用量。
本篇小規格:CONTRACT.md,可對照核心測試與 ADK 介接測試一起閱讀。
小規格先守住以下行為:
| 案例 | 預期行為 |
|---|---|
| 本人、同內容、同資料版本、期限內 | 留下這份內容的確認紀錄(confirmation_recorded)。 |
| 等待期間資料換版 | 原確認不接受,回報需要確認新版(version_changed)。 |
| 草稿問題或呈現內容改了 | 原確認不接受,因操作指紋不符需重新確認(content_changed)。 |
| 另一位使用者或另一個 Session 回覆 | 保留原本待確認內容,不接受錯配回覆(wrong_actor)。 |
| 正好到期 | 原確認不接受,需要另行提出新的確認(expired)。 |
| 同一份確認重送 | 回傳既有確認結果與收據,不重複新增(already_confirmed)。 |
這裡說的 Harness(驗收框架),就是把輸入、時鐘、目前版本與預期狀態固定下來,讓每次修改都用同一套條件重新檢查。一般回歸先由 Python 測試跑,模型整合再看 ADK 的真實事件。測試裡的同意輸入是明確布林選擇,不用模型猜一句話到底算不算接受。
確認畫面可以先提醒使用者;真正接收確認的函式仍須獨立檢查。同一條規則應同時適用於畫面操作與直接送來的請求。[4]
本篇的 ConfirmationStore 接受呼叫端提供的身分與授權結果。離線測試中的 demo-user-a 是模擬身分,用來檢查兩個使用者是否被分開;它不等於完成真實登入驗證。
後續接到 LINE 時,使用者身分要來自已驗簽的事件,由服務端建立 Session 對應。確認內容、目前採用版本與期限,也從服務端記錄取得,不交給模型或前端欄位決定。[5]
ADK 的 Session 可以保存對話中的工作狀態,但本篇的確認規則另外放在應用層。對話怎麼串接可以改,誰確認過哪一份內容的判定仍有自己的入口。[6]
今天先留下確認紀錄,輸出明確標示 execution_allowed: false。它是 Day 9 的前置資料:下一篇建立請求時,仍要核對權限、有效內容與操作識別,才會真正寫入後端。
這一版以整份演練目錄的版本標籤判定資料是否改變,而不是只看單一欄位。做法容易理解;將來接入多場活動的目錄,其他場次更新也可能讓使用者重新確認。
將來可以縮小到所引用活動或欄位的版本,減少沒有必要的重新確認。但先把目前的版本綁定、操作內容與確認結果看清楚,才有基準比較哪種做法更省事。
本篇先在單一行程中,用記憶體 Session 完成演練。若服務在等待確認時重啟,記憶體裡的對話狀態就會遺失;如何保存狀態、在重啟後接續確認,留給後面的持久化篇。ADK 官方目前仍將工具確認標為實驗性功能,也列有 Session 服務限制;今天對照已安裝版本驗證,不把它直接當成跨服務的交易機制。[1]
Day 7 教 LOCAL 管理資料版本;Day 8 把版本帶到使用者確認,讓一段自然對話能對應到清楚的操作內容。
這篇要帶走的方法是:Gemini 幫忙整理,ADK 串起確認,程式保存並核對同意的對象與資料版本。 成功路徑讓人少打一段字,資料異動時則保留問題、只請人重看必要變更。
下一篇接上 create_handoff_request:內容確認之後,怎麼真的建立一筆請求、拿到編號,而且重送時仍然是同一筆?
你最常遇到哪種「好」說得太快的情境:活動時間更新、詢問內容改了,還是同時聊著好幾件事?
前篇:Day 7|看懂不等於可信:來源、版本與不知道。系列與公開程式:local-service-agent-for-line。
本篇程式與驗證入口位於:
核心確認服務與操作指紋:examples/day08/confirmation.py
核心規格與驗收條件:examples/day08/CONTRACT.md
Google ADK 工具介接層:examples/day08/adk_bridge.py
離線核心與 ADK 整合測試(34 項):examples/day08/verify.py
真實 Gemini API 實測執行入口:examples/day08/run.py
人機確認靜態實測報告:examples/day08/CONFIRM.html
完整操作說明:examples/day08/README.md 與文件 docs/day08/README.md
[3] Python:hashlib。
[6] Google ADK:State。