Files
zhenxun_bot/zhenxun/services/ai/flow/agent/engine/executor.py
T
52f7dbdedf ♻️ 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>
2026-07-14 16:48:33 +08:00

703 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from abc import ABC, abstractmethod
import asyncio
from collections.abc import Awaitable, Callable
import json
from typing import Any, Literal, cast
from zhenxun.services.ai.capabilities import CombinedCapability
from zhenxun.services.ai.core.engine.context_renderer import ContextConverter
from zhenxun.services.ai.core.engine.token_counter import (
parse_usage_info,
token_counter,
)
from zhenxun.services.ai.core.exceptions import (
ControlFlowExit,
UpstreamServerException,
)
from zhenxun.services.ai.core.messages import (
AgentMessage,
AssistantMessage,
AudioPart,
ChatRequest,
ChatResponse,
FilePart,
ImagePart,
LLMMessage,
TextPart,
ToolCallPart,
ToolReturnPart,
VideoPart,
)
from zhenxun.services.ai.core.models import CancellationToken, LLMContext, ToolChoice
from zhenxun.services.ai.core.options import GenerationConfig
from zhenxun.services.ai.core.protocols.tool import ToolExecutable
from zhenxun.services.ai.core.stream_events import (
LLMEndEvent,
LLMStartEvent,
ToolStreamChunkEvent,
)
from zhenxun.services.ai.flow.agent.models import AgentRunResources, AgentState
from zhenxun.services.ai.llm.engine.router import LLMOrchestrator
from zhenxun.services.ai.run import AgentRunResult, RunContext
from zhenxun.services.ai.run.models import OutputDataT
from zhenxun.services.ai.run.session import SessionInfo, session_manager
from zhenxun.services.ai.tools.engine.executor import ToolExecutor
from zhenxun.services.ai.tools.models import ToolResult
from zhenxun.services.ai.utils.logger import log_agent as logger
from zhenxun.utils.pydantic_compat import dump_json_safely, model_construct
from .directive import (
DirectiveHandlerFunc,
directive_manager,
)
class BaseAgentExecutor(ABC):
"""
Agent 核心执行器基类 (Template Method Pattern)。
定义了基于生命周期的大模型控制流。第三方开发者可通过重写特定钩子,
"""
async def run(
self, state: AgentState, resources: AgentRunResources
) -> AgentRunResult[OutputDataT]:
"""
核心模板方法 (Template Method)。
组织整个大模型推导与工具调用的生命周期循环。如无必要,请勿重写此方法。
"""
await self.on_start(state, resources)
try:
for cycle_index in range(resources.config.max_cycles):
state.current_cycle = cycle_index
await self.on_cycle_start(state, resources)
await self.build_llm_request(state, resources)
await self.execute_llm(state, resources)
await self.handle_llm_response(state, resources)
if state.is_finished:
assert state.final_result is not None
return state.final_result
await self.filter_tool_calls(state, resources)
if state.is_finished:
assert state.final_result is not None
return state.final_result
await self.execute_tools(state, resources)
await self.handle_tool_results(state, resources)
if state.is_finished:
assert state.final_result is not None
return state.final_result
return await self.on_fallback(state, resources)
except Exception as e:
raise e
@abstractmethod
async def on_start(self, state: AgentState, resources: AgentRunResources) -> None:
"""生命周期: Agent 启动时调用,用于初始化状态或资源。"""
pass
@abstractmethod
async def on_cycle_start(
self, state: AgentState, resources: AgentRunResources
) -> None:
"""生命周期: 每次推理循环开始时调用。可用于 Token 预估或防死循环检测。"""
pass
@abstractmethod
async def build_llm_request(
self, state: AgentState, resources: AgentRunResources
) -> None:
"""生命周期: 构造请求大模型的 Messages 上下文和 Extra 参数。"""
pass
@abstractmethod
async def execute_llm(
self, state: AgentState, resources: AgentRunResources
) -> None:
"""生命周期: 触发大模型 API 请求并返回响应。"""
pass
@abstractmethod
async def handle_llm_response(
self, state: AgentState, resources: AgentRunResources
) -> None:
"""
生命周期: 处理大模型返回的结果,解析 Token 用量,
并将模型回复追加至对话历史。
"""
pass
@abstractmethod
async def filter_tool_calls(
self, state: AgentState, resources: AgentRunResources
) -> None:
"""生命周期: 从大模型的响应中提取并过滤出需要在本地客户端执行的工具调用请求。"""
pass
@abstractmethod
async def execute_tools(
self, state: AgentState, resources: AgentRunResources
) -> None:
"""生命周期: 并发执行提取出的工具,并收集结果或异常。"""
pass
@abstractmethod
async def handle_tool_results(
self, state: AgentState, resources: AgentRunResources
) -> None:
"""
生命周期: 处理工具返回的结果。
包括异常拦截、UI 渲染、Handoff 移交指令以及将结果追加至对话历史。
"""
pass
@abstractmethod
async def on_fallback(
self, state: AgentState, resources: AgentRunResources
) -> AgentRunResult[OutputDataT]:
"""生命周期: 当大模型思考循环达到 max_cycles 时触发,执行兜底策略。"""
pass
class StandardAgentExecutor(BaseAgentExecutor):
"""
LLM 任务执行器(核心推理引擎)。
负责:生命周期回调触发、工具循环调用、
错误反思(Reflexion)、Token消耗追踪。
"""
def __init__(
self, directive_handlers: dict[str, DirectiveHandlerFunc] | None = None
):
self.tool_executor = ToolExecutor()
self._directive_handlers: dict[str, DirectiveHandlerFunc] = (
directive_handlers or {}
)
def _can_retry_via_llm(self, result: ToolResult) -> bool:
"""通过新版的专属字段直接判断是否允许重试"""
return result.is_retryable
def _check_follow_up(
self, state: AgentState, resources: AgentRunResources, session_info: SessionInfo
) -> bool:
"""检查追加队列,排空并合并数据到上下文,返回是否发现新消息"""
follow_ups = session_info.follow_up_queue.drain()
if follow_ups:
for fm in follow_ups:
state.messages.append(LLMMessage.user(f"💬 [用户追加指示]:{fm}"))
resources.run_context.session.append_only_manager.sync_messages(
state.messages
)
state.is_finished = False
state.final_result = None
return True
return False
async def _invoke_and_record_llm(
self,
state: AgentState,
resources: AgentRunResources,
messages: list[AgentMessage],
tools: list[ToolExecutable | dict[str, Any]] | None,
tool_choice: str | dict[str, Any] | ToolChoice | None = None,
) -> ChatResponse:
"""执行 LLM 请求,处理基础指标遥测统计,并将新对话上下文追加到状态流"""
run_context = resources.run_context
cancellation_token = run_context.run.cancellation_token
current_extra = run_context.state.copy()
current_extra["__global_max_cycles__"] = getattr(
resources.config, "global_max_cycles", None
)
current_extra["__sys_capabilities"] = getattr(run_context, "capabilities", [])
current_extra["run_context"] = run_context
flattened_messages = ContextConverter.flatten_to_llm_messages(
messages, run_context
)
await run_context.run.emit(
LLMStartEvent(
model_name=resources.model_name or "unknown",
messages=flattened_messages,
)
)
response = await self._execute_model_request(
model_name=resources.model_name,
messages=flattened_messages,
config=resources.generation_config or GenerationConfig(),
run_context=run_context,
tools=tools,
tool_choice=tool_choice,
extra=current_extra,
cancellation_token=cancellation_token,
)
await run_context.run.emit(LLMEndEvent(response=response))
assistant_content = (
response.content_parts if response.content_parts else response.text
)
if response.thought_signature and isinstance(assistant_content, list):
for part in assistant_content:
if part.type == "thought":
if part.metadata is None:
part.metadata = {}
part.metadata["thought_signature"] = response.thought_signature
break
assistant_message = AssistantMessage(content=response.content_parts)
if hasattr(response, "parsed_obj") and response.parsed_obj is not None:
if not isinstance(response.parsed_obj, str):
if assistant_message.metadata is None:
assistant_message.metadata = {}
assistant_message.metadata["parsed_obj"] = response.parsed_obj
usage_obj = parse_usage_info(response.usage_info)
state.usage += usage_obj
if usage_obj.completion_tokens > 0:
assistant_message.token_cost = usage_obj.completion_tokens
state.messages.append(assistant_message)
run_context.session.append_only_manager.sync_messages(state.messages)
return response
async def _execute_model_request(
self,
model_name: str | None,
messages: list[LLMMessage],
config: GenerationConfig,
run_context: RunContext,
tools: list[ToolExecutable | dict[str, Any]] | None = None,
tool_choice: str | dict[str, Any] | ToolChoice | None = None,
extra: dict[str, Any] | None = None,
cancellation_token: CancellationToken | None = None,
) -> ChatResponse:
request = ChatRequest(
messages=messages,
config=config,
tools=tools,
tool_choice=tool_choice,
extra=extra or {},
)
sys_caps = request.extra.pop("__sys_capabilities", [])
llm_context = LLMContext(request=request, cancellation_token=cancellation_token)
combined_cap = CombinedCapability(sys_caps)
async def inner_handler(ctx: LLMContext[Any, Any]) -> ChatResponse:
return await LLMOrchestrator.invoke(
request=ctx.request,
model_name=model_name,
task="chat",
override_config=config,
cancellation_token=ctx.cancellation_token,
)
return await combined_cap.wrap_model_request(
run_context, llm_context, inner_handler
)
async def on_start(self, state: AgentState, resources: AgentRunResources) -> None:
resources.run_context.run.messages = state.messages
async def _execute_phase(
self,
phase_func: Callable[[AgentState, AgentRunResources], Awaitable[None]],
state: AgentState,
resources: AgentRunResources,
session_info: SessionInfo,
) -> Literal["NEXT", "RESET_CYCLE", "FINISH"]:
"""统一处理阶段执行与状态机完成/追加检查的高阶函数"""
await phase_func(state, resources)
if state.is_finished:
if self._check_follow_up(state, resources, session_info):
return "RESET_CYCLE"
return "FINISH"
return "NEXT"
async def run(
self, state: AgentState, resources: AgentRunResources
) -> AgentRunResult[OutputDataT]:
"""覆盖基类的模板方法,实现灵活的 while 控制流和 FOLLOW_UP 合并"""
await self.on_start(state, resources)
session_info = await session_manager.get_or_create(
resources.run_context.session_id or "default_session"
)
try:
cycle_count = 0
while cycle_count < resources.config.max_cycles:
state.current_cycle = cycle_count
await self.on_cycle_start(state, resources)
await self.build_llm_request(state, resources)
await self.execute_llm(state, resources)
status = await self._execute_phase(
self.handle_llm_response, state, resources, session_info
)
if status == "RESET_CYCLE":
cycle_count = 0
continue
if status == "FINISH":
assert state.final_result is not None
return state.final_result
status = await self._execute_phase(
self.filter_tool_calls, state, resources, session_info
)
if status == "RESET_CYCLE":
cycle_count = 0
continue
if status == "FINISH":
assert state.final_result is not None
return state.final_result
await self.execute_tools(state, resources)
status = await self._execute_phase(
self.handle_tool_results, state, resources, session_info
)
if status == "RESET_CYCLE":
cycle_count = 0
continue
if status == "FINISH":
assert state.final_result is not None
return state.final_result
cycle_count += 1
if self._check_follow_up(state, resources, session_info):
return await self.run(state, resources)
return await self.on_fallback(state, resources)
except Exception as e:
raise e
async def on_cycle_start(
self, state: AgentState, resources: AgentRunResources
) -> None:
cancellation_token = resources.run_context.run.cancellation_token
if cancellation_token:
cancellation_token.raise_if_cancelled()
try:
est_tokens = token_counter.count_context(
state.messages, resources.model_name or "", base_overhead=0
)
logger.debug(
f"(Iter {state.current_cycle + 1}) "
f"预估将消耗 {est_tokens} Token "
f"(Model: {resources.model_name or 'Unknown'})"
)
except Exception:
pass
async def build_llm_request(
self, state: AgentState, resources: AgentRunResources
) -> None:
run_context = resources.run_context
session_info = await session_manager.get_or_create(
run_context.session_id or "default_session"
)
steer_msgs = session_info.steer_queue.drain()
if steer_msgs:
for sm in steer_msgs:
state.messages.append(LLMMessage.user(f"💬 [用户实时修正指示]:{sm}"))
run_context.session.append_only_manager.sync_messages(state.messages)
messages_to_send = []
if state.static_system_prompt:
if isinstance(state.static_system_prompt, list):
for sp in state.static_system_prompt:
if sp and sp.strip():
messages_to_send.append(LLMMessage.system(sp))
else:
if state.static_system_prompt and state.static_system_prompt.strip():
messages_to_send.append(
LLMMessage.system(state.static_system_prompt)
)
if state.dynamic_system_messages:
messages_to_send.extend(state.dynamic_system_messages)
if (
hasattr(run_context.run, "dynamic_prompts")
and run_context.run.dynamic_prompts
):
for prompt_text in run_context.run.dynamic_prompts.values():
if prompt_text and prompt_text.strip():
messages_to_send.append(LLMMessage.system(prompt_text))
messages_to_send.extend(state.messages)
state.current_request_messages = messages_to_send
async def execute_llm(
self, state: AgentState, resources: AgentRunResources
) -> None:
tools = state.tools
state.current_response = await self._invoke_and_record_llm(
state=state,
resources=resources,
messages=state.current_request_messages,
tools=list(tools) if tools else None,
tool_choice=None,
)
async def handle_llm_response(
self, state: AgentState, resources: AgentRunResources
) -> None:
response = state.current_response
if not response:
return
if not response.tool_calls:
logger.debug("✅ 模型未请求工具调用,推理循环结束。")
state.is_finished = True
state.final_result = model_construct(
AgentRunResult,
output=response.text,
messages=state.messages,
usage=state.usage,
)
async def filter_tool_calls(
self, state: AgentState, resources: AgentRunResources
) -> None:
response = state.current_response
if not response:
return
tools = state.tools
event_bus = resources.run_context.run.event_bus
completed_call_ids = {
p.tool_call_id
for p in response.content_parts
if isinstance(p, ToolReturnPart)
}
client_tool_calls = []
for call in response.tool_calls:
tool_inst = tools.get(call.tool_name) if tools else None
is_server_side = call.id in completed_call_ids or (
tool_inst and getattr(tool_inst, "execution_side", "client") == "server"
)
if is_server_side:
logger.debug(
f"☁️ 检测到云端工具调用: {call.tool_name},已跳过本地执行。"
)
async with self.tool_executor._tool_stream_scope(
event_bus,
call.tool_name,
call.args if isinstance(call.args, dict) else {},
getattr(call, "intent", None),
) as box:
return_part = next(
(
p
for p in response.content_parts
if isinstance(p, ToolReturnPart)
and p.tool_call_id == call.id
),
None,
)
if return_part:
box["result"] = ToolResult(output=return_part.output)
else:
client_tool_calls.append(call)
if not client_tool_calls:
logger.info("✅ 无本地客户端工具需执行,推理循环平滑结束。")
state.is_finished = True
state.final_result = model_construct(
AgentRunResult,
output=response.text,
messages=state.messages,
usage=state.usage,
)
state.current_tool_calls = client_tool_calls
async def execute_tools(
self, state: AgentState, resources: AgentRunResources
) -> None:
run_context = resources.run_context
tools = state.tools
event_bus = run_context.run.event_bus
tool_calls = state.current_tool_calls
if not tool_calls:
return
val_tasks = [
self.tool_executor.validate_tool_call(
call,
tools,
run_context,
event_bus=event_bus,
)
for call in tool_calls
]
validated_calls = await asyncio.gather(*val_tasks)
exec_tasks = [
self.tool_executor.execute_tool_call(
val_call,
tools,
run_context,
event_bus=event_bus,
)
for val_call in validated_calls
]
tool_results = await asyncio.gather(*exec_tasks, return_exceptions=True)
state.current_tool_results = tool_results
def _assemble_tool_message(
self,
original_call: ToolCallPart,
res_or_exc: BaseException | tuple[ToolCallPart, ToolResult],
tool_res: ToolResult | None,
state: AgentState,
) -> LLMMessage:
"""负责处理异常、解析多模态、序列化,并装配为最终的工具消息载体"""
media_parts = []
final_content = "Success"
if isinstance(res_or_exc, BaseException):
if isinstance(res_or_exc, ControlFlowExit):
raise res_or_exc
final_content = json.dumps(
{"error": str(res_or_exc), "status": "failed"},
ensure_ascii=False,
)
elif tool_res is not None:
if isinstance(tool_res.output, list):
texts = []
for item in tool_res.output:
if isinstance(item, ImagePart | AudioPart | VideoPart | FilePart):
media_parts.append(item)
elif isinstance(item, TextPart):
texts.append(item.text)
else:
texts.append(str(item))
final_content = " ".join(texts) if texts else "Success"
elif isinstance(tool_res.output, str):
final_content = tool_res.output
else:
final_content = dump_json_safely(tool_res.output, ensure_ascii=False)
tool_usage = getattr(tool_res, "usage", None)
if tool_usage is not None:
state.usage += tool_usage
msg = LLMMessage.tool_response(
original_call.id, original_call.tool_name, final_content
)
if media_parts:
msg.content.extend(media_parts)
return msg
async def handle_tool_results(
self, state: AgentState, resources: AgentRunResources
) -> None:
"""处理所有工具执行结果,调度副作用指令并装配对话回传报文。"""
tool_calls = state.current_tool_calls
tool_results = state.current_tool_results
if not tool_calls or not tool_results:
return
for i, res_or_exc in enumerate(tool_results):
original_call = tool_calls[i]
tool_res = None
if not isinstance(res_or_exc, BaseException):
_, raw_tool_res = res_or_exc
tool_res = raw_tool_res
msg = self._assemble_tool_message(
original_call, res_or_exc, tool_res, state
)
state.messages.append(msg)
if tool_res and getattr(tool_res, "directive", None):
ns = getattr(resources.run_context.session, "namespace", "global")
handler = directive_manager.get_handler(
tool_res.directive.name, namespace=ns
)
if handler:
await handler(state, resources, tool_res)
if state.is_finished:
resources.run_context.session.append_only_manager.sync_messages(
state.messages
)
return
else:
logger.warning(
f"⚠️ 未能找到名为 '{tool_res.directive.name}' "
f"的指令处理器 (Namespace: {ns})"
)
resources.run_context.session.append_only_manager.sync_messages(state.messages)
async def on_fallback(
self, state: AgentState, resources: AgentRunResources
) -> AgentRunResult[OutputDataT]:
run_context = resources.run_context
if not resources.config.enable_fallback_summary:
raise UpstreamServerException(
f"超过最大工具调用循环次数 ({resources.config.max_cycles})。",
)
logger.warning(
f"达到最大循环次数 ({resources.config.max_cycles}),触发兜底总结机制。"
)
await run_context.run.emit(
ToolStreamChunkEvent(
tool_name="System",
content="⏳ 思考过程过于复杂,正在强制生成最终总结...",
)
)
fallback_msg = LLMMessage.user(
"### 🚨 [系统强制指令]\n"
"你的任务执行已达到最大工具调用循环次数上限,当前思考流已被框架强制中断。\n"
"请**诚实地**向用户总结:你目前进行到了哪一步?遇到了什么困难导致循环耗尽?还有哪些预期步骤未能完成?\n"
"**绝对禁止**对用户撒谎声声称你已经完成了任务。严禁再次尝试调用任何工具!请直接输出纯文本结果。"
)
state.messages.append(fallback_msg)
fallback_response = await self._invoke_and_record_llm(
state=state,
resources=resources,
messages=state.messages,
tools=[],
tool_choice="none",
)
return cast(
AgentRunResult[OutputDataT],
model_construct(
AgentRunResult,
output=fallback_response.text,
messages=state.messages,
structured_data=None,
usage=state.usage,
),
)