包含多个会话的累积代码变更: - 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>
129 lines
5.6 KiB
Markdown
129 lines
5.6 KiB
Markdown
# 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` 字段,前端据此展示不同的用户提示。
|