iT邦幫忙

2026 iThome 鐵人賽

DAY 9
1

Day 9 | Sessions 管理:對話狀態、Rewind

讀完能做到:說出 Session 的四個核心屬性、用官方推薦的兩種正確方式更新 state(而不是直接改物件)、知道 Rewind 能救你什麼、不能救你什麼,以及升級到 ADK 2.0 時 session 資料庫為什麼可能需要遷移。

你不會每則對話都從零重講一次

官方文件開場用了一個很生活化的比喻:你跟朋友傳訊息,不會每一則都重新自我介紹一次,對話有它自己的脈絡。Agent 也一樣——沒有一個機制記住「這段對話目前談到哪」,agent 就只能每次都當作第一次見面。Session 就是專門用來追蹤與管理這些個別對話串的物件,管理它的是 SessionService

https://ithelp.ithome.com.tw/upload/images/20260906/20183762294dhGt7pz.png

Session 的四個核心屬性

使用者開始跟 agent 互動時,SessionService 會建立一個 Session 物件,作為這一段特定對話串的容器,裡面裝著:

  • 識別idapp_nameuser_id)——id 是這段對話串的唯一識別碼;app_name 標示這段對話屬於哪個 agent 應用;user_id 把對話跟特定使用者連結起來。
  • 歷史events)——這段對話串裡發生的所有互動(使用者訊息、agent 回應、工具動作)按時間順序排列的清單。
  • Session Statestate)——只跟這一段、目前進行中的對話相關的暫存資料,agent 互動過程中的便條紙。
  • 活動追蹤last_update_time)——這段對話串最後一次有 event 發生的時間戳記。
from google.adk.sessions import InMemorySessionService, Session

temp_service = InMemorySessionService()
example_session = await temp_service.create_session(
    app_name="my_app",
    user_id="example_user",
    state={"initial_key": "initial_value"}
)

print(f"ID: {example_session.id}")
print(f"State: {example_session.state}")  # 這裡只顯示初始 state
print(f"Events: {example_session.events}")  # 一開始是空的

官方特別加註一句容易被忽略的話:這裡看到的 state 只是初始值——之後所有的更新,都要透過 event 完成,不是直接改物件屬性。這句話看似平淡,卻是整篇文章接下來要花最多篇幅解釋的規則。

Session 生命週期:四個步驟

一輪對話裡,SessionSessionService 協作的簡化流程是:開始或接續(呼叫 create_session 開新對話,或用既有 session id 接續)→ 提供 ContextRunner 取得 Session 物件,讓 agent 能存取這段 session 的 stateevents)→ Agent 處理(分析問題,可能參考 state 與 events 歷史)→ 回應與 State 更新(agent 產生回應,Runner 把這一切包成一個 Event)。

四種 SessionService 實作

  • InMemorySessionService——開發用,程序結束就沒了。
  • VertexAiSessionService——託管在 Google Cloud(Python / Go / Java / Kotlin v0.7.0)。免建 GCP 專案的捷徑:Google Cloud Agent Platform express mode 讓你只用一把 API key 就初始化 VertexAiSessionService(agent_engine_id=APP_ID),不用傳 projectlocation。代價是專案只有 90 天效期、額度受限(免費層每分鐘 10 次 session 建立/更新、30 次 event append)——寫 demo 很夠,正式上線前得掛付款帳戶解除限制。
  • DatabaseSessionService——自己的資料庫(Python / Go)。文件有專節談 Concurrency and locking,多實例部署時一定要看——這是自建 session 儲存最容易忽略、卻在高併發下最先出問題的部分。
  • FirestoreSessionService僅 Java)——接 Google Cloud Firestore 的第四種官方實作。兩個容易踩的雷:google-adkgoogle-adk-firestore-session-service 兩個依賴版本要對齊;綁定的 FirestoreDatabaseRunner 內部會初始化 GcsArtifactService沒設定 gcs.adk.bucket.name 屬性會在啟動時直接丟 RuntimeException——這種「等你部署那一刻才發現」的坑,比程式碼寫錯更難排查。

長對話的效能考量:不管用哪一種 SessionService,session 存活得夠久,事件歷史就會一直長大——每次 invocation 都把完整歷史整包載入,遲早會拖慢回應也拉高成本。RunConfigget_session_config(Python-only)就是為此設計:用 num_recent_eventsafter_timestamp 限制載入的事件範圍,不用整包歷史都吃進來。細節與另外兩個容易被忽略的一般性 RunConfig 欄位(include_thoughts_from_other_agentsmodel_input_context)見 [[ADK Runtime 與開發介面]]。

State:agent 的便條紙,但有正確的改法跟危險的改法

session.state 是輕量的 key-value store,用四種前綴決定作用域:app:(整個 app 的所有使用者共享)、user:(綁定使用者,跨他的所有 session)、temp:(本次 invocation 結束就丟棄)、無前綴(存活於該 session)。

官方對「怎麼改 state」的說明,分成正確做法危險做法兩邊講得很清楚。正確做法是透過 ContextCallbackContextToolContext)上的 state 屬性去改,例如 callback_context.state['my_key'] = 'new_value',這種寫法會被框架自動追蹤成 event 的一部分。危險做法是拿到一個從 SessionService 直接取回的 Session 物件後,直接改它的 state——這繞過了整個事件記錄系統。

具體有兩種推薦更新方式。第一種是最簡單的 output_key,適合把 agent 的文字回應直接存進 state:

