mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-19 03:00:09 +08:00
✨ feat(llm): 全面重构LLM服务模块,增强多模态与工具支持 (#1953)
* ✨ feat(llm): 全面重构LLM服务模块,增强多模态与工具支持 🚀 核心功能增强 - 多模型链式调用:新增 `pipeline_chat` 支持复杂任务流处理 - 扩展提供商支持:新增 ARK(火山方舟)、SiliconFlow(硅基流动) 适配器 - 多模态处理增强:支持URL媒体文件下载转换,提升输入灵活性 - 历史对话支持:AI.analyze 方法支持历史消息上下文和可选 UniMessage 参数 - 文本嵌入功能:新增 `embed`、`analyze_multimodal`、`search_multimodal` 等API - 模型能力系统:新增 `ModelCapabilities` 统一管理模型特性(多模态、工具调用等) 🔧 架构重构与优化 - MCP工具系统重构:配置独立化至 `data/llm/mcp_tools.json`,预置常用工具 - API调用逻辑统一:提取通用 `_perform_api_call` 方法,消除代码重复 - 跨平台兼容:Windows平台MCP工具npx命令自动包装处理 - HTTP客户端增强:兼容不同版本httpx代理配置(0.28+版本适配) 🛠️ API与配置完善 - 统一返回类型:`AI.analyze` 统一返回 `LLMResponse` 类型 - 消息转换工具:新增 `message_to_unimessage` 转换函数 - Gemini适配器增强:URL图片下载编码、动态安全阈值配置 - 缓存管理:新增模型实例缓存和管理功能 - 配置预设:扩展 CommonOverrides 预设配置选项 - 历史管理优化:支持多模态内容占位符替换,提升效率 📚 文档与开发体验 - README全面重写:新增完整使用指南、API参考和架构概览 - 文档内容扩充:补充嵌入模型、缓存管理、工具注册等功能说明 - 日志记录增强:支持详细调试信息输出 - API简化:移除冗余函数,优化接口设计 * 🎨 feat(llm): 统一LLM服务函数文档格式 * ✨ feat(llm): 添加新模型并简化提供者配置加载 * 🚨 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:
co-authored by
webjoin111
pre-commit-ci[bot]
parent
1e7ae38684
commit
48cbb2bf1d
+316
-265
@@ -6,11 +6,13 @@ LLM 模型实现类
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Awaitable, Callable
|
||||
from contextlib import AsyncExitStack
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from zhenxun.services.log import logger
|
||||
|
||||
from .adapters.base import RequestData
|
||||
from .config import LLMGenerationConfig
|
||||
from .config.providers import get_ai_config
|
||||
from .core import (
|
||||
@@ -30,6 +32,8 @@ from .types import (
|
||||
ModelDetail,
|
||||
ProviderConfig,
|
||||
)
|
||||
from .types.capabilities import ModelCapabilities, ModelModality
|
||||
from .utils import _sanitize_request_body_for_logging
|
||||
|
||||
|
||||
class LLMModelBase(ABC):
|
||||
@@ -42,7 +46,17 @@ class LLMModelBase(ABC):
|
||||
history: list[dict[str, str]] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> str:
|
||||
"""生成文本"""
|
||||
"""
|
||||
生成文本
|
||||
|
||||
参数:
|
||||
prompt: 输入提示词。
|
||||
history: 对话历史记录。
|
||||
**kwargs: 其他参数。
|
||||
|
||||
返回:
|
||||
str: 生成的文本。
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@@ -54,7 +68,19 @@ class LLMModelBase(ABC):
|
||||
tool_choice: str | dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> LLMResponse:
|
||||
"""生成高级响应"""
|
||||
"""
|
||||
生成高级响应
|
||||
|
||||
参数:
|
||||
messages: 消息列表。
|
||||
config: 生成配置。
|
||||
tools: 工具列表。
|
||||
tool_choice: 工具选择策略。
|
||||
**kwargs: 其他参数。
|
||||
|
||||
返回:
|
||||
LLMResponse: 模型响应。
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@@ -64,7 +90,17 @@ class LLMModelBase(ABC):
|
||||
task_type: EmbeddingTaskType | str = EmbeddingTaskType.RETRIEVAL_DOCUMENT,
|
||||
**kwargs: Any,
|
||||
) -> list[list[float]]:
|
||||
"""生成文本嵌入向量"""
|
||||
"""
|
||||
生成文本嵌入向量
|
||||
|
||||
参数:
|
||||
texts: 文本列表。
|
||||
task_type: 嵌入任务类型。
|
||||
**kwargs: 其他参数。
|
||||
|
||||
返回:
|
||||
list[list[float]]: 嵌入向量列表。
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@@ -77,12 +113,14 @@ class LLMModel(LLMModelBase):
|
||||
model_detail: ModelDetail,
|
||||
key_store: KeyStatusStore,
|
||||
http_client: LLMHttpClient,
|
||||
capabilities: ModelCapabilities,
|
||||
config_override: LLMGenerationConfig | None = None,
|
||||
):
|
||||
self.provider_config = provider_config
|
||||
self.model_detail = model_detail
|
||||
self.key_store = key_store
|
||||
self.http_client: LLMHttpClient = http_client
|
||||
self.capabilities = capabilities
|
||||
self._generation_config = config_override
|
||||
|
||||
self.provider_name = provider_config.name
|
||||
@@ -99,6 +137,34 @@ class LLMModel(LLMModelBase):
|
||||
|
||||
self._is_closed = False
|
||||
|
||||
def can_process_images(self) -> bool:
|
||||
"""检查模型是否支持图片作为输入。"""
|
||||
return ModelModality.IMAGE in self.capabilities.input_modalities
|
||||
|
||||
def can_process_video(self) -> bool:
|
||||
"""检查模型是否支持视频作为输入。"""
|
||||
return ModelModality.VIDEO in self.capabilities.input_modalities
|
||||
|
||||
def can_process_audio(self) -> bool:
|
||||
"""检查模型是否支持音频作为输入。"""
|
||||
return ModelModality.AUDIO in self.capabilities.input_modalities
|
||||
|
||||
def can_generate_images(self) -> bool:
|
||||
"""检查模型是否支持生成图片。"""
|
||||
return ModelModality.IMAGE in self.capabilities.output_modalities
|
||||
|
||||
def can_generate_audio(self) -> bool:
|
||||
"""检查模型是否支持生成音频 (TTS)。"""
|
||||
return ModelModality.AUDIO in self.capabilities.output_modalities
|
||||
|
||||
def can_use_tools(self) -> bool:
|
||||
"""检查模型是否支持工具调用/函数调用。"""
|
||||
return self.capabilities.supports_tool_calling
|
||||
|
||||
def is_embedding_model(self) -> bool:
|
||||
"""检查这是否是一个嵌入模型。"""
|
||||
return self.capabilities.is_embedding_model
|
||||
|
||||
async def _get_http_client(self) -> LLMHttpClient:
|
||||
"""获取HTTP客户端"""
|
||||
if self.http_client.is_closed:
|
||||
@@ -135,24 +201,54 @@ class LLMModel(LLMModelBase):
|
||||
|
||||
return selected_key
|
||||
|
||||
async def _execute_embedding_request(
|
||||
async def _perform_api_call(
|
||||
self,
|
||||
adapter,
|
||||
texts: list[str],
|
||||
task_type: EmbeddingTaskType | str,
|
||||
http_client: LLMHttpClient,
|
||||
prepare_request_func: Callable[[str], Awaitable["RequestData"]],
|
||||
parse_response_func: Callable[[dict[str, Any]], Any],
|
||||
http_client: "LLMHttpClient",
|
||||
failed_keys: set[str] | None = None,
|
||||
) -> list[list[float]]:
|
||||
"""执行单次嵌入请求 - 供重试机制调用"""
|
||||
log_context: str = "API",
|
||||
) -> Any:
|
||||
"""
|
||||
执行API调用的通用核心方法。
|
||||
|
||||
该方法封装了以下通用逻辑:
|
||||
1. 选择API密钥。
|
||||
2. 准备和记录请求。
|
||||
3. 发送HTTP POST请求。
|
||||
4. 处理HTTP错误和API特定错误。
|
||||
5. 记录密钥使用状态。
|
||||
6. 解析成功的响应。
|
||||
|
||||
参数:
|
||||
prepare_request_func: 准备请求的函数。
|
||||
parse_response_func: 解析响应的函数。
|
||||
http_client: HTTP客户端。
|
||||
failed_keys: 失败的密钥集合。
|
||||
log_context: 日志上下文。
|
||||
|
||||
返回:
|
||||
Any: 解析后的响应数据。
|
||||
"""
|
||||
api_key = await self._select_api_key(failed_keys)
|
||||
|
||||
try:
|
||||
request_data = adapter.prepare_embedding_request(
|
||||
model=self,
|
||||
api_key=api_key,
|
||||
texts=texts,
|
||||
task_type=task_type,
|
||||
request_data = await prepare_request_func(api_key)
|
||||
|
||||
logger.info(
|
||||
f"🌐 发起LLM请求 - 模型: {self.provider_name}/{self.model_name} "
|
||||
f"[{log_context}]"
|
||||
)
|
||||
logger.debug(f"📡 请求URL: {request_data.url}")
|
||||
masked_key = (
|
||||
f"{api_key[:8]}...{api_key[-4:] if len(api_key) > 12 else '***'}"
|
||||
)
|
||||
logger.debug(f"🔑 API密钥: {masked_key}")
|
||||
logger.debug(f"📋 请求头: {dict(request_data.headers)}")
|
||||
|
||||
sanitized_body = _sanitize_request_body_for_logging(request_data.body)
|
||||
request_body_str = json.dumps(sanitized_body, ensure_ascii=False, indent=2)
|
||||
logger.debug(f"📦 请求体: {request_body_str}")
|
||||
|
||||
http_response = await http_client.post(
|
||||
request_data.url,
|
||||
@@ -160,121 +256,16 @@ class LLMModel(LLMModelBase):
|
||||
json=request_data.body,
|
||||
)
|
||||
|
||||
if http_response.status_code != 200:
|
||||
error_text = http_response.text
|
||||
logger.error(
|
||||
f"HTTP嵌入请求失败: {http_response.status_code} - {error_text}"
|
||||
)
|
||||
await self.key_store.record_failure(api_key, http_response.status_code)
|
||||
|
||||
error_code = LLMErrorCode.API_REQUEST_FAILED
|
||||
if http_response.status_code in [401, 403]:
|
||||
error_code = LLMErrorCode.API_KEY_INVALID
|
||||
elif http_response.status_code == 429:
|
||||
error_code = LLMErrorCode.API_RATE_LIMITED
|
||||
|
||||
raise LLMException(
|
||||
f"HTTP嵌入请求失败: {http_response.status_code}",
|
||||
code=error_code,
|
||||
details={
|
||||
"status_code": http_response.status_code,
|
||||
"response": error_text,
|
||||
"api_key": api_key,
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
response_json = http_response.json()
|
||||
adapter.validate_embedding_response(response_json)
|
||||
embeddings = adapter.parse_embedding_response(response_json)
|
||||
except Exception as e:
|
||||
logger.error(f"解析嵌入响应失败: {e}", e=e)
|
||||
await self.key_store.record_failure(api_key, None)
|
||||
if isinstance(e, LLMException):
|
||||
raise
|
||||
else:
|
||||
raise LLMException(
|
||||
f"解析API嵌入响应失败: {e}",
|
||||
code=LLMErrorCode.RESPONSE_PARSE_ERROR,
|
||||
cause=e,
|
||||
)
|
||||
|
||||
await self.key_store.record_success(api_key)
|
||||
return embeddings
|
||||
|
||||
except LLMException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"生成嵌入时发生未预期错误: {e}", e=e)
|
||||
await self.key_store.record_failure(api_key, None)
|
||||
raise LLMException(
|
||||
f"生成嵌入失败: {e}",
|
||||
code=LLMErrorCode.EMBEDDING_FAILED,
|
||||
cause=e,
|
||||
)
|
||||
|
||||
async def _execute_with_smart_retry(
|
||||
self,
|
||||
adapter,
|
||||
messages: list[LLMMessage],
|
||||
config: LLMGenerationConfig | None,
|
||||
tools_dict: list[dict[str, Any]] | None,
|
||||
tool_choice: str | dict[str, Any] | None,
|
||||
http_client: LLMHttpClient,
|
||||
):
|
||||
"""智能重试机制 - 使用统一的重试装饰器"""
|
||||
ai_config = get_ai_config()
|
||||
max_retries = ai_config.get("max_retries_llm", 3)
|
||||
retry_delay = ai_config.get("retry_delay_llm", 2)
|
||||
retry_config = RetryConfig(max_retries=max_retries, retry_delay=retry_delay)
|
||||
|
||||
return await with_smart_retry(
|
||||
self._execute_single_request,
|
||||
adapter,
|
||||
messages,
|
||||
config,
|
||||
tools_dict,
|
||||
tool_choice,
|
||||
http_client,
|
||||
retry_config=retry_config,
|
||||
key_store=self.key_store,
|
||||
provider_name=self.provider_name,
|
||||
)
|
||||
|
||||
async def _execute_single_request(
|
||||
self,
|
||||
adapter,
|
||||
messages: list[LLMMessage],
|
||||
config: LLMGenerationConfig | None,
|
||||
tools_dict: list[dict[str, Any]] | None,
|
||||
tool_choice: str | dict[str, Any] | None,
|
||||
http_client: LLMHttpClient,
|
||||
failed_keys: set[str] | None = None,
|
||||
) -> LLMResponse:
|
||||
"""执行单次请求 - 供重试机制调用,直接返回 LLMResponse"""
|
||||
api_key = await self._select_api_key(failed_keys)
|
||||
|
||||
try:
|
||||
request_data = adapter.prepare_advanced_request(
|
||||
model=self,
|
||||
api_key=api_key,
|
||||
messages=messages,
|
||||
config=config,
|
||||
tools=tools_dict,
|
||||
tool_choice=tool_choice,
|
||||
)
|
||||
|
||||
http_response = await http_client.post(
|
||||
request_data.url,
|
||||
headers=request_data.headers,
|
||||
json=request_data.body,
|
||||
)
|
||||
logger.debug(f"📥 响应状态码: {http_response.status_code}")
|
||||
logger.debug(f"📄 响应头: {dict(http_response.headers)}")
|
||||
|
||||
if http_response.status_code != 200:
|
||||
error_text = http_response.text
|
||||
logger.error(
|
||||
f"HTTP请求失败: {http_response.status_code} - {error_text}"
|
||||
f"❌ HTTP请求失败: {http_response.status_code} - {error_text} "
|
||||
f"[{log_context}]"
|
||||
)
|
||||
logger.debug(f"💥 完整错误响应: {error_text}")
|
||||
|
||||
await self.key_store.record_failure(api_key, http_response.status_code)
|
||||
|
||||
@@ -299,69 +290,165 @@ class LLMModel(LLMModelBase):
|
||||
|
||||
try:
|
||||
response_json = http_response.json()
|
||||
response_data = adapter.parse_response(
|
||||
model=self,
|
||||
response_json=response_json,
|
||||
is_advanced=True,
|
||||
)
|
||||
|
||||
from .types.models import LLMToolCall
|
||||
|
||||
response_tool_calls = []
|
||||
if response_data.tool_calls:
|
||||
for tc_data in response_data.tool_calls:
|
||||
if isinstance(tc_data, LLMToolCall):
|
||||
response_tool_calls.append(tc_data)
|
||||
elif isinstance(tc_data, dict):
|
||||
try:
|
||||
response_tool_calls.append(LLMToolCall(**tc_data))
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"无法将工具调用数据转换为LLMToolCall: {tc_data}, "
|
||||
f"error: {e}"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"工具调用数据格式未知: {tc_data}")
|
||||
|
||||
llm_response = LLMResponse(
|
||||
text=response_data.text,
|
||||
usage_info=response_data.usage_info,
|
||||
raw_response=response_data.raw_response,
|
||||
tool_calls=response_tool_calls if response_tool_calls else None,
|
||||
code_executions=response_data.code_executions,
|
||||
grounding_metadata=response_data.grounding_metadata,
|
||||
cache_info=response_data.cache_info,
|
||||
response_json_str = json.dumps(
|
||||
response_json, ensure_ascii=False, indent=2
|
||||
)
|
||||
logger.debug(f"📋 响应JSON: {response_json_str}")
|
||||
parsed_data = parse_response_func(response_json)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"解析响应失败: {e}", e=e)
|
||||
logger.error(f"解析 {log_context} 响应失败: {e}", e=e)
|
||||
await self.key_store.record_failure(api_key, None)
|
||||
|
||||
if isinstance(e, LLMException):
|
||||
raise
|
||||
else:
|
||||
raise LLMException(
|
||||
f"解析API响应失败: {e}",
|
||||
f"解析API {log_context} 响应失败: {e}",
|
||||
code=LLMErrorCode.RESPONSE_PARSE_ERROR,
|
||||
cause=e,
|
||||
)
|
||||
|
||||
await self.key_store.record_success(api_key)
|
||||
|
||||
return llm_response
|
||||
logger.debug(f"✅ API密钥使用成功: {masked_key}")
|
||||
logger.info(f"🎯 LLM响应解析完成 [{log_context}]")
|
||||
return parsed_data
|
||||
|
||||
except LLMException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"生成响应时发生未预期错误: {e}", e=e)
|
||||
error_log_msg = f"生成 {log_context.lower()} 时发生未预期错误: {e}"
|
||||
logger.error(error_log_msg, e=e)
|
||||
await self.key_store.record_failure(api_key, None)
|
||||
|
||||
raise LLMException(
|
||||
f"生成响应失败: {e}",
|
||||
code=LLMErrorCode.GENERATION_FAILED,
|
||||
error_log_msg,
|
||||
code=LLMErrorCode.GENERATION_FAILED
|
||||
if log_context == "Generation"
|
||||
else LLMErrorCode.EMBEDDING_FAILED,
|
||||
cause=e,
|
||||
)
|
||||
|
||||
async def _execute_embedding_request(
|
||||
self,
|
||||
adapter,
|
||||
texts: list[str],
|
||||
task_type: EmbeddingTaskType | str,
|
||||
http_client: LLMHttpClient,
|
||||
failed_keys: set[str] | None = None,
|
||||
) -> list[list[float]]:
|
||||
"""执行单次嵌入请求 - 供重试机制调用"""
|
||||
|
||||
async def prepare_request(api_key: str) -> RequestData:
|
||||
return adapter.prepare_embedding_request(
|
||||
model=self,
|
||||
api_key=api_key,
|
||||
texts=texts,
|
||||
task_type=task_type,
|
||||
)
|
||||
|
||||
def parse_response(response_json: dict[str, Any]) -> list[list[float]]:
|
||||
adapter.validate_embedding_response(response_json)
|
||||
return adapter.parse_embedding_response(response_json)
|
||||
|
||||
return await self._perform_api_call(
|
||||
prepare_request_func=prepare_request,
|
||||
parse_response_func=parse_response,
|
||||
http_client=http_client,
|
||||
failed_keys=failed_keys,
|
||||
log_context="Embedding",
|
||||
)
|
||||
|
||||
async def _execute_with_smart_retry(
|
||||
self,
|
||||
adapter,
|
||||
messages: list[LLMMessage],
|
||||
config: LLMGenerationConfig | None,
|
||||
tools: list[LLMTool] | None,
|
||||
tool_choice: str | dict[str, Any] | None,
|
||||
http_client: LLMHttpClient,
|
||||
):
|
||||
"""智能重试机制 - 使用统一的重试装饰器"""
|
||||
ai_config = get_ai_config()
|
||||
max_retries = ai_config.get("max_retries_llm", 3)
|
||||
retry_delay = ai_config.get("retry_delay_llm", 2)
|
||||
retry_config = RetryConfig(max_retries=max_retries, retry_delay=retry_delay)
|
||||
|
||||
return await with_smart_retry(
|
||||
self._execute_single_request,
|
||||
adapter,
|
||||
messages,
|
||||
config,
|
||||
tools,
|
||||
tool_choice,
|
||||
http_client,
|
||||
retry_config=retry_config,
|
||||
key_store=self.key_store,
|
||||
provider_name=self.provider_name,
|
||||
)
|
||||
|
||||
async def _execute_single_request(
|
||||
self,
|
||||
adapter,
|
||||
messages: list[LLMMessage],
|
||||
config: LLMGenerationConfig | None,
|
||||
tools: list[LLMTool] | None,
|
||||
tool_choice: str | dict[str, Any] | None,
|
||||
http_client: LLMHttpClient,
|
||||
failed_keys: set[str] | None = None,
|
||||
) -> LLMResponse:
|
||||
"""执行单次请求 - 供重试机制调用,直接返回 LLMResponse"""
|
||||
|
||||
async def prepare_request(api_key: str) -> RequestData:
|
||||
return await adapter.prepare_advanced_request(
|
||||
model=self,
|
||||
api_key=api_key,
|
||||
messages=messages,
|
||||
config=config,
|
||||
tools=tools,
|
||||
tool_choice=tool_choice,
|
||||
)
|
||||
|
||||
def parse_response(response_json: dict[str, Any]) -> LLMResponse:
|
||||
response_data = adapter.parse_response(
|
||||
model=self,
|
||||
response_json=response_json,
|
||||
is_advanced=True,
|
||||
)
|
||||
from .types.models import LLMToolCall
|
||||
|
||||
response_tool_calls = []
|
||||
if response_data.tool_calls:
|
||||
for tc_data in response_data.tool_calls:
|
||||
if isinstance(tc_data, LLMToolCall):
|
||||
response_tool_calls.append(tc_data)
|
||||
elif isinstance(tc_data, dict):
|
||||
try:
|
||||
response_tool_calls.append(LLMToolCall(**tc_data))
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"无法将工具调用数据转换为LLMToolCall: {tc_data}, "
|
||||
f"error: {e}"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"工具调用数据格式未知: {tc_data}")
|
||||
|
||||
return LLMResponse(
|
||||
text=response_data.text,
|
||||
usage_info=response_data.usage_info,
|
||||
raw_response=response_data.raw_response,
|
||||
tool_calls=response_tool_calls if response_tool_calls else None,
|
||||
code_executions=response_data.code_executions,
|
||||
grounding_metadata=response_data.grounding_metadata,
|
||||
cache_info=response_data.cache_info,
|
||||
)
|
||||
|
||||
return await self._perform_api_call(
|
||||
prepare_request_func=prepare_request,
|
||||
parse_response_func=parse_response,
|
||||
http_client=http_client,
|
||||
failed_keys=failed_keys,
|
||||
log_context="Generation",
|
||||
)
|
||||
|
||||
async def close(self):
|
||||
"""
|
||||
标记模型实例的当前使用周期结束。
|
||||
@@ -400,7 +487,17 @@ class LLMModel(LLMModelBase):
|
||||
history: list[dict[str, str]] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> str:
|
||||
"""生成文本 - 通过 generate_response 实现"""
|
||||
"""
|
||||
生成文本 - 通过 generate_response 实现
|
||||
|
||||
参数:
|
||||
prompt: 输入提示词。
|
||||
history: 对话历史记录。
|
||||
**kwargs: 其他参数。
|
||||
|
||||
返回:
|
||||
str: 生成的文本。
|
||||
"""
|
||||
self._check_not_closed()
|
||||
|
||||
messages: list[LLMMessage] = []
|
||||
@@ -439,11 +536,21 @@ class LLMModel(LLMModelBase):
|
||||
config: LLMGenerationConfig | None = None,
|
||||
tools: list[LLMTool] | None = None,
|
||||
tool_choice: str | dict[str, Any] | None = None,
|
||||
tool_executor: Callable[[str, dict[str, Any]], Awaitable[Any]] | None = None,
|
||||
max_tool_iterations: int = 5,
|
||||
**kwargs: Any,
|
||||
) -> LLMResponse:
|
||||
"""生成高级响应 - 实现完整的工具调用循环"""
|
||||
"""
|
||||
生成高级响应
|
||||
|
||||
参数:
|
||||
messages: 消息列表。
|
||||
config: 生成配置。
|
||||
tools: 工具列表。
|
||||
tool_choice: 工具选择策略。
|
||||
**kwargs: 其他参数。
|
||||
|
||||
返回:
|
||||
LLMResponse: 模型响应。
|
||||
"""
|
||||
self._check_not_closed()
|
||||
|
||||
from .adapters import get_adapter_for_api_type
|
||||
@@ -468,109 +575,43 @@ class LLMModel(LLMModelBase):
|
||||
merged_dict.update(config.to_dict())
|
||||
final_request_config = LLMGenerationConfig(**merged_dict)
|
||||
|
||||
tools_dict: list[dict[str, Any]] | None = None
|
||||
if tools:
|
||||
tools_dict = []
|
||||
for tool in tools:
|
||||
if hasattr(tool, "model_dump"):
|
||||
model_dump_func = getattr(tool, "model_dump")
|
||||
tools_dict.append(model_dump_func(exclude_none=True))
|
||||
elif isinstance(tool, dict):
|
||||
tools_dict.append(tool)
|
||||
else:
|
||||
try:
|
||||
tools_dict.append(dict(tool))
|
||||
except (TypeError, ValueError):
|
||||
logger.warning(f"工具 '{tool}' 无法转换为字典,已忽略。")
|
||||
|
||||
http_client = await self._get_http_client()
|
||||
current_messages = list(messages)
|
||||
|
||||
for iteration in range(max_tool_iterations):
|
||||
logger.debug(f"工具调用循环迭代: {iteration + 1}/{max_tool_iterations}")
|
||||
async with AsyncExitStack() as stack:
|
||||
activated_tools = []
|
||||
if tools:
|
||||
for tool in tools:
|
||||
if tool.type == "mcp" and callable(tool.mcp_session):
|
||||
func_obj = getattr(tool.mcp_session, "func", None)
|
||||
tool_name = (
|
||||
getattr(func_obj, "__name__", "unknown")
|
||||
if func_obj
|
||||
else "unknown"
|
||||
)
|
||||
logger.debug(f"正在激活 MCP 工具会话: {tool_name}")
|
||||
|
||||
active_session = await stack.enter_async_context(
|
||||
tool.mcp_session()
|
||||
)
|
||||
|
||||
activated_tools.append(
|
||||
LLMTool.from_mcp_session(
|
||||
session=active_session, annotations=tool.annotations
|
||||
)
|
||||
)
|
||||
else:
|
||||
activated_tools.append(tool)
|
||||
|
||||
llm_response = await self._execute_with_smart_retry(
|
||||
adapter,
|
||||
current_messages,
|
||||
messages,
|
||||
final_request_config,
|
||||
tools_dict if iteration == 0 else None,
|
||||
tool_choice if iteration == 0 else None,
|
||||
activated_tools if activated_tools else None,
|
||||
tool_choice,
|
||||
http_client,
|
||||
)
|
||||
|
||||
response_tool_calls = llm_response.tool_calls or []
|
||||
|
||||
if not response_tool_calls or not tool_executor:
|
||||
logger.debug("模型未请求工具调用,或未提供工具执行器。返回当前响应。")
|
||||
return llm_response
|
||||
|
||||
logger.info(f"模型请求执行 {len(response_tool_calls)} 个工具。")
|
||||
|
||||
assistant_message_content = llm_response.text if llm_response.text else ""
|
||||
current_messages.append(
|
||||
LLMMessage.assistant_tool_calls(
|
||||
content=assistant_message_content, tool_calls=response_tool_calls
|
||||
)
|
||||
)
|
||||
|
||||
tool_response_messages: list[LLMMessage] = []
|
||||
for tool_call in response_tool_calls:
|
||||
tool_name = tool_call.function.name
|
||||
try:
|
||||
tool_args_dict = json.loads(tool_call.function.arguments)
|
||||
logger.debug(f"执行工具: {tool_name},参数: {tool_args_dict}")
|
||||
|
||||
tool_result = await tool_executor(tool_name, tool_args_dict)
|
||||
logger.debug(
|
||||
f"工具 '{tool_name}' 执行结果: {str(tool_result)[:200]}..."
|
||||
)
|
||||
|
||||
tool_response_messages.append(
|
||||
LLMMessage.tool_response(
|
||||
tool_call_id=tool_call.id,
|
||||
function_name=tool_name,
|
||||
result=tool_result,
|
||||
)
|
||||
)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(
|
||||
f"工具 '{tool_name}' 参数JSON解析失败: "
|
||||
f"{tool_call.function.arguments}, 错误: {e}"
|
||||
)
|
||||
tool_response_messages.append(
|
||||
LLMMessage.tool_response(
|
||||
tool_call_id=tool_call.id,
|
||||
function_name=tool_name,
|
||||
result={
|
||||
"error": "Argument JSON parsing failed",
|
||||
"details": str(e),
|
||||
},
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"执行工具 '{tool_name}' 失败: {e}", e=e)
|
||||
tool_response_messages.append(
|
||||
LLMMessage.tool_response(
|
||||
tool_call_id=tool_call.id,
|
||||
function_name=tool_name,
|
||||
result={
|
||||
"error": "Tool execution failed",
|
||||
"details": str(e),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
current_messages.extend(tool_response_messages)
|
||||
|
||||
logger.warning(f"已达到最大工具调用迭代次数 ({max_tool_iterations})。")
|
||||
raise LLMException(
|
||||
"已达到最大工具调用迭代次数,但模型仍在请求工具调用或未提供最终文本回复。",
|
||||
code=LLMErrorCode.GENERATION_FAILED,
|
||||
details={
|
||||
"iterations": max_tool_iterations,
|
||||
"last_messages": current_messages[-2:],
|
||||
},
|
||||
)
|
||||
return llm_response
|
||||
|
||||
async def generate_embeddings(
|
||||
self,
|
||||
@@ -578,7 +619,17 @@ class LLMModel(LLMModelBase):
|
||||
task_type: EmbeddingTaskType | str = EmbeddingTaskType.RETRIEVAL_DOCUMENT,
|
||||
**kwargs: Any,
|
||||
) -> list[list[float]]:
|
||||
"""生成文本嵌入向量"""
|
||||
"""
|
||||
生成文本嵌入向量
|
||||
|
||||
参数:
|
||||
texts: 文本列表。
|
||||
task_type: 嵌入任务类型。
|
||||
**kwargs: 其他参数。
|
||||
|
||||
返回:
|
||||
list[list[float]]: 嵌入向量列表。
|
||||
"""
|
||||
self._check_not_closed()
|
||||
if not texts:
|
||||
return []
|
||||
|
||||
Reference in New Issue
Block a user