✨ 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
@@ -1,18 +1,15 @@
import json
import time
from typing import Any
from zhenxun.services.llm import (
LLMException,
get_global_default_model_name,
from zhenxun.configs.path_config import DATA_PATH
from zhenxun.services.ai.core.exceptions import LLMException
from zhenxun.services.ai.llm.api import chat
from zhenxun.services.ai.llm.manager import (
get_model_instance,
list_available_models,
set_global_default_model_name,
)
from zhenxun.services.llm.core import KeyStatus
from zhenxun.services.llm.manager import (
reset_key_status,
)
from zhenxun.services.llm.types import LLMMessage
from zhenxun.services.ai.tools.providers.mcp.provider import mcp_provider
class DataSource:
@@ -39,27 +36,12 @@ class DataSource:
except LLMException:
return None
@staticmethod
async def get_default_model() -> str | None:
"""获取全局默认模型"""
return get_global_default_model_name()
@staticmethod
async def set_default_model(model_name_str: str) -> tuple[bool, str]:
"""设置全局默认模型"""
success = set_global_default_model_name(model_name_str)
if success:
return True, f"✅ 成功将默认模型设置为: {model_name_str}"
else:
return False, f"❌ 设置失败,模型 '{model_name_str}' 不存在或无效。"
@staticmethod
async def test_model_connectivity(model_name_str: str) -> tuple[bool, str]:
"""测试模型连通性"""
start_time = time.monotonic()
try:
async with await get_model_instance(model_name_str) as model:
await model.generate_response([LLMMessage.user("你好")])
await chat("你好", model=model_name_str)
end_time = time.monotonic()
latency = (end_time - start_time) * 1000
return (
@@ -70,7 +52,7 @@ class DataSource:
return (
False,
f"❌ 模型 '{model_name_str}' 连接测试失败:\n"
f"{e.user_friendly_message}\n错误码: {e.code.name}",
f"{e.user_friendly_message}\n错误类型: {e.__class__.__name__}",
)
except Exception as e:
return False, f"❌ 测试时发生未知错误: {e!s}"
@@ -78,7 +60,7 @@ class DataSource:
@staticmethod
async def get_key_status(provider_name: str) -> list[dict[str, Any]] | None:
"""获取并排序指定提供商的API Key状态"""
from zhenxun.services.llm.manager import get_key_usage_stats
from zhenxun.services.ai.llm.manager import get_key_usage_stats
all_stats = await get_key_usage_stats()
provider_stats = all_stats.get(provider_name)
@@ -93,11 +75,30 @@ class DataSource:
]
def sort_key(item: dict[str, Any]):
status_priority = item.get("status_enum", KeyStatus.UNUSED).value
status_map = {
"DISABLED": 0,
"ERROR": 1,
"COOLDOWN": 2,
"WARNING": 3,
"HEALTHY": 4,
"UNUSED": 5,
}
status_str = item.get("status", "HEALTHY")
if (
item.get("successes", 0) == 0
and item.get("failures", 0) == 0
and status_str == "HEALTHY"
):
status_str = "UNUSED"
status_priority = status_map.get(status_str, 5)
total = item.get("successes", 0) + item.get("failures", 0)
success_rate = (
(item.get("successes", 0) / total * 100) if total > 0 else 100.0
)
return (
status_priority,
100 - item.get("success_rate", 100.0),
-item.get("total_calls", 0),
100 - success_rate,
-total,
)
sorted_stats_list = sorted(stats_list, key=sort_key)
@@ -105,17 +106,159 @@ class DataSource:
return sorted_stats_list
@staticmethod
async def reset_key(provider_name: str, api_key: str | None) -> tuple[bool, str]:
"""重置API Key状态"""
success = await reset_key_status(provider_name, api_key)
if success:
if api_key:
if len(api_key) > 8:
target = f"API Key '{api_key[:4]}...{api_key[-4:]}'"
else:
target = f"API Key '{api_key}'"
async def get_mcp_list() -> list[dict[str, Any]]:
"""获取排序后的 MCP 列表"""
await mcp_provider.initialize()
if not mcp_provider._config:
return []
mcp_servers = mcp_provider._config.mcpServers
sorted_names = sorted(mcp_servers.keys())
result = []
for idx, name in enumerate(sorted_names):
conf = mcp_servers[name]
target = ""
if conf.transport in ("stdio", "sandbox_proxy") and conf.command:
target = f"{conf.command} {' '.join(conf.args)}"
elif conf.transport in ("sse", "streamable-http") and conf.url:
target = conf.url
result.append(
{
"id": idx + 1,
"name": name,
"enabled": conf.enabled,
"transport": conf.transport,
"target": target,
}
)
return result
@staticmethod
async def resolve_mcp_targets(
targets: tuple[Any, ...],
) -> tuple[list[str], list[str]]:
"""将输入的 ID 或名称解析为实际的 MCP 服务名称"""
await mcp_provider.initialize()
if not mcp_provider._config:
return [], list(map(str, targets))
mcp_servers = mcp_provider._config.mcpServers
sorted_names = sorted(mcp_servers.keys())
valid_names = []
invalid_targets = []
for tgt in targets:
tgt_str = str(tgt)
target_name = None
if tgt_str.isdigit():
idx = int(tgt_str) - 1
if 0 <= idx < len(sorted_names):
target_name = sorted_names[idx]
else:
target = "所有API Keys"
return True, f"✅ 成功重置提供商 '{provider_name}' 的 {target} 的状态。"
else:
return False, "❌ 重置失败,请检查提供商名称或API Key是否正确。"
if tgt_str in mcp_servers:
target_name = tgt_str
if target_name:
valid_names.append(target_name)
else:
invalid_targets.append(tgt_str)
return list(dict.fromkeys(valid_names)), list(dict.fromkeys(invalid_targets))
@staticmethod
async def toggle_mcp_servers(
targets: tuple[Any, ...], is_enable: bool
) -> tuple[list[str], list[str]]:
"""批量切换 MCP 状态"""
valid_names, invalid_targets = await DataSource.resolve_mcp_targets(targets)
if not mcp_provider._config:
return [], invalid_targets
mcp_servers = mcp_provider._config.mcpServers
success_names = []
for target_name in valid_names:
conf = mcp_servers[target_name]
if conf.enabled != is_enable:
conf.enabled = is_enable
if not is_enable:
if tk := mcp_provider._toolkits.pop(target_name, None):
await tk.close()
else:
if target_name not in mcp_provider._toolkits:
mcp_provider._setup_toolkit(target_name, conf)
success_names.append(target_name)
if success_names:
mcp_provider._discovered_tools = None
mcp_provider._save_config()
return success_names, invalid_targets
@staticmethod
async def reload_mcp_config() -> None:
"""完全重新加载 MCP 配置"""
await mcp_provider.shutdown()
mcp_provider._config = None
mcp_provider._discovered_tools = None
await mcp_provider.initialize()
@staticmethod
async def delete_mcp_servers(names: list[str]) -> None:
"""删除指定的 MCP 服务"""
for name in names:
await mcp_provider.unregister_server(name)
@staticmethod
async def add_mcp_servers_from_json(json_str: str) -> tuple[bool, str]:
"""将 JSON 字符串解析并合并到 mcp.json"""
mcp_path = DATA_PATH / "ai" / "mcp.json"
try:
json_str = json_str.strip()
if json_str.startswith("```"):
lines = json_str.split("\n")
if lines[0].startswith("```"):
lines = lines[1:]
if lines and lines[-1].startswith("```"):
lines = lines[:-1]
json_str = "\n".join(lines).strip()
new_config = json.loads(json_str)
if not isinstance(new_config, dict) or "mcpServers" not in new_config:
return False, "❌ JSON 格式不正确,必须包含顶层键 'mcpServers'。"
new_servers = new_config["mcpServers"]
if not isinstance(new_servers, dict) or not new_servers:
return False, "❌ 'mcpServers' 不能为空且必须为 JSON 对象(dict)。"
if mcp_path.exists():
with mcp_path.open("r", encoding="utf-8") as f:
current_config = json.load(f)
else:
current_config = {"mcpServers": {}}
if "mcpServers" not in current_config:
current_config["mcpServers"] = {}
added_names = []
for name, conf in new_servers.items():
current_config["mcpServers"][name] = conf
added_names.append(name)
mcp_path.parent.mkdir(parents=True, exist_ok=True)
with mcp_path.open("w", encoding="utf-8") as f:
json.dump(current_config, f, ensure_ascii=False, indent=2)
await DataSource.reload_mcp_config()
return True, f"✅ 成功添加/更新 MCP 服务: {', '.join(added_names)}"
except json.JSONDecodeError as e:
return False, f"❌ JSON 解析失败: {e}"
except Exception as e:
return False, f"❌ 添加 MCP 服务时发生未知错误: {e}"