♻️ refactor(core): 重构 AI 编排框架与记忆及 RAG 子系统 (#2149)

* ♻️ refactor(core): 重构 AI 编排框架与记忆及 RAG 子系统

- 【重构】重构 `BaseRunnable` 并引入统一的 `RunIntent` 意图载体,规范 Agent、Team 和 Workflow 的执行流
- 【解耦】将中期记忆槽和长期向量记忆从 `MemoryConfig` 中解耦,转为独立的能力组件与工具箱进行管理
- 【记忆】移除 `MemoryReader` 和 `MemoryWriter`,统一封装为 `SessionMemoryContext` 会话记忆门面
- 【RAG】重构检索器与存储后端接口,统一采用 `QueryRequest` 进行多维度联合检索,并引入 `InMemoryScorer` 提升打分性能
- 【事件】优化 `EventBus` 异步事件分发机制,引入队列机制确保事件按序处理,避免并发竞态问题
- 【依赖注入】移除 `memory` 注入项,优化 `DependencyInjector` 的签名解析缓存以提升性能

* 🚨 auto fix by pre-commit hooks

---------

Co-authored-by: webjoin111 <455457521@qq.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Rumio
2026-07-14 16:48:33 +08:00
committed by GitHub
co-authored by webjoin111 pre-commit-ci[bot]
parent 922d092650
commit 52f7dbdedf
66 changed files with 2131 additions and 2353 deletions
@@ -1,9 +1,7 @@
from .builder import MemoryBuilder
from .compression import MemoryPolicy
from .facades import AgentSessionFacade
from .manager import memory_manager
from .models import (
BaseMemoryIngestionMiddleware,
MemoryConfig,
)
from .types import (
@@ -12,8 +10,6 @@ from .types import (
)
__all__ = [
"AgentSessionFacade",
"BaseMemoryIngestionMiddleware",
"Isolation",
"MemoryBuilder",
"MemoryConfig",
+5 -92
View File
@@ -6,27 +6,19 @@ from typing_extensions import Self
from pydantic import BaseModel
from zhenxun.services.ai.context.memory.compression import MemoryPolicy
from zhenxun.services.ai.context.memory.models import (
from zhenxun.services.ai.utils.scope import ScopeBuilder
from .compression import MemoryPolicy
from .models import (
ContextCompressionConfig,
IngestionConfig,
LongTermConfig,
MemoryConfig,
MemorySlot,
ShortTermConfig,
SlotMemoryConfig,
)
from zhenxun.services.ai.context.memory.storage.interfaces import (
from .storage.interfaces import (
BaseChatContext,
BaseMemoryIngestionMiddleware,
BaseSlotContext,
)
from zhenxun.services.ai.context.memory.types import (
AutoRecallPolicy,
)
from zhenxun.services.ai.context.rag.backends import Embedder, StorageBackend
from zhenxun.services.ai.context.rag.engine import ScopedRAGClient
from zhenxun.services.ai.utils.scope import ScopeBuilder
class MemoryBuilder:
@@ -42,8 +34,6 @@ class MemoryBuilder:
"""
self._config = MemoryConfig(
short_term=ShortTermConfig(enable=False),
slots=SlotMemoryConfig(enable=False),
long_term=LongTermConfig(enable=False),
compression=ContextCompressionConfig(),
ingestion=IngestionConfig(),
)
@@ -68,17 +58,10 @@ class MemoryBuilder:
if isinstance(memory, bool):
return MemoryConfig(
short_term=ShortTermConfig(enable=memory),
long_term=LongTermConfig(enable=memory),
)
return MemoryConfig(short_term=ShortTermConfig(enable=False))
def with_base_isolation(self, isolation: ScopeBuilder) -> Self:
"""设置顶层基准隔离级别,短期/中期/长期记忆将默认继承此级别"""
self._config.base_isolation = isolation
self._config.short_term.isolation = isolation
return self
def with_short_term(
self,
enable: bool = True,
@@ -95,77 +78,11 @@ class MemoryBuilder:
"""
self._config.short_term.enable = enable
if isolation is not None:
self._config.base_isolation = isolation
self._config.short_term.isolation = isolation
if backend is not None:
self._config.short_term.backend = backend
return self
def with_slots(
self,
enable: bool = True,
scopes: dict[str, ScopeBuilder] | None = None,
default_slots: list[MemorySlot] | None = None,
backend: str | BaseSlotContext | None = None,
instructions: str | None = None,
) -> Self:
"""
配置核心槽位记忆 (Memory Slots)。
参数:
enable: 是否启用槽位记忆。
scopes: 语义化作用域映射字典。如果只有一个键值对,则大模型不可见该参数。
default_slots: 首次初始化时自动写入的默认槽位列表。
backend: 槽位记忆存储后端,如果为 None 则使用全局默认后端。
instructions: 覆写内置槽位管理工具箱的默认系统提示词规则。
"""
self._config.slots.enable = enable
if scopes is not None:
self._config.slots.scopes = scopes
if default_slots is not None:
self._config.slots.default_slots = default_slots
if backend is not None:
self._config.slots.backend = backend
if instructions is not None:
self._config.slots.instructions = instructions
return self
def with_long_term(
self,
enable: bool = True,
scopes: dict[str, ScopeBuilder] | None = None,
engine: ScopedRAGClient | None = None,
backend: str | StorageBackend | None = None,
embedder: Embedder | str | None = None,
agentic: bool = True,
auto_recall: AutoRecallPolicy = False,
instructions: str | None = None,
) -> Self:
"""
配置长期向量记忆与 RAG 设定。
参数:
enable: 是否启用长期记忆。
scopes: 语义化作用域映射字典。如果只有一个键值对,则大模型不可见该参数。
engine: 高级 RAG 检索引擎实例 (推荐)。若提供,将接管记忆的底层检索、混合与重排。
backend: 长期记忆存储后端。
embedder: 用于向量化的文本嵌入模型实例。
agentic: 是否开启主动智能体记忆管理 (增删改查工具自动注入)。
auto_recall: 长期记忆的自动召回策略,支持 bool 或 Callable 函数。
instructions: 覆写内置长期记忆工具箱的默认系统提示词规则。
""" # noqa: E501
self._config.long_term.enable = enable
self._config.long_term.engine = engine
if scopes is not None:
self._config.long_term.scopes = scopes
self._config.long_term.backend = backend
self._config.long_term.embedder = embedder
self._config.long_term.agentic = agentic
self._config.long_term.auto_recall = auto_recall
if instructions is not None:
self._config.long_term.instructions = instructions
return self
def with_multimodal_window(self, window_size: int = 5) -> Self:
"""
配置多模态历史视窗大小。
@@ -261,8 +178,4 @@ class MemoryBuilder:
"""
生成最终构建好的 MemoryConfig 配置对象。
"""
if not self._config.slots.scopes:
self._config.slots.scopes = {"私有": self._config.base_isolation}
if not self._config.long_term.scopes:
self._config.long_term.scopes = {"私有": self._config.base_isolation}
return self._config
@@ -1,52 +1,270 @@
import inspect
from typing import Any
from zhenxun.services.ai.capabilities.base import AbstractCapability
from zhenxun.services.ai.context.memory.models import MemoryConfig
from zhenxun.services.ai.context.rag.backends import Embedder, StorageBackend
from zhenxun.services.ai.context.rag.engine import ScopedRAGClient
from zhenxun.services.ai.run.context import RunContext
from zhenxun.services.ai.tools.core.toolkit import BaseToolkit
from zhenxun.services.ai.tools.providers.builtin.memory import MemoryManagementToolkit
from zhenxun.services.ai.utils.logger import log_memory as logger
from zhenxun.services.ai.utils.runtime import ContextUtils
from zhenxun.services.ai.utils.scope import ScopeBuilder
from .manager import memory_manager
from .models import MemoryScoringConfig
from .storage.backends import MemoryScope
from .storage.interfaces import BaseSlotContext
from .types import (
AutoRecallPolicy,
Isolation,
MemorySlot,
SessionMetadata,
)
class AgenticMemoryCapability(AbstractCapability):
class LongTermMemoryCapability(AbstractCapability):
"""
智能体主动记忆管理能力 (Agentic Memory Management)。
当 `MemoryConfig.long_term.enable == True` 且 `agentic == True` 时隐式挂载,
在运行时动态组装并向大模型提供 `MemoryManagementToolkit` 工具箱。
长期向量记忆 (RAG) 核心能力组件。
负责静默执行自动召回 (Auto Recall),并在必要时提供读写 RAG 数据库的工具链。
"""
def __init__(self, memory_config: MemoryConfig, namespace: str):
self.memory_config = memory_config
def __init__(
self,
engine: ScopedRAGClient | None = None,
storage_backend: StorageBackend | None = None,
embedder: Embedder | str | None = None,
scopes: dict[str, ScopeBuilder] | ScopeBuilder | None = None,
toolkit: bool | BaseToolkit = True,
scoring_config: MemoryScoringConfig | None = None,
auto_recall: AutoRecallPolicy = False,
recall_limit: int = 5,
recall_threshold: float = 0.5,
namespace: str | None = None,
):
"""
初始化长期记忆能力组件。
参数:
engine: RAG 客户端引擎实例。若为 None 则在运行时按需构建。
storage_backend: RAG 向量存储后端。若为 None 则从管理器按命名空间提取。
embedder: 嵌入模型实例或模型名称。
scopes: 控制长期记忆的隔离级别。支持单 ScopeBuilder 或 映射字典。
toolkit: 是否启用记忆管理工具箱,或传入自定义的工具箱实例。
scoring_config: 记忆检索打分配置(包含时间衰减等参数)。
auto_recall: 自动召回策略,可以是布尔值或自定义的回调函数。
recall_limit: 自动召回的记忆条数限制。
recall_threshold: 自动召回的相似度分数阈值。
namespace: 指定的命名空间,用于自动路由存储后端及构建器。
"""
self.engine = engine
self.storage_backend = storage_backend
self.embedder = embedder
if isinstance(scopes, ScopeBuilder):
self.scopes = {"默认": scopes}
else:
self.scopes = scopes or {"私有": Isolation.AGENT_USER()}
self.toolkit = toolkit
self.scoring_config = scoring_config or MemoryScoringConfig()
self.auto_recall = auto_recall
self.recall_limit = recall_limit
self.recall_threshold = recall_threshold
self.namespace = namespace
async def get_tools(self, context: RunContext) -> list[Any]:
kwargs = self.memory_config.long_term.toolkit_kwargs.copy()
kwargs["memory_config"] = self.memory_config
kwargs["namespace"] = self.namespace
if self.memory_config.long_term.instructions is not None:
kwargs["instructions"] = self.memory_config.long_term.instructions
def _build_session_meta(self, context: RunContext) -> SessionMetadata:
scope_builder = next(iter(self.scopes.values())) if self.scopes else None
return ContextUtils.build_session_meta(
context=context,
target_builder=scope_builder,
extra_scopes=self.scopes,
custom_namespace=self.namespace,
)
toolkit = MemoryManagementToolkit(**kwargs)
return [toolkit]
def _ensure_engine(self, context: RunContext) -> ScopedRAGClient | None:
if self.engine is not None:
return self.engine
ns = self.namespace or getattr(context.session, "namespace", "global")
storage_instance = self.storage_backend
if not storage_instance:
factory = memory_manager._storage_factories.get(
ns
) or memory_manager._storage_factories.get("global")
if factory:
storage_instance = factory()
if not storage_instance:
return None
embedder_instance = self.embedder
if isinstance(embedder_instance, str):
from zhenxun.services.ai.context.rag.backends.embedders import (
DefaultEmbedder,
)
embedder_instance = DefaultEmbedder(model_name=embedder_instance)
from zhenxun.services.ai.context.rag.builder import RAGBuilder
builder = RAGBuilder(storage_instance).with_scope("/")
if embedder_instance:
builder.with_embedder(embedder_instance)
builder.enable_lifecycle_scoring(
half_life_days=self.scoring_config.recency_half_life_days,
decay_weight=self.scoring_config.recency_weight,
semantic_weight=self.scoring_config.semantic_weight,
importance_weight=self.scoring_config.importance_weight,
reinforcement_weight=self.scoring_config.reinforcement_weight,
)
self.engine = builder.build()
return self.engine
async def get_system_prompts(self, context: RunContext) -> list[str]:
engine = self._ensure_engine(context)
user_input = context.run.user_input
if not user_input or not engine:
return []
should_recall = False
session_meta = self._build_session_meta(context)
if isinstance(self.auto_recall, bool):
should_recall = self.auto_recall
elif callable(self.auto_recall):
try:
res = self.auto_recall(user_input, session_meta)
if inspect.isawaitable(res):
should_recall = await res
else:
should_recall = bool(res)
except Exception as e:
logger.error(
f"[LongTermMemoryCapability] auto_recall 函数执行失败: {e}"
)
should_recall = False
if not should_recall:
return []
scope = MemoryScope(rag_client=engine)
matches = await scope.recall(
session=session_meta,
query=user_input,
limit=self.recall_limit,
)
if matches:
valid_matches = [m for m in matches if m.score >= self.recall_threshold]
if valid_matches:
fact_str = "\n".join(f"- {m.record.content}" for m in valid_matches)
return [f"[系统补充:有关用户的长期记忆设定]\n{fact_str}"]
return []
async def get_tools(self, context: RunContext) -> list[Any]:
if self.toolkit is False:
return []
engine = self._ensure_engine(context)
if not engine:
return []
if isinstance(self.toolkit, BaseToolkit):
tk = self.toolkit.clone_with(
rag_client=engine, scopes=self.scopes, _namespace=self.namespace
)
return [tk]
return [
MemoryManagementToolkit(
rag_client=engine, scopes=self.scopes, namespace=self.namespace
)
]
class SlotMemoryCapability(AbstractCapability):
"""
槽位记忆能力组件。
当 `MemoryConfig.slots.enable == True` 时隐式挂载,
在运行时动态组装并向大模型提供 `MemorySlotToolkit` 工具箱。
独立的槽位记忆 (Memory Slots) 能力组件。
直接作为插件挂载至 Agent 的 capabilities 列表中。
"""
def __init__(self, memory_config: MemoryConfig, namespace: str):
self.memory_config = memory_config
def __init__(
self,
scopes: dict[str, ScopeBuilder] | ScopeBuilder | None = None,
default_slots: list[MemorySlot] | None = None,
backend: BaseSlotContext | None = None,
toolkit: bool | BaseToolkit = True,
namespace: str | None = None,
):
"""
初始化槽位记忆能力组件。
参数:
scopes: 控制槽位记忆的隔离级别。支持单 ScopeBuilder 或 映射字典。
default_slots: 默认记忆槽列表,初始时自动创建未存在的槽位。
backend: 中期记忆槽持久化后端。若为 None 则从管理器按命名空间提取。
toolkit: 是否启用记忆槽管理工具箱,或传入自定义的工具箱实例。
namespace: 指定的命名空间,用于自动路由后端及工具箱。
"""
if isinstance(scopes, ScopeBuilder):
self.scopes = {"默认": scopes}
else:
self.scopes = scopes or {"私有": Isolation.AGENT_USER()}
self.default_slots = default_slots or []
self.backend = backend
self.toolkit = toolkit
self.namespace = namespace
async def _get_slot_ctx_and_meta(self, context: RunContext):
ns = self.namespace or getattr(context.session, "namespace", "global")
slot_ctx = self.backend or memory_manager.get_backend("slots", namespace=ns)
target_builder = next(iter(self.scopes.values())) if self.scopes else None
session_meta = ContextUtils.build_session_meta(
context=context,
target_builder=target_builder,
extra_scopes=self.scopes,
custom_namespace=self.namespace,
)
return slot_ctx, session_meta
async def get_system_prompts(self, context: RunContext) -> list[str]:
slot_ctx, session_meta = await self._get_slot_ctx_and_meta(context)
if not slot_ctx:
return []
for default_slot in self.default_slots:
if not await slot_ctx.get_slot(session_meta, default_slot.label):
await slot_ctx.set_slot(session_meta, default_slot)
slots = await slot_ctx.list_pinned_slots(session_meta)
if not slots:
return []
xml_parts = ["<memory_slots>"]
for slot in slots:
semantic_name = session_meta.scope_name_mapping.get(slot.scope, "未知")
xml_parts.append(
f' <slot name="{slot.label}" scope="{semantic_name}">\n'
f" {slot.content}\n"
" </slot>"
)
xml_parts.append("</memory_slots>")
return ["\n".join(xml_parts)]
async def get_tools(self, context: RunContext) -> list[Any]:
from zhenxun.services.ai.tools.providers.builtin.slots import MemorySlotToolkit
kwargs = self.memory_config.slots.toolkit_kwargs.copy()
kwargs["memory_config"] = self.memory_config
kwargs["namespace"] = self.namespace
if self.memory_config.slots.instructions is not None:
kwargs["instructions"] = self.memory_config.slots.instructions
if self.toolkit is False:
return []
toolkit = MemorySlotToolkit(**kwargs)
if isinstance(self.toolkit, BaseToolkit):
toolkit = self.toolkit.clone_with(
scopes=self.scopes, backend=self.backend, _namespace=self.namespace
)
else:
toolkit = MemorySlotToolkit(
scopes=self.scopes, backend=self.backend, namespace=self.namespace
)
return [toolkit]
@@ -4,6 +4,7 @@ from typing import Any, Generic, TypeVar
from pydantic import BaseModel, Field
from zhenxun.services.ai.context.memory.models import MemoryConfig
from zhenxun.services.ai.core.engine.token_counter import token_counter
from zhenxun.services.ai.core.messages import (
AudioPart,
@@ -386,7 +387,7 @@ class LLMSummarizerReducer(AbstractSummarizerReducer):
) -> LLMMessage | None:
prompt_text = f"### 📋 [对话摘要任务]\n{self.summarization_prompt}\n\n"
if prev_summary:
prompt_text += "#### önceki_summary (参考先前的快照):\n"
prompt_text += "#### prev_summary (参考先前的快照):\n"
prompt_text += f"> {prev_summary}\n\n"
prompt_text += "#### 待处理的历史消息流:\n"
for m in to_summarize:
@@ -512,7 +513,7 @@ class CondenserPipeline:
@classmethod
def create_from_configs(
cls, memory_config: Any, model_name: str
cls, memory_config: MemoryConfig | None, model_name: str
) -> "CondenserPipeline":
"""基于全局和局部配置组装压缩管线工厂方法"""
from zhenxun.services.ai.config import get_llm_config
@@ -602,7 +603,7 @@ class CondenserPipeline:
class MemoryPolicy:
"""
记忆策略工厂 (Strategy Factory Facade)。
记忆策略工厂。
为开发者提供开箱即用的上下文压缩管线组装方案。
"""
+19 -150
View File
@@ -1,8 +1,9 @@
from collections.abc import Sequence
from typing import Any, cast
from typing import cast
from zhenxun.services.ai.core.engine.context_renderer import ContextConverter
from zhenxun.services.ai.core.messages import AgentMessage
from zhenxun.services.ai.run.context import RunContext
from zhenxun.services.ai.utils.logger import log_memory as logger
from zhenxun.utils.pydantic_compat import model_copy
@@ -14,144 +15,37 @@ from .models import MemoryConfig
from .types import SessionMetadata
class MemoryReader:
class SessionMemoryContext:
"""
记忆读取器 (Memory Reader)。
负责从数据库中提取短期上下文历史,召回长期的背景知识,并执行自动压缩。
会话记忆门面。
封装了当前会话记忆的读写操作、上下文压缩管线以及入库清洗中间件。
"""
def __init__(
self, session_meta: SessionMetadata, memory_config: MemoryConfig | None
self,
session_meta: SessionMetadata,
memory_config: MemoryConfig | None,
context: RunContext,
):
"""
初始化记忆读取器。
初始化会话记忆门面。
参数:
session_meta: 会话元数据,包含 Namespace 与作用域映射等上下文信息。
memory_config: 记忆系统的配置对象,控制长期、短期及槽位记忆的启用与逻辑。
memory_config: 记忆系统的配置对象,控制长期、短期记忆的启用与逻辑。
context: 必填的运行时上下文环境 (RunContext),供中间件进行依赖注入。
"""
self.session_meta = session_meta
self.memory_config = memory_config
self.context = context
async def get_long_term_context(self, user_input: str) -> str:
"""
基于用户输入召回长期记忆(RAG),返回格式化后的背景提示词。
"""
if (
not self.memory_config
or not self.memory_config.long_term.enable
or not user_input
):
return ""
policy = self.memory_config.long_term.auto_recall
should_recall = False
if isinstance(policy, bool):
should_recall = policy
elif callable(policy):
import inspect
try:
res = policy(user_input, self.session_meta)
if inspect.isawaitable(res):
should_recall = await res
else:
should_recall = bool(res)
except Exception as e:
logger.error(f"[MemoryReader] 自定义 auto_recall 函数执行失败: {e}")
should_recall = False
if not should_recall:
return ""
ltm_scope = memory_manager.get_long_term_memory(
self.memory_config,
namespace=self.session_meta.selector.namespace or "global",
)
if not ltm_scope:
return ""
matches = await ltm_scope.recall(session=self.session_meta, query=user_input)
if matches:
logger.debug(f"🧠 [MemoryReader] 长期记忆召回详情 (Query: '{user_input}'):")
for i, m in enumerate(matches):
logger.debug(
f" [{i + 1}] 得分: {m.score:.4f} | 内容: {m.record.content}"
)
threshold = self.memory_config.long_term.recall_threshold
valid_matches = [m for m in matches if m.score >= threshold]
if not valid_matches:
logger.debug("🧠 [MemoryReader] 召回的记忆均未达到相关性阈值,已丢弃。")
return ""
fact_str = "\n".join(f"- {m.record.content}" for m in valid_matches)
logger.debug(
f"🧠 [MemoryReader]"
f"成功截取并注入 {len(valid_matches)} 条高价值长期记忆。"
)
return f"[系统补充:有关用户的长期记忆设定]\n{fact_str}"
return ""
async def get_slots_context(self) -> str:
"""
读取并组装核心槽位记忆 (Memory Slots),返回 XML 格式字符串供大模型使用。
"""
if not self.memory_config or not self.memory_config.slots.enable:
return ""
slot_ctx = memory_manager.get_slot_context(
self.memory_config,
namespace=self.session_meta.selector.namespace or "global",
)
if not slot_ctx:
return ""
if self.memory_config.slots.default_slots:
for default_slot in self.memory_config.slots.default_slots:
existing = await slot_ctx.get_slot(
self.session_meta, default_slot.label
)
if not existing:
await slot_ctx.set_slot(self.session_meta, default_slot)
slots = await slot_ctx.list_pinned_slots(self.session_meta)
if not slots:
return ""
show_scope = False
if (
self.memory_config
and self.memory_config.slots.scopes
and len(self.memory_config.slots.scopes) > 1
):
show_scope = True
xml_parts = ["<memory_slots>"]
for slot in slots:
if show_scope:
semantic_name = self.session_meta.scope_name_mapping.get(
slot.scope, "未知"
)
xml_parts.append(
f' <slot name="{slot.label}" scope="{semantic_name}">\n'
f" {slot.content}\n"
" </slot>"
)
else:
xml_parts.append(
f' <slot name="{slot.label}">\n {slot.content}\n </slot>'
)
xml_parts.append("</memory_slots>")
return "\n".join(xml_parts)
async def get_short_term_context(
async def read(
self,
model_name: str,
override_history: Sequence[AgentMessage] | None = None,
) -> list[AgentMessage]:
"""
拉取短期对话历史,并执行 Token 压缩。
拉取短期对话历史,并执行 Token 压缩与管线修剪。
"""
current_history: list[AgentMessage] = []
if override_history is not None:
@@ -187,43 +81,18 @@ class MemoryReader:
if changed:
await chat_context.set_messages(self.session_meta, new_history)
logger.info(
"💾 [MemoryReader] 压缩截断完毕,已同步覆写数据库。"
"💾 [SessionMemory] 压缩截断完毕,已同步覆写数据库。"
f"压缩后条数: {len(new_history)}"
)
current_history = cast(list[AgentMessage], new_history)
return current_history
class MemoryWriter:
"""
记忆写入器 (Memory Writer)。
负责将对话增量安全地写入数据库。
"""
def __init__(
self,
session_meta: SessionMetadata,
memory_config: MemoryConfig | None,
context: Any = None,
):
"""
初始化记忆写入器。
参数:
session_meta: 会话元数据,包含 Namespace 与作用域映射等上下文信息。
memory_config: 记忆系统的配置对象,控制记忆存入的逻辑。
context: 运行时上下文环境,作为可选参数传入,供中间件使用,默认 None。
"""
self.session_meta = session_meta
self.memory_config = memory_config
self.context = context
async def save_new_messages(
async def write(
self,
new_messages: Sequence[AgentMessage],
):
"""将新产生的对话增量保存到数据库"""
) -> None:
"""将新产生的对话增量,经过入库中间件清洗后保存到数据库"""
if not new_messages:
return
@@ -1,129 +0,0 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Literal
from zhenxun.services.ai.core.messages import AgentMessage, LLMMessage
from .manager import GlobalMemoryManager
from .storage.interfaces import (
BaseChatContext,
BaseSlotContext,
)
from .types import (
MemorySlot,
SessionMetadata,
)
class ChatHistoryFacade:
"""短期对话历史门面"""
def __init__(self, manager: GlobalMemoryManager, session_meta: SessionMetadata):
self.manager = manager
self.session_meta = session_meta
@property
def _backend(self) -> BaseChatContext | None:
return self.manager.get_chat_context(
None, self.session_meta.namespace or "global"
)
async def get(self, limit: int | None = None) -> list[LLMMessage]:
"""获取当前会话的历史消息"""
if not self._backend:
return []
msgs = await self._backend.get_messages(self.session_meta)
return msgs[-limit:] if limit else msgs
async def add(self, messages: Sequence[AgentMessage] | AgentMessage) -> None:
"""向当前会话追加一条或多条历史消息"""
if not self._backend:
return
from zhenxun.services.ai.core.engine.context_renderer import ContextConverter
msgs = messages if isinstance(messages, Sequence) else [messages]
flattened = ContextConverter.flatten_to_llm_messages(msgs)
if flattened:
await self._backend.add_messages(self.session_meta, flattened)
async def clear(self) -> None:
"""清空当前会话的短期对话历史"""
if not self._backend:
return
await self._backend.clear(self.session_meta)
class SlotFacade:
"""中期记忆槽门面"""
def __init__(self, manager: GlobalMemoryManager, session_meta: SessionMetadata):
self.manager = manager
self.session_meta = session_meta
@property
def _backend(self) -> BaseSlotContext | None:
"""获取底层槽位存储后端"""
return self.manager.get_slot_context(
None, self.session_meta.namespace or "global"
)
async def get(self, label: str) -> str | None:
"""获取指定标识的槽位记忆内容"""
if not self._backend:
return None
slot = await self._backend.get_slot(self.session_meta, label)
return slot.content if slot else None
async def set(
self,
label: str,
content: str,
scope: Literal["session", "global"] = "session",
size_limit: int = 2000,
pinned: bool = True,
) -> None:
"""设置或更新指定的槽位记忆"""
if not self._backend:
return
slot = MemorySlot(
label=label,
content=content,
scope=scope,
size_limit=size_limit,
pinned=pinned,
)
await self._backend.set_slot(self.session_meta, slot)
async def delete(
self, label: str, scope: Literal["session", "global"] = "session"
) -> None:
"""删除指定的槽位记忆"""
if not self._backend:
return
await self._backend.delete_slot(self.session_meta, label, scope)
async def list_all(self) -> dict[str, str]:
"""获取当前会话所有被置顶的槽位记忆"""
if not self._backend:
return {}
slots = await self._backend.list_pinned_slots(self.session_meta)
return {s.label: s.content for s in slots}
class AgentSessionFacade:
"""
提供给第三方开发者的会话记忆访问聚合门面 (Facade)。
"""
def __init__(self, manager: GlobalMemoryManager, session_meta: SessionMetadata):
self.manager = manager
self.session_meta = session_meta
self.history = ChatHistoryFacade(manager, session_meta)
self.slots = SlotFacade(manager, session_meta)
async def clear_all(self) -> None:
"""一键清空当前会话下的短期对话历史与记忆槽"""
cleaner = self.manager.cleaner().session(self.session_meta.session_id)
await cleaner.clear_short_term()
await cleaner.clear_slots()
+48 -117
View File
@@ -1,7 +1,8 @@
from collections import defaultdict
from collections.abc import Callable
from typing import Any, cast
from zhenxun.services.ai.context.rag.backends import Embedder, StorageBackend
from zhenxun.services.ai.context.rag.backends import StorageBackend
from zhenxun.services.ai.utils.logger import log_memory as logger
from zhenxun.services.ai.utils.scope import BaseScopeBuilder
from zhenxun.utils.utils import infer_plugin_namespace
@@ -9,11 +10,10 @@ from zhenxun.utils.utils import infer_plugin_namespace
from .models import MemoryConfig
from .storage.backends import (
InMemoryChatContext,
MemoryScope,
)
from .storage.interfaces import (
BaseChatContext,
BaseSlotContext,
IClearableBackend,
)
@@ -33,41 +33,46 @@ class MemoryCleaner(BaseScopeBuilder["MemoryCleaner"]):
self._config = cfg.build() if hasattr(cfg, "build") else cfg
return self
async def clear_target(self, target_name: str):
"""底层派发器:定向清理指定注册名称的泛型扩展后端数据"""
ns_dict = self.manager._backends.get(target_name, {})
for backend in ns_dict.values():
if isinstance(backend, IClearableBackend):
await backend.clear_by_query(self._selector)
else:
logger.warning(
f"后端 {backend.__class__.__name__}"
"未实现 IClearableBackend 协议,已跳过清理。"
)
async def clear_short_term(self):
"""一键清理目标范围下的短期对话历史记忆"""
if self._config and self._config.short_term.backend:
if isinstance(self._config, MemoryConfig) and isinstance(
self._config.short_term.backend, IClearableBackend
):
await self._config.short_term.backend.clear_by_query(self._selector)
else:
for backend in self.manager._chat_backends.values():
await backend.clear_by_query(self._selector)
await self.clear_target("chat")
async def clear_slots(self):
"""一键清理目标范围下的中期记忆槽 (Memory Slots)"""
if self._config and self._config.slots.backend:
await self._config.slots.backend.clear_by_query(self._selector)
else:
for backend in self.manager._slot_backends.values():
await backend.clear_by_query(self._selector)
"""一键清理目标范围下的记忆槽数据"""
await self.clear_target("slots")
async def clear_long_term(self):
"""一键清理目标范围下的长期向量记忆 (RAG Vector Database)"""
if self._config and self._config.long_term.backend:
from zhenxun.services.ai.context.rag.backends import StorageBackend
storage = cast(StorageBackend, self._config.long_term.backend)
await storage.clear_by_query(self._selector)
else:
for factory in self.manager._storage_factories.values():
storage = factory()
if hasattr(storage, "clear_by_query"):
await storage.clear_by_query(self._selector)
else:
await storage.delete(scope_prefix=self._selector.scope_prefix)
for factory in self.manager._storage_factories.values():
storage = factory()
if isinstance(storage, IClearableBackend):
await storage.clear_by_query(self._selector)
async def clear_all(self):
"""一键清理指定范围下的所有生命周期记忆(对话、槽位、RAG)"""
await self.clear_short_term()
await self.clear_slots()
"""一键清理指定范围下的所有生命周期记忆(对话、记忆槽、RAG、及其他泛型扩展后端)"""
if isinstance(self._config, MemoryConfig) and isinstance(
self._config.short_term.backend, IClearableBackend
):
await self._config.short_term.backend.clear_by_query(self._selector)
for target_name in self.manager._backends:
await self.clear_target(target_name)
await self.clear_long_term()
logger.info(
f"🧹 成功清理作用域 '{self._selector.scope_prefix}'下的所有记忆痕迹!"
@@ -81,10 +86,8 @@ class GlobalMemoryManager:
"""
def __init__(self):
self._chat_backends: dict[str, BaseChatContext] = {
"global": InMemoryChatContext()
}
self._slot_backends: dict[str, BaseSlotContext] = {}
self._backends: dict[str, dict[str, Any]] = defaultdict(dict)
self.register_backend("chat", InMemoryChatContext(), "global")
from zhenxun.services.ai.context.rag.backends import DictStorageBackend
@@ -92,19 +95,23 @@ class GlobalMemoryManager:
"global": lambda: DictStorageBackend()
}
def register_backend(
self, backend_type: str, backend: Any, scope: str | None = None
) -> None:
"""泛型注册:注册任意类型的存储后端"""
ns = scope if scope is not None else infer_plugin_namespace()
self._backends[backend_type][ns] = backend
def get_backend(self, backend_type: str, namespace: str = "global") -> Any | None:
"""泛型获取:获取任意类型的存储后端"""
backends = self._backends.get(backend_type, {})
return backends.get(namespace) or backends.get("global")
def register_chat_backend(
self, backend: BaseChatContext, scope: str | None = None
) -> None:
"""注册特定命名空间的短期记忆存储后端。"""
ns = scope if scope is not None else infer_plugin_namespace()
self._chat_backends[ns] = backend
def register_slot_backend(
self, backend: BaseSlotContext, scope: str | None = None
) -> None:
"""注册特定命名空间的中期记忆槽存储后端。"""
ns = scope if scope is not None else infer_plugin_namespace()
self._slot_backends[ns] = backend
self.register_backend("chat", backend, scope)
def register_storage_factory(
self, factory: Callable[[], StorageBackend], scope: str | None = None
@@ -117,20 +124,6 @@ class GlobalMemoryManager:
"""获取声明式记忆清理构建器,供第三方开发者极速清理指定记忆"""
return MemoryCleaner(self)
def get_embedder(self, embedder_val: "Embedder | str | None") -> Embedder | None:
"""获取向量化引擎实例。如果传入的是字符串,则视为 API 模型名称。"""
if not embedder_val:
return None
if isinstance(embedder_val, str):
from zhenxun.services.ai.context.rag.backends.embedders import (
DefaultEmbedder,
)
return DefaultEmbedder(model_name=embedder_val)
return embedder_val
def get_chat_context(
self, config: MemoryConfig | None, namespace: str = "global"
) -> BaseChatContext | None:
@@ -142,69 +135,7 @@ class GlobalMemoryManager:
if backend_cfg is not None:
return cast(BaseChatContext, backend_cfg)
return self._chat_backends.get(namespace) or self._chat_backends["global"]
def get_slot_context(
self, config: MemoryConfig | None, namespace: str = "global"
) -> BaseSlotContext | None:
"""根据配置分配对应的槽位记忆实例"""
if not config or not config.slots.enable:
return None
backend_cfg = config.slots.backend
if backend_cfg is not None:
return cast(BaseSlotContext, backend_cfg)
return self._slot_backends.get(namespace) or self._slot_backends["global"]
def get_long_term_memory(
self, config: MemoryConfig | None, namespace: str = "global"
) -> MemoryScope | None:
"""根据声明式配置动态组装长期向量记忆实例"""
if not config or not config.long_term.enable:
return None
if config.long_term.engine is not None:
return MemoryScope(
rag_client=config.long_term.engine,
)
storage_instance = None
backend_cfg = config.long_term.backend
if backend_cfg is not None:
storage_instance = cast(StorageBackend, backend_cfg)
else:
factory = (
self._storage_factories.get(namespace)
or self._storage_factories["global"]
)
storage_instance = factory()
embedder = self.get_embedder(config.long_term.embedder)
from zhenxun.services.ai.context.rag.builder import RAGBuilder
builder = RAGBuilder(storage_instance).with_scope("/")
if embedder:
builder.with_embedder(embedder)
from .models import MemoryScoringConfig
scoring_cfg = MemoryScoringConfig()
builder.enable_lifecycle_scoring(
half_life_days=scoring_cfg.recency_half_life_days,
decay_weight=scoring_cfg.recency_weight,
semantic_weight=scoring_cfg.semantic_weight,
importance_weight=scoring_cfg.importance_weight,
reinforcement_weight=scoring_cfg.reinforcement_weight,
)
client = builder.build()
return MemoryScope(
rag_client=client,
)
return self.get_backend("chat", namespace)
memory_manager = GlobalMemoryManager()
@@ -2,50 +2,21 @@
记忆域类型定义
"""
from typing import Any
from pydantic import BaseModel, ConfigDict, Field
from zhenxun.services.ai.context.rag.backends import Embedder, StorageBackend
from zhenxun.services.ai.context.rag.engine import ScopedRAGClient
from zhenxun.services.ai.utils.scope import ScopeBuilder
from .storage.interfaces import (
BaseChatContext,
BaseMemoryIngestionMiddleware,
BaseMemoryReducer,
BaseSlotContext,
)
from .types import (
AutoRecallPolicy,
Isolation,
MemorySlot,
SessionMetadata,
)
class SlotMemoryConfig(BaseModel):
"""槽位记忆 (Memory Slots) 配置"""
model_config = ConfigDict(arbitrary_types_allowed=True)
enable: bool = Field(default=False)
"""是否启用中期记忆槽"""
scopes: dict[str, ScopeBuilder] | None = Field(default=None)
"""语义化作用域映射字典,供大模型作为 Literal 选择。如果只有一个,则自动隐藏参数"""
default_slots: list[MemorySlot] = Field(default_factory=list)
"""首次初始化时自动写入的默认槽位列表"""
backend: str | BaseSlotContext | None = Field(default=None)
"""
指定底层槽位记忆数据库注册名称,或直接传入 BaseSlotContext 实例。
为空则使用全局默认
"""
instructions: str | None = Field(default=None)
"""覆写内置槽位管理工具箱的系统提示词"""
toolkit_kwargs: dict[str, Any] = Field(default_factory=dict)
"""透传给底层 MemorySlotToolkit 的高级参数 (如 prefix, exclude, shared_options)"""
class MemoryScoringConfig(BaseModel):
"""长期记忆的复合打分与检索配置"""
@@ -57,7 +28,6 @@ class MemoryScoringConfig(BaseModel):
"""重要性权重"""
recency_half_life_days: int = Field(default=30)
"""时间衰减的半衰期(天)"""
reinforcement_weight: float = Field(default=0.2)
"""访问强化的加权权重 (被检索越多得分越高)"""
@@ -78,44 +48,6 @@ class ShortTermConfig(BaseModel):
"""单一的记忆隔离级别 (ScopeBuilder),决定短期记忆存储边界"""
class LongTermConfig(BaseModel):
"""长期向量记忆配置"""
model_config = ConfigDict(arbitrary_types_allowed=True)
enable: bool = Field(default=False)
"""是否启用长期记忆(开启后自动赋予 Agent 存取记忆的工具,并附加 RAG 召回能力)"""
engine: ScopedRAGClient | None = Field(default=None)
"""
[推荐] 指定底层的高级 RAG 检索引擎实例。若传入此项,将覆盖默认的 backend
和 embedder 配置。
"""
backend: str | StorageBackend | None = Field(default=None)
"""
指定底层长期向量数据库 (Storage) 注册名称,或直接传入 StorageBackend 实例。
为空则使用全局默认
"""
scopes: dict[str, ScopeBuilder] | None = Field(default=None)
"""语义化作用域映射字典,决定长期记忆存储边界。如果只有一个,则自动隐藏参数"""
embedder: str | Embedder | None = Field(default=None)
"""
指定底层向量化引擎 (Embedder) 实例,若为字符串则视为 API 模型名称。
为空则使用全局默认
"""
agentic: bool = Field(default=True)
"""是否赋予大模型主动管理记忆的能力 (Agentic Memory)"""
auto_recall: AutoRecallPolicy = Field(default=False)
"""长期记忆的自动召回策略,默认 False (从不自动召回),由大模型自主
决定调用搜索工具"""
recall_threshold: float = Field(default=0.5)
"""长期记忆召回的最低余弦相似度要求"""
instructions: str | None = Field(default=None)
"""覆写内置长期记忆管理工具箱的系统提示词"""
toolkit_kwargs: dict[str, Any] = Field(default_factory=dict)
"""透传给底层 MemoryManagementToolkit 的高级参数"""
class ContextCompressionConfig(BaseModel):
"""上下文压缩与管理配置"""
@@ -144,14 +76,8 @@ class MemoryConfig(BaseModel):
"""统一的记忆配置项声明"""
model_config = ConfigDict(arbitrary_types_allowed=True)
base_isolation: ScopeBuilder = Field(default_factory=Isolation.AGENT_USER)
"""顶层基准隔离级别,短期/中期/长期记忆将默认继承此级别"""
short_term: ShortTermConfig = Field(default_factory=ShortTermConfig)
"""短期对话记忆配置"""
slots: SlotMemoryConfig = Field(default_factory=SlotMemoryConfig)
"""槽位记忆配置"""
long_term: LongTermConfig = Field(default_factory=LongTermConfig)
"""长期向量记忆配置"""
compression: ContextCompressionConfig = Field(
default_factory=ContextCompressionConfig
)
@@ -161,12 +87,10 @@ class MemoryConfig(BaseModel):
__all__ = [
"AutoRecallPolicy",
"BaseMemoryIngestionMiddleware",
"ContextCompressionConfig",
"IngestionConfig",
"Isolation",
"LongTermConfig",
"MemoryConfig",
"MemoryScoringConfig",
"SessionMetadata",
@@ -95,7 +95,9 @@ class DBMessageSerializer:
return content_parts
@staticmethod
def serialize_content(content_payload: Any) -> list[dict[str, Any]]:
def serialize_content(
content_payload: list[LLMContentPart] | str,
) -> list[dict[str, Any]]:
"""将 LLMMessage 消息内容序列化为可存储于数据库的 JSON 格式"""
if isinstance(content_payload, str):
return [{"type": "text", "text": content_payload}]
@@ -134,7 +136,7 @@ class MemoryScope:
):
"""初始化长期记忆作用域与 RAG 客户端"""
self.rag_client = rag_client
self._background_tasks: set[Any] = set()
self._background_tasks: set[asyncio.Task[Any]] = set()
async def remember(
self,
@@ -492,3 +494,13 @@ def get_orm_slot_context(model_class: type[AbstractSlotRecord]) -> TortoiseSlotC
[工厂方法] 供第三方开发者调用,将 Tortoise ORM 表直接包装为记忆槽存储系统。
"""
return TortoiseSlotContext(model_class=model_class)
__all__ = [
"AbstractMemoryRecord",
"AbstractSlotRecord",
"InMemoryChatContext",
"MemoryScope",
"TortoiseChatContext",
"TortoiseSlotContext",
]
@@ -1,5 +1,6 @@
from abc import ABC, abstractmethod
from collections.abc import Sequence
from typing import Protocol, runtime_checkable
from zhenxun.services.ai.context.memory.types import (
MemorySlot,
@@ -10,7 +11,14 @@ from zhenxun.services.ai.run.context import RunContext
from zhenxun.services.ai.utils.scope import ScopeSelector
class BaseChatContext(ABC):
@runtime_checkable
class IClearableBackend(Protocol):
"""支持声明式清理的作用域后端协议"""
async def clear_by_query(self, query: ScopeSelector) -> int | None: ...
class BaseChatContext(IClearableBackend, ABC):
"""短期对话历史记忆接口"""
@abstractmethod
@@ -44,13 +52,8 @@ class BaseChatContext(ABC):
"""清空当前会话的历史消息。"""
...
@abstractmethod
async def clear_by_query(self, query: ScopeSelector) -> None:
"""根据条件领域查询对象清理对话历史。"""
...
class BaseSlotContext(ABC):
class BaseSlotContext(IClearableBackend, ABC):
"""中期记忆槽持久化接口"""
@abstractmethod
@@ -80,11 +83,6 @@ class BaseSlotContext(ABC):
"""列出当前会话的所有记忆槽(包括未置顶的)。"""
...
@abstractmethod
async def clear_by_query(self, query: ScopeSelector) -> None:
"""根据条件领域查询对象清理记忆槽。"""
...
class BaseMemoryReducer(ABC):
"""记忆压缩器基类"""
@@ -97,7 +95,19 @@ class BaseMemoryReducer(ABC):
model_name: str,
base_overhead: int = 0,
) -> tuple[list[LLMMessage], bool, int]:
"""对消息列表进行压缩处理。"""
"""
执行记忆压缩处理,精简或提炼对话上下文以降低 Token 消耗。
参数:
messages: 需要进行压缩的原始 LLM 消息历史列表。
current_tokens: 压缩前消息列表的当前 Token 总数。
model_name: 用于判定压缩阈值或计算 Token 的底层大模型名称。
base_overhead: 基础系统提示词等静态开销的 Token 计数。
返回:
tuple[list[LLMMessage], bool, int]: 包含压缩后的新消息历史列表、
本次是否实际触发了压缩的布尔标记、以及压缩后的新 Token 总数。
"""
...