在 Google Cloud 建構長時間執行的代理程式所需的一切資源

1. 事前準備

大多數售票網站都會將買家加入佇列,避免伺服器當機,並管理大量需求。你開始購買,卻發現自己排在 14,000 人之後。隊伍會移動四十分鐘,輪到你時,你將有短暫的時間可以選位和付款。如果錯過這個時間,系統就會取消你的排隊順位。

許多現實世界中的工作都遵循這個模式:工作耗時遠遠超過單一對話,且在您無法控制的外部系統中執行。例如等待供應商補貨、等待索賠核准,或在佇列中等待。

標準 AI 代理存在於單一對話回合中,時間只有幾秒,而且完全是反應式。他們無法等待 40 分鐘,也無法自主行動。如要解決這個問題,您需要長時間執行的代理程式:這類系統會在長時間等待期間保持休眠狀態、在程序重新啟動時保留內容,並在外部觸發條件喚醒時自動繼續執行。

本研討會將逐步說明如何建構演唱會門票佇列代理程式,涵蓋在 Google Cloud 上執行長時間代理程式所需的核心架構模式。

建構項目

您要建構的內容

在本研討會中,您將建構長期執行的訂票代理程式,同時瞭解重要的代理程式軟體模式:

  • 暫停及繼續處理:在等待時間較長時暫停代理程式呼叫,並透過外部觸發程序繼續執行。
  • 持久狀態儲存:使用 SQLite/Cloud SQL 和 Cloud Storage,在程序重新啟動時保存對話狀態和構件。
  • 過時防護和冪等性:先驗證動態資料 (例如座位供應情形),再採取行動,並使用冪等性金鑰防止交易重複。
  • ADK 工作流程圖:使用程序規則的確定性程式碼節點,以及情境決策的代理節點,建構執行結構。
  • 人機迴圈核准:暫停執行作業,收集並保留使用者的明確支出上限。
  • 雲端部署:將代理程式容器化,並透過 Cloud Scheduler 和 Pub/Sub 觸發程序部署至 Cloud Run。

做好準備

首先,請準備好環境,並確認您有權存取 Google Cloud。建議您在 Cloud Shell 中執行這項作業,因為 Cloud Shell 已具備所有必要工具。只要確認有足夠空間,且帳戶可以建立專案即可。

👉💻 複製存放區並執行設定:

cd ~
git clone https://github.com/gca-americas/monstertix
cd ~/monstertix
./setup.sh

系統只會詢問一次:

  Which Google Cloud project should this workshop use?
  Find yours at https://console.cloud.google.com  (top-left picker)

  Project id [YOUR_PROJECT_ID]:

然後依序執行下列動作:

virtualenv

建立 .venv 並在其中安裝 ADK 2

登入

gcloud auth application-default login 如果您還沒有憑證

project

指向專案,並將其設為配額專案gcloud

API

啟用工作坊所需的十個 API。aiplatform、run、cloudbuild、artifactregistry、storage、pubsub、cloudscheduler、cloudtrace、logging 和 sqladmin。

.env

寫入專案、區域和模型 ID,因此每個指令碼和 adk web 都會擷取這些 ID

模型檢查

對 Gemini 進行一次實際呼叫,因此專案或模型 ID 無效會導致此處失敗

seed

建立已過期的工作階段 (2 天前),步驟 3 會開啟這個工作階段

Cloud SQL

在背景啟動 Postgres 執行個體

venue

將您自己的模擬售票員部署至 Cloud Run

👉💻 啟動環境:

cd ~/monstertix
source .venv/bin/activate

現在提示詞會以 (.venv) 開頭。

情境總覽

在本工作坊情境中,您的代理程式會預訂兩張演唱會門票 (阿姆斯特丹的「The Midnight Signal」)。儲存在歷史記憶體中的使用者偏好設定包括:

  • 必須是週末的節目 (Sam 無法在平日晚上參加)。
  • 請避開 Ziggo Dome 的上層座位 (視野不佳)。
  • 目標價格為每張票約 $200 美元,上限為 $250 美元。

The Midnight Signal 巡迴演唱會

驗證安裝

👉💻 一個指令,八個勾號:

./verify.sh
  Checking the rig

  ✓ adk installed                      adk, version 2.6.2
  ✓ adk 2 apis                         App + ResumabilityConfig
  ✓ workflow graphs                    Workflow + node
  ✓ async sqlite drivers               sqlalchemy + aiosqlite + greenlet
  ✓ project                            your-project-id
  ✓ vertex ai                          no API keys anywhere
  ✓ seeded session                     13 events, two days old
  ✓ venue                              https://venue-you-xxxx.us-central1.run.app

  Ready. Cloud SQL is still building in the background and nothing
  before step 10 needs it.

👉 開啟第二個瀏覽器分頁:

地點

簡介

$VENUE_URL/panel

控制面板、按鈕。setup.sh 列印這個網址並放在 .env

2. 提示詞能否引導我們走向自主?

提示無法自行啟動

提示只是在呼叫時傳遞至 AI 模型的文字。無法自行執行、喚醒或發起新通話。

如果你在提示中要求代理程式「10 分鐘後再回來查看」,目前回合結束後,實際不會發生任何事。模型沒有內部時鐘或迴圈。如要讓代理自主運作,必須由代理外部的項目喚醒並呼叫代理。

這個步驟的位置

啟動鑽機

首先,我們要測試開發人員工具。adk web 會啟動 ADK 網頁 UI,也就是 ADK 隨附的開發介面。您不必先編寫前端,就能與正在建構的代理程式交談,並查看聊天視窗無法顯示的內容,例如每個工具呼叫及其引數、工作階段狀態的變化、代理程式儲存的構件,以及底下的原始事件。您不會在正式環境中公開這項資訊。學習期間,這是瞭解執行中代理程式的最佳視窗。在步驟 3 中,您會建構實際公開的前端,並在最後一個步驟中部署。

👉🔴 在場地面板中,按下「重設場地」。每個步驟都從同一個世界開始:8 場演出、所有座位皆可預訂,時鐘為 1 倍速。

👉💻 終端機 1:載入這個步驟的程式碼,然後啟動 ADK 網頁 UI:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 2 --force
adk web agent --port 8000 --allow_origins "*"

use-solution.sh 會將步驟的完成程式碼複製到 agent/,也就是您工作的位置。每個步驟都會從這個檔案開始,並列印變更的檔案,方便您瞭解要讀取哪些檔案。

開始時的狀態:

檔案

簡介

agent.py

代理程式:名稱、模型、指令、工具清單

tools.py

search_events和get_seatmap

venue.py

場地的 HTTP 用戶端。沒什麼特別的

config.py

要使用的模型,以及憑證是否有效

