Files
zhenxun_bot/zhenxun/services/ai/llm/engine/service.py
T
80fc5b86a7 ✨ 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>
2026-07-03 08:53:56 +08:00

281 lines
9.1 KiB
Python

"""
LLM 模型实现类
包含 LLM 模型的抽象基类和具体实现,负责与各种 AI 提供商的 API 交互。
"""
from __future__ import annotations
from typing import Any, TypeVar
from pydantic import BaseModel
from zhenxun.services.ai.config import ProviderConfig, get_llm_config
from zhenxun.services.ai.core.exceptions import ConfigurationException
from zhenxun.services.ai.core.messages import (
AudioResponse,
BaseRequest,
ChatRequest,
ChatResponse,
EmbeddingRequest,
EmbeddingResponse,
ImageRequest,
ImageResponse,
RerankRequest,
RerankResponse,
SpeechRequest,
)
from zhenxun.services.ai.core.models import (
CancellationToken,
LLMContext,
ModelCapabilities,
ModelDetail,
ModelIdentity,
)
from zhenxun.services.ai.core.options import (
GenerationConfig,
)
from zhenxun.services.ai.core.protocols.llm import (
SupportsChat,
SupportsImageGeneration,
SupportsReranking,
SupportsSpeechSynthesis,
SupportsTextEmbedding,
)
from zhenxun.services.ai.core.protocols.middleware import LLMMiddleware
from zhenxun.services.ai.llm.system.models import RetryConfig
from zhenxun.services.ai.llm.system.network import HealthManager, LLMHttpClient
from zhenxun.services.log import logger
T = TypeVar("T", bound=BaseModel)
class LLMModel(
SupportsChat,
SupportsTextEmbedding,
SupportsSpeechSynthesis,
SupportsReranking,
SupportsImageGeneration,
):
"""LLM 模型实现类"""
def __init__(
self,
provider_config: ProviderConfig,
model_detail: ModelDetail,
health_manager: HealthManager,
http_client: LLMHttpClient,
capabilities: ModelCapabilities,
config_override: GenerationConfig | None = None,
):
self.provider_config = provider_config
self.model_detail = model_detail
self.health_manager = health_manager
self.http_client: LLMHttpClient = http_client
self.capabilities = capabilities
self._generation_config = config_override
self.provider_name = provider_config.name
self.api_type = model_detail.api_type or provider_config.api_type
self.api_base = provider_config.api_base
self.path_prefix = model_detail.path_prefix
self.api_keys = (
[provider_config.api_key]
if isinstance(provider_config.api_key, str)
else provider_config.api_key
)
self.model_name = model_detail.model_name
self.temperature = model_detail.temperature
self.generation_max_tokens = model_detail.generation_max_tokens
self._is_closed = False
self._ref_count = 0
self.identity = ModelIdentity(
provider_name=self.provider_name,
model_name=self.model_name,
api_type=self.api_type,
api_base=self.api_base,
path_prefix=self.path_prefix,
capabilities=self.capabilities,
generation_config=self._generation_config,
)
from zhenxun.services.ai.llm.engine.middlewares import MiddlewarePipeline
self.pipeline = MiddlewarePipeline()
self._setup_default_pipeline()
def add_middleware(self, middleware: LLMMiddleware) -> None:
"""注册一个中间件到处理管道的最外层"""
self.pipeline.add_middleware(middleware)
def _setup_default_pipeline(self) -> None:
from zhenxun.services.ai.llm.adapters.factory import get_adapter_for_api_type
from zhenxun.services.ai.llm.engine.middlewares import (
ConfigMergeMiddleware,
FailoverAndRetryMiddleware,
LLMCacheMiddleware,
LoggingMiddleware,
ModalityFilterMiddleware,
OutputValidationMiddleware,
ResponseRescueMiddleware,
)
client_settings = get_llm_config().client_settings
retry_config = RetryConfig(
max_retries=client_settings.max_retries,
retry_delay=client_settings.retry_delay,
)
adapter = get_adapter_for_api_type(self.api_type)
self.pipeline.add_middleware(LLMCacheMiddleware(self.model_name))
self.pipeline.add_middleware(ConfigMergeMiddleware(self._generation_config))
self.pipeline.add_middleware(
ModalityFilterMiddleware(self.model_name, self.capabilities)
)
self.pipeline.add_middleware(
FailoverAndRetryMiddleware(
retry_config, self.health_manager, self.provider_name, self.api_keys
)
)
self.pipeline.add_middleware(OutputValidationMiddleware())
self.pipeline.add_middleware(ResponseRescueMiddleware())
self.pipeline.add_middleware(
LoggingMiddleware(
self.provider_name, self.model_name, adapter, self.identity
)
)
async def _select_api_key(self, failed_keys: set[str] | None = None) -> str:
"""选择可用的API密钥(使用轮询策略)"""
if not self.api_keys:
raise ConfigurationException(
f"提供商 {self.provider_name} 没有配置API密钥",
)
selected_key = await self.health_manager.get_next_available_key(
self.provider_name, self.api_keys, failed_keys
)
if not selected_key:
raise ConfigurationException(
f"提供商 {self.provider_name} 的所有API密钥当前都不可用",
details={
"total_keys": len(self.api_keys),
"failed_keys": len(failed_keys or set()),
},
)
return selected_key
async def close(self):
"""标记模型实例的当前使用周期结束"""
if self._is_closed:
return
self._is_closed = True
logger.debug(
f"LLMModel实例的使用周期已结束: {self} (共享HTTP客户端状态不受影响)"
)
async def __aenter__(self):
if self._is_closed:
logger.debug(
f"Re-entering context for closed LLMModel {self}. "
f"Resetting _is_closed to False."
)
self._is_closed = False
self._check_not_closed()
self._ref_count += 1
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""异步上下文管理器出口"""
_ = exc_type, exc_val, exc_tb
self._ref_count -= 1
if self._ref_count <= 0:
self._ref_count = 0
await self.close()
def _check_not_closed(self):
"""检查实例是否已关闭"""
if self._is_closed:
raise RuntimeError(f"LLMModel实例已关闭: {self}")
async def invoke(
self,
request: BaseRequest,
cancellation_token: CancellationToken | None = None,
) -> Any:
"""
大一统命令执行核心入口 (Command Pattern)。
整合所有中间件执行管线,屏蔽具体模态差异。
"""
self._check_not_closed()
context = LLMContext(
request=request,
cancellation_token=cancellation_token,
)
from zhenxun.services.ai.llm.adapters.factory import get_adapter_for_api_type
from zhenxun.services.ai.llm.engine.middlewares import HttpExecutionMiddleware
adapter = get_adapter_for_api_type(self.api_type)
execution_middleware = HttpExecutionMiddleware(
http_client=self.http_client,
identity=self.identity,
health_manager=self.health_manager,
adapter=adapter,
)
async def terminal_handler(ctx: LLMContext[Any, Any]) -> Any:
async def _noop(c: LLMContext[Any, Any]) -> Any:
raise RuntimeError("HttpExecutionMiddleware 不应调用 next_call")
return await execution_middleware(ctx, _noop)
handler = self.pipeline.build(terminal_handler)
return await handler(context)
async def generate_response(
self,
request: ChatRequest,
cancellation_token: CancellationToken | None = None,
) -> ChatResponse:
return await self.invoke(request, cancellation_token)
async def generate_embeddings(
self,
request: EmbeddingRequest,
) -> EmbeddingResponse:
return await self.invoke(request)
async def rerank(
self,
request: RerankRequest,
) -> RerankResponse:
return await self.invoke(request)
async def generate_image(
self,
request: ImageRequest,
) -> ImageResponse:
return await self.invoke(request)
async def generate_speech(
self,
request: SpeechRequest,
) -> AudioResponse:
return await self.invoke(request)
def __str__(self) -> str:
status = "closed" if self._is_closed else "active"
return f"LLMModel({self.provider_name}/{self.model_name}, {status})"
def __repr__(self) -> str:
status = "closed" if self._is_closed else "active"
return (
f"LLMModel(provider={self.provider_name}, model={self.model_name}, "
f"api_type={self.api_type}, status={status})"
)