From 2e11f3e1a371dab6390bb4cd1d0bf60b8ce6aca2 Mon Sep 17 00:00:00 2001 From: lintsinghua Date: Fri, 19 Dec 2025 11:41:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(llm):=20=E5=A2=9E=E5=BC=BALLM=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E5=A4=84=E7=90=86=E5=92=8C=E8=B0=83=E8=AF=95=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在LLMError异常类中添加api_response字段存储原始错误信息 实现_extract_api_response方法从异常中提取API响应 前端增加调试信息展示面板,显示详细的错误诊断数据 后端测试接口返回完整的调试信息,包括耗时、错误类型等 --- backend/app/api/v1/endpoints/config.py | 138 +++++++++++++----- .../services/llm/adapters/litellm_adapter.py | 62 +++++++- backend/app/services/llm/types.py | 4 +- .../src/components/system/SystemConfig.tsx | 75 ++++++++-- 4 files changed, 228 insertions(+), 51 deletions(-) diff --git a/backend/app/api/v1/endpoints/config.py b/backend/app/api/v1/endpoints/config.py index 2e3eef0..0b1b1c0 100644 --- a/backend/app/api/v1/endpoints/config.py +++ b/backend/app/api/v1/endpoints/config.py @@ -292,6 +292,8 @@ class LLMTestResponse(BaseModel): message: str model: Optional[str] = None response: Optional[str] = None + # 调试信息 + debug: Optional[dict] = None @router.post("/test-llm", response_model=LLMTestResponse) @@ -302,8 +304,19 @@ async def test_llm_connection( """测试LLM连接是否正常""" from app.services.llm.factory import LLMFactory, NATIVE_ONLY_PROVIDERS from app.services.llm.adapters import LiteLLMAdapter, BaiduAdapter, MinimaxAdapter, DoubaoAdapter - from app.services.llm.types import LLMConfig, LLMProvider, LLMRequest, LLMMessage, DEFAULT_MODELS - + from app.services.llm.types import LLMConfig, LLMProvider, LLMRequest, LLMMessage, DEFAULT_MODELS, DEFAULT_BASE_URLS + import traceback + import time + + start_time = time.time() + debug_info = { + "provider": request.provider, + "model_requested": request.model, + "base_url_requested": request.baseUrl, + "api_key_length": len(request.apiKey) if request.apiKey else 0, + "api_key_prefix": request.apiKey[:8] + "..." if request.apiKey and len(request.apiKey) > 8 else "(empty)", + } + try: # 解析provider provider_map = { @@ -319,17 +332,26 @@ async def test_llm_connection( 'doubao': LLMProvider.DOUBAO, 'ollama': LLMProvider.OLLAMA, } - + provider = provider_map.get(request.provider.lower()) if not provider: + debug_info["error_type"] = "unsupported_provider" return LLMTestResponse( success=False, - message=f"不支持的LLM提供商: {request.provider}" + message=f"不支持的LLM提供商: {request.provider}", + debug=debug_info ) - + # 获取默认模型 model = request.model or DEFAULT_MODELS.get(provider) - + base_url = request.baseUrl or DEFAULT_BASE_URLS.get(provider, "") + + debug_info["model_used"] = model + debug_info["base_url_used"] = base_url + debug_info["is_native_adapter"] = provider in NATIVE_ONLY_PROVIDERS + + print(f"[LLM Test] 开始测试: provider={provider.value}, model={model}, base_url={base_url}") + # 创建配置 config = LLMConfig( provider=provider, @@ -339,7 +361,7 @@ async def test_llm_connection( timeout=30, # 测试使用较短的超时时间 max_tokens=50, # 测试使用较少的token ) - + # 直接创建新的适配器实例(不使用缓存),确保使用最新的配置 if provider in NATIVE_ONLY_PROVIDERS: native_adapter_map = { @@ -348,59 +370,105 @@ async def test_llm_connection( LLMProvider.DOUBAO: DoubaoAdapter, } adapter = native_adapter_map[provider](config) + debug_info["adapter_type"] = type(adapter).__name__ else: adapter = LiteLLMAdapter(config) - + debug_info["adapter_type"] = "LiteLLMAdapter" + # 获取 LiteLLM 实际使用的模型名 + debug_info["litellm_model"] = getattr(adapter, '_get_model_name', lambda: model)() if hasattr(adapter, '_get_model_name') else model + test_request = LLMRequest( messages=[ LLMMessage(role="user", content="Say 'Hello' in one word.") ], max_tokens=50, ) - + + print(f"[LLM Test] 发送测试请求...") response = await adapter.complete(test_request) - + + elapsed_time = time.time() - start_time + debug_info["elapsed_time_ms"] = round(elapsed_time * 1000, 2) + # 验证响应内容 if not response or not response.content: + debug_info["error_type"] = "empty_response" + debug_info["raw_response"] = str(response) if response else None + print(f"[LLM Test] 空响应: {response}") return LLMTestResponse( success=False, - message="LLM 返回空响应,请检查 API Key 和配置" + message="LLM 返回空响应,请检查 API Key 和配置", + debug=debug_info ) - + + debug_info["response_length"] = len(response.content) + debug_info["usage"] = { + "prompt_tokens": getattr(response, 'prompt_tokens', None), + "completion_tokens": getattr(response, 'completion_tokens', None), + "total_tokens": getattr(response, 'total_tokens', None), + } + + print(f"[LLM Test] 成功! 响应: {response.content[:50]}... 耗时: {elapsed_time:.2f}s") + return LLMTestResponse( success=True, - message="LLM连接测试成功", + message=f"连接成功 ({elapsed_time:.2f}s)", model=model, - response=response.content[:100] if response.content else None + response=response.content[:100] if response.content else None, + debug=debug_info ) - + except Exception as e: + elapsed_time = time.time() - start_time error_msg = str(e) + error_type = type(e).__name__ + + debug_info["elapsed_time_ms"] = round(elapsed_time * 1000, 2) + debug_info["error_type"] = error_type + debug_info["error_message"] = error_msg + debug_info["traceback"] = traceback.format_exc() + + # 提取 LLMError 中的 api_response + if hasattr(e, 'api_response') and e.api_response: + debug_info["api_response"] = e.api_response + if hasattr(e, 'status_code') and e.status_code: + debug_info["status_code"] = e.status_code + + print(f"[LLM Test] 失败: {error_type}: {error_msg}") + print(f"[LLM Test] Traceback:\n{traceback.format_exc()}") + # 提供更友好的错误信息 - if "401" in error_msg or "invalid_api_key" in error_msg.lower() or "incorrect api key" in error_msg.lower(): - return LLMTestResponse( - success=False, - message="API Key 无效或已过期,请检查后重试" - ) + friendly_message = error_msg + + # 优先检查余额不足(因为某些 API 用 429 表示余额不足) + if any(keyword in error_msg for keyword in ["余额不足", "资源包", "充值", "quota", "insufficient", "balance", "402"]): + friendly_message = "账户余额不足或配额已用尽,请充值后重试" + debug_info["error_category"] = "insufficient_balance" + elif "401" in error_msg or "invalid_api_key" in error_msg.lower() or "incorrect api key" in error_msg.lower(): + friendly_message = "API Key 无效或已过期,请检查后重试" + debug_info["error_category"] = "auth_invalid_key" elif "authentication" in error_msg.lower(): - return LLMTestResponse( - success=False, - message="认证失败,请检查 API Key 是否正确" - ) + friendly_message = "认证失败,请检查 API Key 是否正确" + debug_info["error_category"] = "auth_failed" elif "timeout" in error_msg.lower(): - return LLMTestResponse( - success=False, - message="连接超时,请检查网络或 API 地址是否正确" - ) - elif "connection" in error_msg.lower(): - return LLMTestResponse( - success=False, - message="无法连接到 API 服务,请检查网络或 API 地址" - ) - + friendly_message = "连接超时,请检查网络或 API 地址是否正确" + debug_info["error_category"] = "timeout" + elif "connection" in error_msg.lower() or "connect" in error_msg.lower(): + friendly_message = "无法连接到 API 服务,请检查网络或 API 地址" + debug_info["error_category"] = "connection" + elif "rate" in error_msg.lower() and "limit" in error_msg.lower(): + friendly_message = "API 请求频率超限,请稍后重试" + debug_info["error_category"] = "rate_limit" + elif "model" in error_msg.lower() and ("not found" in error_msg.lower() or "does not exist" in error_msg.lower()): + friendly_message = f"模型 '{debug_info.get('model_used', 'unknown')}' 不存在或无权访问" + debug_info["error_category"] = "model_not_found" + else: + debug_info["error_category"] = "unknown" + return LLMTestResponse( success=False, - message=f"LLM连接测试失败: {error_msg}" + message=friendly_message, + debug=debug_info ) diff --git a/backend/app/services/llm/adapters/litellm_adapter.py b/backend/app/services/llm/adapters/litellm_adapter.py index c0821cb..842406c 100644 --- a/backend/app/services/llm/adapters/litellm_adapter.py +++ b/backend/app/services/llm/adapters/litellm_adapter.py @@ -109,6 +109,45 @@ class LiteLLMAdapter(BaseLLMAdapter): return f"{prefix}/{model}" + def _extract_api_response(self, error: Exception) -> Optional[str]: + """从异常中提取 API 服务器返回的原始响应信息""" + error_str = str(error) + + # 尝试提取 JSON 格式的错误信息 + import re + import json + + # 匹配 {'error': {...}} 或 {"error": {...}} 格式 + json_pattern = r"\{['\"]error['\"]:\s*\{[^}]+\}\}" + match = re.search(json_pattern, error_str) + if match: + try: + # 将单引号替换为双引号以便 JSON 解析 + json_str = match.group().replace("'", '"') + error_obj = json.loads(json_str) + if 'error' in error_obj: + err = error_obj['error'] + code = err.get('code', '') + message = err.get('message', '') + return f"[{code}] {message}" if code else message + except: + pass + + # 尝试提取 message 字段 + message_pattern = r"['\"]message['\"]:\s*['\"]([^'\"]+)['\"]" + match = re.search(message_pattern, error_str) + if match: + return match.group(1) + + # 尝试从 litellm 异常中获取原始消息 + if hasattr(error, 'message'): + return error.message + if hasattr(error, 'llm_provider'): + # litellm 异常通常包含原始错误信息 + return error_str.split(' - ')[-1] if ' - ' in error_str else None + + return None + def _get_api_base(self) -> Optional[str]: """获取 API 基础 URL""" # 优先使用用户配置的 base_url @@ -200,20 +239,31 @@ class LiteLLMAdapter(BaseLLMAdapter): # 调用 LiteLLM response = await litellm.acompletion(**kwargs) except litellm.exceptions.AuthenticationError as e: - raise LLMError(f"API Key 无效或已过期: {str(e)}", self.config.provider, 401) + api_response = self._extract_api_response(e) + raise LLMError(f"API Key 无效或已过期", self.config.provider, 401, api_response=api_response) except litellm.exceptions.RateLimitError as e: - raise LLMError(f"API 调用频率超限: {str(e)}", self.config.provider, 429) + error_msg = str(e) + api_response = self._extract_api_response(e) + # 区分"余额不足"和"频率超限" + if any(keyword in error_msg for keyword in ["余额不足", "资源包", "充值", "quota", "insufficient", "balance"]): + raise LLMError(f"账户余额不足或配额已用尽,请充值后重试", self.config.provider, 402, api_response=api_response) + raise LLMError(f"API 调用频率超限,请稍后重试", self.config.provider, 429, api_response=api_response) except litellm.exceptions.APIConnectionError as e: - raise LLMError(f"无法连接到 API 服务: {str(e)}", self.config.provider) + api_response = self._extract_api_response(e) + raise LLMError(f"无法连接到 API 服务", self.config.provider, api_response=api_response) except litellm.exceptions.APIError as e: - raise LLMError(f"API 错误: {str(e)}", self.config.provider, getattr(e, 'status_code', None)) + api_response = self._extract_api_response(e) + raise LLMError(f"API 错误", self.config.provider, getattr(e, 'status_code', None), api_response=api_response) except Exception as e: # 捕获其他异常并重新抛出 error_msg = str(e) + api_response = self._extract_api_response(e) if "invalid_api_key" in error_msg.lower() or "incorrect api key" in error_msg.lower(): - raise LLMError(f"API Key 无效: {error_msg}", self.config.provider, 401) + raise LLMError(f"API Key 无效", self.config.provider, 401, api_response=api_response) elif "authentication" in error_msg.lower(): - raise LLMError(f"认证失败: {error_msg}", self.config.provider, 401) + raise LLMError(f"认证失败", self.config.provider, 401, api_response=api_response) + elif any(keyword in error_msg for keyword in ["余额不足", "资源包", "充值", "quota", "insufficient", "balance"]): + raise LLMError(f"账户余额不足或配额已用尽", self.config.provider, 402, api_response=api_response) raise # 解析响应 diff --git a/backend/app/services/llm/types.py b/backend/app/services/llm/types.py index 697090f..ffb35ea 100644 --- a/backend/app/services/llm/types.py +++ b/backend/app/services/llm/types.py @@ -79,12 +79,14 @@ class LLMError(Exception): message: str, provider: Optional[LLMProvider] = None, status_code: Optional[int] = None, - original_error: Optional[Any] = None + original_error: Optional[Any] = None, + api_response: Optional[str] = None ): super().__init__(message) self.provider = provider self.status_code = status_code self.original_error = original_error + self.api_response = api_response # API 服务器返回的原始错误信息 # 各平台默认模型 (2025年最新推荐) diff --git a/frontend/src/components/system/SystemConfig.tsx b/frontend/src/components/system/SystemConfig.tsx index db53d59..0903bef 100644 --- a/frontend/src/components/system/SystemConfig.tsx +++ b/frontend/src/components/system/SystemConfig.tsx @@ -51,7 +51,8 @@ export function SystemConfig() { const [showApiKey, setShowApiKey] = useState(false); const [hasChanges, setHasChanges] = useState(false); const [testingLLM, setTestingLLM] = useState(false); - const [llmTestResult, setLlmTestResult] = useState<{ success: boolean; message: string } | null>(null); + const [llmTestResult, setLlmTestResult] = useState<{ success: boolean; message: string; debug?: Record } | null>(null); + const [showDebugInfo, setShowDebugInfo] = useState(false); useEffect(() => { loadConfig(); }, []); @@ -372,16 +373,72 @@ export function SystemConfig() { {llmTestResult && (
-
- {llmTestResult.success ? ( - - ) : ( - +
+
+ {llmTestResult.success ? ( + + ) : ( + + )} + + {llmTestResult.message} + +
+ {llmTestResult.debug && ( + )} - - {llmTestResult.message} -
+ {showDebugInfo && llmTestResult.debug && ( +
+
+
调试信息:
+
Provider: {String(llmTestResult.debug.provider)}
+
Model: {String(llmTestResult.debug.model_used || llmTestResult.debug.model_requested || 'N/A')}
+
Base URL: {String(llmTestResult.debug.base_url_used || llmTestResult.debug.base_url_requested || '(default)')}
+
Adapter: {String(llmTestResult.debug.adapter_type || 'N/A')}
+
API Key: {String(llmTestResult.debug.api_key_prefix)} (长度: {String(llmTestResult.debug.api_key_length)})
+
耗时: {String(llmTestResult.debug.elapsed_time_ms || 'N/A')} ms
+ {llmTestResult.debug.error_category && ( +
错误类型: {String(llmTestResult.debug.error_category)}
+ )} + {llmTestResult.debug.error_type && ( +
异常类型: {String(llmTestResult.debug.error_type)}
+ )} + {llmTestResult.debug.status_code && ( +
HTTP 状态码: {String(llmTestResult.debug.status_code)}
+ )} + {llmTestResult.debug.api_response && ( +
+
API 服务器返回:
+
+                            {String(llmTestResult.debug.api_response)}
+                          
+
+ )} + {llmTestResult.debug.error_message && ( +
+
完整错误信息:
+
+                            {String(llmTestResult.debug.error_message)}
+                          
+
+ )} + {llmTestResult.debug.traceback && ( +
+ 完整堆栈跟踪 +
+                            {String(llmTestResult.debug.traceback)}
+                          
+
+ )} +
+
+ )}
)}