♻️ 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
@@ -7,7 +7,7 @@ from pydantic import BaseModel, Field
from zhenxun.services.ai.core.exceptions import AbortException, ControlFlowExit
from zhenxun.services.ai.core.stream_events import ToolStreamChunkEvent
from zhenxun.services.ai.flow.base import BaseRunnable
from zhenxun.services.ai.flow.core.base import BaseRunnable
from zhenxun.services.ai.run import RunContext
from zhenxun.services.ai.tools.core.tool import BaseTool
from zhenxun.services.ai.tools.models import ToolResult
@@ -49,7 +49,9 @@ class DelegateTool(BaseTool):
"""
resolved_name = name or getattr(runnable, "name", "SubRunnable")
resolved_desc = description or getattr(
runnable, "description", f"将子任务委派给 {resolved_name} 执行"
runnable,
"profile_summary",
getattr(runnable, "description", f"将子任务委派给 {resolved_name} 执行"),
)
final_name = (
f"delegate_to_{resolved_name}"
@@ -450,7 +450,7 @@ def bind_matcher(
"""
if require_prefix:
ns = infer_plugin_namespace(default="global")
ns = infer_plugin_namespace()
if ns and ns not in ("global", "unknown"):
if not name.startswith(f"{ns}_"):
name = f"{ns}_{name}"
+1 -1
View File
@@ -218,7 +218,7 @@ def tool(
if require_prefix:
from zhenxun.utils.utils import infer_plugin_namespace
ns = infer_plugin_namespace(default="global")
ns = infer_plugin_namespace()
if ns and ns not in ("global", "unknown"):
if not tool_name.startswith(f"{ns}_"):
tool_name = f"{ns}_{tool_name}"
@@ -217,6 +217,14 @@ class BaseToolkit:
)
return f"<{tag_name}>\n{text}\n</{tag_name}>"
def clone_with(self, **kwargs: Any) -> "BaseToolkit":
"""克隆当前工具箱原型,并透明注入新的运行时属性。"""
new_tk = copy.copy(self)
for k, v in kwargs.items():
setattr(new_tk, k, v)
new_tk._cached_tools = None
return new_tk
def prefixed(self, prefix: str) -> "BaseToolkit":
"""克隆工具箱并为其中所有工具追加统一的前缀"""
new_tk = copy.copy(self)
+2 -2
View File
@@ -365,7 +365,7 @@ class ToolProviderManager:
def register_tool(self, tool: ToolExecutable):
"""注册由 @tool 生成的单一工具"""
ns = infer_plugin_namespace(default="global")
ns = infer_plugin_namespace()
self.local_provider.register_tool(tool, ns)
tags = getattr(getattr(tool, "settings", None), "tags", [])
tag_str = f" | Tags: {tags}" if tags else ""
@@ -376,7 +376,7 @@ class ToolProviderManager:
"""
注册一个完整的 Toolkit 实例,使其可通过智能字符串路由(Tag或Name)被动态发现。
"""
ns = infer_plugin_namespace(default="global")
ns = infer_plugin_namespace()
self.local_provider.register_toolkit(toolkit, ns)
tk_name = getattr(toolkit, "__class__", type).__name__
+2 -5
View File
@@ -4,7 +4,7 @@
from dataclasses import dataclass, field
import fnmatch
from typing import TYPE_CHECKING, Any, Literal
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field
@@ -13,9 +13,6 @@ from zhenxun.services.ai.run.context import RunContext
from zhenxun.services.ai.utils.logger import log_tool as logger
from zhenxun.utils.pydantic_compat import model_dump, model_validate
if TYPE_CHECKING:
from zhenxun.services.ai.tools.core.tool import BaseTool
class DirectivePayload(BaseModel):
"""工具执行产生的副作用控制流指令载荷"""
@@ -272,7 +269,7 @@ class Query(BaseModel):
metadata_filter: dict[str, Any] | None = Field(default=None)
"""如果提供,则工具的 metadata 必须包含这里列出的所有键值对。"""
def match(self, tool: "BaseTool") -> bool:
def match(self, tool: Any) -> bool:
"""判断某个工具或工具箱是否符合当前 Query 的筛选条件"""
def _match_pattern(val: str, pattern: str | list[str]) -> bool:
@@ -2,15 +2,17 @@ from typing import Any, Literal, Optional
from pydantic import Field, create_model
from zhenxun.services.ai.context.memory.manager import memory_manager
from zhenxun.services.ai.context.memory.models import MemoryConfig
from zhenxun.services.ai.context.memory.storage.backends import MemoryScope
from zhenxun.services.ai.context.memory.types import SessionMetadata
from zhenxun.services.ai.context.rag.engine import ScopedRAGClient
from zhenxun.services.ai.run.context import RunContext
from zhenxun.services.ai.tools.core.decorators import tool
from zhenxun.services.ai.tools.core.tool import BaseTool
from zhenxun.services.ai.tools.core.toolkit import BaseToolkit
from zhenxun.services.ai.tools.models import ToolOptions, ToolResult
from zhenxun.services.ai.utils.logger import log_tool as logger
from zhenxun.services.ai.utils.runtime import ContextUtils
from zhenxun.services.ai.utils.scope import ScopeBuilder
class MemoryManagementToolkit(BaseToolkit):
@@ -26,23 +28,45 @@ class MemoryManagementToolkit(BaseToolkit):
shared_options = ToolOptions(silent=True)
default_instructions = """\
## 🧠 长期记忆管理系统 (Long-Term Memory)
该系统是你的「无限档案馆」。⚠️ **注意:系统默认不会主动向你提供所有历史信息,你必须通过主动搜索来回忆。**
_INTRO_TEXT = (
"## 🧠 长期记忆管理系统 (Long-Term Memory)\n"
"该系统是你的「无限档案馆」。系统默认不会主动向你提供所有历史信息,你必须通过主动搜索来回忆。\n\n"
"### 📝 职责说明\n"
)
_READ_GUIDE = (
"- **寻找历史线索**:当遇到未知情况,或用户提及过去的事情、"
"特定设定时,必须主动检索历史库(使用 `search_memory`)。\n"
)
_WRITE_GUIDE = (
"- **记录离散事实与经验**:当需要记录某个独立事件、历史经验、"
"问题解决方案或具体事实时(使用 `save_memory`)。\n"
"- **隐式记录**:当接收到值得记忆的重要信息时,请静默记录。"
"除非用户主动提问,否则无需向用户显式汇报'我已记住'。\n"
"- **按需更新**:如果发现某项历史记录已过时或状态发生扭转,"
"请先检索出它的 ID,再修改(`update_memory`)或废弃(`delete_memory`)。\n"
"- **精准提炼**:保存记忆时请提炼核心价值,避免保存无意义的闲聊。\n"
)
### 📝 何时使用长期记忆?
- **记录离散事实与经验**:当需要记录某个独立事件、历史经验、问题解决方案或具体事实时(使用 `save_memory`)。
- **寻找历史线索**:当遇到未知情况,或用户提及过去的事情、特定设定时,必须主动检索历史库(使用 `search_memory`)。
default_instructions = _INTRO_TEXT + _READ_GUIDE + _WRITE_GUIDE
### ⚙️ 操作规范
1. **隐式记录**:当接收到值得记忆的重要信息时,请静默记录。除非用户主动提问,否则无需向用户显式汇报"我已记住"。
2. **按需更新**:如果发现某项历史记录已过时或状态发生扭转,请先检索出它的 ID,再进行修改(使用 `update_memory`)或废弃(使用 `delete_memory`)。
3. **精准提炼**:保存记忆时请提炼核心价值,避免保存无意义的闲聊。\
""" # noqa: E501
@classmethod
def read_only(cls, **kwargs) -> "MemoryManagementToolkit":
"""[工厂方法] 创建一个只读模式的长期记忆工具箱。"""
kwargs["include"] = ["search_memory"]
kwargs.setdefault("instructions", cls._INTRO_TEXT + cls._READ_GUIDE)
return cls(**kwargs)
@classmethod
def write_only(cls, **kwargs) -> "MemoryManagementToolkit":
"""[工厂方法] 创建一个仅写入模式的长期记忆工具箱。"""
kwargs["exclude"] = ["search_memory"]
kwargs.setdefault("instructions", cls._INTRO_TEXT + cls._WRITE_GUIDE)
return cls(**kwargs)
def __init__(
self,
memory_config: MemoryConfig | None = None,
rag_client: ScopedRAGClient | None = None,
scopes: dict[str, ScopeBuilder] | None = None,
namespace: str | None = None,
**kwargs: Any,
):
@@ -50,85 +74,43 @@ class MemoryManagementToolkit(BaseToolkit):
初始化主动记忆管理工具箱。
参数:
memory_config: 记忆系统的全局配置对象,为空则使用全局默认。
rag_client: 底层 RAG 检索引擎客户端实例。
scopes: 作用域构建器映射字典,用于动态限定存储的分区。
namespace: 当前隔离环境的命名空间。
kwargs: 其他透传给 BaseToolkit 的参数。
"""
super().__init__(**kwargs)
self.memory_config = memory_config
self.rag_client = rag_client
from zhenxun.services.ai.context.memory.types import Isolation
self.scopes = scopes or {"私有": Isolation.AGENT_USER()}
self._namespace = namespace
def _get_runtime_meta_and_scope(
self, context: RunContext, scope_name: str | None = None
) -> tuple[Any, SessionMetadata]:
"""动态获取当前运行时的数据库实例与会话元信息,实现无状态化"""
ns = self._namespace or getattr(context.session, "namespace", "global")
scope = memory_manager.get_long_term_memory(self.memory_config, namespace=ns)
scope = MemoryScope(rag_client=self.rag_client) if self.rag_client else None
scope_builder = None
if (
self.memory_config
and self.memory_config.long_term
and self.memory_config.long_term.scopes
):
scopes_dict = self.memory_config.long_term.scopes
if not scope_name:
scope_builder = next(iter(scopes_dict.values()))
else:
scope_builder = scopes_dict.get(scope_name)
if not scope_builder:
scope_builder = getattr(self.memory_config, "base_isolation", None)
if not scope_builder:
from zhenxun.services.ai.context.memory.types import Isolation
scope_builder = Isolation.AGENT_USER()
selector = scope_builder.resolve(
deps=context.deps,
prefix="",
default_namespace=ns,
default_agent=context.run.agent_name,
scope_builder = (
self.scopes.get(scope_name)
if scope_name
else next(iter(self.scopes.values()), None)
)
parts = selector.get_scope_parts()
all_scopes = {"/"}
current_path = ""
for part in parts:
current_path += f"/{part}"
all_scopes.add(current_path)
accessible_scopes = list(all_scopes)
accessible_scopes.sort(key=lambda x: len(x.split("/")))
scope_name_mapping = {}
if (
self.memory_config
and self.memory_config.long_term
and self.memory_config.long_term.scopes
):
for name, builder in self.memory_config.long_term.scopes.items():
sel = builder.resolve(
deps=context.deps,
prefix="",
default_namespace=ns,
default_agent=context.run.agent_name,
)
scope_name_mapping[sel.scope_prefix] = name
session_meta = SessionMetadata(
session_id=context.session_id or "default_session",
selector=selector,
scope_prefix=selector.scope_prefix,
accessible_scopes=accessible_scopes,
scope_name_mapping=scope_name_mapping,
session_meta = ContextUtils.build_session_meta(
context=context,
target_builder=scope_builder,
extra_scopes=self.scopes,
custom_namespace=self._namespace,
)
return scope, session_meta
async def get_tools(self, context: RunContext | None = None) -> dict[str, BaseTool]:
tools = await super().get_tools(context)
if not self.memory_config or not self.memory_config.long_term.enable:
if not getattr(self, "rag_client", None):
return tools
scopes_dict = self.memory_config.long_term.scopes
scopes_dict = getattr(self, "scopes", {})
if not scopes_dict:
return tools
@@ -6,7 +6,6 @@ from typing import Any, Literal
from pydantic import Field, create_model
from zhenxun.services.ai.context.memory.manager import memory_manager
from zhenxun.services.ai.context.memory.models import MemoryConfig
from zhenxun.services.ai.context.memory.types import (
MemorySlot,
SessionMetadata,
@@ -16,6 +15,7 @@ from zhenxun.services.ai.tools.core.decorators import tool
from zhenxun.services.ai.tools.core.tool import BaseTool
from zhenxun.services.ai.tools.core.toolkit import BaseToolkit
from zhenxun.services.ai.tools.models import ToolOptions, ToolResult
from zhenxun.services.ai.utils.runtime import ContextUtils
_SLOT_LOCKS: dict[str, asyncio.Lock] = {}
_GLOBAL_LOCK = asyncio.Lock()
@@ -45,119 +45,78 @@ class MemorySlotToolkit(BaseToolkit):
shared_options = ToolOptions(silent=True)
default_instructions = """\
## 📋 状态与规则面板 (Memory Slots / 中期记忆)
该系统是你的「桌面便利贴」或「共享黑板」,用于保存你当前需要随时查阅的核心状态与全局规范。
_INTRO_TEXT = (
"## 📋 状态与规则面板 (Memory Slots / 中期记忆)\n"
"该系统是你的「桌面便利贴」或「共享黑板」,用于保存你当前需要随时查阅的核心状态与全局规范。\n\n"
"### 💡 核心机制\n"
"- 记忆槽内容会在每次对话时**直接注入提示词中**,你无需搜索即可看见。\n"
"- 槽位容量极其有限,仅用于维持最新的运行状态。\n\n"
"### 📝 职责说明\n"
)
_READ_GUIDE = (
"- **探索可用面板**:接手新任务时,可使用 `list_slots` "
"宏观查看当前存在哪些面板。\n"
)
_WRITE_GUIDE = (
"- **维护规范与进度**:如设定'沟通口吻'等规范(`update_slot`),"
"或记录'待办清单'(`append_slot`)。\n"
"- **保持精简**:内容过长时,主动将其归档到长期记忆,"
"再重新提炼或调用 `delete_slot` 删除。\n"
)
### 💡 核心机制
- 被保存在记忆槽中的内容(如果已置顶),会在每次对话时**直接注入到你的上下文提示词中**,你无需任何搜索即可看见。
- 槽位容量极其有限,仅用于维持当前最新的运行状态。
default_instructions = _INTRO_TEXT + _READ_GUIDE + _WRITE_GUIDE
### 📝 何时使用记忆槽?
- **维护全局规则**:例如设定"用户整体偏好"、"沟通口吻"、"全局指导原则"等需要时刻遵守的规范(使用 `update_slot`)。
- **追踪当前进度**:例如记录"待办事项清单"、"当前任务进度"、"上下文摘要"(使用 `append_slot` 列表或 `update_slot` 覆盖)。
@classmethod
def read_only(cls, **kwargs) -> "MemorySlotToolkit":
"""[工厂方法] 创建一个只读模式的记忆槽工具箱。"""
kwargs["include"] = ["list_slots", "read_slot"]
kwargs.setdefault("instructions", cls._INTRO_TEXT + cls._READ_GUIDE)
return cls(**kwargs)
### ⚙️ 操作规范
1. **探索可用面板**:接手新任务时,可使用 `list_slots` 宏观查看当前存在哪些状态面板。
2. **保持精简**:槽位有严格的字符数限制。当内容过长时,请主动将其归档到长期记忆后,重新提炼并覆盖槽位,或直接调用 `delete_slot` 删除不再需要的槽位。\
""" # noqa: E501
@classmethod
def write_only(cls, **kwargs) -> "MemorySlotToolkit":
"""[工厂方法] 创建一个仅写入模式的记忆槽工具箱。"""
kwargs["exclude"] = ["list_slots", "read_slot"]
kwargs.setdefault("instructions", cls._INTRO_TEXT + cls._WRITE_GUIDE)
return cls(**kwargs)
def __init__(
self,
memory_config: MemoryConfig | None = None,
scopes: dict[str, Any] | None = None,
backend: Any = None,
namespace: str | None = None,
**kwargs: Any,
):
"""
初始化中期记忆槽工具箱。
参数:
memory_config: 记忆系统的全局配置对象,为空则使用全局默认。
namespace: 当前隔离环境的命名空间。
kwargs: 其他透传给 BaseToolkit 的参数。
"""
super().__init__(**kwargs)
self.memory_config = memory_config
self.scopes = scopes or {}
self.backend = backend
self._namespace = namespace
def _get_runtime_meta_and_ctx(
self, context: RunContext, scope_name: str | None = None
) -> tuple[Any, SessionMetadata]:
ns = self._namespace or getattr(context.session, "namespace", "global")
slot_ctx = memory_manager.get_slot_context(self.memory_config, namespace=ns)
slot_ctx = self.backend or memory_manager.get_backend("slots", namespace=ns)
scope_builder = None
if (
self.memory_config
and self.memory_config.slots
and self.memory_config.slots.scopes
):
scopes_dict = self.memory_config.slots.scopes
if not scope_name:
scope_builder = next(iter(scopes_dict.values()))
else:
scope_builder = scopes_dict.get(scope_name)
if not scope_builder:
scope_builder = getattr(self.memory_config, "base_isolation", None)
if not scope_builder:
from zhenxun.services.ai.context.memory.types import Isolation
scope_builder = Isolation.AGENT_USER()
selector = scope_builder.resolve(
deps=context.deps,
prefix="",
default_namespace=ns,
default_agent=context.run.agent_name,
scope_builder = (
self.scopes.get(scope_name)
if scope_name
else next(iter(self.scopes.values()), None)
)
parts = selector.get_scope_parts()
all_scopes = {"/"}
current_path = ""
for part in parts:
current_path += f"/{part}"
all_scopes.add(current_path)
accessible_scopes = list(all_scopes)
accessible_scopes.sort(key=lambda x: len(x.split("/")))
scope_name_mapping = {}
if (
self.memory_config
and self.memory_config.slots
and self.memory_config.slots.scopes
):
for name, builder in self.memory_config.slots.scopes.items():
sel = builder.resolve(
deps=context.deps,
prefix="",
default_namespace=ns,
default_agent=context.run.agent_name,
)
scope_name_mapping[sel.scope_prefix] = name
session_meta = SessionMetadata(
session_id=context.session_id or "default_session",
selector=selector,
scope_prefix=selector.scope_prefix,
accessible_scopes=accessible_scopes,
scope_name_mapping=scope_name_mapping,
session_meta = ContextUtils.build_session_meta(
context=context,
target_builder=scope_builder,
extra_scopes=self.scopes,
custom_namespace=self._namespace,
)
return slot_ctx, session_meta
async def get_tools(self, context: RunContext | None = None) -> dict[str, BaseTool]:
tools = await super().get_tools(context)
if (
not self.memory_config
or not self.memory_config.slots
or not self.memory_config.slots.enable
):
if not self.scopes:
return tools
scopes_dict = self.memory_config.slots.scopes
if not scopes_dict:
return tools
scope_keys = tuple(scopes_dict.keys())
scope_keys = tuple(self.scopes.keys())
if len(scope_keys) > 1:
ScopeType = Literal[scope_keys]
@@ -237,12 +196,7 @@ class MemorySlotToolkit(BaseToolkit):
res = ["已创建的记忆槽列表:"]
show_scope = False
if (
self.memory_config
and self.memory_config.slots
and self.memory_config.slots.scopes
and len(self.memory_config.slots.scopes) > 1
):
if len(self.scopes) > 1:
show_scope = True
for s in slots:
@@ -295,6 +295,9 @@ class SkillMetaToolkit(BaseToolkit, SkillSandboxExecutionMixin):
if sandbox is None and context is not None:
sandbox = Inject._providers["sandbox"]["global"](context)
if sandbox is None:
return ToolResult(output="❌ 缺少沙箱环境或执行上下文").as_error()
executor = await sandbox.get_or_create_session(session_id, blueprint=bp)
fs_executor = cast(SupportsFileSystem, executor)