news 2026/9/13 13:46:52

ADK-Python 会话机制完全指南:Session 与 BaseSessionService 的原理、存储与实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ADK-Python 会话机制完全指南:Session 与 BaseSessionService 的原理、存储与实战

ADK-Python 会话机制完全指南:Session 与 BaseSessionService 的原理、存储与实战

【免费下载链接】adk-pythonAn open-source, code-first Python toolkit for building, evaluating, and deploying sophisticated AI agents with flexibility and control.项目地址: https://gitcode.com/GitHub_Trending/ad/adk-python

导读

在 Google ADK(Agent Development Kit)Python 版中,Session是贯穿多轮对话的"会话记录"——它承载会话 id、归属(app/user)、state状态字典与有序的事件历史(event history);而BaseSessionService则是负责创建、读取、列举、删除这些记录并向其追加事件的存储接口。本文以官方指南 docs/guides/sessions/session/index.md 为核心骨架,结合仓库源码(sessions 模块 与 runners.py)深入讲解:会话的生命周期、状态作用域(app:/user:/temp:前缀)、历史裁剪配置、四种内置存储后端的选择与切换、以及如何将自定义后端接入 Runner。读完本文,你将能独立完成"内存开发 → SQLite/数据库持久化 → Vertex AI 生产部署"的存储层平滑迁移,并掌握自定义会话存储的完整实现路径。

Session 与 BaseSessionService:一文一存,职责分离

ADK 对"会话"的设计有一个核心理念:单个 agent 的一次运行(run)本身是无状态的——模型只能看到你喂给它的东西。真正把对话跨轮次"背"起来的是Session

  • Session是一个纯 Pydantic 模型(定义见 session.py),包含idapp_nameuser_idstate(会话状态字典)和events(有序事件历史,涵盖用户输入、模型回复、工具调用/结果等),以及last_update_time(最近一次更新的 Unix 时间戳)。它自身从不接触存储
  • 所有需要持久化的工作都经由BaseSessionService(定义见 base_session_service.py)完成。该抽象基类声明了四个抽象方法——create_sessionget_sessionlist_sessionsdelete_session——并附带一个所有后端都继承的具体实现append_event

正是这种"Session 只管数据结构、Service 只管读写"的拆分,让同一套 agent 代码在开发期使用进程内字典(InMemorySessionService)、在生产期使用共享数据库时完全无需改动——你只换 service,不换 agent。

Runner(见 runners.py)把session_service作为必填参数,并在运行过程中替你驱动get_sessionappend_event,因此绝大多数应用只需要直接调用 service 来创建、列举和删除会话。

快速上手:零配置的 InMemorySessionService

InMemorySessionService不需要任何配置即可使用。下面的示例(源自官方文档,可直接运行)创建会话、追加两个事件、再读回验证:

import asyncio from google.adk.events import Event from google.adk.sessions import InMemorySessionService APP_NAME = "hello_world" USER_ID = "user-123" async def main() -> None: session_service = InMemorySessionService() # 1. Create. Omit session_id to have one generated for you. session = await session_service.create_session( app_name=APP_NAME, user_id=USER_ID, state={"locale": "en-US"}, ) # 2. Append events. Each one lands in session.events, and any state the # event carries is merged into session.state. await session_service.append_event( session, Event(author="user", message="What is the weather?") ) await session_service.append_event( session, Event( author="weather_agent", message="It is sunny.", state={"last_city": "Zurich"}, ), ) # 3. Read it back. get_session returns None when nothing is stored. loaded = await session_service.get_session( app_name=APP_NAME, user_id=USER_ID, session_id=session.id ) assert loaded is not None print(len(loaded.events), loaded.state) if __name__ == "__main__": asyncio.run(main())

输出为:2 {'locale': 'en-US', 'last_city': 'Zurich'}

几个必须记住的约定:

  • 参数风格:除append_event以位置参数接收 session 与 event 外,其余所有方法均为关键字专用参数(keyword-only)。
  • 三元组标识:一个会话由(app_name, user_id, session_id)三元组唯一标识,而不是仅靠session_id,因此每次读取都必须提供全部三个参数
  • 事件即状态载体:事件中的state字段最终会被映射为EventActions.state_delta(见 event_actions.py),并在append_event时合并进session.state

底层实现:内存存储的数据结构

