ADK-Python 捕获 StaleSessionError 后如何重读会话并重放 append_event 修复并发写冲突
【免费下载链接】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
在 ADK-Python 中用DatabaseSessionService、SqliteSessionService或FirestoreSessionService持久化会话时,如果两个进程(或两个写者)持有同一个会话的副本并先后调用append_event,后写入的一方会收到StaleSessionError。这是 ADK 的乐观并发保护:你手里的内存Session已经落后于存储中的副本,如果不拦截,你的追加会静默覆盖另一个写者刚写入的历史。本文给出排查这个异常、修正错误捕获方式,以及按官方恢复路径重读会话、重放append_event的完整操作。
先确认这个异常什么时候会抛
StaleSessionError只由做了并发写检查的三种会话服务在append_event时抛出:DatabaseSessionService、SqliteSessionService、FirestoreSessionService。触发条件是你持有的内存Session在你读取之后、写入之前,已经被别人更新过。InMemorySessionService不做这项检查,文档也明确说明它不适合多线程生产使用,所以如果你在开发环境没看到这个异常,不代表生产环境不会有。
异常的完整定义见 异常说明,会话生命周期见 Session and BaseSessionService。
第一步:检查是否被宽泛的except ValueError吞掉了
这是排查StaleSessionError时最容易被忽略的一点。StaleSessionError为了向后兼容继承了ValueError,所以会话代码附近任何except ValueError都会把它静默吞掉。错误的写法看起来像严谨的错误处理,实际效果是把这一轮的对话历史直接扔掉:
try: await session_service.append_event(session, event) except ValueError: logger.warning("bad event, skipping")用户侧的症状是对话缺了一整段,且日志里只有一行无关紧要的 warning。如果你正在排查"会话历史突然少了几条事件",先全局搜索except ValueError是否覆盖了append_event调用。
修正方式是捕获具体类型,并且把StaleSessionError子句放在任何ValueError子句之前(Python 取第一个匹配的except子句,宽泛的放前面会遮住下面所有窄的):
try: await session_service.append_event(session, event) except StaleSessionError: # 恢复逻辑见下一步 ... except ValueError: # 其他真正的参数错误 ...如果一个except子句确实需要同时覆盖多个 ADK 异常,用元组显式列出,例如except (StaleSessionError, SessionNotFoundError)。ADK 的六个自定义异常没有公共基类,不存在能兜住所有 ADK 错误的单个except。导入方式只有包级入口:
from google.adk.errors import StaleSessionError注意反向的坑:AlreadyExistsError、NotFoundError、ToolExecutionError继承的是Exception而不是ValueError,except ValueError对它们一无所获。
第二步:重读会话并对新副本重放 append_event
StaleSessionError是六个 ADK 异常中唯一有官方恢复路径的:用get_session重新读取会话,再对新副本重放本次append_event。完整写法:
try: await session_service.append_event(session, event) except StaleSessionError: session = await session_service.get_session( app_name=session.app_name, user_id=session.user_id, session_id=session.id, ) await session_service.append_event(session, event)两个细节决定这段代码能否直接跑通:
- 会话由
(app_name, user_id, session_id)三元组唯一标识,get_session三个参数都必须传,不能只靠session_id。 - 重放前先用旧副本上的
app_name、user_id、id重新读取,拿到的是包含其他写者最新写入的会话副本;对新副本追加时,存储层会再次做版本校验,通过后才落库。
如果你在写自定义后端(继承BaseSessionService),append_event里要先持久化事件,再调用await super().append_event(session, event)保持内存副本同步,否则自己也会制造 stale 会话。
第三步:验证写入没有再丢事件
修复是否生效,用一次get_session读回即可判断,检查两点:
- 被重放的
event出现在session.events里; - 该事件
actions.state_delta中的键值已合并进session.state。
仓库单元测试对同一场景的断言可以作为参照(见 测试文件):对一个 stale 会话直接append_event会抛StaleSessionError,消息匹配modified in storage;失败的追加不会落库,读回的会话events只包含已提交的事件,stale 事件携带的sk3键在state中不存在。执行你的恢复逻辑后,读回的会话应同时包含其他写者的事件和你重放的事件,两者都不缺。
错误消息、异常细节与文档间的口径
- 异常消息来自服务实现中的常量(见 database_session_service.py):
The session has been modified in storage since it was loaded. Please reload the session before appending more events.按这个文本可以精确定位是否命中了 stale 检查。 StaleSessionError没有定义自己的构造函数,也没有.message属性,str(StaleSessionError())是空字符串——不要依赖读异常上的默认文本来判断,直接用类型判断。- Session 指南在"Detecting a stale session"一节里说
DatabaseSessionService"raisesValueError",与 异常文档的StaleSessionError不矛盾:后者就是前者的具体子类,两份文档指向同一个异常。 - 六个 ADK 异常之间没有结构化错误码或 cause 字段,除
ToolExecutionError的error_type外,消息字符串是全部信息。
修复完成后,如果你的会话由Runner驱动,日常代码通常只直接调用服务来创建、列举和删除会话,get_session与append_event由Runner代劳;上面这段手动恢复逻辑适用于你直接调用会话服务的场景,例如自定义服务封装或多进程共享同一会话的部署。
【免费下载链接】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),仅供参考