✨ 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
+431
View File
@@ -0,0 +1,431 @@
from abc import ABC
from collections.abc import Callable
from enum import Enum
import inspect
from typing import Any
from nonebot.utils import is_coroutine_callable
from pydantic import BaseModel, Field
from zhenxun.services.ai.core.exceptions import (
GuardrailFatalException,
GuardrailViolationError,
)
from zhenxun.services.ai.core.messages import ChatResponse, LLMMessage, TextPart
from zhenxun.services.ai.run.context import RunContext
class GuardrailAction(str, Enum):
PASS = "PASS"
"""放行"""
REJECT = "REJECT"
"""致命拦截:直接中断大模型思考"""
REFLECT = "REFLECT"
"""打回反思:触发自愈闭环"""
MUTATE = "MUTATE"
"""数据变异:就地修改数据后放行"""
class GuardrailResult(BaseModel):
"""护栏验证结果的统一载体"""
action: GuardrailAction = GuardrailAction.PASS
"""验证动作(放行、拦截、反思、变异)"""
feedback: str | None = None
"""未通过时的校验失败反馈原因或拒绝理由"""
mutated_text: str | None = None
"""变异后的新文本内容(MUTATE 模式下生效)"""
mutated_obj: Any | None = None
"""变异后的新解析对象(MUTATE 模式下生效)"""
@property
def success(self) -> bool:
return self.action == GuardrailAction.PASS
def input_guardrail(func: Callable | None = None, *, max_attempts: int = 0):
"""显式标记为输入护栏。支持指定最大评估次数 (max_attempts)"""
def decorator(f: Callable):
setattr(f, "__guardrail_type__", "input")
setattr(f, "__guardrail_max_attempts__", max_attempts)
return f
return decorator(func) if func else decorator
def output_guardrail(func: Callable | None = None, *, max_attempts: int = 0):
"""显式标记为输出护栏。支持指定最大评估次数 (max_attempts)"""
def decorator(f: Callable):
setattr(f, "__guardrail_type__", "output")
setattr(f, "__guardrail_max_attempts__", max_attempts)
return f
return decorator(func) if func else decorator
class BaseGuardrail(ABC):
"""大一统的业务逻辑护栏抽象基类"""
max_attempts: int = 0
"""当前护栏在单次上下文中允许触发的最大评估/拦截次数。0 代表无限制。"""
async def validate_input(
self, messages: list[LLMMessage], context: RunContext | None = None
) -> GuardrailResult:
"""执行输入拦截与变异逻辑"""
return GuardrailResult(action=GuardrailAction.PASS)
async def validate_output(
self,
response: ChatResponse | str,
parsed_obj: Any,
context: RunContext | None = None,
) -> GuardrailResult:
"""执行输出反思、拦截与变异逻辑"""
return GuardrailResult(action=GuardrailAction.PASS)
class FunctionalGuardrail(BaseGuardrail):
"""包装普通 Python 函数的智能护栏"""
def __init__(self, func: Callable[..., Any]):
self.func = func
self.guardrail_type = getattr(func, "__guardrail_type__", None)
self.max_attempts = getattr(func, "__guardrail_max_attempts__", 0)
if not self.guardrail_type:
sig = inspect.signature(func)
is_input = False
is_output = False
for param in sig.parameters.values():
if param.annotation == inspect.Parameter.empty:
continue
anno_str = str(param.annotation)
if "LLMMessage" in anno_str:
is_input = True
if "ChatResponse" in anno_str:
is_output = True
if is_input and not is_output:
self.guardrail_type = "input"
elif is_output and not is_input:
self.guardrail_type = "output"
else:
raise ValueError(
"无法自动推断护栏函数 '{func.__name__}' 的作用阶段。\n"
"请使用明确的类型注解 (如 list[LLMMessage] 或 ChatResponse),\n"
"或使用 @input_guardrail / @output_guardrail 装饰器明确声明。"
)
def _bind_core_args(
self, sig: inspect.Signature, core_arg_dict: dict[str, Any]
) -> dict[str, Any]:
"""将框架提供的核心参数按名称或位置绑定到用户的签名上"""
from zhenxun.services.ai.run.di import DependencyInjector
bound_kwargs = {}
unmapped_cores = []
for core_name, core_val in core_arg_dict.items():
if core_name in sig.parameters:
bound_kwargs[core_name] = core_val
else:
unmapped_cores.append(core_val)
if unmapped_cores:
val_idx = 0
for name, param in sig.parameters.items():
if name in ("self", "cls") or name in bound_kwargs:
continue
if DependencyInjector.can_resolve_statically(param):
continue
bound_kwargs[name] = unmapped_cores[val_idx]
val_idx += 1
if val_idx >= len(unmapped_cores):
break
return bound_kwargs
async def _execute_with_di(
self, core_args: dict[str, Any], context: RunContext | None, is_input: bool
) -> GuardrailResult:
"""统一执行带有 DI 依赖注入的护栏逻辑"""
from zhenxun.services.ai.run.di import DependencyInjector
safe_context = context or RunContext()
try:
sig = inspect.signature(self.func)
call_kwargs = self._bind_core_args(sig, core_args)
resolved_kwargs = await DependencyInjector.resolve_all(
sig=sig, call_kwargs=call_kwargs, context=safe_context
)
filtered_kwargs = {
k: v for k, v in resolved_kwargs.items() if k in sig.parameters
}
res = (
await self.func(**filtered_kwargs)
if is_coroutine_callable(self.func)
else self.func(**filtered_kwargs)
)
return self._parse_result(res, is_input=is_input)
except (ValueError, AssertionError) as e:
action = GuardrailAction.REJECT if is_input else GuardrailAction.REFLECT
return GuardrailResult(action=action, feedback=str(e))
except Exception as e:
from zhenxun.services.ai.core.exceptions import ControlFlowExit
if isinstance(e, ControlFlowExit):
raise
action = GuardrailAction.REJECT if is_input else GuardrailAction.REFLECT
stage_str = "输入" if is_input else "输出"
return GuardrailResult(
action=action, feedback=f"{stage_str}护栏执行异常: {e}"
)
async def validate_input(
self, messages: list[LLMMessage], context: RunContext | None = None
) -> GuardrailResult:
if self.guardrail_type != "input":
return GuardrailResult(action=GuardrailAction.PASS)
return await self._execute_with_di(
{"messages": messages}, context, is_input=True
)
async def validate_output(
self,
response: ChatResponse | str,
parsed_obj: Any,
context: RunContext | None = None,
) -> GuardrailResult:
if self.guardrail_type != "output":
return GuardrailResult(action=GuardrailAction.PASS)
return await self._execute_with_di(
{"response": response, "parsed_obj": parsed_obj}, context, is_input=False
)
def _parse_result(self, res: Any, is_input: bool) -> GuardrailResult:
"""统一处理返回值类型推导"""
if isinstance(res, GuardrailResult):
return res
if res is False:
action = GuardrailAction.REJECT if is_input else GuardrailAction.REFLECT
return GuardrailResult(
action=action,
feedback=f"自定义护栏函数 '{self.func.__name__}' 校验未通过",
)
elif isinstance(res, str):
action = GuardrailAction.REJECT if is_input else GuardrailAction.REFLECT
return GuardrailResult(action=action, feedback=res)
return GuardrailResult(action=GuardrailAction.PASS)
class JudgeViolation(BaseModel):
rule: str
"""违反的规则内容"""
reason: str
"""违反该规则的具体原因和证据。如果没有违反,填无"""
class JudgeResponse(BaseModel):
passed: bool
"""文本是否完全遵守了所有的规则。如果有任何一条违反,此处必须为 False"""
violations: list[JudgeViolation] = Field(default_factory=list)
"""违反的规则列表及原因。如果没有违反,返回空列表"""
class LLMJudgeConfig(BaseModel):
"""LLM 裁判的全局设定"""
judge_model: str | None = None
"""指定的裁判模型名称,如果为空则优先使用当前对话模型,其次为全局默认模型"""
system_prompt_template: str | None = None
"""自定义裁判 Prompt 模板。必须包含 {rules} 和 {text} 占位符"""
max_attempts: int = 0
"""最大裁判评估次数。超过该次数后大模型裁判自动放弃并放行 (0 表示无限制)"""
class LLMGuardrail(BaseGuardrail):
"""基于 LLM-as-a-Judge 的自然语言规则裁判护栏"""
def __init__(self, rules: list[str], config: LLMJudgeConfig | None = None):
self.rules = rules
self.config = config or LLMJudgeConfig()
self.max_attempts = self.config.max_attempts
async def validate_output(
self,
response: ChatResponse | str,
parsed_obj: Any,
context: RunContext | None = None,
) -> GuardrailResult:
if not self.rules:
return GuardrailResult(action=GuardrailAction.PASS)
text = response if isinstance(response, str) else response.text
from zhenxun.services.ai.llm.api import generate_structured
from zhenxun.services.ai.llm.manager import get_default_model
rules_str = "\n".join([f"{i + 1}. {r}" for i, r in enumerate(self.rules)])
if self.config.system_prompt_template:
prompt = self.config.system_prompt_template.format(
rules=rules_str, text=text
)
else:
prompt = (
"你是一个严格的内容风控与业务合规裁判。\n"
"请评估以下[待检测内容]是否违反了任何一条[规则列表]。\n\n"
f"### [规则列表]\n{rules_str}\n\n"
f"### [待检测内容]\n{text}\n\n"
"请严格按照规则评估。只要违反了其中任意一条,"
"passed 必须为 false,并在 violations 中详细说明理由。"
)
model = self.config.judge_model
if not model and context and context.run.current_model:
model = context.run.current_model
if not model:
model = get_default_model("chat")
try:
res = await generate_structured(
prompt, response_model=JudgeResponse, model=model
)
if res.passed:
return GuardrailResult(action=GuardrailAction.PASS)
feedbacks = [
f"违反规则: 【{v.rule}】, 原因: {v.reason}" for v in res.violations
]
return GuardrailResult(
action=GuardrailAction.REFLECT, feedback="\n".join(feedbacks)
)
except Exception as e:
return GuardrailResult(
action=GuardrailAction.REFLECT, feedback=f"系统护栏裁判模型异常: {e}"
)
GuardrailSource = Callable[..., Any] | str | BaseGuardrail | LLMJudgeConfig
"""护栏来源(函数、自然语言规则、BaseGuardrail 实例或裁判配置)"""
def parse_guardrails(guardrails: list[GuardrailSource] | None) -> list[BaseGuardrail]:
"""工具方法:将各种类型的 Guardrail 解析为标准的 BaseGuardrail 列表"""
v_list = []
llm_rules = []
judge_config = None
for v in guardrails or []:
if isinstance(v, BaseGuardrail):
v_list.append(v)
elif callable(v):
v_list.append(FunctionalGuardrail(v))
elif isinstance(v, str):
llm_rules.append(v)
elif isinstance(v, LLMJudgeConfig):
judge_config = v
if llm_rules:
v_list.append(LLMGuardrail(rules=llm_rules, config=judge_config))
return v_list
class GuardrailPipeline:
"""护栏管线引擎"""
def __init__(self, guardrails: list[BaseGuardrail]):
self.guardrails = guardrails
async def run_input_pipeline(
self, messages: list[LLMMessage], context: RunContext | None = None
) -> list[LLMMessage]:
"""执行 Input 护栏拦截与变异"""
for g in self.guardrails:
if g.max_attempts > 0 and context:
counts = context.state.setdefault("__guardrail_input_counts__", {})
g_id = id(g)
if counts.get(g_id, 0) >= g.max_attempts:
continue
counts[g_id] = counts.get(g_id, 0) + 1
res = await g.validate_input(messages, context)
if res.action == GuardrailAction.REJECT:
raise GuardrailFatalException(
guard_name=g.__class__.__name__, reason=res.feedback or "输入被拦截"
)
elif (
res.action == GuardrailAction.MUTATE
and res.mutated_text is not None
and messages
):
text_replaced = False
for p in messages[-1].content:
if isinstance(p, TextPart):
p.text = res.mutated_text
text_replaced = True
break
if not text_replaced:
messages[-1].content.append(TextPart(text=res.mutated_text))
return messages
async def run_output_pipeline(
self,
response: ChatResponse | str,
parsed_obj: Any,
context: RunContext | None = None,
) -> tuple[ChatResponse | str, Any]:
"""执行 Output 护栏拦截、反思和变异"""
feedbacks = []
current_response = response
current_obj = parsed_obj
for g in self.guardrails:
if g.max_attempts > 0 and context:
counts = context.state.setdefault("__guardrail_output_counts__", {})
g_id = id(g)
if counts.get(g_id, 0) >= g.max_attempts:
continue
counts[g_id] = counts.get(g_id, 0) + 1
res = await g.validate_output(current_response, current_obj, context)
if res.action == GuardrailAction.REJECT:
raise GuardrailFatalException(
guard_name=g.__class__.__name__, reason=res.feedback or "输出被拒绝"
)
elif res.action == GuardrailAction.MUTATE:
if res.mutated_text is not None:
if isinstance(current_response, str):
current_response = res.mutated_text
else:
text_found = False
for p in current_response.content_parts:
if isinstance(p, TextPart):
p.text = res.mutated_text
text_found = True
break
if not text_found:
current_response.content_parts.insert(
0, TextPart(text=res.mutated_text)
)
if res.mutated_obj is not None:
current_obj = res.mutated_obj
elif res.action == GuardrailAction.REFLECT:
feedbacks.append(res.feedback or f"{g.__class__.__name__} 校验未通过")
if feedbacks:
raise GuardrailViolationError("\n".join(feedbacks))
return current_response, current_obj