♻️ 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
+17
View File
@@ -0,0 +1,17 @@
from .base import BaseRunnable
from .models import (
BaseRuntimeConfig,
ConcurrencyPolicy,
ConcurrencyScope,
InterventionPolicy,
)
from .runner import FlowRunner
__all__ = [
"BaseRunnable",
"BaseRuntimeConfig",
"ConcurrencyPolicy",
"ConcurrencyScope",
"FlowRunner",
"InterventionPolicy",
]
+214
View File
@@ -0,0 +1,214 @@
from __future__ import annotations
from abc import ABC, abstractmethod
import asyncio
from collections.abc import AsyncIterator
import contextlib
from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
from nonebot.params import Depends
from zhenxun.services.ai.core.exceptions import (
ConcurrencyInterruptException,
ControlFlowExit,
)
from zhenxun.services.ai.core.models import CancellationToken
from zhenxun.services.ai.core.stream_events import AgentStreamEvent, EventBus
from zhenxun.services.ai.run.context import RunContext
from zhenxun.services.ai.run.models import AgentRunError, RunIntent, StreamedRunResult
from zhenxun.services.ai.run.subscribers import DefaultUISubscriber, TelemetrySubscriber
from zhenxun.services.ai.run.ui import UIController
from zhenxun.services.ai.utils import ContextUtils
from zhenxun.services.ai.utils.logger import log_flow as logger
if TYPE_CHECKING:
from zhenxun.services.ai.flow.agent.models import Persona
from zhenxun.services.ai.core.messages import PromptInput
from .models import (
BaseRuntimeConfig,
ConcurrencyPolicy,
)
T_RunResult = TypeVar("T_RunResult")
class BaseRunnable(ABC, Generic[T_RunResult]):
"""
所有可执行 AI 编排实体的统一基类
统一了 Agent, Team, Workflow 的核心契约,支持物理上的任意嵌套。
"""
name: str
"""可执行实体的名称标识"""
description: str
"""可执行实体的详细描述。用于外部路由(Router)或上层智能体(DelegateTool)决定是否调用它"""
persona: "Persona | None" = None
"""(可选) 实体的角色设定 (Persona)。包含 role 和 goal,
在多智能体路由移交时优先级最高"""
runtime_config: BaseRuntimeConfig
"""运行时配置,如是否无状态、UI输出模式等"""
@property
def profile_summary(self) -> str:
"""获取该实体的标准化简要画像/描述,供上层路由和规划决策使用"""
if self.persona:
return f"角色:{self.persona.role},目标:{self.persona.goal}"
return self.description or "处理节点"
def bind(self, **kwargs: Any) -> Any:
"""DI 注入语法糖:返回 Depends,自动绑定当前上下文"""
from .runner import FlowRunner
async def _dependency() -> FlowRunner[Any]:
return FlowRunner[Any](self, **kwargs)
return Depends(_dependency)
async def reply(
self,
prompt: PromptInput | None = None,
reply_to: bool = False,
*,
context: RunContext | None = None,
**kwargs: Any,
) -> T_RunResult:
"""交互执行语法糖,自动渲染流式进度并最终将结果回复给终端用户"""
from .runner import FlowRunner
runner = FlowRunner(self, context=context, **kwargs)
return cast(T_RunResult, await runner.reply(prompt=prompt, reply_to=reply_to))
async def run(
self,
prompt: PromptInput | None = None,
*,
context: RunContext | None = None,
**kwargs: Any,
) -> T_RunResult:
"""阻塞式核心运行入口,安全捕获内部抛出的静默退出信号"""
try:
async with self.run_stream(
prompt=prompt, context=context, **kwargs
) as stream_result:
return cast(T_RunResult, await stream_result.get_run_result())
except ControlFlowExit as e:
logger.info(f"[{self.name}] 触发底层控制流,已安全退出: {e}")
await UIController.handle_control_flow_exit_display(e, context)
raise asyncio.CancelledError()
@contextlib.asynccontextmanager
async def run_stream(
self,
prompt: PromptInput | None = None,
*,
context: RunContext | None = None,
deps: Any = None,
event_bus: EventBus | None = None,
**kwargs: Any,
) -> AsyncIterator[StreamedRunResult[Any]]:
"""统一的流式运行入口,负责生命周期调度、并发锁管理和事件总线挂载。"""
from .concurrency import apply_concurrency_policy
intent = RunIntent.from_input(prompt)
bus = event_bus or EventBus()
if context is None:
safe_context = RunContext(session_id=kwargs.get("session_id"))
if deps is not None:
safe_context.deps = deps
else:
safe_context = context
if deps is not None and safe_context.deps is None:
safe_context.deps = deps
is_root = not safe_context.state.get("__is_root_run_executed__", False)
if is_root:
safe_context.state["__is_root_run_executed__"] = True
TelemetrySubscriber().attach(bus)
if (
safe_context.get_bot()
and safe_context.get_event()
and safe_context.run.delegate_depth == 0
):
config_obj = getattr(self, "config", self.runtime_config)
verbose_ui = getattr(config_obj, "verbose_ui", False)
DefaultUISubscriber(safe_context, verbose=verbose_ui).attach(bus)
policy = getattr(self.runtime_config, "concurrency_policy", None)
if policy is None:
policy = (
ConcurrencyPolicy.ALLOW
if getattr(self.runtime_config, "stateless", True)
else ConcurrencyPolicy.QUEUE
)
intervention_policy = getattr(self.runtime_config, "intervention_policy", None)
lock_id = ContextUtils.extract_concurrency_lock_id(
safe_context,
getattr(self.runtime_config, "concurrency_scope", None),
safe_context.session_id or "default_session",
)
async def _execution_task():
cancel_token = safe_context.run.cancellation_token or CancellationToken()
safe_context.run.cancellation_token = cancel_token
try:
async with apply_concurrency_policy(
session_id=safe_context.session_id or "default_session",
lock_id=lock_id,
policy=policy,
cancel_token=cancel_token,
intervention_policy=intervention_policy,
intent=intent,
):
async for event in self._execute_stream(
intent=intent,
context=safe_context,
cancel_token=cancel_token,
event_bus=bus,
**kwargs,
):
await bus.emit(event)
except ControlFlowExit as e:
await bus.emit(AgentRunError(error=e))
except asyncio.CancelledError:
logger.debug(f"[{self.name}] 执行被并发策略中断取消。")
await bus.emit(
AgentRunError(
error=ConcurrencyInterruptException("任务已被新请求打断并接管")
)
)
except Exception as e:
await bus.emit(AgentRunError(error=e))
finally:
await bus.end()
task = asyncio.create_task(_execution_task())
result_obj = StreamedRunResult[Any](bus)
try:
yield result_obj
finally:
if not task.done():
task.cancel()
@abstractmethod
async def _execute_stream(
self,
intent: RunIntent,
context: RunContext,
cancel_token: CancellationToken,
event_bus: EventBus,
**kwargs: Any,
) -> AsyncIterator[AgentStreamEvent]:
"""核心执行流(由子类实现),通过 yield 返回执行事件。"""
if False:
yield cast(Any, None)
@@ -0,0 +1,114 @@
import asyncio
from contextlib import asynccontextmanager
from zhenxun.services.ai.core.exceptions import (
ConcurrencyRejectException,
InterventionHandledException,
)
from zhenxun.services.ai.core.models import CancellationToken
from zhenxun.services.ai.run.models import RunIntent
from zhenxun.services.ai.run.session import LockContext, session_manager
from zhenxun.services.ai.utils.logger import log_flow as logger
from .models import ConcurrencyPolicy, InterventionPolicy
@asynccontextmanager
async def apply_concurrency_policy(
session_id: str,
lock_id: str,
policy: ConcurrencyPolicy,
cancel_token: CancellationToken,
intervention_policy: InterventionPolicy | None = None,
intent: RunIntent | None = None,
):
"""
异步上下文管理器:对大模型执行流应用特定的并发及消息干预调度策略。
负责请求互斥锁竞争、任务中断/拒绝处理,以及运行时用户实时指令的插队控制。
参数:
session_id: 当前会话的唯一标识,用于在会话管理器中隔离上下文。
lock_id: 当前锁域标识,决定了哪些 Agent 或任务使用同一套互斥锁竞争机制。
policy: 当发生并发锁占用时执行的策略(允许、拒绝、中断、排队)。
cancel_token: 运行时用于监听取消请求的取消令牌实例。
intervention_policy: 用户消息干预策略(转向、追加)。
message: 并发竞争发生时新入站的用户请求消息或 AgentTask 载荷。
返回:
AsyncGenerator: 返回异步生成器,供 async with 消费,包裹大模型的整个执行环节。
"""
current_task = asyncio.current_task()
task_tuple = (cancel_token, current_task)
if session_id not in session_manager.live_tasks:
session_manager.live_tasks[session_id] = []
session_manager.live_tasks[session_id].append(task_tuple)
try:
exec_lock = session_manager.get_exec_lock(lock_id)
lock_ctx = session_manager.lock_contexts.setdefault(lock_id, LockContext())
if exec_lock.locked():
if intervention_policy in (
InterventionPolicy.STEER,
InterventionPolicy.FOLLOW_UP,
):
session = await session_manager.get_or_create(session_id)
if intervention_policy == InterventionPolicy.STEER:
session.steer_queue.enqueue(intent.text if intent else "")
raise InterventionHandledException(
"Steer successful",
display_content="💬 已将您的补充信息传递给正在思考的 AI...",
)
elif intervention_policy == InterventionPolicy.FOLLOW_UP:
session.follow_up_queue.enqueue(intent.text if intent else "")
raise InterventionHandledException(
"Follow-up successful",
display_content="📝 已记录,AI 处理完当前任务后即刻执行...",
)
if policy == ConcurrencyPolicy.ALLOW:
yield
return
if policy == ConcurrencyPolicy.REJECT:
if exec_lock.locked():
raise ConcurrencyRejectException(
f"并发域 {lock_id} 正忙,新请求被拒绝。"
)
elif policy == ConcurrencyPolicy.INTERRUPT:
if exec_lock.locked():
if lock_ctx.cancel_token:
lock_ctx.cancel_token.cancel()
if lock_ctx.active_task and not lock_ctx.active_task.done():
lock_ctx.active_task.cancel()
elif policy == ConcurrencyPolicy.QUEUE:
if exec_lock.locked():
logger.info(
f"⏳ [并发控制] 锁域 {lock_id} 被占用,"
"新请求已进入后台等待队列 (QUEUE)..."
)
async with exec_lock:
session = await session_manager.get_or_create(session_id)
session.active_task = asyncio.current_task()
session.cancel_token = cancel_token
lock_ctx.active_task = asyncio.current_task()
lock_ctx.cancel_token = cancel_token
try:
yield
finally:
session.active_task = None
session.cancel_token = None
lock_ctx.active_task = None
lock_ctx.cancel_token = None
finally:
if session_id in session_manager.live_tasks:
if task_tuple in session_manager.live_tasks[session_id]:
session_manager.live_tasks[session_id].remove(task_tuple)
if not session_manager.live_tasks[session_id]:
del session_manager.live_tasks[session_id]
+53
View File
@@ -0,0 +1,53 @@
from enum import Enum
from pydantic import BaseModel, Field
class ConcurrencyPolicy(str, Enum):
"""并发执行策略枚举"""
ALLOW = "allow"
"""允许并发:不做任何限制(适用于无状态或独立任务)"""
REJECT = "reject"
"""拒绝新请求:当前有任务在执行时,直接丢弃新任务并提醒"""
QUEUE = "queue"
"""排队等待:当前有任务在执行时,新任务排队等待(先进先出)"""
INTERRUPT = "interrupt"
"""中断旧任务:新任务到达时,立即强制取消并覆盖正在执行的旧任务"""
class ConcurrencyScope(str, Enum):
"""并发作用域枚举(决定锁的粒度,解耦于会话隔离)"""
GLOBAL = "global"
"""全局互斥:整个系统同一时间只能执行一个该任务"""
GROUP = "group"
"""群组互斥:同一群组内串行排队(私聊退化为用户级),防止抢话刷屏"""
USER = "user"
"""用户互斥:同一用户发起的任务串行排队(允许同群不同人并行)"""
SESSION = "session"
"""会话互斥:跟随记忆 SessionID 进行物理锁隔离"""
class InterventionPolicy(str, Enum):
"""运行时消息干预策略枚举"""
IGNORE = "ignore"
"""忽略干预:丢弃在任务执行期间收到的额外消息(默认)"""
STEER = "steer"
"""动态转向:将额外消息立即注入到下一轮大模型推理历史中,影响其思考方向"""
FOLLOW_UP = "follow_up"
"""追加执行:将额外消息放入队列,在当前大模型意图(所有工具等)执行完毕后追加推理"""
class BaseRuntimeConfig(BaseModel):
"""所有可执行实体(Agent/Team/Workflow)的通用基础运行时配置"""
stateless: bool = Field(default=True)
"""是否使用临时会话,不持久化历史记录"""
concurrency_policy: ConcurrencyPolicy | None = Field(default=None)
"""并发执行策略。如果未显式指定,无状态(stateless=True)默认为ALLOW,有状态(stateless=False)默认为QUEUE。"""
concurrency_scope: ConcurrencyScope | None = Field(default=None)
"""并发作用域,决定锁的粒度。如果未显式指定,默认为 GROUP 级排队。"""
intervention_policy: InterventionPolicy | None = Field(default=None)
"""运行时干预策略,决定在大模型执行期间接收到新消息时该如何处理数据流合并。"""
+154
View File
@@ -0,0 +1,154 @@
import asyncio
from typing import Any, Generic, cast
from typing_extensions import TypeVar
import uuid
from nonebot.adapters import Bot, Event
from nonebot_plugin_alconna.uniseg import UniMessage
from zhenxun.services.ai.core.exceptions import (
ConcurrencyInterruptException,
ConcurrencyRejectException,
ControlFlowExit,
InterventionHandledException,
)
from zhenxun.services.ai.core.messages import PromptInput, UsageInfo
from zhenxun.services.ai.flow.core.base import BaseRunnable
from zhenxun.services.ai.run import AgentRunResult, RunContext
from zhenxun.services.ai.run.models import AgentRunEnd, AgentRunError, AgentTask
from zhenxun.services.ai.run.ui import UIController
from zhenxun.services.ai.utils.logger import log_flow as logger
from zhenxun.utils.message import MessageUtils
from zhenxun.utils.platform import PlatformUtils
T_Deps = TypeVar("T_Deps", default=Any)
T_Out = TypeVar("T_Out", default=str)
class FlowRunner(Generic[T_Out]):
"""
执行流交互运行器。
负责将大模型的纯净数据流包装为平台交互动作(发消息、UI渲染)。
自带 ContextVars 隐式上下文提取魔法。
"""
def __init__(
self,
runnable: BaseRunnable,
context: RunContext | None = None,
**kwargs: Any,
):
self.runnable = runnable
self.context = context or RunContext(**kwargs)
is_stateless = (
getattr(self.runnable.runtime_config, "stateless", True)
if hasattr(self.runnable, "runtime_config")
else True
)
if is_stateless and self.context.session_id:
if not self.context.session_id.startswith("stateless_"):
self.context.session_id = (
f"stateless_{self.context.session_id}_{uuid.uuid4().hex[:8]}"
)
if self.context.session:
self.context.session.session_id = self.context.session_id
@property
def _bot(self) -> Bot | None:
return self.context.get_bot()
@property
def _event(self) -> Event | None:
return self.context.get_event()
async def reply(
self,
prompt: PromptInput | AgentTask | None = None,
reply_to: bool = False,
**kwargs: Any,
) -> AgentRunResult[T_Out]:
"""交互式执行:将 Agent 运行过程中的工具调用状态和最终结果自动发送给用户。"""
final_result = None
profile = kwargs.pop("profile", None)
try:
async with self.runnable.run_stream(
prompt=prompt,
context=self.context,
profile=profile,
**kwargs,
) as stream_result:
async for stream_event in stream_result.stream_events():
if isinstance(stream_event, AgentRunEnd):
final_result = stream_event.result
elif isinstance(stream_event, AgentRunError):
raise stream_event.error
except ControlFlowExit as e:
if isinstance(e, InterventionHandledException):
logger.info(f"✨ {self.runnable.name} 触发运行时干预: {e.message}")
if e.display_content and self._bot and self._event:
await MessageUtils.build_message(str(e.display_content)).send(
reply_to=reply_to
)
return cast(
AgentRunResult[T_Out], AgentRunResult(output="", usage=UsageInfo())
)
if isinstance(e, ConcurrencyRejectException):
logger.warning(
f"⏳ {self.runnable.name} 触发并发拒绝 (REJECT): {e.message}"
)
return cast(
AgentRunResult[T_Out], AgentRunResult(output="", usage=UsageInfo())
)
if isinstance(e, ConcurrencyInterruptException):
logger.warning(
f"🛑 {self.runnable.name} 触发并发中断 (INTERRUPT): {e.message}"
)
return cast(
AgentRunResult[T_Out], AgentRunResult(output="", usage=UsageInfo())
)
logger.debug(
f"{self.runnable.name} 控制流正常中断: {type(e).__name__} - {e}"
)
await UIController.handle_control_flow_exit_display(
e, self.context, reply_to
)
raise asyncio.CancelledError()
except Exception as e:
logger.error(f"{self.runnable.name} 运行失败: {e}", e=e)
if self._bot and self._event:
await MessageUtils.build_message(f"❌ 运行发生错误: {e}").send()
raise e
if final_result and final_result.output and self._bot:
msg_to_send = (
final_result.output
if isinstance(final_result.output, UniMessage)
else MessageUtils.build_message(str(final_result.output))
)
if self._event:
await msg_to_send.send(self._event, bot=self._bot, reply_to=reply_to)
else:
target = PlatformUtils.get_target(
user_id=self.context.get_user_id(),
group_id=self.context.get_group_id(),
)
if target:
await msg_to_send.send(target=target, bot=self._bot)
if isinstance(final_result.output, UniMessage):
final_result.output = final_result.output.extract_plain_text()
if final_result is None:
raise RuntimeError("智能体运行流异常结束:未返回最终结果。")
return cast(AgentRunResult[T_Out], final_result)