从源码看(in_memory_session_service.py),InMemorySessionService内部用三层嵌套字典组织数据:

  • sessions[app_name][user_id][session_id] -> Session:会话本体;
  • user_state[app_name][user_id][key] -> value:用户级状态;
  • app_state[app_name][key] -> value:应用级状态。

这解释了文档中"state lives in process dicts"的说法。此外,append_event在追加前会做事件去重if any(e == event for e in storage_session.events if e.id == event.id)),防止编排器广播共享状态 delta 时对同一事件的重复应用(in_memory_session_service.py)。

工作原理:生命周期、状态作用域与裁剪

会话生命周期

  • create_session:不传session_id时自动生成 UUID;传入的 id 已被占用时抛出AlreadyExistsError(定义见 already_exists_error.py)。初始state中的前缀键会被拆分到对应的 app/user/session 作用域存储(in_memory_session_service.py)。
  • get_session:会话不存在时返回None而非抛异常。
  • list_sessions:返回ListSessionsResponse(见 base_session_service.py),按last_update_time从旧到新排序,且省略事件历史
  • append_event:这是"两份会话"交汇的地方——基类实现先把事件的state_delta应用到内存中的Session并追加到session.events;各后端再覆写该方法把事件写入存储。event.partialTrue的部分事件会被原样返回且绝不落库,这正是流式输出分片不进历史的原因。
  • 失败要响亮:向存储不认识的会话追加事件时,所有后端都会抛出SessionNotFoundError(继承自ValueError,见 session_not_found_error.py),而不是静默丢弃事件。

基类append_event的具体流程(base_session_service.py)依次是:partial 事件短路返回 →_apply_temp_statetemp:状态应用到内存会话 →_trim_temp_delta_state从事件 delta 中剥离temp:键 →_update_session_state合并状态 → 追加事件。

状态作用域(State Scoping)

state中的键通过前缀划分作用域,前缀常量定义在State类上(state.py):

前缀常量作用域
仅当前会话。
app:State.APP_PREFIX该应用的所有会话。
user:State.USER_PREFIX该用户在该应用内的所有会话。
temp:State.TEMP_PREFIX仅当前 invocation;绝不持久化。

带前缀的键可以像普通键一样写在create_session(state=...)或事件的 state delta 中。service 会将其路由到正确的存储作用域,并在读取时(前缀保留)合并回session.statetemp:键是唯一例外:它先被应用到内存会话(保证同一 invocation 内后续 agent 可读,例如SequentialAgent中的output_key='temp:my_key'),随后在事件写入前被剥离(对应源码_apply_temp_state_trim_temp_delta_state)。

特别地,get_user_state(app_name=..., user_id=...)可以在没有 session id的情况下读取用户级状态,返回的是去掉user:前缀的原始键——非常适合在create_session之前引导上下文,避免为读取用户数据而做昂贵的list_sessions。注意:它不是抽象方法,基类默认实现直接抛出NotImplementedError,所以自定义后端若不覆写它,调用时会失败(base_session_service.py)。

关于状态校验值得一提:State类支持可选的 Pydanticschema,无前缀键的写入会按 schema 校验,前缀键(含:的键)跳过校验(state.py)。

裁剪加载的历史(GetSessionConfig)

通过向get_session传入GetSessionConfig可以限制读回的历史量。注意它位于google.adk.sessions.base_session_service子模块而非包根目录:

from google.adk.sessions.base_session_service import GetSessionConfig # The 20 most recent events. Use num_recent_events=0 for metadata and state # only, or after_timestamp=<unix seconds> to cut the history by time instead. recent = await session_service.get_session( app_name=APP_NAME, user_id=USER_ID, session_id=session_id, config=GetSessionConfig(num_recent_events=20), )

GetSessionConfig的两个字段(base_session_service.py):

  • num_recent_events:返回最近 N 个事件;0表示只取元数据与状态(不含事件);None表示不过滤;负数会触发ValueError(字段校验器强制>= 0,对应测试见 test_session_service.py)。
  • after_timestamp:只返回时间戳>=给定 Unix 秒的事件。

关键点在于:过滤发生在 service 内部,因此在数据库后端上,它会真正减少读取量,而不仅仅是"读回来再裁给你看"。内存后端的裁剪逻辑参考 in_memory_session_service.py,数据库后端的读取过滤见 database_session_service.py 中的get_session实现。

