mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-09-29 00:32:06 +08:00
* ♻️ refactor(core): 重构 AI 编排框架与记忆及 RAG 子系统 - 【重构】重构 `BaseRunnable` 并引入统一的 `RunIntent` 意图载体,规范 Agent、Team 和 Workflow 的执行流 - 【解耦】将中期记忆槽和长期向量记忆从 `MemoryConfig` 中解耦,转为独立的能力组件与工具箱进行管理 - 【记忆】移除 `MemoryReader` 和 `MemoryWriter`,统一封装为 `SessionMemoryContext` 会话记忆门面 - 【RAG】重构检索器与存储后端接口,统一采用 `QueryRequest` 进行多维度联合检索,并引入 `InMemoryScorer` 提升打分性能 - 【事件】优化 `EventBus` 异步事件分发机制,引入队列机制确保事件按序处理,避免并发竞态问题 - 【依赖注入】移除 `memory` 注入项,优化 `DependencyInjector` 的签名解析缓存以提升性能 * 🚨 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>
525 lines
18 KiB
Python
525 lines
18 KiB
Python
"""
|
|
LLM 服务的高级 API 接口 - 便捷函数入口 (无状态)
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any, Literal, TypeVar, overload
|
|
|
|
from pydantic import BaseModel
|
|
|
|
from zhenxun.services.ai.config import get_llm_config
|
|
from zhenxun.services.ai.core.exceptions import (
|
|
ControlFlowExit,
|
|
LLMException,
|
|
ModelRetry,
|
|
UpstreamServerException,
|
|
get_user_friendly_error_message,
|
|
)
|
|
from zhenxun.services.ai.core.messages import (
|
|
AudioResponse,
|
|
ChatRequest,
|
|
ChatResponse,
|
|
EmbeddingRequest,
|
|
EmbeddingResponse,
|
|
ImageRequest,
|
|
ImageResponse,
|
|
LLMMessage,
|
|
PromptInput,
|
|
RerankRequest,
|
|
RerankResult,
|
|
SpeechRequest,
|
|
UsageInfo,
|
|
)
|
|
from zhenxun.services.ai.core.models import ModelName
|
|
from zhenxun.services.ai.core.options import (
|
|
GenerationConfig,
|
|
LLMEmbeddingConfig,
|
|
OutputFormatConfig,
|
|
ResponseFormat,
|
|
StructuredOutputStrategy,
|
|
TTSConfig,
|
|
)
|
|
from zhenxun.services.ai.guardrails import GuardrailSource, parse_guardrails
|
|
from zhenxun.services.ai.utils.logger import log_llm as logger
|
|
|
|
from .builder import IntentBuilder
|
|
from .engine.router import LLMOrchestrator
|
|
|
|
T = TypeVar("T", bound=BaseModel)
|
|
|
|
|
|
async def chat(
|
|
message: PromptInput | list[LLMMessage],
|
|
*,
|
|
model: ModelName = None,
|
|
instruction: str | None = None,
|
|
config: GenerationConfig | IntentBuilder | None = None,
|
|
timeout: float | None = None,
|
|
) -> ChatResponse:
|
|
"""
|
|
无状态的聊天对话便捷函数,单次执行后立即销毁上下文。
|
|
|
|
示例:
|
|
response = await chat("你好", model="OpenAI/gpt-4o", instruction="你是一个助手")
|
|
print(response.text)
|
|
|
|
参数:
|
|
message: 用户输入的消息内容,支持多种格式。
|
|
model: 要使用的模型名称,如果为None则使用默认模型。
|
|
instruction: 系统指令,用于指导AI的行为和回复风格。
|
|
config: (可选) 配置构建器 IntentBuilder 或 GenerationConfig 对象。
|
|
timeout: (可选) HTTP 请求超时时间(秒)。
|
|
|
|
返回:
|
|
ChatResponse: 包含AI回复内容、使用信息和工具调用等的完整响应对象。
|
|
|
|
异常:
|
|
LLMException: 当网络超时、模型不存在或 API 返回错误时抛出,建议外层捕获。
|
|
"""
|
|
try:
|
|
from zhenxun.services.ai.message_builder import MessageBuilder
|
|
|
|
messages = await MessageBuilder.normalize_to_llm_messages(
|
|
message, instruction=instruction
|
|
)
|
|
return await generate(
|
|
messages=messages,
|
|
model=model,
|
|
config=config,
|
|
timeout=timeout,
|
|
)
|
|
except LLMException as e:
|
|
raise e.with_traceback(None) from None
|
|
except Exception as e:
|
|
friendly_msg = get_user_friendly_error_message(e)
|
|
logger.error(f"执行 chat 函数失败: {e} | 建议: {friendly_msg}", e=e)
|
|
raise LLMException(f"聊天执行失败: {friendly_msg}").with_traceback(
|
|
None
|
|
) from None
|
|
|
|
|
|
@overload
|
|
async def embed(
|
|
input_batch: PromptInput,
|
|
*,
|
|
model: ModelName = None,
|
|
task: Literal[
|
|
"general", "query", "document", "similarity", "classification", "clustering"
|
|
] = "general",
|
|
dimensions: int | None = None,
|
|
multimodal: bool | list[str] = False,
|
|
config: LLMEmbeddingConfig | None = None,
|
|
) -> EmbeddingResponse: ...
|
|
|
|
|
|
@overload
|
|
async def embed(
|
|
input_batch: list[PromptInput],
|
|
*,
|
|
model: ModelName = None,
|
|
task: Literal[
|
|
"general", "query", "document", "similarity", "classification", "clustering"
|
|
] = "general",
|
|
dimensions: int | None = None,
|
|
multimodal: bool | list[str] = False,
|
|
config: LLMEmbeddingConfig | None = None,
|
|
) -> EmbeddingResponse: ...
|
|
|
|
|
|
async def embed(
|
|
input_batch: PromptInput | list[PromptInput],
|
|
*,
|
|
model: ModelName = None,
|
|
task: Literal[
|
|
"general", "query", "document", "similarity", "classification", "clustering"
|
|
] = "general",
|
|
dimensions: int | None = None,
|
|
multimodal: bool | list[str] = False,
|
|
config: LLMEmbeddingConfig | None = None,
|
|
) -> EmbeddingResponse:
|
|
"""
|
|
无状态的向量嵌入便捷函数,支持文本批量与图文多模态融合 (Fused Embeddings)。
|
|
|
|
参数:
|
|
input_batch: 要生成嵌入的内容。传入单条字符串/消息视为单向量;传入多条视为批量。
|
|
model: 要使用的嵌入模型名称,如果为None则使用默认模型。
|
|
task: 生成意图
|
|
(query检索词 / document目标文档 / similarity相似度 等),将自动翻译到底层。
|
|
dimensions: 强制降低返回的向量维度 (降维)。
|
|
multimodal: 是否开启多模态嵌入提取。默认 False (极速安全的纯文本模式)。
|
|
config: 嵌入配置对象。
|
|
|
|
返回:
|
|
EmbeddingResponse: 包含向量和 Token 消耗统计的富响应对象。
|
|
"""
|
|
final_config = config or LLMEmbeddingConfig()
|
|
if multimodal is not False:
|
|
final_config.multimodal = multimodal
|
|
|
|
from zhenxun.services.ai.message_builder import MessageBuilder
|
|
|
|
batch = await MessageBuilder.normalize_to_embed_batch(
|
|
input_batch, config=final_config
|
|
)
|
|
|
|
if not batch.payloads:
|
|
return EmbeddingResponse(
|
|
embeddings=[], usage=UsageInfo(), model_name=str(model)
|
|
)
|
|
|
|
if dimensions is not None:
|
|
final_config.output_dimensionality = dimensions
|
|
|
|
if task != "general":
|
|
task_map = {
|
|
"query": "RETRIEVAL_QUERY",
|
|
"document": "RETRIEVAL_DOCUMENT",
|
|
"similarity": "SEMANTIC_SIMILARITY",
|
|
"classification": "CLASSIFICATION",
|
|
"clustering": "CLUSTERING",
|
|
}
|
|
final_config.task_type = task_map.get(task)
|
|
|
|
try:
|
|
request = EmbeddingRequest(batch=batch, config=final_config)
|
|
return await LLMOrchestrator.invoke(request, model_name=model, task="embedding")
|
|
except LLMException as e:
|
|
raise e.with_traceback(None) from None
|
|
except Exception as e:
|
|
friendly_msg = get_user_friendly_error_message(e)
|
|
logger.error(f"文本嵌入失败: {e} | 建议: {friendly_msg}", e=e)
|
|
raise UpstreamServerException(
|
|
f"文本嵌入失败: {friendly_msg}",
|
|
cause=e,
|
|
).with_traceback(None) from None
|
|
|
|
|
|
async def rerank(
|
|
query: str,
|
|
documents: list[str | dict[str, str]],
|
|
top_n: int = 3,
|
|
*,
|
|
model: ModelName = None,
|
|
) -> list[RerankResult]:
|
|
"""
|
|
无状态的文本重排便捷函数。
|
|
|
|
参数:
|
|
query: 用户查询问题
|
|
documents: 候选文档列表 (支持纯文本或 {"image": "url", "text": "xxx"} 图文格式)
|
|
top_n: 返回匹配度最高的前 n 个文档
|
|
model: 重排模型名称 (如 BAAI/bge-reranker-v2-m3)
|
|
"""
|
|
try:
|
|
request = RerankRequest(query=query, documents=documents, top_n=top_n)
|
|
response = await LLMOrchestrator.invoke(
|
|
request, model_name=model, task="rerank"
|
|
)
|
|
return response.results
|
|
except Exception as e:
|
|
friendly_msg = get_user_friendly_error_message(e)
|
|
logger.error(f"文档重排失败: {e} | 建议: {friendly_msg}", e=e)
|
|
raise LLMException(f"文档重排失败: {friendly_msg}").with_traceback(
|
|
None
|
|
) from None
|
|
|
|
|
|
async def generate_structured(
|
|
message: PromptInput | list[LLMMessage],
|
|
response_model: type[T],
|
|
*,
|
|
guardrails: list[GuardrailSource] | None = None,
|
|
model: ModelName = None,
|
|
config: GenerationConfig | IntentBuilder | None = None,
|
|
max_retries: int | None = None,
|
|
error_prompt_template: str | None = None,
|
|
instruction: str | None = None,
|
|
timeout: float | None = None,
|
|
) -> T:
|
|
"""
|
|
请求大模型生成结构化数据,并自动验证/解析为指定的 Pydantic 模型。
|
|
|
|
示例:
|
|
class UserInfo(BaseModel):
|
|
name: str
|
|
info = await generate_structured("提取张三的信息", response_model=UserInfo)
|
|
|
|
参数:
|
|
message: 输入的消息内容,支持纯文本、UniMessage、消息对象列表等。
|
|
response_model: 目标结构化输出的强类型 Pydantic 模型类。
|
|
guardrails: 护栏来源列表,支持自然语言规则、自定义校验函数
|
|
model: 强制指定调用的模型路由或名称,若为空则使用默认模型。
|
|
config: 大模型生成的通用配置或意图构建器。
|
|
max_retries: 格式解析或护栏校验失败时的最大自我反思重试次数(IVR),若为空则使用全局配置。
|
|
error_prompt_template: 自定义校验失败时引导大模型自我修正的提示词模板。
|
|
instruction: 注入到系统提示词中的全局任务指令或前置设定。
|
|
timeout: 本次 API 请求的超时时间限制(秒)。
|
|
|
|
返回:
|
|
T: 解析验证通过后的 Pydantic 模型实例。
|
|
""" # noqa: E501
|
|
try:
|
|
from zhenxun.services.ai.core.engine.structured_parser import (
|
|
BaseOutputProcessor,
|
|
)
|
|
|
|
if max_retries is None:
|
|
max_retries = get_llm_config().client_settings.structured_retries
|
|
|
|
parsed_guardrails = parse_guardrails(guardrails)
|
|
|
|
output_processor = BaseOutputProcessor(
|
|
response_model=response_model,
|
|
error_template=error_prompt_template,
|
|
)
|
|
json_schema = output_processor.get_json_schema()
|
|
|
|
structured_config = GenerationConfig(
|
|
output=OutputFormatConfig(
|
|
response_format=ResponseFormat.JSON,
|
|
response_schema=json_schema,
|
|
structured_output_strategy=StructuredOutputStrategy.NATIVE,
|
|
)
|
|
)
|
|
|
|
prompt_parts: list[str] = []
|
|
if instruction:
|
|
prompt_parts.append(instruction)
|
|
|
|
schema_str = json.dumps(json_schema, ensure_ascii=False, indent=2)
|
|
prompt_parts.append(
|
|
"### ⚠️ [结构化输出要求]\n"
|
|
"请严格按照以下 JSON Schema 格式进行回复,禁止包含任何额外纯文本解释:\n"
|
|
f"```json\n{schema_str}\n```"
|
|
)
|
|
|
|
system_prompt = "\n\n".join(prompt_parts) if prompt_parts else None
|
|
|
|
from zhenxun.services.ai.message_builder import MessageBuilder
|
|
|
|
messages = await MessageBuilder.normalize_to_llm_messages(
|
|
message if message is not None else [], instruction=system_prompt
|
|
)
|
|
|
|
if isinstance(config, IntentBuilder):
|
|
config = config.build()
|
|
|
|
final_config = (
|
|
structured_config.merge_with(config) if config else structured_config
|
|
)
|
|
|
|
from zhenxun.services.ai.capabilities.builtin import (
|
|
ReflexionCapability,
|
|
)
|
|
|
|
extra_context = {
|
|
"output_processor": output_processor,
|
|
"guardrails": parsed_guardrails,
|
|
"max_retries": max_retries,
|
|
"__sys_capabilities": [ReflexionCapability()],
|
|
}
|
|
|
|
response = await generate(
|
|
messages=messages,
|
|
model=model,
|
|
config=final_config,
|
|
timeout=timeout,
|
|
extra=extra_context,
|
|
)
|
|
|
|
if not hasattr(response, "parsed_obj") or response.parsed_obj is None:
|
|
raise LLMException("结构化输出失败:中间件未返回解析后的对象。")
|
|
|
|
return response.parsed_obj
|
|
except (LLMException, ModelRetry, ControlFlowExit) as e:
|
|
raise e.with_traceback(None) from None
|
|
except Exception as e:
|
|
friendly_msg = get_user_friendly_error_message(e)
|
|
logger.error(f"生成结构化响应失败: {e} | 建议: {friendly_msg}", e=e)
|
|
raise LLMException(f"生成结构化响应失败: {friendly_msg}").with_traceback(
|
|
None
|
|
) from None
|
|
|
|
|
|
async def generate(
|
|
messages: list[LLMMessage],
|
|
*,
|
|
model: ModelName = None,
|
|
config: GenerationConfig | IntentBuilder | None = None,
|
|
timeout: float | None = None,
|
|
extra: dict[str, Any] | None = None,
|
|
) -> ChatResponse:
|
|
"""
|
|
[内部 API/高级用法] 直接传入底层消息实体列表生成响应。一般业务插件推荐使用 `chat`。
|
|
|
|
参数:
|
|
messages: 完整的消息历史列表,包括系统指令、用户消息和助手回复。
|
|
model: 要使用的模型名称,如果为None则使用默认模型。
|
|
config: (可选) 生成配置对象,将与默认配置合并后传递。
|
|
|
|
返回:
|
|
ChatResponse: 包含AI回复内容、使用信息和工具调用等的完整响应对象。
|
|
"""
|
|
try:
|
|
resolved_config: GenerationConfig | None = None
|
|
if isinstance(config, IntentBuilder):
|
|
resolved_config = config.build()
|
|
else:
|
|
resolved_config = config
|
|
|
|
request = ChatRequest(
|
|
messages=messages,
|
|
config=resolved_config,
|
|
timeout=timeout,
|
|
extra=extra or {},
|
|
)
|
|
|
|
sys_caps = request.extra.pop("__sys_capabilities", [])
|
|
run_ctx = request.extra.pop("run_context", None)
|
|
|
|
if sys_caps:
|
|
from zhenxun.services.ai.capabilities import CombinedCapability
|
|
from zhenxun.services.ai.core.models import LLMContext
|
|
from zhenxun.services.ai.run import RunContext
|
|
|
|
run_context = run_ctx or RunContext()
|
|
llm_context = LLMContext(request=request)
|
|
combined_cap = CombinedCapability(sys_caps)
|
|
|
|
async def inner_handler(
|
|
ctx: LLMContext[ChatRequest, ChatResponse],
|
|
) -> ChatResponse:
|
|
return await LLMOrchestrator.invoke(
|
|
ctx.request,
|
|
model_name=model,
|
|
task="chat",
|
|
override_config=resolved_config,
|
|
)
|
|
|
|
return await combined_cap.wrap_model_request(
|
|
run_context, llm_context, inner_handler
|
|
)
|
|
else:
|
|
return await LLMOrchestrator.invoke(
|
|
request, model_name=model, task="chat", override_config=resolved_config
|
|
)
|
|
except (LLMException, ModelRetry, ControlFlowExit) as e:
|
|
raise e.with_traceback(None) from None
|
|
except Exception as e:
|
|
friendly_msg = get_user_friendly_error_message(e)
|
|
logger.error(f"生成响应失败: {e} | 建议: {friendly_msg}", e=e)
|
|
raise LLMException(f"生成响应失败: {friendly_msg}").with_traceback(
|
|
None
|
|
) from None
|
|
|
|
|
|
@overload
|
|
async def create_image(
|
|
prompt: PromptInput,
|
|
*,
|
|
images: None = None,
|
|
model: ModelName = None,
|
|
config: GenerationConfig | IntentBuilder | None = None,
|
|
) -> ImageResponse:
|
|
"""根据文本提示生成一张新图片。"""
|
|
...
|
|
|
|
|
|
@overload
|
|
async def create_image(
|
|
prompt: PromptInput,
|
|
*,
|
|
images: list[Path | bytes | str] | Path | bytes | str,
|
|
model: ModelName = None,
|
|
config: GenerationConfig | IntentBuilder | None = None,
|
|
) -> ImageResponse:
|
|
"""在给定图片的基础上,根据文本提示进行编辑或重新生成。"""
|
|
...
|
|
|
|
|
|
async def create_image(
|
|
prompt: PromptInput,
|
|
*,
|
|
images: list[Path | bytes | str] | Path | bytes | str | None = None,
|
|
model: ModelName = None,
|
|
config: GenerationConfig | IntentBuilder | None = None,
|
|
) -> ImageResponse:
|
|
"""
|
|
多模态图片生成/编辑函数。
|
|
|
|
示例:
|
|
res = await create_image("画一只猫", model="OpenAI/dall-e-3")
|
|
img_bytes = res.images[0]
|
|
|
|
说明:
|
|
- 若 `images` 为 None,执行文本生成图片 (Text-to-Image)。
|
|
- 若提供 `images`,执行图像编辑 (Image-to-Image)。
|
|
"""
|
|
text_prompt = getattr(prompt, "extract_plain_text", lambda: str(prompt))()
|
|
|
|
image_list = []
|
|
if images:
|
|
if isinstance(images, list):
|
|
image_list.extend(images)
|
|
else:
|
|
image_list.append(images)
|
|
|
|
if isinstance(config, IntentBuilder):
|
|
config = config.build()
|
|
config = config or GenerationConfig()
|
|
|
|
try:
|
|
request = ImageRequest(
|
|
prompt=text_prompt,
|
|
images=image_list if image_list else None,
|
|
config=config,
|
|
)
|
|
return await LLMOrchestrator.invoke(
|
|
request, model_name=model, task="image", override_config=config
|
|
)
|
|
except LLMException as e:
|
|
raise e.with_traceback(None) from None
|
|
except Exception as e:
|
|
friendly_msg = get_user_friendly_error_message(e)
|
|
logger.error(f"图片生成执行发生未知错误: {e} | 建议: {friendly_msg}", e=e)
|
|
raise LLMException(f"图片生成失败: {friendly_msg}").with_traceback(
|
|
None
|
|
) from None
|
|
|
|
|
|
async def create_speech(
|
|
text: str,
|
|
voice: str | None = None,
|
|
*,
|
|
model: ModelName = None,
|
|
config: TTSConfig | None = None,
|
|
) -> AudioResponse:
|
|
"""
|
|
通用文本转语音便捷函数。
|
|
|
|
参数:
|
|
text: 待合成的文本内容。
|
|
voice: 快捷音色指定,若为空则自动使用目标模型的缺省最优音色。
|
|
model: 指定生成语音的模型名称。
|
|
config: 语音生成的额外设置。
|
|
|
|
示例:
|
|
res = await create_speech("你好,世界", voice="alloy", model="OpenAI/tts-1")
|
|
Path("out.mp3").write_bytes(res.audio_bytes)
|
|
"""
|
|
if not text:
|
|
raise LLMException("TTS 输入文本不能为空")
|
|
|
|
try:
|
|
request = SpeechRequest(input_text=text, voice=voice, config=config)
|
|
return await LLMOrchestrator.invoke(request, model_name=model, task="tts")
|
|
except LLMException as e:
|
|
raise e.with_traceback(None) from None
|
|
except Exception as e:
|
|
friendly_msg = get_user_friendly_error_message(e)
|
|
logger.error(f"语音生成执行发生未知错误: {e} | 建议: {friendly_msg}", e=e)
|
|
raise LLMException(f"语音生成失败: {friendly_msg}").with_traceback(
|
|
None
|
|
) from None
|