from google.adk.agents import LlmAgent

greeting_agent = LlmAgent(
    name="Greeter",
    model="gemini-flash-latest",
    instruction="Generate a short, friendly greeting.",
    output_key="last_greeting"  # 回應會被存進 state['last_greeting']
)

第二種是更完整的 EventActions.state_delta,適合一次改多個 key、非字串值、或指定 user:/app: 作用域:

from google.adk.events import Event, EventActions
import time

state_changes = {
    "task_status": "active",
    "user:login_count": session.state.get("user:login_count", 0) + 1,
    "user:last_login_ts": time.time(),
    "temp:validation_needed": True  # 會被丟棄,不會留在最終 state 裡
}
actions_with_update = EventActions(state_delta=state_changes)
system_event = Event(invocation_id="inv_login_update", author="system",
                      actions=actions_with_update, timestamp=time.time())
await session_service.append_event(session, system_event)

append_event 內部會做四件事:把 event 加進 session.events、讀出 state_delta 套用到對應作用域、更新 last_update_time、以及序列化同一 session 的併發更新——DatabaseSessionService 對該 session 取鎖,但 InMemorySessionService 不是 thread-safe 的,多執行緒環境下直接用它有競態風險,這點在寫教學程式碼範例時很少被提到,卻是真正上生產環境會咬人的地方。

為什麼不能直接改 state? 官方列了四個具體後果:繞過事件歷史、失去可稽核性;破壞持久化(DatabaseSessionServiceVertexAiSessionService 很可能根本不會存下這個變更);不是 thread-safe,可能競態遺失更新;忽略時間戳與相關 event 邏輯。

還有兩件跟 state 相關、常被忽略的事:

  1. state 是輕量 key-value store,大量資料或檔案內容要走 Artifacts 或資料庫工具,就不要塞進 state。
  2. dirty reads:同一個 invocation 內,還沒提交的 state 變更可能被讀到,寫 ParallelAgent 這類並行流程時必須理解這個行為,否則會遇到「明明剛寫進去的值,另一個平行分支讀不到」的詭異現象。詳細可參考官網最下面說明修改state 的警示。

Rewind:把對話倒帶

Rewind 讓你把 session 退回到某個較早的請求狀態——適合「使用者說錯了、重來一次」,或除錯時想重現特定的對話狀態。用法是指定一個要撤銷的 invocation:如果你有三個請求 A、B、C,想退回到 A 的狀態,就指定 B(會撤銷 B 跟 C 的變更)。

from google.adk.runners import InMemoryRunner

runner = InMemoryRunner(agent=agent.root_agent, app_name=APP_NAME)
session = await runner.session_service.create_session(app_name=APP_NAME, user_id=USER_ID)

await call_agent_async(runner, USER_ID, session.id, "set state color to red")
events_list = await call_agent_async(runner, USER_ID, session.id, "update state color to blue")

rewind_invocation_id = events_list[1].invocation_id
await runner.rewind_async(
    user_id=USER_ID,
    session_id=session.id,
    rewind_before_invocation_id=rewind_invocation_id,
)

運作原理值得說清楚:Rewind 不是把記錄刪掉,而是建立一個特殊的「rewind 請求」,把 session 的 state 與 artifacts 恢復到 rewind 點之前的狀態。所有請求,包括被倒帶的那些,都還留在記錄裡,供之後除錯、分析或稽核用;只是系統在準備下一次送給模型的請求時,會忽略被倒帶掉的那些請求——效果上就是模型「忘記」了 rewind 點之後的互動。

三個限制一定要知道:

  1. 只還原 session 層級的 state 跟 artifacts,app 層級跟 user 層級的不會被還原
  2. 不管理外部依賴——如果工具動作觸碰了外部系統(發了一封信、扣了一筆款),Rewind 不會幫你把外部系統也退回去,那是你自己的責任
  3. 不是原子操作——state 更新、artifact 更新、event 持久化不是包在同一個交易裡完成,所以應該避免對正在使用中的 session 做 rewind,或在 rewind 過程中同時操作該 session 的 artifacts,否則可能留下不一致的狀態。

Session 與 Memory 的差別

Session單一對話的短期脈絡;跨 session 的長期記憶是另一個機制(Memory,之後章節細講)。這是最容易在教學文章裡講混的地方——一個常見的錯誤心智模型是「session 存久一點不就等於 memory 了」,但兩者的儲存後端、查詢方式(session 依 id 取回、memory 靠語意搜尋)、跟生命週期設計目的完全不同。把兩者混為一談,最常見的後果是工程師想省事只用 session 硬撐長期記憶的需求,結果查詢效能與擴充性都撐不住。

銜接

Session 跟 State 解決的是「這段對話記得住什麼」;但記得住不代表便宜——對話一長,塞進模型的 context 就跟著變大,延遲跟成本都會被拖著往上走。明天要處理的正是這個問題:Context 壓縮與 Token 最佳化。


Google ADK 官方網站
GitHub - Agent Development Kit (ADK) 2.0

GitHub 開源實作:https://github.com/SeanLinH/adk_tutor

下一章 Day 10 - 像管理原始碼一樣管理 Context:壓縮與 Token 最佳化


上一篇
Day 08 - 資料落地:Grounding 與查證機制
下一篇
Day 10 - 像管理原始碼一樣管理 Context:壓縮與 Token 最佳化
系列文
Google ADK Agent 教戰:30 天從原型到可上線的 AI Agent 系統21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言