agent_memory.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  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 get_checkpointer() -> Any:
  26. """取进程级 checkpointer。
  27. ``memory`` 档在首次调用时**惰性创建**——InMemorySaver 是同步可构造的,
  28. 没有任何初始化依赖,却要求调用方先进 lifespan 是没必要的仪式,也会让
  29. 脚本、测试、调度器这些非 HTTP 入口平白多一层耦合。
  30. ``sqlite`` 档仍然要求先 ``await init_checkpointer()``:它需要打开异步
  31. 连接,没法在同步函数里做,而且**绝不能静默回落到内存**——那会让
  32. 「重启后中断恢复失效」变成一个没人发现的行为变化。
  33. """
  34. global _saver
  35. if _saver is None and _kind == "memory":
  36. from langgraph.checkpoint.memory import InMemorySaver
  37. _saver = InMemorySaver()
  38. logger.info("checkpointer=memory 惰性创建(图状态不落盘)")
  39. if _saver is None:
  40. raise CheckpointerConfigError(
  41. "checkpointer 尚未初始化:请在 async with RuntimeEnvironment() 内执行任务"
  42. )
  43. return _saver
  44. def is_initialized() -> bool:
  45. """是否已初始化(/health 用)。"""
  46. return _saver is not None
  47. async def init_checkpointer() -> Any:
  48. """初始化 checkpointer。幂等,重复调用直接返回已有实例。"""
  49. global _saver, _kind
  50. async with _init_lock:
  51. if _saver is not None:
  52. return _saver
  53. if _kind == "memory":
  54. from langgraph.checkpoint.memory import InMemorySaver
  55. _saver = InMemorySaver()
  56. logger.warning(
  57. "checkpointer=memory:图状态不落盘,重启后中断恢复失效"
  58. "(会话历史仍从 MySQL 补种)"
  59. )
  60. return _saver
  61. if _kind != "sqlite":
  62. raise CheckpointerConfigError(
  63. f"不支持的 CHECKPOINTER={_kind!r},可选值:sqlite | memory"
  64. )
  65. try:
  66. import aiosqlite
  67. from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
  68. except ImportError as exc:
  69. raise CheckpointerConfigError(
  70. f"checkpointer=sqlite 需要 aiosqlite 与 langgraph-checkpoint-sqlite:{exc}"
  71. ) from exc
  72. path = config.resolve_path(config.CHECKPOINT_DB_PATH)
  73. conn = None
  74. try:
  75. conn = await aiosqlite.connect(str(path))
  76. saver = AsyncSqliteSaver(conn)
  77. await saver.setup()
  78. except BaseException as exc:
  79. if conn is not None:
  80. await conn.close()
  81. if not isinstance(exc, Exception):
  82. raise
  83. raise CheckpointerConfigError(
  84. f"打开 checkpoint 库失败 path={path}:{type(exc).__name__}: {exc}"
  85. ) from exc
  86. _saver = saver
  87. logger.info("checkpointer=sqlite 就绪 path=%s", path)
  88. return _saver
  89. async def close_checkpointer() -> None:
  90. """关闭底层连接(RuntimeEnvironment 退出时调用)。"""
  91. global _saver
  92. saver = _saver
  93. _saver = None
  94. if saver is None:
  95. return
  96. conn = getattr(saver, "conn", None)
  97. if conn is not None and hasattr(conn, "close"):
  98. try:
  99. await conn.close()
  100. except Exception as exc: # pragma: no cover - 关闭失败不影响进程退出
  101. logger.warning("关闭 checkpoint 连接失败:%s: %s", type(exc).__name__, exc)
  102. logger.info("checkpointer 已关闭")
  103. def thread_config(thread_id: str) -> dict[str, Any]:
  104. """构造 LangGraph 的 configurable config。
  105. ``thread_id`` 恒等于 ``session_id``(由 api/chat_routes 强制),
  106. 这是防止越权读写别人会话状态的第一道闸。
  107. """
  108. return {"configurable": {"thread_id": thread_id}}
  109. async def has_checkpoint(thread_id: str) -> bool:
  110. """该 thread 是否已有图状态。
  111. 冷线程需要从 MySQL 补种历史消息;热线程只喂当轮新消息,否则历史会被
  112. 重复追加,上下文按 O(N²) 膨胀。
  113. """
  114. saver = get_checkpointer()
  115. try:
  116. tup = await saver.aget_tuple(thread_config(thread_id))
  117. except Exception as exc: # pragma: no cover - 取决于底层库
  118. logger.warning("探测 thread 状态失败 thread=%s:%s: %s", thread_id, type(exc).__name__, exc)
  119. raise
  120. return tup is not None
  121. async def delete_thread(thread_id: str) -> None:
  122. """删除某 thread 的全部图状态。删会话时必须同步调用,否则图状态残留。"""
  123. saver = get_checkpointer()
  124. delete = getattr(saver, "adelete_thread", None)
  125. if delete is None:
  126. raise RuntimeError("当前 checkpointer 不支持删除 thread")
  127. try:
  128. await delete(thread_id)
  129. logger.info("已清除图状态 thread=%s", thread_id)
  130. except Exception as exc:
  131. logger.warning("清除图状态失败 thread=%s:%s: %s", thread_id, type(exc).__name__, exc)
  132. raise