mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-09-28 16:20:56 +08:00
* ✨ 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>
312 lines
12 KiB
Python
312 lines
12 KiB
Python
"""
|
|
LLM 模型管理器
|
|
对外提供统一的配置查询、模型发现与实例化入口。
|
|
"""
|
|
|
|
from typing import Any
|
|
|
|
from zhenxun.services.ai.config import (
|
|
ProviderConfig,
|
|
get_ai_config,
|
|
get_llm_config,
|
|
)
|
|
from zhenxun.services.ai.core.exceptions import ConfigurationException
|
|
from zhenxun.services.ai.core.models import ModelDetail
|
|
from zhenxun.services.ai.core.options import GenerationConfig
|
|
from zhenxun.services.ai.llm.system.capabilities import get_model_capabilities
|
|
from zhenxun.services.ai.llm.system.network import health_manager
|
|
from zhenxun.services.log import logger
|
|
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
|
|
from zhenxun.utils.pydantic_compat import model_dump
|
|
|
|
_RESOLVED_GROUP_CACHE: dict[str, list[str]] = {}
|
|
"""路由组解析缓存,避免每次调用重复打印剔除警告并提升性能"""
|
|
|
|
|
|
def clear_resolved_group_cache() -> None:
|
|
global _RESOLVED_GROUP_CACHE
|
|
_RESOLVED_GROUP_CACHE.clear()
|
|
|
|
|
|
def parse_provider_model_string(name_str: str | None) -> tuple[str | None, str | None]:
|
|
"""解析 'ProviderName/ModelName' 格式的字符串"""
|
|
if not name_str or "/" not in name_str:
|
|
return None, None
|
|
parts = name_str.split("/", 1)
|
|
if len(parts) == 2 and parts[0].strip() and parts[1].strip():
|
|
return parts[0].strip(), parts[1].strip()
|
|
return None, None
|
|
|
|
|
|
def _get_group_name(name_str: str) -> str | None:
|
|
"""判断名称是否是组名,如果是则提取并返回组名,否则返回 None"""
|
|
name_str = name_str.strip()
|
|
if "/" not in name_str:
|
|
return name_str
|
|
return None
|
|
|
|
|
|
def get_default_api_base_for_type(api_type: str) -> str | None:
|
|
"""根据API类型获取默认的API基础地址"""
|
|
default_api_bases = {
|
|
"openai": "https://api.openai.com",
|
|
"doubao": "https://ark.cn-beijing.volces.com/api",
|
|
"deepseek": "https://api.deepseek.com",
|
|
"jina": "https://api.jina.ai",
|
|
"glm": "https://open.bigmodel.cn",
|
|
"gemini": "https://generativelanguage.googleapis.com",
|
|
"openrouter": "https://openrouter.ai/api",
|
|
"smart": None,
|
|
"openai_responses": None,
|
|
}
|
|
return default_api_bases.get(api_type)
|
|
|
|
|
|
def get_configured_providers() -> list[ProviderConfig]:
|
|
"""从配置中获取Provider列表"""
|
|
ai_config = get_ai_config()
|
|
providers = ai_config.get("PROVIDERS", [])
|
|
if not isinstance(providers, list):
|
|
logger.error("配置项 AI.PROVIDERS 的值不是一个列表,将使用空列表。")
|
|
return []
|
|
valid_providers = []
|
|
for i, item in enumerate(providers):
|
|
if isinstance(item, ProviderConfig):
|
|
if not item.api_base:
|
|
default_api_base = get_default_api_base_for_type(item.api_type)
|
|
if default_api_base:
|
|
item.api_base = default_api_base
|
|
valid_providers.append(item)
|
|
else:
|
|
logger.warning(
|
|
f"配置文件中第 {i + 1} 项未能正确解析为 ProviderConfig 对象,已跳过。"
|
|
)
|
|
return valid_providers
|
|
|
|
|
|
def find_model_config(
|
|
provider_name: str, model_name: str
|
|
) -> tuple[ProviderConfig, ModelDetail] | None:
|
|
"""在配置中查找指定 Provider 与 ModelDetail。"""
|
|
providers = get_configured_providers()
|
|
for provider in providers:
|
|
if provider.name.lower() == provider_name.lower():
|
|
for model_detail in provider.models:
|
|
if model_detail.model_name.lower() == model_name.lower():
|
|
return provider, model_detail
|
|
return None
|
|
|
|
|
|
def _resolve_model_group(group_name: str, visited: set | None = None) -> list[str]:
|
|
"""递归解析模型组,展开为扁平的真实模型列表,并防止循环嵌套。"""
|
|
global _RESOLVED_GROUP_CACHE
|
|
if visited is None and group_name in _RESOLVED_GROUP_CACHE:
|
|
return _RESOLVED_GROUP_CACHE[group_name]
|
|
if visited is None:
|
|
visited = set()
|
|
if group_name in visited:
|
|
logger.warning(f"检测到模型路由组嵌套死循环: {group_name},已安全跳过该分支。")
|
|
return []
|
|
visited.add(group_name)
|
|
llm_config = get_llm_config()
|
|
if group_name not in llm_config.model_groups:
|
|
logger.warning(f"模型路由组 '{group_name}' 不存在于配置中。")
|
|
return []
|
|
resolved_models = []
|
|
for item in llm_config.model_groups[group_name]:
|
|
item = item.strip()
|
|
sub_group = _get_group_name(item)
|
|
if sub_group:
|
|
resolved_models.extend(_resolve_model_group(sub_group, visited.copy()))
|
|
else:
|
|
prov_mod = parse_provider_model_string(item)
|
|
if prov_mod[0] and prov_mod[1]:
|
|
if find_model_config(prov_mod[0], prov_mod[1]):
|
|
if item not in resolved_models:
|
|
resolved_models.append(item)
|
|
else:
|
|
logger.warning(
|
|
f"⚠️ [Router] 路由组 '{group_name}' 中的模型 "
|
|
f"'{item}' 未在配置,已被自动剔除!"
|
|
)
|
|
else:
|
|
logger.warning(f"路由组 '{group_name}' 包含无效格式的项目 '{item}'。")
|
|
if len(visited) == 1:
|
|
_RESOLVED_GROUP_CACHE[group_name] = resolved_models
|
|
return resolved_models
|
|
|
|
|
|
def _get_model_identifiers(provider_name: str, model_detail: ModelDetail) -> list[str]:
|
|
"""获取模型的所有可用标识符"""
|
|
return [f"{provider_name}/{model_detail.model_name}"]
|
|
|
|
|
|
def list_available_models() -> list[dict[str, Any]]:
|
|
"""列出所有已配置的可用模型及其信息。"""
|
|
providers = get_configured_providers()
|
|
model_list = []
|
|
for provider in providers:
|
|
for model_detail in provider.models:
|
|
caps = get_model_capabilities(model_detail.model_name)
|
|
model_info = {
|
|
"provider_name": provider.name,
|
|
"model_name": model_detail.model_name,
|
|
"full_name": f"{provider.name}/{model_detail.model_name}",
|
|
"api_type": provider.api_type or "auto-detect",
|
|
"api_base": provider.api_base,
|
|
"is_available": model_detail.is_available,
|
|
"is_embedding_model": caps.is_embedding_model,
|
|
"max_input_tokens": caps.max_input_tokens,
|
|
"available_identifiers": _get_model_identifiers(
|
|
provider.name, model_detail
|
|
),
|
|
}
|
|
model_list.append(model_info)
|
|
return model_list
|
|
|
|
|
|
def list_embedding_models() -> list[dict[str, Any]]:
|
|
"""列出所有支持嵌入能力的模型。"""
|
|
all_models = list_available_models()
|
|
return [model for model in all_models if model.get("is_embedding_model", False)]
|
|
|
|
|
|
def list_model_identifiers() -> dict[str, list[str]]:
|
|
"""列出所有模型的可用标识符映射。"""
|
|
providers = get_configured_providers()
|
|
result = {}
|
|
for provider in providers:
|
|
for model_detail in provider.models:
|
|
full_name = f"{provider.name}/{model_detail.model_name}"
|
|
identifiers = _get_model_identifiers(provider.name, model_detail)
|
|
result[full_name] = identifiers
|
|
return result
|
|
|
|
|
|
def get_default_model(task: str = "chat") -> str | None:
|
|
"""根据任务类型获取默认模型名称"""
|
|
config = get_llm_config()
|
|
return getattr(config.default_models, task, None)
|
|
|
|
|
|
async def get_key_usage_stats() -> dict[str, Any]:
|
|
"""获取所有 Provider 的 Key 使用统计。"""
|
|
providers = get_configured_providers()
|
|
stats = {}
|
|
for provider in providers:
|
|
keys = (
|
|
[provider.api_key]
|
|
if isinstance(provider.api_key, str)
|
|
else provider.api_key
|
|
)
|
|
provider_stats = {}
|
|
provider_state = health_manager.state.providers.get(provider.name)
|
|
if provider_state:
|
|
for k in keys:
|
|
stat_data = provider_state.api_keys.get(k)
|
|
if stat_data:
|
|
provider_stats[health_manager._get_key_id(k)] = model_dump(
|
|
stat_data
|
|
)
|
|
stats[provider.name] = {
|
|
"total_keys": len(
|
|
[provider.api_key]
|
|
if isinstance(provider.api_key, str)
|
|
else provider.api_key
|
|
),
|
|
"key_stats": provider_stats,
|
|
}
|
|
return stats
|
|
|
|
|
|
async def reset_key_status(provider_name: str, api_key: str | None = None) -> bool:
|
|
"""重置指定 Provider 的 Key 状态。"""
|
|
providers = get_configured_providers()
|
|
target_provider = None
|
|
for provider in providers:
|
|
if provider.name.lower() == provider_name.lower():
|
|
target_provider = provider
|
|
break
|
|
if not target_provider:
|
|
logger.error(f"未找到Provider: {provider_name}")
|
|
return False
|
|
provider_keys = (
|
|
[target_provider.api_key]
|
|
if isinstance(target_provider.api_key, str)
|
|
else target_provider.api_key
|
|
)
|
|
if api_key:
|
|
if api_key in provider_keys:
|
|
await health_manager.reset_key_status(target_provider.name, api_key)
|
|
logger.info(f"已重置Provider '{provider_name}' 的指定Key状态")
|
|
return True
|
|
else:
|
|
logger.error(f"指定的Key不属于Provider '{provider_name}'")
|
|
return False
|
|
else:
|
|
for key in provider_keys:
|
|
await health_manager.reset_key_status(target_provider.name, key)
|
|
logger.info(f"已重置Provider '{provider_name}' 的所有Key状态")
|
|
return True
|
|
|
|
|
|
async def get_model_instance(
|
|
provider_model_name: str | None = None,
|
|
override_config: dict[str, Any] | GenerationConfig | None = None,
|
|
task: str = "chat",
|
|
) -> Any:
|
|
"""作为门面 API,解析字符串并调用底层的 get_or_create_model"""
|
|
resolved_model_name_str = provider_model_name
|
|
if resolved_model_name_str is None:
|
|
resolved_model_name_str = get_default_model(task)
|
|
if resolved_model_name_str is None:
|
|
available_models_list = list_available_models()
|
|
if not available_models_list:
|
|
raise ConfigurationException("未配置任何AI模型")
|
|
resolved_model_name_str = available_models_list[0]["full_name"]
|
|
logger.warning(f"未指定模型,使用第一个可用模型: {resolved_model_name_str}")
|
|
|
|
prov_name_str, mod_name_str = parse_provider_model_string(resolved_model_name_str)
|
|
if not prov_name_str or not mod_name_str:
|
|
raise ConfigurationException(f"无效的模型名称格式: '{resolved_model_name_str}'")
|
|
|
|
config_tuple_found = find_model_config(prov_name_str, mod_name_str)
|
|
if not config_tuple_found:
|
|
raise ConfigurationException(f"未找到模型: '{resolved_model_name_str}'. ")
|
|
|
|
provider_config_found, model_detail_found = config_tuple_found
|
|
|
|
from zhenxun.services.ai.llm.system.cache import get_or_create_model
|
|
|
|
return await get_or_create_model(
|
|
provider_config_found, model_detail_found, override_config
|
|
)
|
|
|
|
|
|
def clear_all_cache() -> None:
|
|
"""
|
|
清空模型实例缓存与路由组解析缓存。
|
|
"""
|
|
from zhenxun.services.ai.llm.system.cache import clear_model_cache
|
|
|
|
clear_model_cache()
|
|
clear_resolved_group_cache()
|
|
logger.debug("已清空全局模型实例与路由组缓存")
|
|
|
|
|
|
@PriorityLifecycle.on_startup(priority=10)
|
|
async def _init_llm_config_on_startup():
|
|
"""启动时初始化 LLM 配置、密钥状态并预热工具提供者管理器。"""
|
|
logger.info("正在初始化 LLM 配置并加载遥测状态...")
|
|
try:
|
|
from zhenxun.services.ai.config import get_llm_config
|
|
from zhenxun.services.ai.llm.system.network import health_manager
|
|
from zhenxun.services.ai.tools.engine.registry import tool_provider_manager
|
|
|
|
get_llm_config()
|
|
await health_manager.initialize()
|
|
await tool_provider_manager.initialize()
|
|
|
|
except Exception as e:
|
|
logger.error(f"LLM 配置或遥测状态初始化时发生错误: {e}", e=e)
|