✨ feat!(llm): 重构并升级大语言模型服务为全新 AI 智能体框架 (#2146)

* ✨ feat!(llm): 重构并升级大语言模型服务为全新 AI 智能体框架

- 【重构】将原 services/llm 重构并迁移至全新的 services/ai 架构,提供向下兼容垫片
- 【新增】引入 Agent、Team、Workflow 三大智能体与工作流编排范式
- 【新增】引入基于 RAG 的长期向量记忆与中期槽位记忆系统
- 【新增】引入基于 Docker 的安全代码执行沙箱环境
- 【新增】支持 MCP 协议,允许动态管理和调用 MCP 服务
- 【新增】引入输入输出安全合规护栏与自愈反思机制
- 【优化】重构并优化多厂商 API 适配器 (Gemini, OpenAI, DeepSeek, GLM 等)
- 【优化】优化日志脱敏与 Token 预估机制
- 【移除】移除旧版 llm default 和 llm reset-key 命令,新增 llm mcp 管理命令

* 🔧 chore(deps): 更新项目依赖与配置

- 添加 mcp、jieba 和 aiodocker 依赖到配置文件及 requirements.txt
- 在 pyright 配置中设置 reportMissingImports 为 none
- 调整 .gitignore 中 resources 目录的忽略规则

* ♻️ refactor(tools): 重构工具终止机制并清理知识库日志输出

- 统一使用 `context.state["__end_run__"]` 替代 `EndRunResult` 控制任务结束
- 移除文件系统和向量知识库检索工具中 `ToolResult` 的 `.with_log` 调用
- 调整指令处理器(Directive)的返回值为 `tool_res.output`
- 修复部分类型检查警告并优化联合类型判断语法

* ♻️ refactor(tools): 重构工具副作用指令与控制流熔断机制

- 引入 `DirectivePayload` 及 `ToolResult` 的子类以结构化表达工具副作用
- 移除通过 `context.state` 传递魔术变量的隐式控制流设计
- 重构 `DirectiveManager` 处理器接口,直接在处理器中修改 `AgentState` 并构建 `AgentRunResult`
- 在 `StandardAgentExecutor` 中统一通过 `directive_manager` 调度工具返回的副作用指令
- 补全 `MessageBuilder` 中部分核心方法的文档注释

* 🐛 fix(sandbox): 修复 Docker 沙箱容器状态检测与会话清理逻辑

-【修复】修正 `is_alive` 中直接读取私有属性的问题,改用 `show()` 返回值
-【修复】解决 `execute_code` 中缓存的执行器与当前会话不一致的问题
-【优化】在清理工作区前增加容器存活检测,避免向已死容器发送请求
-【优化】创建容器时增加运行状态校验,若已停止则自动从缓存中移除并重建
-【优化】优化容器销毁和清理逻辑,静默处理容器不存在 (404) 的异常

* 📝 docs(core): 补充核心模块初始化方法的文档注释

* 🚨 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-03 08:53:56 +08:00
committed by GitHub
co-authored by webjoin111 pre-commit-ci[bot]
parent bdc1374848
commit 80fc5b86a7
223 changed files with 41885 additions and 10006 deletions
@@ -0,0 +1,691 @@
from abc import ABC, abstractmethod
import asyncio
import json
from typing import Any, cast
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,
AssistantContentUnion,
AssistantMessage,
AudioPart,
ChatRequest,
ChatResponse,
FilePart,
ImagePart,
LLMMessage,
TextPart,
ToolCallPart,
ToolReturnPart,
VideoPart,
)
from zhenxun.services.ai.core.models import LLMContext
from zhenxun.services.ai.core.options import GenerationConfig
from zhenxun.services.ai.core.stream_events import (
LLMEndEvent,
LLMStartEvent,
ToolStreamChunkEvent,
)
from zhenxun.services.ai.flow.agent.engine.directive import (
DirectiveHandlerFunc,
)
from zhenxun.services.ai.flow.agent.models import AgentRunResources, AgentState
from zhenxun.services.ai.run import AgentRunResult, RunContext
from zhenxun.services.ai.tools.engine.executor import ToolExecutor
from zhenxun.services.ai.tools.models import ToolResult
from zhenxun.services.log import logger
from zhenxun.utils.pydantic_compat import dump_json_safely, model_construct
class BaseAgentExecutor(ABC):
"""
Agent 核心执行器基类 (Template Method Pattern)。
定义了基于生命周期的大模型控制流。第三方开发者可通过重写特定钩子,
"""
async def run(
self, state: AgentState, resources: AgentRunResources
) -> AgentRunResult[Any]:
"""
核心模板方法 (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[Any]:
"""生命周期: 当大模型思考循环达到 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: Any
) -> 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[Any] | None,
tool_choice: Any = 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
)
if run_context.run.event_bus:
await run_context.run.event_bus.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,
)
if run_context.run.event_bus:
await run_context.run.event_bus.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=cast(list[AssistantContentUnion], 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[Any] | None = None,
tool_choice: Any = None,
extra: dict[str, Any] | None = None,
cancellation_token: Any = None,
) -> ChatResponse:
from zhenxun.services.ai.capabilities import CombinedCapability
from zhenxun.services.ai.llm.engine.router import LLMOrchestrator
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 run(
self, state: AgentState, resources: AgentRunResources
) -> AgentRunResult[Any]:
"""覆盖基类的模板方法,实现灵活的 while 控制流和 FOLLOW_UP 合并"""
await self.on_start(state, resources)
from zhenxun.services.ai.run.session import session_manager
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)
await self.handle_llm_response(state, resources)
if state.is_finished:
if self._check_follow_up(state, resources, session_info):
cycle_count = 0
continue
assert state.final_result is not None
return state.final_result
await self.filter_tool_calls(state, resources)
if state.is_finished:
if self._check_follow_up(state, resources, session_info):
cycle_count = 0
continue
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:
if self._check_follow_up(state, resources, session_info):
cycle_count = 0
continue
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"[TokenTracker] (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
from zhenxun.services.ai.run.session import session_manager
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("✅ AgentExecutor:模型未请求工具调用,推理循环结束。")
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(
"☁️ [AgentExecutor] 检测到云端工具调用: "
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:
from zhenxun.services.ai.tools.models import ToolResult
box["result"] = ToolResult(output=return_part.output)
else:
client_tool_calls.append(call)
if not client_tool_calls:
logger.info("✅ AgentExecutor:无本地客户端工具需执行,推理循环平滑结束。")
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: Any,
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
from zhenxun.services.ai.flow.agent.engine.directive import directive_manager
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[Any]:
run_context = resources.run_context
event_bus = run_context.run.event_bus
if not resources.config.enable_fallback_summary:
raise UpstreamServerException(
f"超过最大工具调用循环次数 ({resources.config.max_cycles})。",
)
logger.warning(
f"AgentExecutor 达到最大循环次数 ({resources.config.max_cycles}),"
"触发兜底总结机制。"
)
if event_bus:
await event_bus.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 model_construct(
AgentRunResult,
output=fallback_response.text,
messages=state.messages,
structured_data=None,
usage=state.usage,
)