选择会话存储后端

服务导入路径适用场景
InMemorySessionServicegoogle.adk.sessions开发与测试。状态存于进程内字典,类注释明确声明不适用于多线程生产环境。
DatabaseSessionServicegoogle.adk.sessions需要持久化,或多个进程共享同一个会话。底层是 SQLAlchemy 异步引擎,需要安装dbextra。
VertexAiSessionServicegoogle.adk.sessions部署在 Vertex AI Agent Engine 上,使用其托管的会话存储,需要gcpextra。
SqliteSessionServicegoogle.adk.sessions.sqlite_session_service想要一个本地 SQLite 文件、无需服务器。ADK CLI 使用的正是它;注意它不会从包根目录再导出

补充:google.adk.sessions包根的__init__.py中(init.py),InMemorySessionServiceVertexAiSessionService采用惰性导入,而DatabaseSessionService在缺少 SQLAlchemy 依赖时会抛出带dbextra 提示的ImportError,这正是文档中"requires the db extra"的源码依据。

DatabaseSessionService:URL 或自持引擎,二选一

DatabaseSessionService接受一个 URL 或一个你已拥有的 engine,且恰好只能提供其一

from google.adk.sessions import DatabaseSessionService async with DatabaseSessionService("sqlite+aiosqlite:///./sessions.db") as svc: await svc.prepare_tables() # optional; otherwise done on first use session = await svc.create_session(app_name=APP_NAME, user_id=USER_ID)

