# P5.1→NS3 缺失项 #5:LLM 调用的错误处理和降级策略 ## 简要结论 - 状态:⚠️ 部分解决 - 风险等级:🟠 中 - 重试机制和异常分类已完整实现,调用链级容错(步骤失败后继续)也已到位。但缺少限流(429)降级回退、模型不可用时的静态兜底内容、以及面向前端的统一错误码体系。 ## 详细审查 ### 审查范围 - `apps/backend/app/ai/bailian_client.py` — 百炼 API 统一封装层 - `apps/backend/app/ai/dispatcher.py` — AI 事件调度与调用链编排器 - `apps/backend/app/ai/apps/app1_chat.py` — App1 流式对话错误处理 - `apps/backend/app/ai/apps/app3_clue.py` — App3 数据获取降级示例 - `apps/backend/app/main.py` — AI 事件处理器注册的容错 ### 发现 #### ✅ 已实现部分 1. **重试机制(指数退避)**:`bailian_client.py` 的 `_call_with_retry()` 实现了完整的重试策略: - 最多重试 3 次(`MAX_RETRIES = 3`) - 间隔:1s → 2s → 4s(指数退避,`BASE_INTERVAL = 1`) - 5xx / 超时 / 连接错误:重试 - 4xx(400/401/403/404/422/429):不重试,直接抛出 2. **异常分类体系**:定义了 4 个异常类: - `BailianApiError`:通用 API 错误(含 status_code) - `BailianJsonParseError`:JSON 解析失败(含 raw_content) - `BailianAuthError`:API Key 无效(401) - 继承关系清晰,调用方可按需捕获 3. **调用链级容错**:`dispatcher.py` 的 `_run_step()` 实现了步骤级容错: - 某步失败 → 记录错误日志 + 写入失败 conversation 记录 → 返回 None - 后续步骤继续执行,使用已有缓存 - 整条链后台异步执行(`asyncio.create_task`),不阻塞业务请求 4. **App1 流式错误处理**:`app1_chat.py` 的 `chat_stream()` 在异常时 yield `SSEEvent(type="error", message=str(e))`,前端可据此展示错误提示。 5. **数据获取降级**:`app3_clue.py` 的 `build_prompt()` 在消费数据获取失败时降级为空值: ```python except Exception: member_data = _default_member_data() data_fetch_failed = True ``` 6. **启动容错**:`main.py` 中 AI 事件处理器注册包裹在 try/except 中,API Key 缺失或注册失败不影响后端启动。 #### ❌ 未实现部分 1. **限流(429)降级回退**: - 当前 429 RateLimitError 直接抛出,不重试 - 缺少限流时的降级策略(如返回缓存中的上一次结果、排队等待、降低调用频率) - 消费事件链中如果百炼 API 限流,整个 App 步骤直接失败 2. **模型不可用时的静态兜底**: - 当百炼 API 完全不可用(如网络中断、服务宕机)时,重试 3 次后抛出 `BailianApiError` - 对于 App2(财务洞察)、App4(关系分析)等前端直接展示的应用,缺少静态兜底内容 - 前端读取 ai_cache 时如果没有缓存记录(首次调用就失败),会得到空结果 3. **统一错误码体系**: - App1 的 SSEEvent error 只传 `message=str(e)`,缺少结构化错误码 - 前端无法区分"网络超时"、"API Key 过期"、"限流"等不同错误类型 - 不同错误类型应有不同的用户提示(如限流→"AI 繁忙请稍后"、认证→"服务配置异常") 4. **熔断机制**: - 缺少熔断器(Circuit Breaker):连续失败 N 次后暂停调用,避免无效重试 - 高并发场景下(如多个消费事件同时触发),可能产生大量失败请求 ### 证据 `bailian_client.py` 中的重试和异常处理: ```python # 429 限流:直接抛出,不重试 except openai.RateLimitError as e: logger.error("百炼 API 限流: %s", e) raise BailianApiError(str(e), status_code=429) from e # 5xx / 超时 / 连接错误:重试 except (openai.InternalServerError, openai.APIConnectionError, openai.APITimeoutError) as e: last_error = e if attempt < self.MAX_RETRIES - 1: wait_time = self.BASE_INTERVAL * (2 ** attempt) await asyncio.sleep(wait_time) ``` `dispatcher.py` 中的步骤级容错: ```python async def _run_step(self, app_name, run_func, context) -> dict | None: try: result = await run_func(context, self.bailian, ...) return result except Exception: logger.exception("调用链步骤失败: %s", app_name) # 写入失败 conversation 记录 # ... return None ``` `main.py` 中的启动容错: ```python try: if _api_key and _base_url: # ... 注册 AI 事件处理器 register_ai_handlers(_dispatcher) except Exception: _log.getLogger(__name__).warning( "AI 事件处理器注册失败,AI 功能不可用", exc_info=True) ``` ### 建议 1. **429 限流降级**:捕获 `RateLimitError` 后,尝试返回 ai_cache 中该 (cache_type, site_id, target_id) 的最新缓存结果,并在结果中标注 `"_from_cache": true`,让前端知道这是缓存数据。 2. **统一错误码枚举**: ```python class AIErrorCode(str, Enum): RATE_LIMITED = "rate_limited" # AI 繁忙,请稍后再试 AUTH_FAILED = "auth_failed" # 服务配置异常 TIMEOUT = "timeout" # 请求超时 SERVICE_DOWN = "service_down" # AI 服务暂时不可用 PARSE_ERROR = "parse_error" # AI 返回格式异常 ``` 3. **熔断器**:在 `BailianClient` 中增加简单的熔断逻辑——连续失败 5 次后,后续 60 秒内直接返回缓存或静态兜底,不再调用 API。 4. **App1 SSE 错误结构化**:将 `SSEEvent(type="error")` 扩展为包含 `error_code` 字段,前端据此展示不同的用户提示。