# LLM 错误码对齐设计(retry/skip/abort UX) ## 1. 背景与动机 `api-design.md` §7 错误码表定义了 LLM 相关错误码及其「用户选项」(`retry / skip / abort` / `配置 Key`),但 **engine 层没有暴露任何可供调用方/UI 决策的机器可读信息**: - `ChatResult` / `StructuredResult` 失败时仅有人类可读的 `error: str` - 调用方无法区分「值得重试的网络/超时错误」与「重试无用的配置/结构错误」 - api 层的 `LLM_NETWORK_ERROR` 概念缺失(`LLMNetworkError` 的可重试语义无处映射) 本 backlog 目标:在 **engine 层**暴露结构化的 `error_code`,并让 api-design §7 表与异常树五类型一一对齐,为后续 UI 的 retry/skip/abort 对话框提供唯一事实源。 ## 2. 非目标(明确不包含) - 不实现 UI、不新增 API 端点、不动 state machine - 不改变错误字符串格式与既有字段 - 不实现「按错误码自动重试」的自动化策略(本 backlog 只提供信息,决策权留在调用方) ## 3. 设计 ### 3.1 异常树携带 error_code(事实源) `src/genesis/inference/exceptions.py`:`LLMError` 基类新增类属性,五子类各覆写: ```python class LLMError(Exception): """LLM 调用异常基类。error_code 与 api-design §7 错误码表一一对应。""" error_code: str | None = None class LLMNetworkError(LLMError): error_code = "LLM_NETWORK_ERROR" class LLMTimeoutError(LLMError): error_code = "LLM_TIMEOUT" class LLMNotConfiguredError(LLMError): error_code = "LLM_NOT_CONFIGURED" class LLMResponseError(LLMError): error_code = "LLM_PARSE_ERROR" ``` - `error_code` 是 docs api-design §7 表中「错误码」列的机器可读值 - 新增未来异常类型时**必须**覆写 `error_code`(否则走 `None`,绝不伪造) ### 3.2 结果对象携带 error_code `ChatResult` / `StructuredResult` 增加字段(默认 `None`,向后兼容): ```python @dataclass class ChatResult: ... error_code: str | None = None # 失败时的 api §7 错误码;成功为 None @dataclass class StructuredResult: ... error_code: str | None = None # 失败/parse_error 时的错误码;成功为 None ``` ### 3.3 engine 透传(同源语义) - `chat()` 失败路径:`error_code` 与 `error` 字符串**同源**——取最后一次失败(fallback 链)的 `LLMError.error_code` - `chat_structured()` 解析耗尽:`status="parse_error"` + `error_code="LLM_PARSE_ERROR"`(来自导致解析失败的最后一次调用;若无 LLM 异常则取最近一次 JSONDecodeError 对应的 `LLM_PARSE_ERROR`) - `chat_structured()` 网络/超时失败:`status="failed"` + `error_code=<对应异常>` - 退化:异常非 `LLMError`(不捕获、直接冒出)→ engine 不产生 error_code(`None`),由上层兜底 ### 3.4 error_code 语义表(与 api §7 对齐) | 异常 | error_code | api §7 用户选项 | retryable 语义 | |------|-----------|----------------|---------------| | `LLMTimeoutError` | `LLM_TIMEOUT` | retry / skip / abort | ✅ 可重试 | | `LLMNetworkError` | `LLM_NETWORK_ERROR` | retry / skip / abort | ✅ 可重试 | | `LLMResponseError` | `LLM_PARSE_ERROR` | retry | ⚠️ 结构化可重试;chat 不建议 | | `LLMNotConfiguredError` | `LLM_NOT_CONFIGURED` | 配置 Key | ❌ 配置问题 | | 其他异常 | `None` | — | 上层兜底(`INTERNAL_ERROR` 语义由 API 层负责) | > 注:api-design §7 表仍保留 `INTERNAL_ERROR` 行作为 API 层兜底(非 LLMError 异常由上层翻译),engine 层不产生该值。 ### 3.5 文档修订 `docs/api-design.md` §7 错误码表新增行(对齐异常树): ``` | `LLM_NETWORK_ERROR` | 502 | 网络失败/5xx 重试耗尽 | retry / skip / abort | exceptions.LLMNetworkError | ``` 并对 `LLM_PARSE_ERROR` / `LLM_TIMEOUT` 行的「来源」列保持与异常类型对应(已存在,无需改)。 ## 4. 测试策略 - `tests/test_inference_errors.py`:各异常 `error_code` 类属性恒定值断言 - `tests/test_inference_types.py`:`ChatResult`/`StructuredResult` 新字段默认 `None` - `tests/test_inference_engine.py`: - `chat()` 失败时 `error_code == "LLM_TIMEOUT"` / `"LLM_NETWORK_ERROR"` / `"LLM_NOT_CONFIGURED"`(对应脚本) - 主模型超时 + 备用网络失败 → `error_code == "LLM_NETWORK_ERROR"`(同源:取最后一次) - `chat_structured()` parse_error → `error_code == "LLM_PARSE_ERROR"`;网络失败 → `error_code == "LLM_NETWORK_ERROR"` - 全量回归 `python -m pytest -v`(覆盖红线 fail_under=99,当前基线 119 passed / 100%) ## 5. 验收标准 1. 五异常 `error_code` 与 api-design §7 一一对应 2. `ChatResult`/`StructuredResult` 失败路径返回结构化 `error_code`,成功路径为 `None` 3. `error_code` 与 `error` 同源(取最后一次失败异常),有测试证明 4. api-design §7 补 `LLM_NETWORK_ERROR` 行 5. 全量回归 ≥ 119 passed / 覆盖率 100% 6. 零新增依赖、零 UI 变更、向后兼容(旧构造不含 error_code 字段仍工作) ## 6. 涉及文件 - 修改:`src/genesis/inference/exceptions.py`、`src/genesis/inference/types.py`、`src/genesis/inference/engine.py` - 修改:`docs/api-design.md`(§7 表格) - 测试:`tests/test_inference_errors.py`、`tests/test_inference_types.py`、`tests/test_inference_engine.py` ## 7. 版本与状态 | 版本 | 日期 | 说明 | |------|------|------| | v1.0 | 2026-08-09 | 定稿版(含 2 项设计健全性修订:保留 INTERNAL_ERROR 兜底行、error_code 与 error 同源明写) |