agent_memory.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. """图状态(checkpoint)存储。
  2. 与 1.5 的关键差异:1.5 用了一个自定义的 ``_PerLoopSaver`` 去桥接
  3. 「异步按事件循环分发 / 同步每次开新连接」——那套桥接是为了应付多事件
  4. 循环的同进程场景,代价是每个同步调用都要新开一次 sqlite 连接(见 1.5
  5. agents/agent_memory.py 的注释)。2.0 的单进程约束下(uvicorn workers=1)
  6. 只有一个事件循环,不需要这层桥接:启动时创建一次 saver,全进程复用。
  7. 代价是 **不能多 worker**:多进程各自持有自己的 sqlite 文件句柄与图状态,
  8. 并发锁与调度器也仍使用进程级状态。RuntimeEnvironment 启动时提示单进程约束。
  9. """
  10. from __future__ import annotations
  11. import asyncio
  12. from typing import Any
  13. import config
  14. from obs import get_logger
  15. logger = get_logger(__name__)
  16. # 进程级单例。初始化前为 None,避免在无事件循环时(import 期)创建连接。
  17. _saver: Any | None = None
  18. _init_lock = asyncio.Lock()
  19. _kind: str = config.CHECKPOINTER
  20. class CheckpointerConfigError(RuntimeError):
  21. """checkpointer 配置不可用。刻意 fail-fast,不静默回落到内存。"""
  22. def saver_kind() -> str:
  23. """当前档位:sqlite | memory。"""
  24. return _kind
  25. def _serializer():
  26. """允许应用声明的业务结果类型,其他自定义类不从 checkpoint 动态反序列化。"""
  27. from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
  28. from extensions.registry import load_extensions
  29. allowed = [(spec.result_schema.__module__, spec.result_schema.__name__)
  30. for spec in load_extensions().business_agents.values()]
  31. return JsonPlusSerializer(allowed_msgpack_modules=allowed)
  32. def get_checkpointer() -> Any:
  33. """取进程级 checkpointer。
  34. ``memory`` 档在首次调用时**惰性创建**——InMemorySaver 是同步可构造的,
  35. 没有任何初始化依赖,却要求调用方先进 lifespan 是没必要的仪式,也会让
  36. 脚本、测试、调度器这些非 HTTP 入口平白多一层耦合。
  37. ``sqlite`` 档仍然要求先 ``await init_checkpointer()``:它需要打开异步
  38. 连接,没法在同步函数里做,而且**绝不能静默回落到内存**——那会让
  39. 「重启后中断恢复失效」变成一个没人发现的行为变化。
  40. """
  41. global _saver
  42. if _saver is None and _kind == "memory":
  43. from langgraph.checkpoint.memory import InMemorySaver
  44. _saver = InMemorySaver(serde=_serializer())
  45. logger.info("checkpointer=memory 惰性创建(图状态不落盘)")
  46. if _saver is None:
  47. raise CheckpointerConfigError(
  48. "checkpointer 尚未初始化:请在 async with RuntimeEnvironment() 内执行任务"
  49. )
  50. return _saver
  51. def is_initialized() -> bool:
  52. """是否已初始化(/health 用)。"""
  53. return _saver is not None
  54. async def init_checkpointer() -> Any:
  55. """初始化 checkpointer。幂等,重复调用直接返回已有实例。"""
  56. global _saver, _kind
  57. async with _init_lock:
  58. if _saver is not None:
  59. return _saver
  60. if _kind == "memory":
  61. from langgraph.checkpoint.memory import InMemorySaver
  62. _saver = InMemorySaver(serde=_serializer())
  63. logger.warning(
  64. "checkpointer=memory:图状态不落盘,重启后中断恢复失效"
  65. "(会话历史仍从业务数据库补种)"
  66. )
  67. return _saver
  68. if _kind != "sqlite":
  69. raise CheckpointerConfigError(
  70. f"不支持的 CHECKPOINTER={_kind!r},可选值:sqlite | memory"
  71. )
  72. try:
  73. import aiosqlite
  74. from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
  75. except ImportError as exc:
  76. raise CheckpointerConfigError(
  77. f"checkpointer=sqlite 需要 aiosqlite 与 langgraph-checkpoint-sqlite:{exc}"
  78. ) from exc
  79. path = config.resolve_path(config.CHECKPOINT_DB_PATH)
  80. conn = None
  81. try:
  82. conn = await aiosqlite.connect(str(path))
  83. saver = AsyncSqliteSaver(conn, serde=_serializer())
  84. await saver.setup()
  85. except BaseException as exc:
  86. if conn is not None:
  87. await conn.close()
  88. if not isinstance(exc, Exception):
  89. raise
  90. raise CheckpointerConfigError(
  91. f"打开 checkpoint 库失败 path={path}:{type(exc).__name__}: {exc}"
  92. ) from exc
  93. _saver = saver
  94. logger.info("checkpointer=sqlite 就绪 path=%s", path)
  95. return _saver
  96. async def close_checkpointer() -> None:
  97. """关闭底层连接(RuntimeEnvironment 退出时调用)。"""
  98. global _saver
  99. saver = _saver
  100. _saver = None
  101. if saver is None:
  102. return
  103. conn = getattr(saver, "conn", None)
  104. if conn is not None and hasattr(conn, "close"):
  105. try:
  106. await conn.close()
  107. except Exception as exc: # pragma: no cover - 关闭失败不影响进程退出
  108. logger.warning("关闭 checkpoint 连接失败:%s: %s", type(exc).__name__, exc)
  109. logger.info("checkpointer 已关闭")
  110. def thread_config(thread_id: str) -> dict[str, Any]:
  111. """构造 LangGraph 的 configurable config。
  112. ``thread_id`` 恒等于 ``session_id``(由 api/chat_routes 强制),
  113. 这是防止越权读写别人会话状态的第一道闸。
  114. """
  115. return {"configurable": {"thread_id": thread_id}}
  116. async def has_checkpoint(thread_id: str) -> bool:
  117. """该 thread 是否已有图状态。
  118. 冷线程需要从业务数据库补种历史消息;热线程只喂当轮新消息,否则历史会被
  119. 重复追加,上下文按 O(N²) 膨胀。
  120. """
  121. saver = get_checkpointer()
  122. try:
  123. tup = await saver.aget_tuple(thread_config(thread_id))
  124. except Exception as exc: # pragma: no cover - 取决于底层库
  125. logger.warning("探测 thread 状态失败 thread=%s:%s: %s", thread_id, type(exc).__name__, exc)
  126. raise
  127. return tup is not None
  128. async def delete_thread(thread_id: str) -> None:
  129. """删除某 thread 的全部图状态。删会话时必须同步调用,否则图状态残留。"""
  130. saver = get_checkpointer()
  131. delete = getattr(saver, "adelete_thread", None)
  132. if delete is None:
  133. raise RuntimeError("当前 checkpointer 不支持删除 thread")
  134. try:
  135. await delete(thread_id)
  136. logger.info("已清除图状态 thread=%s", thread_id)
  137. except Exception as exc:
  138. logger.warning("清除图状态失败 thread=%s:%s: %s", thread_id, type(exc).__name__, exc)
  139. raise