包含多个会话的累积代码变更: - backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔 - admin-web: ETL 状态页、任务管理、调度配置、登录优化 - miniprogram: 看板页面、聊天集成、UI 组件、导航更新 - etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强 - tenant-admin: 项目初始化 - db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8) - packages/shared: 枚举和工具函数更新 - tools: 数据库工具、报表生成、健康检查 - docs: PRD/架构/部署/合约文档更新 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5.6 KiB
5.6 KiB
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 事件处理器注册的容错
发现
✅ 已实现部分
-
重试机制(指数退避):
bailian_client.py的_call_with_retry()实现了完整的重试策略:- 最多重试 3 次(
MAX_RETRIES = 3) - 间隔:1s → 2s → 4s(指数退避,
BASE_INTERVAL = 1) - 5xx / 超时 / 连接错误:重试
- 4xx(400/401/403/404/422/429):不重试,直接抛出
- 最多重试 3 次(
-
异常分类体系:定义了 4 个异常类:
BailianApiError:通用 API 错误(含 status_code)BailianJsonParseError:JSON 解析失败(含 raw_content)BailianAuthError:API Key 无效(401)- 继承关系清晰,调用方可按需捕获
-
调用链级容错:
dispatcher.py的_run_step()实现了步骤级容错:- 某步失败 → 记录错误日志 + 写入失败 conversation 记录 → 返回 None
- 后续步骤继续执行,使用已有缓存
- 整条链后台异步执行(
asyncio.create_task),不阻塞业务请求
-
App1 流式错误处理:
app1_chat.py的chat_stream()在异常时 yieldSSEEvent(type="error", message=str(e)),前端可据此展示错误提示。 -
数据获取降级:
app3_clue.py的build_prompt()在消费数据获取失败时降级为空值:except Exception: member_data = _default_member_data() data_fetch_failed = True -
启动容错:
main.py中 AI 事件处理器注册包裹在 try/except 中,API Key 缺失或注册失败不影响后端启动。
❌ 未实现部分
-
限流(429)降级回退:
- 当前 429 RateLimitError 直接抛出,不重试
- 缺少限流时的降级策略(如返回缓存中的上一次结果、排队等待、降低调用频率)
- 消费事件链中如果百炼 API 限流,整个 App 步骤直接失败
-
模型不可用时的静态兜底:
- 当百炼 API 完全不可用(如网络中断、服务宕机)时,重试 3 次后抛出
BailianApiError - 对于 App2(财务洞察)、App4(关系分析)等前端直接展示的应用,缺少静态兜底内容
- 前端读取 ai_cache 时如果没有缓存记录(首次调用就失败),会得到空结果
- 当百炼 API 完全不可用(如网络中断、服务宕机)时,重试 3 次后抛出
-
统一错误码体系:
- App1 的 SSEEvent error 只传
message=str(e),缺少结构化错误码 - 前端无法区分"网络超时"、"API Key 过期"、"限流"等不同错误类型
- 不同错误类型应有不同的用户提示(如限流→"AI 繁忙请稍后"、认证→"服务配置异常")
- App1 的 SSEEvent error 只传
-
熔断机制:
- 缺少熔断器(Circuit Breaker):连续失败 N 次后暂停调用,避免无效重试
- 高并发场景下(如多个消费事件同时触发),可能产生大量失败请求
证据
bailian_client.py 中的重试和异常处理:
# 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 中的步骤级容错:
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 中的启动容错:
try:
if _api_key and _base_url:
# ... 注册 AI 事件处理器
register_ai_handlers(_dispatcher)
except Exception:
_log.getLogger(__name__).warning(
"AI 事件处理器注册失败,AI 功能不可用", exc_info=True)
建议
-
429 限流降级:捕获
RateLimitError后,尝试返回 ai_cache 中该 (cache_type, site_id, target_id) 的最新缓存结果,并在结果中标注"_from_cache": true,让前端知道这是缓存数据。 -
统一错误码枚举:
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 返回格式异常 -
熔断器:在
BailianClient中增加简单的熔断逻辑——连续失败 5 次后,后续 60 秒内直接返回缓存或静态兜底,不再调用 API。 -
App1 SSE 错误结构化:将
SSEEvent(type="error")扩展为包含error_code字段,前端据此展示不同的用户提示。