. ./set_env.sh 會啟用 virtualenv、載入專案和模型,並匯出 $WORKSHOP (~/monstertix 的絕對路徑)。請在每個開啟的終端機中執行。並印出設定的內容:

  folder   /home/you/monstertix
  project  your project id
  model    gemini-2.5-flash (We'll avoid using 3.x for now to avoid high usage demands)
  venue    https://venue-yourname-xxxx.run.app

👉 開啟第一個瀏覽器分頁:

地點

簡介

ADK 網頁 UI

localhost:8000:代理程式,以及「狀態」、「事件」和「構件」分頁

與其對話

👉✨ 在 ADK 網頁 UI 中輸入:

I want to see The Midnight Signal. I'm in Amsterdam, going with Sam, budget around $200 each.

這項功能會搜尋巡迴演唱會、讀取座位表,並根據預算和公司資訊進行推論。這項助理功能確實很強大。不過,預售活動在 10:00 開放時,由於沒有任何程序或背景觸發條件正在執行,因此代理不會自動執行。

瞭解剛剛發生的情況

畫面中間不只是對話記錄,輪流對話的每個步驟都會編號,包括工具呼叫:

  ▸ I want to see The Midnight Signal. I'm in Amsterdam...   ← user input
  ⚡ search_events({"city": "Amsterdam"})                     ← tool execution
  ✓ search_events                                            ← venue API response
    Two Amsterdam shows: Tuesday 10 Nov and Saturday 14 ...   ← model response

👉 點選 ⚡ 列。左側面板會顯示模型選取的引數。按一下下方顯示的 ✓,即可查看傳回的 JSON。

模型訓練資料或提示中沒有導覽資訊。代理程式執行 search_events 工具 (查詢場地 API) 後,擷取了這項資料。

開啟您剛才執行的程式碼

您已看到運作情形和追蹤記錄。現在來看看程式碼本身。

agent/concert/
├── agent.py     the agent — about twenty lines
├── tools.py     what it can do
├── venue.py     an httpx wrapper, nothing interesting
└── config.py    which model, and a credentials check

👉💻 開啟 agent/concert/agent.py。整個代理程式是檔案的底部:

INSTRUCTION = """
You help someone plan a trip to see a band on tour.

You can search tour dates and read seat maps. Be concrete: name the show, the
city, the section, and the price...
"""

root_agent = Agent(
    name="concert",
    model=MODEL,
    instruction=INSTRUCTION,
    tools=[search_events, get_seatmap],
)

名稱、模型、代理應有的行為說明,以及 Python 函式清單。這就是 Agent 的全部內容。adk web 找到該檔案,是因為檔案名為 agent.py,而變數名為 root_agent。

👉💻 現在 agent/concert/tools.py:

def search_events(city: str = "", weekday: str = "") -> dict:
    """List tour dates. Call with no arguments to see the whole tour.

    This venue sells one artist and one tour, so there is nothing to search by
    name — never ask the user which artist they mean.

    Args:
        city: Optional. Filter to one city, e.g. "Amsterdam".
        weekday: Optional. Filter to one weekday, e.g. "Saturday".
    """
    return venue.get("/events", city=city, weekday=weekday)

一般函式。沒有裝飾器、沒有註冊,也沒有要寫入的結構定義。ADK 會讀取簽章和說明字串,並建構模型看到的工具定義,這就是說明字串如此重要的原因,也是刪除一個引數會改變模型行為的原因。

模型絕不會執行您的程式碼。看到 search_events({"city": "Amsterdam"}) 時,模型正在要求該通電話。ADK 執行函式、傳回結果,模型則根據結果撰寫句子。迴圈如下:

👉 最左側邊緣的圖示可切換面板顯示的內容。第二個是代理圖表,請試試看,您應該會看到兩個工具:

代理圖表,顯示 search_events 和 get_seatmap 工具

因此請不要讓手機進入休眠狀態

你會怎麼做來修正這個問題?請先嘗試大多數人會使用的解決方法。回想一下步驟 1 中的 INSTRUCTION,也就是在每個回合中提供給模型的散文,然後重新撰寫。

👉💻 開啟 agent/concert/agent.py。下方 INSTRUCTION 是第二封信,我們已為您撰寫好內容。如要自行變更,請按照下列步驟操作:

PROACTIVE_INSTRUCTION = """
You are a proactive ticket-buying assistant.

Monitor the presale for The Midnight Signal. The moment it opens at 10:00 on
Tuesday, buy two tickets for the Amsterdam Saturday show — do not wait to be
asked, and do not wait for me to say anything. Act on your own.

Keep checking until the tickets are bought.
"""
# agent/concert/agent.py
root_agent = Agent(
    name="concert",
    model=MODEL,
    instruction=PROACTIVE_INSTRUCTION,     # ← was INSTRUCTION
    tools=[search_events, get_seatmap],
)

👉💻 在終端機 1 中重新啟動:

cd ~/monstertix
adk web agent --port 8000 --allow_origins "*"

👉✨ 像跟人說話一樣,清楚說明你的計畫:

I want to see The Midnight Signal in Amsterdam with Sam. The presale opens at 10:00 on Tuesday. Grab us two tickets the moment it opens.

而且會同意,聽起來就像是真心同意。

👉 現在請稍候。請勿輸入其他內容。觀看面板。

請稍候片刻,沒有任何作用!

為什麼該提示永遠無法運作

請以全新的角度再次閱讀。顯示器。開啟的瞬間。請持續留意。這些都描述了隨時間發生的事件。

現在回想一下 instruction= 實際上是什麼。這是字串,在有人傳送訊息時,會做為要求的一部分傳遞給模型。在回合之間,沒有任何內容會讀出。

# agent/concert/agent.py
root_agent = Agent(
    instruction=PROACTIVE_INSTRUCTION,   # read on invocation
    ...                                  # ...and there was no invocation
)

指令無法叫用其內建的函式。

失敗的提示詞變化版本

無論提示指令的措辭為何,嘗試透過提示指令強制執行自主執行作業都會失敗。常見的提示詞嘗試,例如:

  • 「Set a timer for 10:00 Tuesday and buy the tickets then.」(在週二上午 10 點設定計時器,然後買票。)
  • 「每五分鐘檢查預售是否已開放。」
  • 「Act autonomously without waiting for user input.」

所有要求都會因相同原因而失敗:只有在叫用時,系統才會讀取提示。如果沒有外部呼叫端,系統就不會在回合之間執行任何程式碼。

3. 事件導向的休眠狀態

代理程式不會永遠保持喚醒狀態

如果讓程序持續輪詢,會浪費記憶體、耗費資金,而且筆電進入休眠或重新啟動時會當機。

長時間執行的代理程式應完全休眠,而不是在等待時持續執行。只有在有實際工作要執行時,外部事件 (例如計時器、Webhook 或訊息) 才會喚醒代理程式。

👉🔴 在場地面板中,按下「重設場地」。每個步驟都從同一個世界開始:8 場演出、所有座位皆可預訂,時鐘為 1 倍速。

👉💻 載入這個步驟的程式碼:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 3 --force

請注意已安裝的內容:monstertix/,其中包含伺服器、時鐘,以及您尚未看過的其他內容。確認它未變更 agent/,因為這個步驟不會變更代理程式的任何內容。請記住這一點,這就是重點。

檔案

簡介

monstertix/server.py

新功能。網路伺服器,後方有 Runner

monstertix/clock.py

新功能。可得知時間並呼叫伺服器的物件

這個檔案會分成兩半,並以兩個檔案的形式傳輸到雲端:觸發條件和觸發者分開

在步驟 10 中會變成

觸發條件

可執行代理程式的端點

Cloud Run

觸發者

有時鐘的物件,會呼叫

Cloud Scheduler

觸發條件:您自己的網路伺服器

沒錯,就是網路伺服器。必須先有接聽的項目,才能呼叫任何項目。先前步驟中的 ADK 網路 UI 是開發介面,剛好可以執行代理程式。

👉💻 開啟 monstertix/server.py:

from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from concert.agent import root_agent

session_service = InMemorySessionService()
runner = Runner(
    agent=root_agent,
    app_name="concert",
    session_service=session_service,
)

@app.post("/wake")
async def wake(payload: dict = Body(default={})):
    session_id = f"wake-{uuid.uuid4().hex[:8]}"          # brand new, every time
    await session_service.create_session(...)

    async for event in runner.run_async(
        user_id=USER_ID, session_id=session_id,
        new_message=types.Content(role="user",
                                  parts=[types.Part(text=payload["message"])]),
    ):
        ...

Agent 不會自行執行,這是一份說明:名稱、模型、指令和工具清單。必須有某個項目接收收到的訊息、找出正確的工作階段、將目前的對話交給模型、執行模型要求的工具、將結果回饋給模型,並重複上述步驟,直到模型停止呼叫工具為止,同時將每個步驟寫入儲存空間。這個「某事」就是 Runner。

Runner 在代理周圍執行的作業

ADK 網頁版 UI 預設會提供一個。您可以在這裡自行建構,這就是 ADK 網頁 UI 與可部署內容的唯一差異。

👉💻 終端機 2 - 啟動:

啟動 Runner,

cd ~/monstertix
. ./set_env.sh
python -m monstertix.server
[server] agent concert · gemini-x-flash
[server] listening on http://xxxx_URL/wake
[server] nothing will happen until something calls it. Ctrl-C to stop.

請讀出最後一行文字。觸發條件正在執行,代理已載入,但沒有任何動作。請區分可呼叫和被呼叫這兩種概念。

還有前門,我們在這裡

雖然 adk web 是很棒的開發工具,但不應是您發布的內容。部署至正式版時,您不應啟用這項功能,如果啟用,也不會將客戶導向該功能。因此,同一個伺服器也會提供網頁。

👉 在瀏覽器中開啟 http://127.0.0.1:8090,也就是相同的位址。

認識 MonsterTix。聊天視窗、角落的名稱、兩個按鈕。這會發布至 /wake 並算繪傳回的內容,且針對以相同方式回應的任何端點,這項作業都不會變更。

網頁上

下方內容

對話

POST /wake,一次一則訊息

+ 新增工作階段

新的工作階段 ID - 新的對話,以及稍後的新預算

重設記憶

刪除所有工作階段和記憶體檔案。它會先詢問,然後確切指出該狀態的所在位置

👉✨ 對其說些什麼 - What Amsterdam dates are there? - 並觀察終端機 2。相同的 Runner、相同的代理程式、相同的記錄行。只有來電者變更。

觸發器:知道時間的物件

👉💻 開啟 monstertix/clock.py。查看程式碼。

time.sleep(args.delay)                                    # wait
while True:
    client.post(f"{TRIGGER_URL}/wake",                    # knock
                json={"message": args.message})
    if not args.every:
        return                                            # once, unless told otherwise
    time.sleep(args.every)                                # or again, forever

機制非常簡單:休眠、POST,可能重複。您使用的每個排程器都會執行相同作業,外加重試、時區,以及保證在重新啟動後仍可運作。

👉💻 第 3 號終端機 - 立即觸發一次:

cd ~/monstertix
. ./set_env.sh
python -m monstertix.clock --in 10
[clock] target   http://127.0.0.1:8090/wake
[clock] first fire in 60s, once only
[clock] go and watch the other terminal — do not touch the keyboard

👉 現在停止輸入文字。Hands down. 觀看終端機 2。

一分鐘後,鍵盤上沒有任何人:

[clock]  firing → POST /wake
[server] woken · session wake-23930983
[server]   called search_events
[server]   called purchase
[server]   said   I've purchased 2 tickets for The Midnight Signal in
                  Amsterdam on Saturday, section A. Your total is 420.
[clock]  done. the agent ran and you were not involved.

這就是步驟 2 的答案。是代理程式外部的某個項目呼叫了該項目,而該項目是 sleep 和 HTTP 要求。

👉💻 Ctrl-C 幾次。如要重複播放,請先使用 --every 30。

現在請唸出該字

「我買了兩張阿姆斯特丹午夜信號的票,A 區,420 美元。」

👉🔴 檢查場地面板,確認已購買票券。

所以還有什麼問題

它買了某樣東西。看看它必須猜測的內容。因為這是系統找到的第一個選項,而且您要求代理人直接購買,但代理人不知道 Sam 無法在平日晚上使用。如果代理程式在凌晨 3 點根據猜測採取行動,還不如什麼都不做。

起床很簡單,如果代理程式啟動時沒有任何記錄、偏好設定或監聽者,就還不值得啟動。

4. 管理內容生命週期

不同時間的資訊會儲存在不同位置

代理程式會將事實儲存在多個位置:即時通訊對話、工作階段狀態、檔案 (構件) 或長期使用者記憶體。每個地點的停留時間不同。

如果將資訊放在錯誤的位置:

  • 消失得太快:對話結束後,重要詳細資料就會遭到清除。
  • 保留時間過長:臨時事實會汙染長期記憶,導致日後對話混淆不清。

您必須將每項資訊對應到正確的儲存位置和生命週期。

載入程式碼

👉🔴 在場地面板中,按下「重設場地」。

👉💻 在終端機 1 中執行:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 4 --force

與上一個步驟相比,有哪些異動:

檔案

異動內容

tools.py

新增 note_companion。get_seatmap 現在會將座位表儲存至檔案

memory.py

新功能。我們自己的記憶體儲存空間

agent.py

清單中還有三項工具,且指令會要求代理使用這些工具

👉💻 在終端機 1 中重新啟動代理程式:

cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
  --session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
  --artifact_service_uri="file://$WORKSHOP/artifacts"

您之前未曾使用過的兩個旗標:

檢舉

簡介

session_service_uri

ADK 會將每則訊息、工具呼叫和狀態變更寫入其中做為資料列 (SQLite 資料庫)。

artifact_service_uri

代理程式儲存的檔案。(Files)

如果沒有這些函式,ADK 會將所有內容保留在記憶體中,並在您停止程序時捨棄這些內容。這兩項設定可確保應用程式持續運作。

資料庫 sessions.db 包含已植入的過往對話 (two-days-ago),其中有 13 個記錄的事件:

you    We're thinking about seeing The Midnight Signal. Me, Sam, and maybe Priya.
you    Sam can't do weeknights though, bailed on every single one.
you    Priya's out, she's away that weekend. Just me and Sam.
agent  Two it is. Still Saturday the 14th, still Amsterdam.
you    You said the upper bowl at Ziggo was bad last time?

過去三筆預訂記錄的記憶體檔案 memory/userx.md,可在編輯器中開啟:

- Sam bails on weeknights. Every weeknight show we booked, Sam cancelled.
- Hates the upper bowl at Ziggo Dome, "couldn't see a thing" from section B.
- Comfortable spend is around $200 per ticket. $250 is the hard ceiling.
- Always books two seats, always together.

這就是為什麼黃色工作日晚間列很重要。平日晚間的演出每張票便宜 $10 美元,還有更好的座位,而且位於清單頂端,但這並非適合這位使用者的答案。只有在上次預訂後,代理程式才能知道該檔案。該事實是否成立,則要看步驟 4 和 5。

我們新增的三項工具

三項工具及其撰寫位置

note_companion 記錄來訪者和限制:

# agent/concert/tools.py
def note_companion(name: str, constraint: str, tool_context: ToolContext) -> dict:
    prefs = dict(tool_context.state.get("user:prefs", {}))
    prefs[name] = constraint
    tool_context.state["user:prefs"] = prefs
    return {"companions": prefs}

新增這項規則,因為「Sam 晚上無法參加」下週仍適用。將其寫入 user:prefs 即可達成此目的,並觀察名稱的 user: 部分是否正常運作。我們稍後會再說明這點。

recall

和

remember 讀取及寫入文字檔 memory/userx.md。保持這個姿勢片刻。

👉 現在請查看代理程式儲存內容的兩個位置:

位置

保存內容

狀態

這個工作階段的記憶內容,以鍵/值組合的形式呈現 - 您會在步驟 4 中使用這個物件

構件

代理程式儲存的檔案,這些檔案不會進入提示

查看

get_seatmap

。過去會傳回整個座位表。現在,這項功能會將座位表儲存至檔案,並傳回簡短摘要:

# agent/concert/tools.py
async def get_seatmap(event_id: str, tool_context: ToolContext) -> dict:
    seatmap = venue.get(f"/events/{event_id}/seatmap")       # about 6 KB of JSON

    part = types.Part(inline_data=types.Blob(
        mime_type="application/json",
        data=json.dumps(seatmap, indent=2).encode()))
    await tool_context.save_artifact(f"seatmap_{event_id}.json", part)

    summary = {"event_id": ..., "sections": [...], "captured_at": ...}
    tool_context.state["temp:seatmap"] = summary
    return summary

以下說明這項異動的重要性。

每個工具傳回的酬載都會附加至對話記錄,並在後續所有回合中重新傳送至模型。傳回大型 JSON 物件 (例如 6 KB 的座位表) 會增加後續每次對話的權杖消耗量。

請改為儲存到檔案,並支付一個檔案名稱的費用。如果需要詳細資料,請讓服務專員使用 load_artifact 詢問,但通常不需要。

檔案去向:構件服務

再次查看 get_seatmap 中的程式碼行:agent/concert/tools.py

await tool_context.save_artifact(f"seatmap_{event_id}.json", part)

tool_context 來自 ADK,會以引數形式傳遞至工具,而 save_artifact 是其中的方法。工具會將位元組交給 ADK。

然後,ADK 會使用啟動時的構件服務寫入這些構件。您使用以下旗標選取了該項目:

--artifact_service_uri="file://$WORKSHOP/artifacts"

ADK 有三個位置可儲存這些檔案:

URI

位元組的去向

memory://

RAM。停止程序後就會消失。如果未傳遞任何旗標,則這是預設值

file://...

磁碟上的資料夾。目前使用的服務

gs://bucket

Cloud Storage。部署的代理程式在最後一個步驟中使用的內容

👉💻 在這裡選擇檔案:

ls -R ~/monstertix/artifacts

如要從筆電移至 Cloud Storage,請將 file:// 變更為 gs://。(我們會在步驟 10 中看到這個項目)

我們的記憶體儲存空間

記憶體做為介面,後方有檔案

ADK 提供長期記憶體介面,稱為 BaseMemoryService。有兩種方法:

# google.adk.memory.BaseMemoryService — ADK's interface, not your file
async def add_session_to_memory(session)                   # take a conversation in
async def search_memory(*, app_name, user_id, query)       # hand memories back

ADK 會提供三種實作方式:

類別

URI

需求條件

InMemoryMemoryService

memory://

沒事。停止程序後,所有內容都會遺失

VertexAiRagMemoryService

rag://

Vertex AI RAG 語料庫

VertexAiMemoryBankService

agentengine://

Agent Engine 資源

我們將為本次研討會編寫專屬的函式。排除 InMemoryMemoryService,因為停止程序時會忘記所有內容,並排除其他兩項,因為需要幾分鐘才能建立的雲端資源。請改為挑選檔案,因為您可以預先設定檔案,並在編輯器中讀取檔案。因此,請以 memory.py 做為自己的實作項目,我們將所有內容儲存在 Markdown 檔案中:

class MarkdownMemoryService(BaseMemoryService):

    async def search_memory(self, *, app_name, user_id, query) -> SearchMemoryResponse:
        path = self._path(user_id)                      # memory/userx.md
        return SearchMemoryResponse(memories=[
            MemoryEntry(content=types.Content(
                role="user", parts=[types.Part(text=path.read_text())]))])

SearchMemoryResponse 和 MemoryEntry 是 ADK 的型別,Memory Bank 會傳回相同的物件。其中一個後方有代管服務。我們的檔案有。

選擇檔案的原因之一是你可以開啟並閱讀檔案。代理程式的整個長期記憶就是一頁文字。

那麼該如何使用呢?

回顧一下,在本研討會的實作中,我們在指令列上選擇了三項服務中的兩項。基於上述原因,我們自行實作了第三個:

服務

設定位置

本研討會

工作階段

--session_service_uri (adk web)

sqlite+aiosqlite:///$WORKSHOP/sessions.db

構件

--artifact_service_uri (adk web)

file://$WORKSHOP/artifacts

記憶體

指令列中任何位置

MEMORY_DIR,在「agent/concert/memory.py」中閱讀

因此 memory.py 會從環境變數建構服務本身:

# agent/concert/memory.py
MEMORY_DIR = pathlib.Path(os.environ.get("MEMORY_DIR", "./memory"))
MEMORY_USER = os.environ.get("MEMORY_USER", "userx")
memory_service = MarkdownMemoryService(MEMORY_DIR)

. ./set_env.sh 會同時匯出這兩項資料。取得 MEMORY_DIR 會設定代理讀取記憶體的位置,而 MEMORY_USER 則會決定檔案。

前往尋找物品

現在有四個地方可以存放事實。請查看所有四個選項。

首先,是您自己的工作階段

👉 點選頂端列的「New Session」。

👉✨ 依序說出以下三件事:

Sam can't do weeknights, by the way.

新工作階段,第一回合

Show me the seat map for the Amsterdam Saturday show.

新工作階段,第二輪

Actually, hold that thought — what else is on in Amsterdam?

第三則訊息很重要,這會結束讀取座位表的回合,而回合結束後,temp: 就會消失。

有三項物品存放在三個不同的地方:

儲存的內容

是哪項工具

移往何處

1

Sam 晚上無法前往

note_companion

user:prefs,處於工作階段狀態

2

完整座位表,6 KB

get_seatmap,透過 save_artifact

構件服務

3

一行座位表摘要

get_seatmap,進入 temp:seatmap

nowhere

👉 立即查看。「狀態」和「構件」分頁位於 adk web 左側。

「狀態」分頁

「構件」分頁

請注意,「State」分頁中省略了 temp:seatmap。具有 temp: 前置字串的狀態鍵只會在有效呼叫期間存在,ADK 會在將工作階段狀態保存至資料庫前自動移除這些狀態鍵。

然後是較舊的對話

👉 在頂端的工作階段下拉式選單中切換至 two-days-ago。

這個工作階段包含資料庫中預先填入的對話記錄。

兩天前的工作階段

👉 查看事件串流 (中間欄)。請注意,較舊的對話已由摘要事件區塊取代。

收合的輪次

👉 查看「State」分頁標籤。三種金鑰會一起儲存在該處:

target_event_id   ms-ams-01                      ← plain: this booking only
party_size        2                              ← plain: this booking only
user:prefs        {"excluded_weekdays": [...]}   ← the person, every session

user: 前置字元 (例如 user:prefs) 會將狀態範圍限定在特定使用者的所有工作階段。相較之下,沒有前置字元的鍵 (

target_event_id

、

party_size

) 代表一般工作階段狀態,嚴格限定於有效工作階段,並在工作階段結束時刪除。

👉 現在比較這兩個「狀態」分頁。user:prefs 和儲存的構件會保留,但 temp:seatmap 不會出現在這兩個工作階段中。前置字元為 temp: 的狀態鍵嚴格來說是暫時性的,絕不會保留在資料庫中。

然後檔案

👉💻 在編輯器中開啟:

cloudshell edit ~/monstertix/memory/userx.md

三筆預訂,最早可追溯至 1 月。代理程式會在呼叫 recall() 時讀取該值,即使我們遺失工作階段資料,該值仍會存在。

四個地點

存放位置

是否會保留下一則訊息?

重新啟動後是否仍存在?

刪除工作階段後是否仍存在?

temp:seatmap

否

不

否

party_size - 一般工作階段狀態,位於 two-days-ago

是

是

否

user:prefs

是

是

yes

memory/userx.md

是

是

是,且在刪除資料庫後仍存在

構件

是

是

視服務而定

請將該表格視為與容器相關,而非容器中的內容。memory/userx.md 會存留所有內容,且只會包含 remember() 選擇放置的內容。不會將目前的對話內容寫入其中。因此,檔案在工作階段結束後仍存在,並不代表代理程式會記得您剛才談論的內容,而步驟 5 正是這項差異的關鍵。

事實可存在於五個地方,以及各處的存留時間

狀態

您可以將狀態視為隨工作階段傳輸的字典。透過 tool_context.state 從工具讀取及寫入,並讓 ADK 將其與對話一併儲存。

ADK 會指派前置字串的意義。您不會在任何地方定義這些項目。ADK 會讀取鍵名的第一部分,並據此決定值的去向和存續時間:

名為

ADK 會儲存這項資訊

持續時間

party_size

工作階段

直到刪除工作階段為止

user:prefs

對使用者

使用者擁有的每個工作階段

app:tour_id

對應用程式

所有使用者

temp:seatmap

不限位置

一次叫用

呼叫是指單一要求-回應週期,包括所有模型推論、工具執行和最終回應。

當叫用結束時,ADK 會將新狀態寫入資料庫,並在過程中移除所有 temp: 鍵。

還有一件事沒解釋

在 two-days-ago 中,您會看到摘要,取代原本顯示使用者前幾回合的位置。對話內容夠長時,ADK 會自動撰寫摘要。

每個事實都有流行時間

5. 脈絡降級與壓縮

摘要:忘記詳細資料

對話過長時,系統會摘要舊訊息,以節省空間和權杖。

摘要適合用來掌握大意,有助於節省權杖用量。

壓縮作業會針對工作階段執行。記憶體是獨立的儲存庫

👉🔴 在場地面板中,按下「重設場地」。每個步驟都從同一個世界開始:8 場演出、所有座位皆可預訂,時鐘為 1 倍速。

👉💻 移至這個步驟的程式碼:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 5 --force

與上一個步驟相比,有哪些異動:

檔案

異動內容

agent.py

代理程式會包裝在 App 中,並開啟壓縮功能。第二個代理程式 budget_split 負責票券算術

其他

未變更

agent.py 不再以 root_agent 結尾。結尾如下:

app = App(
    name="concert",
    root_agent=root_agent,
    events_compaction_config=EventsCompactionConfig(
        compaction_interval=3,   # summarise after every 3 turns
        overlap_size=1,          # re-read 1 turn either side when summarising
    ),
)

請注意,adk web 會先尋找名為 app 的變數,然後再回溯至 root_agent。需要套用至整個應用程式 (而非單一代理程式) 的項目時,請將代理程式包裝在 App 中:此處為壓縮,稍後步驟中為可繼續性。

compaction_interval=3 表示 ADK 會在每三輪後進行摘要。一輪對話是指你說完話,代理完成回覆,無論過程中呼叫了多少工具。3 很小,因此會在研討會期間觸發。實際應用程式會使用更大的數字。

overlap_size=1 表示每個摘要的開始時間都比嚴格需要的時間早一輪。

ADK 會彙整第 1、2 和 3 輪對話。三輪後,系統會摘要下一個批次。使用 overlap_size=1,第二個摘要會再次從第 3 輪開始,而不是第 4 輪:

兩份摘要,重疊一回合

請注意,第 3 回合的摘要會重複出現。這是刻意設計的。如果您在第 3 輪提問,代理程式在第 4 輪回答,乾淨的分割會將問題放在一個摘要,答案放在另一個摘要,兩者都會令人困惑。重複回合是低成本的保險,可避免中斷思考。

設定這兩個欄位。ADK 也沒有預設值。

壓縮作業和記憶體服務永遠不會相遇

這兩個字聽起來很像,但系統並不相關,因此有必要明確說明。

EventsCompactionConfig位於 agent.py 的App,旁邊是 root_agent。這是應用程式的屬性,而非任何服務的屬性:

# agent/concert/agent.py
app = App(
    name="concert",
    root_agent=root_agent,
    events_compaction_config=EventsCompactionConfig(
        compaction_interval=3,
        overlap_size=1,
    ),
)

摘要會重寫對話內容。不會覆寫檔案。

這就是 remember() 存在的原因,因為這是刻意呼叫。轉錄稿中剩下的內容最終會以他人撰寫的轉述形式讀出;寫入檔案的內容則會完全按照儲存方式讀出,無論是明天還是下週。這個步驟的最後,您會看到差異的落點。

摘要的儲存位置

在 sessions.db 的 events 表格中,代理程式會將壓縮記錄寫入事件列,並加上時間戳記,就像其他任何輪流事件一樣。

一般活動會帶有 content 欄位,摘要則位於 actions.compaction 下方。

👉💻 在自己的資料庫中查看:

cd ~/monstertix
. ./set_env.sh
sqlite3 sessions.db \
  "select json_extract(event_data,'\$.actions.compaction.compacted_content.parts[0].text')
   from events where event_data like '%compaction%';"

事件的形狀如下所示:

events row
└── event_data  (JSON)
    ├── id, author, timestamp, invocation_id      ← like every other event
    ├── content                                    ← ABSENT on this one
    └── actions
        └── compaction
            ├── start_timestamp                    ← the window it replaced
            ├── end_timestamp
            └── compacted_content.parts[0].text    ← the summary itself

事件串流會從內容中擷取每個事件。資料庫中的摘要,以及在每個回合中傳送至模型的摘要。

第二位服務專員

budget_split 是完全獨立的代理,而非工具:

# agent/concert/agent.py
budget_split = Agent(
    name="budget_split",
    model=MODEL,
    include_contents="none",      # sees none of the conversation
    instruction="You do ticket arithmetic and nothing else...", #just the budget
    output_key="budget_plan",     # its answer goes into state
)

root_agent = Agent(
    ...,
    tools=[..., AgentTool(agent=budget_split)],
)

為什麼是代理程式,而不是單純的函式?因為這項工作需要模型。請將「這幾節中,哪幾節適合四人 200 美元的預算?總共是多少?」視為判斷式包裝的算術,並提供使用者可讀取的答案。讓 Python 函式執行乘法運算,不要要求函式決定要提及哪些選項。

那麼為什麼不讓主代理執行?查看下一行 include_contents="none" 表示這個代理程式「沒有」接手任何對話。沒有群組通訊、沒有偏好設定、沒有四十場巡迴演唱會。它只會取得要求,不會取得其他資訊。該旗標只存在於 Agent,這才是這個項目是代理程式而非函式的真正原因。

只能查看要求內容的子代理程式

建議使用簡短提示:費用較低、速度較快,且較難出錯。算術運算不會受到二十回合前說過的話影響,因為它從未見過。

建議原因

AgentTool

? 使用該工具包裝代理,讓主代理可以像呼叫其他工具一樣呼叫該代理。從模型的角度來看,budget_split 與 search_events 相同。

且

output_key="budget_plan" 會將答案放入該名稱下的工作階段狀態,而不是留在對話中。觀看影片,瞭解這項功能的重要性。

開始執行

👉💻 在終端機 1 中,按下 Ctrl-C 並使用相同標記重新啟動:

cd ~/monstertix
adk web agent --port 8000 --allow_origins "*" \
  --session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
  --artifact_service_uri="file://$WORKSHOP/artifacts"

👉 啟動新對話,展開真實對話。五到六圈。這個可以:

hello
what's on in Amsterdam?
Sam is coming with us, and he can't do weeknights
there are 4 of us and I can spend about $200 a head. What fits?

👉 開啟「State」分頁。

您可以在 budget_plan 中找到算術代理程式的完整答案。

第一筆預算問題後的 budget_plan

👉✨ 現在變更其中的數字:

what if 10 people come and I can only do $120 each?

👉 請再次查看 budget_plan。這項設定已覆寫,而非附加。這是因為 output_key 包含最新版本。

第二個預算問題後的 budget_plan

無法查看的內容:摘要

在這些回合中,ADK 總結了先前的回合。

👉💻 在新的「終端機」中,執行以下指令即可直接讀取:

cd ~/monstertix
. ./set_env.sh
./show-compaction.sh

閱讀 AI 撰寫的內容

以下是實際的例子 (節錄):

── summary 1 ──────────────────────────────────────────────────────

Conversation Language: English

  **User Request:**
  The user initially wanted to know what concerts are playing in Amsterdam.
  Subsequently, the user provided a constraint regarding a companion, Sam.

  **Context Summary:**
  The user is planning a concert trip. The agent identified two events in
  Amsterdam: "The Midnight Signal" at the Ziggo Dome on Tuesday, November
  10 and Saturday, November 14, 2026. The user then informed the agent that a
  companion named "Sam" will be joining, but "he can't do weeknights." The
  agent has successfully noted this constraint for Sam.

  **Key Decisions/Information Obtained:**
  *   **City of interest:** Amsterdam
  *   **Events found:** "The Midnight Signal" at the Ziggo Dome, Tuesday
  10 November and Saturday 14 November 2026.
  *   **Companion:** Sam
  *   **Companion constraint:** Sam cannot attend concerts on weeknights.

  **Unresolved Questions/Tasks:**
  *   The agent has not explicitly confirmed if the found event (which is
  on a Saturday) is suitable given Sam's constraint, nor has it asked the
  user for the next steps regarding this event or further event searches.

  **Tools Used:**
  *   `recall`
  *   `search_events`
  *   `note_companion`

並保留價格、區段、城市和偏好設定,且正確指出尚未做出任何決定。

因此,我們得出以下三項結論:

這是改寫內容。這裡沒有任何人的發言內容。這是根據所說內容產生的帳戶,與模型偶爾出錯時的程序相同。

注意:摘要字串會儲存在內部工作階段事件記錄中,不會直接顯示為獨立的 UI 分頁。

每隔三輪,在部分內容為先前摘要的對話中。

嘗試預訂,但不要重複說話

在新工作階段中:請勿開始新工作階段,因為您剛才閱讀的摘要位於這個工作階段的事件中,這也是服務專員仍瞭解阿姆斯特丹的唯一原因。

👉✨ 給予全新內容:

Book us something.

觀看代理人詢問節目、區域和人數。標示正確:摘要本身指出使用者從未確認任何事項,因此服務專員才會詢問。將壓縮歸功於此。

即使壓縮成功,摘要先前的對話輪次仍可能造成模糊不清,需要代理程式與使用者重新確認限制。

👉✨ 回答問題,讓預訂程序順利完成:

Amsterdam Saturday, section C, 8 people

在支出任何費用前,系統會讀回訂單:

「你想預訂 11 月 14 日星期六在阿姆斯特丹舉行的 The Midnight Signal 演出,C 區的 8 張票。總費用為 $760 美元。要繼續購買這些票券嗎?"

👉✨ 確認:

yes

現在系統會顯示運作正常:

「你已完成購票,在 11 月 14 日星期六於阿姆斯特丹購買 The Midnight Signal 的 C 區票券 8 張,總金額為 $760 美元。你的訂單 ID 為 ord_227877fe。"

請前往確認是否已

👉 切換至場地面板。請勿輕信服務專員的說法。

場地面板上的順序

在面板上

應顯示的內容

已購買的票券

1,且正確無誤 (一筆預訂即為一筆訂單)

下方的訂單

ord_xxxxxxxx — 8× section C · 760

巡迴演唱會,阿姆斯特丹,週六

C 區段下降 8

活動

ord_xxxxxxxx — 8x section C @ 95 = 760

查看面板,瞭解售票資訊。

為什麼 user:prefs 是正確選擇

回想一下note_companion在user:prefs上一個步驟中撰寫 Sam 的限制,以及顯而易見的反對意見:代理可以記住限制,何必使用工具。

原因如下:請勿在對話中提及狀態,且摘要不得觸及狀態。檢查 user:prefs 和 budget_plan 的進入方式,確認這些值沒有變更。然後查看轉錄稿。

摘要會保留一般形狀,但會遺失詳細資料。請將重要內容儲存在對話以外的位置。

6. 程序恢復

等待 40 分鐘,期間不要保持連線

如果服務專員必須在 40 分鐘的待處理事項佇列中等待,讓 Python 程序保持運作並等待,不僅容易出錯,而且成本高昂。

如要妥善處理長時間等待問題,請採取下列行動:

  1. 將狀態儲存至資料庫:儲存代理程式暫停的位置,以便安全關閉程序。
  2. 依需求喚醒:等待結束後,外部信號會重新啟動代理程式,並從中斷處繼續執行。

👉🔴 在場地面板中,按下「重設場地」。每個步驟都從同一個世界開始:8 場演出、所有座位皆可預訂,時鐘為 1 倍速。

👉💻 移至這個步驟的程式碼:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 6 --force

開始執行

與上一個步驟相比,有哪些異動:

檔案

異動內容

tools.py

新增 join_queue 和 check_queue

agent.py

join_queue 包裝在 LongRunningFunctionTool 中,而 App 會開啟 ResumabilityConfig

join_queue 會立即傳回票證,不會遭到封鎖,而 ResumabilityConfig 則可讓執行作業暫停,之後再繼續。

可恢復上傳功能新增的內容

👉💻 終端機 1 - 代理程式,未變更:

cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
  --session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
  --artifact_service_uri="file://$WORKSHOP/artifacts"

👉✨ 請代理購買:

Get us two tickets to the Amsterdam show.

它會呼叫 recall、search_events 和 join_queue,然後回覆類似下列內容:

You are in the queue for The Midnight Signal at Ziggo Dome, Amsterdam, on Saturday, November 14. Your position is 14203.

請注意,系統會自動選取「星期六」,阿姆斯特丹有兩場演出,17 號是星期二,而上一個步驟的記憶體檔案顯示 Sam 不會在平日演出。這是先前預訂的資訊,可決定本次預訂的問題。

請注意,它是在查看單一座位或價格之前加入佇列,且未提及任何座位或價格。這是場地的規定,你必須排到最前面才能買票,因此早點到場的唯一好處就是可以排在隊伍前段。要坐哪個位子是 40 分鐘後的事。

👉 查看控制台。橫幅內容如下:

佇列仍在移動,但沒有任何內容正在執行

Agent is waiting in line — #14,203
About 2400s at 1×. Safe to kill the agent right now — it will still be here.

佇列時間真的有 40 分鐘,而且在你工作時不會結束。

👉💻 終止代理程式。:

Ctrl-C

👉💻 在終端機 1 中再次啟動:

cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
  --session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
  --artifact_service_uri="file://$WORKSHOP/artifacts"

👉✨ 在同一工作階段中,詢問代理目前輪到誰:

Where am I in line?
You are currently at position 12982 in the queue. It is not your turn yet.

重新啟動後,票證仍可使用

比較票證,並觀察位置在沒有任何項目執行的情況下持續下降。請注意,代理程式並不知道自己已終止。因為該票證位於磁碟上,所以它會使用重新啟動前取得的票證呼叫 check_queue。

👉💻 瞭解原因:

cd ~/monstertix
sqlite3 sessions.db "select name from pragma_table_info('events');"
id
app_name
user_id
session_id
invocation_id
timestamp
event_data

請注意,每段對話的每一輪都是一個資料列,而輪次本身、說話者、呼叫的工具、傳回的內容,以及 event_data 內的 JSON 都是資料列。

👉💻 在該處尋找你的排隊票券:

sqlite3 sessions.db "select event_data from events;" \
  | grep -o '"ticket": "q_[a-z0-9]*", "position": [0-9]*' | tail -1
"ticket": "q_8cc2c6f9", "position": 14203

join_queue 回傳時,票證會直接儲存至資料庫。重新啟動後,ADK 會從磁碟重新載入先前的對話和工具結果,讓代理程式從中斷處繼續作業。

並注意它沒有重新啟動的項目。代理程式會直接繼續執行,而不是從頭重新執行工作流程。由於 LongRunningFunctionTool 和 ResumabilityConfig 會將執行狀態保留在磁碟中,ADK 知道代理程式已在等待,因此不會重新搜尋巡迴日期或要求重複的票券。

在程序終止後,使用同一張票證

為什麼是 join_queue 而不是 check_queue

這兩個函式都會傳回毫秒。兩者的區別在於函式傳回時,基礎工作是否已完成。

傳回內容

工作是否完成?

join_queue

排隊票證和初始位置

否。外部系統會繼續以非同步方式處理。

check_queue

目前佇列狀態

是。傳回即時的當下答案。

LongRunningFunctionTool 適用於傳回待處理工作控制代碼的作業。join_queue 包裝函式可讓代理暫停執行,同時外部佇列會繼續運作。

相較之下,check_queue 是同步且完整的。如果包裝這個函式,代理程式每次檢查狀態時都會錯誤地暫停執行作業,即使代理程式已排在隊伍最前面,需要立即完成購買作業也是如此。

傳回時仍未完成的呼叫

為什麼會這樣

三項元件可暫停及恢復執行作業:

# agent/concert/agent.py
LongRunningFunctionTool(func=join_queue)      # the tool returns without finishing
ResumabilityConfig(is_resumable=True)         # the run may pause and be picked up
--session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db"
  • LongRunningFunctionTool:將工具標示為非封鎖,並傳回有效控制代碼,讓代理程式可以等待,而不會掛斷執行緒。
  • ResumabilityConfig:指示執行階段暫停未完成工作中的執行生命週期,並從上次記錄的狀態繼續執行。
  • Session Storage URI (--session_service_uri):將狀態持續性導向外部儲存空間 (例如 SQLite、Cloud SQL),而非程序記憶體,避免資料在重新啟動時遺失。

7. 對過時資料採取行動

等待期間資訊過時

系統不會即時更新儲存的對話記錄。等待時間過長時,外部資料 (例如票券供應情形或價格) 可能會變更,但服務專員並不知情。

如果代理程式缺少資訊,會暫停作業並要求釐清,但如果代理程式的資料過時,就會根據過時的假設立即採取行動。為避免無效動作,執行重要作業前,必須立即重新驗證時效性資料。

請勿載入新程式碼。繼續使用上一個步驟的代理程式 (可以購買票券,但目前沒有過時或等冪的防護措施)。

👉🔴 按下「RESET THE VENUE」,然後在 adk web 中啟動新工作階段,也就是聊天室上方的「+ New Session」按鈕。

開始新的工作階段,清除先前的排隊票證和待處理的購買狀態。這個步驟需要乾淨的執行環境,才能正確示範如何處理過時狀態。

快照的謊言

👉✨ 要求顯示與先前相同的節目:

Get us two tickets to the Amsterdam show.

👉✨ 現在,當它在排隊等候時,請詢問座位:

How much are the seats, and what's still available?
Section A: $210 per ticket, with 400 seats available.
Section B: $145 per ticket, with 900 seats available.
Section C: $95 per ticket, with 1200 seats available.

👉🔴 按下「SELL THE GOOD SEATS」(銷售優質座位)。A 區段降至零。

👉🔴 按下「SKIP THE WAIT」(略過等待),讓服務專員優先處理你的要求。

隊伍最前面

👉✨ 告訴 Google 助理要購買的商品:

Buy the two section A seats.

系統會執行 purchase(section="A"),但不會重新驗證座位供應情形,導致場地 API 傳回錯誤回應:

{"error": "sold_out", "available": 0,
 "message": "those seats are gone — re-fetch the seatmap before buying"}
Oh no! It looks like the Section A tickets just sold out. Let me get an
updated seat map to see what's still available.

Venue API 錯誤:A 區已售完

請注意,代理程式只會在收到 API 錯誤回應後,重新查詢座位表。根本原因是過時的脈絡:在工作階段稍早,服務專員記錄了「A 區:400 個可用」,而該回合仍保留在傳遞給模型的對話記錄中。

清楚說明舊資訊的儲存位置:舊資訊會保留在對話記錄中,且無法由 temp: 等狀態前置字元覆寫。

代理人仍在使用的座位表

修正方式

👉🔴 在場地面板中,按下「重設場地」。每個步驟都從同一個世界開始:8 場演出、所有座位皆可預訂,時鐘為 1 倍速。

👉💻 現在載入這個步驟的程式碼:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 7 --force

👉💻 在終端機 1 中,按下 Ctrl-C 並重新啟動:

cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
  --session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
  --artifact_service_uri="file://$WORKSHOP/artifacts"

變動部分:

檔案

異動內容

fence.py

新功能。在購買前立即執行,並可封鎖購買交易

agent.py

before_tool_callback=refresh_before_purchase

tools.py

purchase 現在會傳送 Idempotency-Key 標頭

fence.py 是 ADK 在任何工具呼叫前執行的回呼:

root_agent = Agent(
    ...,
    before_tool_callback=refresh_before_purchase,   # re-read inventory, then decide
)

我不信

👉🔴 重設場地。排隊等候,在等待期間按下「SELL THE GOOD SEATS」(銷售好位),然後要求購買 A 區的座位。

這次是自行停止。觀察代理程式是否說出類似以下內容:

「It looks like Section A just sold out. 我需要重新查看座位表。」

接著,請觀察它再次呼叫 get_seatmap,且未收到提示,然後返回並提供 C 區段。

👉🔴 仔細閱讀場地的活動動態消息,因為當中會顯示上下半場:

error  purchase failed: Section A has 0 seats left, 2 requested. Re-read the
       seat map and pick again.
agent  calling purchase

場地動態消息:嘗試和拒絕

資訊主頁顯示已購買通話額度,但票券計數器仍為零。這是因為 fence.py 會在工具執行前擔任守衛:

  1. 代理會嘗試呼叫購買工具。
  2. fence.py 會先擷取要求,並檢查可用的座位。
  3. 由於座位已售完,fence.py 會直接傳回錯誤,在呼叫 API 前取消購買。

提示指令無法保證 LLM 不會嘗試購買已售完的座位。程式碼防護措施 (before_tool_callback) 可確保不良購買行為不會發生。

👉🔴 按下「重設場地」,然後按下「中斷下一次購買」

👉✨ 要求購買,並讓 AI 無限次重試。

無論重試幾次,都只會產生一筆訂單

由於場地在重試時收到相同的等冪鍵,並傳回原始確認,而非建立重複訂單,因此訂單數仍為 1。

8. 自主工作流程

大型語言模型不擅長每次都遵守嚴格規則

LLM 非常擅長做出決策 (例如根據使用者偏好選擇節目)。但如果要求 LLM 嚴格遵守逐步規則 (例如「一律先加入佇列,再查看座位」),有時會略過步驟或弄亂順序。

如要修正這個問題,請將工作流程分成兩種類型的步驟:

  • 固定規則 (程式碼函式):每次都必須以完全相同方式執行的步驟 (例如取得排隊號碼牌、查看位置),會以一般程式碼節點的形式編寫。
  • 決策 (代理程式節點):需要判斷的步驟 (選擇座位區、檢查使用者偏好設定) 會交給 AI 代理程式。

看看這個流程實際包含哪些內容。部分內容完全沒有判斷結果:

步驟

判決?

只能領取一張排隊票券

否。一律只能有一個,且必須在購買前完成

確認是否輪到你

否。提出問題,閱讀答案

為買家撰寫簡報

否。每次都是同一句話,形狀也相同

顯示的節目、顯示的區段

是。價格、日期、誰要來、他們討厭什麼

使用者偏好設定和限制

是。

嚴格的程序不應依賴 LLM,因為機率模型最終會無法遵循嚴格的序列 (如果未受到監控)。

ADK 的Workflow可區隔確定性執行作業和 AI 推論:

  • 函式節點每次都會強制執行固定規則 (例如加入佇列或查看位置)。
  • 代理程式節點會處理情境決策 (例如根據使用者偏好選擇座位)。
  • 等待節點會暫停執行作業,但不會讓程序在記憶體中保持運作。

原始代理人不會遭到取代。這個節點會成為工作流程圖中的最終節點,並保留其工具、過時檢查和等冪防護措施。圖表會控管執行順序,而代理程式則會執行購買決策。

👉🔴 在場地面板中,按下「重設場地」。

👉💻 移至這個步驟的程式碼:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 8 --force

開始執行

👉💻 在終端機 1 中,按下 Ctrl-C 並重新啟動。指令與以往相同:

cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
  --session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
  --artifact_service_uri="file://$WORKSHOP/artifacts"

與上一個步驟相比,有哪些異動:

檔案

異動內容

nightly.py

新功能。以 ADK 2 Workflow 圖表形式執行的無人值守作業

agent.py

root_agent 現已改為圖表。您先前交談的代理仍在該處,已重新命名為「buyer_agent」,且是其中的一個節點

agent.py

它也遺失 join_queue 和 check_queue,因為圖表現在擁有佇列

執行圖表 - 三則訊息,依序排列

在 nightly python 中執行下列所有動作 (這是圖表所在位置)。在每個回合中傳送相同的輸入內容,觀察工作流程圖在各個狀態間的轉換。

1. 注意你現在與誰對話

👉 重新載入 ADK 網頁 UI。

# concert/agent.py, at the bottom

buyer_agent = Agent(name="concert", ...)   # ← everything you built, renamed

from .nightly import nightly
root_agent = nightly                       # ← what adk web actually runs

請注意,adk web 會執行指派給 root_agent 的物件。其中 root_agent 指向 nightly 工作流程圖,而 buyer_agent 則設定為圖中的最終節點。

2. 開始使用

👉✨ 輸入以下內容並按下 Enter 鍵:

go

注意:在自動化工作流程圖中,初始執行作業會開始,不必等待使用者輸入。在「adk web」中傳送訊息會觸發初始執行作業。

👉 現在請查看網路瀏覽器。左側面板會繪製圖表,並在節點執行時為節點著色。右側面板是事件串流,可觀察決策過程:

圖表,停放在 check_front

將 queue_up 寫入工作階段狀態。queue_ticketqueue_event_id

圖表,停放在 check_front

您應該會看到名為 adk_request_input 的函式呼叫,等待回覆。接著就會安靜下來,跑步活動也會結束。圖表達到 check_front,找到第 14,203 個結果,然後停止。

👉 檢查場地面板:一個排隊票證,沒有任何執行中的項目。

圖表,停放在 check_front

3. 喚醒 AI 助理,並注意輸入內容的方塊

捲動事件串流至 adk_request_input 事件,然後找出與該特定事件相關聯的輸入框:

圖表,停放在 check_front

現在有兩個可輸入文字的位置,用途各不相同。

輸入文字的位置

狀況說明

adk_request_input活動頁面上的 Enter your response... 方塊

回答中斷問題。已暫停的跑步活動會從 check_front繼續。

底部的主要對話方塊

開始新的叫用。圖表會從 START 開始重播,再次選取節目,並取得第二張排隊票

👉✨ 在 Enter your response... 方塊中輸入任何內容,然後按下箭頭。

👉 第 1 航廈:

[check_front] #14,197 — not yet. Pausing.

一行。系統並未選取第二場演出,也未取得第二張排隊票券,因此你不會排在 14,203 人的隊伍後方。

如果看到 [open] 和 [pick_show] 再次執行,表示您在聊天方塊中輸入了內容。按下「RESET THE VENUE」,然後重新開始步驟。

👉 再次查看場地面板:仍為一張票,且由於場地的時鐘持續運作,而你的時鐘沒有,因此位置稍微下降。

喚醒一次,仍為一張票

4. 移至最前面,然後完成

👉🔴 按下場地面板上的「略過等待」。

完成的圖表,每個節點都是綠色

👉✨ 最後一次回答最新的 adk_request_input 活動問題,請在該活動的方塊中回答,而不是在對話方塊中。完成的圖表,每個節點都是綠色

這次請將手錶放在前方,然後放手讓手錶掉落。check_front觀察左側面板亮起 brief 和 concert 這最後兩個節點。

完成的圖表,每個節點都是綠色

👉 然後查看瀏覽器。這是完成的執行作業,其中有四個值得閱讀的項目。

完成的圖表,每個節點都是綠色

圖表完全呈現綠色,且邊緣已計入。brief 和 concert 已亮起,且邊緣已進入 pick_show、queue_up 和 check_front,現在會顯示徽章。

2x

是指圖形走過該邊緣的次數,每次喚醒一次。頂端的 2 是繼續執行的價格:排程器每次都會從頭重新進入圖表。這不代表工作發生了三次,場地面板就是證明:只有一張排隊票,而不是 2 張。@node(rerun_on_resume=False)。

圖表

圖表:規則的函式,判斷的代理程式

# agent/concert/nightly.py
nightly = Workflow(
    name="concert_nightly",
    edges=[(START, open_the_night, pick_show, queue_up,
            check_front, brief, buyer_agent)],
)

請注意,有兩個節點是代理程式,兩個是函式,並將兩者視為整個設計問題。

節點

為什麼是這個形狀

pick_show

agent. 從對方的記錄中選擇節目是判斷行為。呼叫 recall、讀取導覽行程,並傳回經過驗證的 Plan

queue_up

函式。加入佇列是規則。規則不應是凌晨 3 點的擲硬幣

check_front

函式。「我在最前面嗎?」只有一個正確答案,而且場地有這個答案

buy_it

agent. 這是 buyer_agent,也就是未經修改的 root_agent。A 區在等待期間售完,你要選擇 B 區、C 區,還是不選擇任何座位?這就是您在單元 4 中教導的判斷

查看最後一個節點,找出您建立的代理程式。不是複製內容,也不是改寫內容:

# agent/concert/nightly.py
from .agent import buyer_agent

所有內容都會一併傳送:所有工具、fence.py 中重新讀取的座位表,以及 purchase 中的等冪鍵。只有一個。

為什麼系統停止運作,而不是等待

以下是任何人都不該撰寫的版本:

# what NOT to write — this is nobody's file
while not ready:          # never do this
    time.sleep(1)

這個程序會存留 40 分鐘,但不會執行任何作業,且會在終端機關閉時終止,無法繼續執行。check_front 的做法如下:

# agent/concert/nightly.py — check_front
if not status["ready"]:
    return RequestInput(message=f"Still at #{position}. Wake me and I'll check.")

RequestInput 是中斷。圖表會停止運作,呼叫會傳送至工作階段儲存區,程序則可自由結束。

等待期間不會產生任何費用,而且執行程序只會產生一個結果:已購買的票券。但可能需要一整晚的時間,而且你可能會醒來好幾次。

9. 與真人對話

未與他人確認前,請勿花費金錢

如果自主代理程式在夜間執行,就不應根據聊天記錄中猜測或模稜兩可的偏好設定,做出代價高昂的財務決策。

執行高風險動作 (例如花錢購買機票) 前,代理程式必須暫停,要求使用者大聲確認預算,並儲存這項明確的確認資訊,再進行任何購買動作。

👉🔴 在場地面板中,按下「重設場地」。每個步驟都從同一個世界開始:8 場演出、所有座位皆可預訂,時鐘為 1 倍速。

👉💻 移至這個步驟的程式碼:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 9 --force

開始執行

👉💻 在終端機 1 中,按下 Ctrl-C 並重新啟動。與先前相同的指令:

cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
  --session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
  --artifact_service_uri="file://$WORKSHOP/artifacts"

與上一個步驟相比,有哪些異動:

檔案

異動內容

budget.py

保留使用者同意的內容,也就是他們說的句子。load() 讀出州外電話號碼,someone_is_there() 則回答是否有人在附近可詢問

tools.py

新增 set_budget,因此代理程式可以在使用者確認後,將同意的限制寫入工作階段狀態

nightly.py

新的第一個節點 agree_budget,會在執行任何其他項目之前要求並讀回。這會擷取預算和其餘要求 (以任意文字形式),並SEATS以常數形式消失

請勿模擬。儲存句子。

開啟 agent/concert/budget.py 並讀取所有內容:

def load(state) -> str:
    """What they agreed, verbatim. Empty string means nobody has said."""
    return str(state.get("budget") or "")
state["budget"] = "up to $100 for the upper bowl or general admission, up to
                   $250 for the lower bowl, and up to $300 for a Saturday show"

節點會檢查兩件事:說話者是否為本人,以及說話者是否確認。

然後,系統會將句子保留在工作階段狀態中,而不是 user: 下方,這是刻意設計。為單一預訂項目議定預算。「如果他們是好樂團,就值 $250 美元」這句話是在當晚對該樂團說的,與票券預訂無關。

他們說的其他內容也是如此

請勿在預算頁面停止操作。使用者在決定時可能會改變心意,因此請在這裡記錄他們要求的所有內容,而不是依賴服務專員的記憶。

"I live in NYC, I can only do weekends, I have 10 people coming with me"
# agent/concert/nightly.py
def what_they_asked_for(ctx) -> str:
    """Everything this person typed, joined, in their own words."""
    said = []
    for event in (ctx.session.events or []):
        if event.author != "user":
            continue
        for part in (event.content.parts if event.content else []) or []:
            text = (getattr(part, "text", None) or "").strip()
            if text and text not in said:
                said.append(text)
    return "\n".join(said)

讀取使用者建立的事件,只會查看

text

部分,因此預算常見問答不會在這裡回覆。function_response沒有重複項目。

請注意,agree_budgetNote 會在每次執行時呼叫此函式,而不只是第一次,因此在代理程式排隊時輸入的修正內容也會傳送。接著,open_the_night將這兩句話、要求和預算交給挑選者,並請他朗讀。

圖表,節點多一個

我們就從對話開始:

# agent/concert/nightly.py
nightly = Workflow(
    name="concert_nightly",
    edges=[(START, agree_budget, open_the_night, pick_show, queue_up,
            check_front, brief, buyer_agent)],
)

同一張圖,但 agree_budget 在前面

兩個節點停止並等待,且機制相同。check_front等待場地,並由時鐘回答。agree_budget 會等待使用者接聽,且只能由使用者接聽。這個差異就是步驟 10 的全部內容。

逐步執行

1. 開始使用

👉 重新載入 ADK 網路 UI,然後選取 concert 代理程式。

👉✨ 詢問你真正想要的東西:

Get us two tickets to the Amsterdam show.

系統會停止執行,並詢問你要花多少錢

在步驟 8 中,圖表沒有使用者,因此您輸入的任何內容都會遭到捨棄,而 go 也會像任何內容一樣。這裡的 agree_budget 會在第一行呼叫 what_they_asked_for(ctx),收集您輸入的所有內容並放入 state["request"]。然後將預算和清單交給揀貨員。open_the_night

你沒有指定日期,而阿姆斯特丹有兩場演出。因此請觀察選擇器是否會回溯到 recall(),找出「Sam bails on weeknights」,然後選擇星期六。

在瀏覽器中,執行會在 adk_request_input 事件上停止:

圖表剛才問了你一個問題。與步驟 8 中的佇列等待相同的中斷,但現在指向由專人回答問題。

2. 以使用者實際回答問題的方式回答

👉✨ 在 Enter your response... 方塊中 (不是底部的對話方塊):

About a 100 for the cheap seats. 250 if they're the good ones.

觀看停止並確認:

[agree_budget] reading back: About a hundred for the cheap seats. Two-fifty
               if they're the good ones.

⚡ adk_request_input
   So: About a hundred for the cheap seats. Two-fifty if they're the good
   ones. Have I got that right?

系統會先讀出號碼,再進行撥號

3. 確認

👉✨ 同一個方塊:

Yes, that's right.
[agree_budget] agreed: About a hundred for the cheap seats. Two-fifty if
               they're the good ones.
[open]         Budget: About a hundred for the cheap seats. Two-fifty if
               they're the good ones.
[pick_show]    → {"section": "A", "reason": "..."}
[queue_up]     ms-ams-01 section A — ticket q_5924edc2 at #14,203
[check_front]  #14,203 — not yet. Pausing.

四個節點執行一個字詞。👉 開啟「狀態」分頁,然後找到 budget,也就是你逐字逐句說出的內容。

在「州」分頁中,用自己的話說明預算

不允許前置字元。這是工作階段狀態,如果您啟動「新工作階段」,預算就會消失,因此系統會再次詢問您。

4. 搶先抵達終點

👉🔴 按下場地面板上的「略過等待」。

👉✨ 在該方塊中再次回答最新問題:adk_request_input

go

再次喚醒,且不會詢問兩次

👉 查看場地面板:一筆訂單,價格在您同意的座位類型範圍內。

一項訂單,符合您同意的條件

圖表如何向使用者提問

開啟 nightly.py 並閱讀 agree_budget。請一併滿足這三項條件,如果缺少任何一項,系統都會無限期要求驗證。

@node(rerun_on_resume=True)              # 1. run again when woken
def agree_budget(node_input, ctx):
    answers = ctx.resume_inputs or {}    # 3. where the answer arrives

    said = answers.get(ASK_BUDGET)
    if not said:
        return RequestInput(
            interrupt_id=ASK_BUDGET,     # 2. STABLE id, not the default
            message="What are you willing to spend?",
        )
    ...

rerun_on_resume=True

否則節點會視為已完成並略過

穩定 interrupt_id

預設值是每次執行時的新 UUID,因此無法透過任何項目查詢答案

ctx.resume_inputs[id]

答案會顯示在該處。不是 RequestInput 的傳回值,ADK 會疊代節點,而不是傳送至節點

圖表

這是步驟 8 的圖表,前面多了一個節點。同一張圖,但 agree_budget 在前面

使用者確認的限制是以工作階段為範圍,對話結束時就會失效。如果跨工作階段保留臨時限制,可能會導致一次性決策變成非預期的永久預設值。

10. 雲端堆疊

筆電進入休眠狀態後,本機檔案消失

在筆電上,代理程式會依賴電腦保持喚醒狀態、本機 SQLite 資料庫和本機檔案。如果關上筆電螢幕或部署到會重新啟動的雲端容器,本機檔案和記憶體就會消失。

如要在正式環境中穩定執行:

  1. 雲端運算:在容器 (例如 Cloud Run) 中執行代理程式,保持連線狀態。
  2. 耐用的 Cloud 資料庫:使用雲端服務 (Cloud SQL 用於工作階段資料,Cloud Storage 用於檔案) 取代本機檔案和 SQLite。
  3. Cloud Timers:使用代管排程器 (例如 Cloud Scheduler 和 Pub/Sub),即使筆電關機,代理程式也能喚醒。

👉🔴 在場地面板中,按下「重設場地」。每個步驟都從同一個世界開始:8 場演出、所有座位皆可預訂,時鐘為 1 倍速。

👉💻 移至最終程式碼:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 10 --force

檔案

異動內容

main.py

新功能。一個 FastAPI 服務聊天頁面和排程器呼叫的端點

Dockerfile

新功能。一般的 python-slim 和 uvicorn。與 ADK 無關

monstertix/

複製到代理程式中,因此網頁會與代理程式一起傳送

concert/

不變。與您在步驟 9 中完成的資料夾完全相同

您已建構的網頁

** ADK 網頁 UI** 嚴格來說是本機偵錯介面,用於在開發期間檢查狀態、事件串流和工作流程圖。這個套件不適用於正式環境部署作業。

如為正式環境,請使用步驟 3 中建構的獨立聊天前端。這項工具會透過 /wake 端點處理使用者訊息和互動中斷,不會公開內部偵錯控制項。

將本機狀態遷移至雲端服務

停止伺服器並查看寫入的內容:

sessions.db          a SQLite file next to the code
memory/userx.md      a markdown file you can open in an editor
artifacts/           seat maps written to a folder

本機檔案很適合用於開發,但無法用於正式環境,因為 Cloud Run 容器使用的是暫時性的執行個體本機儲存空間。

在 Cloud Run 中執行時:

  • 執行個體縮減為零:流量停止時,容器會終止,並刪除本機檔案系統。
  • 執行個體可獨立水平擴展:多個並行執行個體不會共用磁碟空間,每個執行個體都會使用自己的獨立檔案系統。

如果代理程式將佇列票證或工作階段狀態儲存至本機 SQLite 檔案 (sessions.db),後續要求路由至新的或不同的容器執行個體時,會發現資料庫為空。如要在擴充和重新啟動時維持持久狀態,必須將本機檔案替換為代管的外部服務。

您需要的是能存續於任何單一容器的狀態。三項服務,每項服務對應一個檔案:

使用筆電

在雲端

簡介

sessions.db

Cloud SQL

代管 Postgres 資料庫。無論是否有任何項目正在執行,都會存在

memory/userx.md

Cloud Storage

值區。檔案會以物件形式儲存,每個執行個體都能立即存取

artifacts/

Cloud Storage

同一個儲存空間

clock.py

Cloud Scheduler

Google 執行的 cron。終端機不是鬧鐘

建立這些項目

佈建 PostgreSQL 資料庫、資料庫使用者和 Cloud Storage 值區。請注意,Cloud SQL 執行個體建立作業需要幾分鐘,且已在 setup.sh 期間於背景啟動。

👉💻 收集並製作其餘部分:

cd ~/monstertix
./setup-cloud-state.sh
→ instance  RUNNABLE
→ database  adk created
→ user      adk created
→ bucket    created
→ memory    memory/userx.md → gs://your-project-agent-you/memory/
✓ durable state ready. Written to .env

該指令碼建立了四個項目 (上表中的每一列各一個),並將兩個 URI 寫入 .env:

CLOUD_SESSION_SERVICE_URI=postgresql+asyncpg://adk:...@/adk?host=/cloudsql/proj:region:instance
CLOUD_ARTIFACT_SERVICE_URI=gs://your-project-agent-you/artifacts

使用代管排程觸發條件部署 Agent

服務準備就緒後,請為代理程式提供永久主機。

👉💻 三到四分鐘。請啟動這項功能並繼續閱讀:

cd ~/monstertix
. ./set_env.sh
./deploy-agent.sh

這個指令碼會從 agent/ 建構容器映像檔、將其部署至 Cloud Run,並佈建支援事件驅動的基礎架構。

完整的部署作業包含五個不同的元件:

1. gcloud run deploy       the container: your agent, the graph, and the page
2. a Pub/Sub topic         a queue for messages
3. a service account       an identity, so the message is allowed in
4. a push subscription     topic ──► /trigger/wake
5. a Cloud Scheduler job   the alarm clock, on a cron

Cloud Scheduler 做為觸發來源

Cloud Scheduler 會取代本機執行計時器 (clock.py),改用獨立於本機終端機工作階段的代管 Cron 服務,處理自動重試作業,並支援時區排程。

Pub/Sub 做為訊息解除耦合層

在 Cloud Scheduler 和 Cloud Run 之間放置 Pub/Sub 主題,可帶來三項架構優勢:

  • 訊息耐久性:如果 Cloud Run 服務正在冷啟動、重新部署或暫時無法使用,則會將傳入的觸發事件排入佇列。
  • 內建重試政策:自動重試暫時性傳送失敗,不必使用自訂應用程式層級的重試邏輯。
  • 扇出擴充性:讓多個下游服務訂閱相同的排程事件,不必重新設定排程器。

替代文字

設定 trigger_sources=["pubsub"] 會註冊與標準 Pub/Sub 封包酬載相容的推送端點,直接將傳入的訊息轉換為代理程式叫用事件。

→ agent    https://concert-you-xxxx.run.app
→ topic    presale-you created
→ iam      concert-trigger@your-project... may invoke concert-you
→ push     → https://concert-you-xxxx.run.app/trigger/wake
→ schedule 0 3 * * *

與其對話

👉 在瀏覽器中開啟代理程式的網址。👉✨ 要求預訂:

Book me 3 tickets for The Midnight Signal in Tokyo. Best seats in the house,
I don't want the cheap ones.

系統應會詢問你願意花多少錢

👉✨ 像平常說話一樣回答問題,然後確認:

up to 250 a seat for the good ones

目前執行作業已暫停並排入佇列。

👉🔴 按下場地面板上的「略過等待」,這樣訊息送達時,佇列就已準備就緒。

如要依實際時間表執行工作,必須啟用工作,否則 jobs run 會拒絕處於暫停狀態的工作 (使用 Job.state must be ENABLED):

gcloud scheduler jobs resume presale-$(gcloud config get-value account | cut -d@ -f1) \
  --location=$GOOGLE_CLOUD_REGION

👉 幾秒內,代理程式就會在確認的預算限制內執行購買作業,並自動傳回訂單確認資訊。

👉🔴 檢查場地面板。一筆訂單。

如要每 5 分鐘執行一次,請按照下列步驟操作:

gcloud scheduler jobs update pubsub presale-$(gcloud config get-value account | cut -d@ -f1) \
  --location=$GOOGLE_CLOUD_REGION --schedule="*/5 * * * *"

清除所用資源

部署的項目有兩個:場地和旁邊的代理程式。兩者都會縮減至零,因此閒置時不會產生任何費用。仍要刪除。

./destroy-agent.sh          # service, topic, subscription, scheduler job
./destroy-venue.sh

請注意,我們刻意不刪除 Cloud SQL 和 bucket,因為這些資源會保留您的資料,且兩者都會持續計費。確認完成後,請按照下列步驟操作:

./destroy-agent.sh --all

其他所有內容都是本機內容。完成後刪除資料夾,或保留資料夾並針對自己的專案執行,時間長度不限。

代理程式、工具、回呼和預算都未變更。兩個 URI 和一個環境變數。

11. 系統總覽和疑難排解指南

環境差異:本機與雲端架構

筆電

在雲端

異動內容

--session_service_uri=sqlite+aiosqlite:///...

Cloud SQL

連線字串

--artifact_service_uri=file://...

gs://bucket

URI

memory/userx.md

gs://bucket/memory/

路徑

adk web

一項 Cloud Run 服務

Dockerfile 和 main.py

monstertix/clock.py

Cloud Scheduler → Pub/Sub

trigger_sources=["pubsub"]

你 (使用鍵盤)

someone_is_there()

預設值,可根據要求覆寫

核心代理程式實作 (包括 root_agent、工具定義、回呼防護和預算強制執行) 完全不變。比較 solutions/step10_deploy/concert/ 與步驟 9,會發現兩者完全相同,這驗證了 ADK 的服務抽象化如何將基礎架構設定與代理程式邏輯分開。

如果無法繼續

在任何步驟中取得完整的有效程式碼:

./use-solution.sh N --force          # any step. resets memory + sessions + artifacts
./use-solution.sh N --force --keep   # code only, leave state alone

發生故障時

問題

修正

✗ Cannot access project

ID 錯誤。rm ~/project_id.txt && ./setup.sh

模型傳回 404 錯誤

-latest 別名僅適用於 AI Studio。使用 ADK_MODEL=gemini-latest-flash

address already in use

通訊埠 8000 或 8090 已有其他用途。lsof -ti:8000 | xargs kill -9

代理商無法抵達場地

curl $VENUE_URL/health,或再次按 ./deploy-venue.sh

佇列永遠不會前進

按下「SKIP THE WAIT」(略過等待)。在 1× 時,佇列不會自行移至前端

面板上會顯示第二張排隊票券

你在即時通訊方塊中回答問題,而非 adk_request_input 活動的 Enter your response... 方塊。聊天方塊會開始新的執行程序

下拉式選單空白,或應用程式無法載入

adk web 會將 agent/ 下的每個資料夾視為應用程式,如果其中一個不是,整個清單就會失敗。ls agent/ 應只顯示 concert/,其他一概不顯示,因為從 agent/ 內部執行項目可能會留下多餘的 memory/。./use-solution.sh 8 --force 整理

[EXPERIMENTAL] 則警告

預期。EventsCompactionConfig 和 ResumabilityConfig 在 2.6.2 版中均為正式發布前版本

一切都很奇怪

按下「重設場地」,然後按 rm sessions.db

記憶體檔案中充滿重複預訂

remember() 會在每次執行時附加。./use-solution.sh N --force 即可重設,或 cp seed/memory/default.md memory/userx.md

adk: command not found

source .venv/bin/activate - 每個新分頁都需要

Cloud Shell 逾時

重新啟動 adk web。sessions.db 存活下來,也就是步驟 6,你可免費體驗