Files
zhenxun_bot/zhenxun/services/ai/llm/api.py
T
52f7dbdedf ♻️ refactor(core): 重构 AI 编排框架与记忆及 RAG 子系统 (#2149)
* ♻️ 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>
2026-07-14 16:48:33 +08:00

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