注意事项(均有源码支撑,见 database_session_service.py):

  • URL 必须使用异步驱动:如sqlite+aiosqlitepostgresql+asyncpg等。若 URL 解析到同步驱动,构造器会抛出带修复建议的ValueError(如提示改用{backend}+{async_driver}://)。
  • db_engine=<AsyncEngine>则复用应用自己的引擎,service不会关闭它不拥有的引擎
  • 作为异步上下文管理器退出时,只关闭自己创建的引擎;否则需要手动调用close()
  • SQLite 内存库会自动配置StaticPoolcheck_same_thread=False;非 SQLite 后端默认开启pool_pre_ping
  • prepare_tables()可选,首次使用时也会自动建表(表创建有asyncio.Lock保证线程安全)。
  • 内部会为每个会话建立 per-session 锁来串行化进程内的append_event调用(self._session_locks),并使用行级锁(with_for_update)保护并发写入。

VertexAiSessionService:app_name 的特别约束

切换到VertexAiSessionService前有一个必须知道的差异:在这里app_name不是自由字符串。它必须是 reasoning engine id(纯数字),或完整的projects/.../locations/.../reasoningEngines/N资源名;除非你向构造函数传了agent_engine_id。源码中的解析逻辑(vertex_ai_session_service.py)用正则^projects/([a-zA-Z0-9-_]+)/locations/([a-zA-Z0-9-_]+)/reasoningEngines/(\d+)$校验完整资源名,app_name.isdigit()时直接视为 engine id,否则抛出ValueError提示使用完整资源名或 engine id。

SqliteSessionService:ADK CLI 的本地落盘选择

SqliteSessionService(sqlite_session_service.py)基于aiosqlite将事件以 JSON 形式存入本地 SQLite 文件,构造参数db_path接受文件系统路径或sqlite:/sqlite+aiosqlite:URL(_parse_db_path会做归一化),不需要任何服务器。若数据库文件使用的是旧 schema,初始化时会给出迁移提示——仓库 migration 目录 中提供了从旧 SQLAlchemy 存储迁移到新格式的工具(migrate_from_sqlalchemy_pickle/migrate_from_sqlalchemy_sqlite)。

进阶应用

把 service 接入 Runner

  • 要解决的问题:让"每一段对话存到哪里"由一个地方统一决定。
  • 实现方式:把 service 传给Runner(session_service=...),并在第一次运行前创建好会话。Runner默认auto_create_session=False(runners.py),因此遇到未知的session_id会抛出SessionNotFoundError,而不是静默开启一段新对话(对应逻辑见 runners.py)。
from google.adk.runners import Runner runner = Runner( agent=my_agent, session_service=session_service, # 统一决定会话存储位置 auto_create_session=False, # 显式创建,避免误开新会话 )

编写自己的后端

  • 要解决的问题:会话需要存到 ADK 未内置的存储(如 Redis、MongoDB 等)。
  • 实现方式:继承BaseSessionService,实现四个抽象方法(create_sessionget_sessionlist_sessionsdelete_session)。随后:
    • 覆写append_event持久化事件,并务必调用await super().append_event(session, event),让内存中的会话状态保持同步(基类会处理 temp 状态应用、delta 裁剪与状态合并)。
    • 如果存储能回答用户级状态查询,覆写get_user_state;基类默认实现抛NotImplementedError
    • 如果采用缓冲写入,覆写flush——基类的flush是空操作,Runner关闭时会调用它(见 runners.py)。

检测过期会话(Stale Session)

  • 要解决的问题:两个 worker 持有同一个Session对象并同时追加事件,其中一个会静默覆盖另一个的历史。
  • 实现方式:无需自己写代码。DatabaseSessionService为每个会话跟踪一个存储修订标记(storage revision)。从数据库读回的Session会携带精确的内部标记(Session._storage_update_marker私有属性,见 session.py);当append_event发现内存副本的标记落后于存储时,抛出StaleSessionError(即文档所述的ValueError语义,定义见 _stale_session_error.py)。恢复方法:重新调用get_session拿到最新会话,再对新会话重放追加。相关实现位于 database_session_service.py,并被测试覆盖(见 test_session_service.py 中test_append_event_to_stale_session与并发场景测试)。

限制与边界

  • InMemorySessionService不适合生产:重启后一切消失、worker 之间不共享、且不加锁。它只适合开发与测试(类 docstring 亦明确声明,见 in_memory_session_service.py)。
  • list_sessions返回的是"残缺"会话:事件历史被丢弃,state填充多少取决于后端实现。需要完整数据时请用get_session按需加载。
  • DatabaseSessionService需要dbextra、VertexAiSessionService需要gcpextra:缺失对应依赖时导入会失败并得到明确的 extra 提示。
  • get_user_state依赖后端支持:自定义后端若不覆写,调用即抛NotImplementedError,可按文档提示改用list_sessions+get_session枚举的方式读取用户状态。

总结

ADK 的会话体系用"纯模型 + 存储接口"的极简抽象,把多轮对话的持久化问题压缩为一个可插拔的session_service参数:开发期用InMemorySessionService零配置起步,本地发布用SqliteSessionService落盘,生产期切到DatabaseSessionServiceVertexAiSessionService而 agent 代码零改动。理解会话生命周期、app:/user:/temp:状态作用域、GetSessionConfig历史裁剪以及 stale 检测机制,是写出健壮多轮应用的基础——这些知识同样适用于编写自定义后端,将 ADK 的会话体系无缝接入你自己的存储设施。

延伸阅读:本指南对应的状态详解参见 docs/guides/sessions/state/index.md;会话状态相关工具与测试分别位于 sessions 源码目录 与 tests/unittests/sessions;Runner 与会话服务的协作细节见 runners.py。

【免费下载链接】adk-pythonAn open-source, code-first Python toolkit for building, evaluating, and deploying sophisticated AI agents with flexibility and control.项目地址: https://gitcode.com/GitHub_Trending/ad/adk-python

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 13:44:27

LKY_OfficeTools:重装系统后,下载、安装、激活 Office 一气呵成

LKY_OfficeTools&#xff1a;重装系统后&#xff0c;下载、安装、激活 Office 一气呵成 【免费下载链接】LKY_OfficeTools 一键自动化 下载、安装、激活 Office 的利器。 项目地址: https://gitcode.com/GitHub_Trending/lk/LKY_OfficeTools 重装完系统&#xff0c;装 O…

作者头像 李华
网站建设 2026/9/13 13:42:19

WolfCut开源剪辑器:Rust+Tauri打造的本地化高性能视频编辑工具

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 13:40:34

Async Tool Calling与Mid-turn Steering实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 13:40:28

Bun 运行时核心原理与工程落地指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 13:38:31

Nuxt.js数据请求方案对比与实战优化

1. Nuxt.js 数据请求方案全景解析在 Nuxt.js 项目中处理数据请求时&#xff0c;开发者通常会面临三种核心方案的选择&#xff1a;直接使用$fetch、组合式函数useFetch以及useAsyncData。这些方法看似功能相似&#xff0c;实则各有其设计哲学和适用场景。作为经历过多个 Nuxt 项…

作者头像 李华