From a0a40afd45386eec76cd8561e7eee25132856202 Mon Sep 17 00:00:00 2001 From: lhl Date: Sun, 9 Aug 2026 13:28:45 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20LLM=20=E9=94=99=E8=AF=AF=E7=A0=81?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=E5=AE=9E=E6=96=BD=E8=AE=A1=E5=88=92=EF=BC=88?= =?UTF-8?q?4=20=E4=BB=BB=E5=8A=A1=20TDD=20=E5=88=86=E8=A7=A3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-08-09-llm-errorcode-alignment.md | 391 ++++++++++++++++++ 1 file changed, 391 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-09-llm-errorcode-alignment.md diff --git a/docs/superpowers/plans/2026-08-09-llm-errorcode-alignment.md b/docs/superpowers/plans/2026-08-09-llm-errorcode-alignment.md new file mode 100644 index 0000000..dd922f2 --- /dev/null +++ b/docs/superpowers/plans/2026-08-09-llm-errorcode-alignment.md @@ -0,0 +1,391 @@ +# LLM 错误码对齐(error_code)实施计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 在 engine 层暴露结构化 `error_code`(对齐 api-design §7),使调用方/UI 可依据错误码执行 retry/skip/abort/配置决策。 + +**Architecture:** 单一事实源 = `LLMError.error_code` 类属性(五异常覆写);`ChatResult`/`StructuredResult` 增加 `error_code` 字段透传;engine 失败路径取最后一次异常的同源 error_code;api-design §7 补 `LLM_NETWORK_ERROR` 行。 + +**Tech Stack:** Python 3.11+、pytest(覆盖红线 fail_under=99)、dataclass + +## Global Constraints + +- 项目为中文交流:注释、docstring、commit 消息用中文;标识符英文 +- 零真实网络:单测一律注入 FakeLLMClient / MockTransport;`DEEPSEEK_API_KEY` 不落代码 +- 覆盖红线:`pyproject.toml` `fail_under = 99`(当前基线 119 passed / 100.00%),新增分支需足量测试 +- 全量回归:`python -m pytest -v`;聚焦:`python -m pytest tests/ -v` +- 提交消息:`feat:` / `test:` / `docs:` + 简中文描述 +- 每次修改后追加 `_AI_USAGE_LOG.md`(范式步骤:"Agent 实现"/"测试验证",模型:"deepseek-v4-flash-free") +- 本仓库工作在 master 分支(项目惯例);Windows/PowerShell 环境 +- Spec: `docs/superpowers/specs/2026-08-09-llm-errorcode-alignment-design.md` + +--- + +### Task 1: 异常树携带 error_code(exceptions.py) + +**Files:** +- Modify: `src/genesis/inference/exceptions.py` +- Test: `tests/test_inference_errors.py` + +**Interfaces:** +- Consumes: 现有 `LLMError`/`LLMNetworkError`/`LLMTimeoutError`/`LLMNotConfiguredError`/`LLMResponseError`(已存在) +- Produces: 每个异常类新增类属性 `error_code: str | None`——`LLMNetworkError.error_code == "LLM_NETWORK_ERROR"`、`LLMTimeoutError.error_code == "LLM_TIMEOUT"`、`LLMNotConfiguredError.error_code == "LLM_NOT_CONFIGURED"`、`LLMResponseError.error_code == "LLM_PARSE_ERROR"`、基类 `LLMError.error_code is None` + +- [ ] **Step 1: 写失败测试** + +在 `tests/test_inference_errors.py` 追加(import 上方 `LLMError` 已引入): + +```python +def test_error_code_class_attributes(): + # error_code 与 api-design §7 错误码表一一对应(机器可读事实源) + assert LLMError.error_code is None + assert LLMTimeoutError.error_code == "LLM_TIMEOUT" + assert LLMNetworkError.error_code == "LLM_NETWORK_ERROR" + assert LLMNotConfiguredError.error_code == "LLM_NOT_CONFIGURED" + assert LLMResponseError.error_code == "LLM_PARSE_ERROR" + + +def test_error_code_inherited_by_instance(): + e = LLMNetworkError("network") + assert e.error_code == "LLM_NETWORK_ERROR" +``` + +- [ ] **Step 2: 运行验证失败** + +Run: `python -m pytest tests/test_inference_errors.py -v` +Expected: FAIL(`AttributeError: type object 'LLMError' has no attribute 'error_code'`) + +- [ ] **Step 3: 实现** + +在 `src/genesis/inference/exceptions.py` 中为每个类加类属性: + +```python +class LLMError(Exception): + """LLM 调用相关的异常基类(api-design §7 映射基底)""" + error_code: str | None = None # api §7 错误码(机器可读);新增子类必须覆写 + + +class LLMNetworkError(LLMError): + """网络失败 / 5xx 重试耗尽(可重试语义)""" + error_code = "LLM_NETWORK_ERROR" + + +class LLMTimeoutError(LLMError): + """LLM 调用超时(api-error: LLM_TIMEOUT 502)""" + error_code = "LLM_TIMEOUT" + + +class LLMNotConfiguredError(LLMError): + """Key / 模型缺失(api-error: LLM_NOT_CONFIGURED 503)""" + error_code = "LLM_NOT_CONFIGURED" + + +class LLMResponseError(LLMError): + """响应结构损坏(JSON 解析失败等)""" + error_code = "LLM_PARSE_ERROR" +``` + +- [ ] **Step 4: 运行验证通过** + +Run: `python -m pytest tests/test_inference_errors.py -v` +Expected: PASS(4 passed) + +- [ ] **Step 5: 追加 `_AI_USAGE_LOG.md`** + +范式步骤:"Agent 实现",摘要:异常树 error_code 类属性(五类型 + 基类 None)。 + +- [ ] **Step 6: 提交** + +```bash +git add src/genesis/inference/exceptions.py tests/test_inference_errors.py _AI_USAGE_LOG.md +git commit -m "feat: 异常树 error_code 类属性(对齐 api-design §7 错误码)" +``` + +--- + +### Task 2: 结果对象 error_code 字段(types.py) + +**Files:** +- Modify: `src/genesis/inference/types.py` +- Test: `tests/test_inference_types.py` + +**Interfaces:** +- Consumes: 现有 `ChatResult` / `StructuredResult` dataclass(Task 1 的 error_code 常量不直接依赖,但语义对应) +- Produces: `ChatResult.error_code: str | None = None`;`StructuredResult.error_code: str | None = None`(均默认 None,向后兼容) + +- [ ] **Step 1: 写失败测试** + +在 `tests/test_inference_types.py` 追加: + +```python +def test_chat_result_error_code_default_none(): + r = ChatResult( + text="t", model="m", prompt_version="v1", usage=TokenUsage(), + duration_ms=10, status="ok", + ) + assert r.error_code is None + + +def test_structured_result_error_code_default_none(): + r = StructuredResult( + data={"a": 1}, raw_text='{"a":1}', parse_attempts=1, model="m", + prompt_version="v1", usage=TokenUsage(), duration_ms=10, status="ok", + ) + assert r.error_code is None +``` + +- [ ] **Step 2: 运行验证失败** + +Run: `python -m pytest tests/test_inference_types.py -v` +Expected: FAIL(`TypeError: __init__() got an unexpected keyword argument 'error_code'` 或 AttributeError——取决于 dataclass 是否带默认;实际为 `AttributeError: 'ChatResult' object has no attribute 'error_code'`) + +- [ ] **Step 3: 实现** + +在 `src/genesis/inference/types.py` 两个 dataclass 的 `error` 字段后追加: + +```python +@dataclass +class ChatResult: + """chat() 的返回值""" + text: str + model: str + prompt_version: str + usage: TokenUsage + duration_ms: int + status: Literal["ok", "fallback", "failed"] + error: str | None = None + error_code: str | None = None # 失败时的 api §7 错误码;成功为 None + + +@dataclass +class StructuredResult: + """chat_structured() 的返回值(补丁 1:含 status 字段)""" + data: dict + raw_text: str + parse_attempts: int + model: str + prompt_version: str + usage: TokenUsage + duration_ms: int + status: Literal["ok", "fallback", "parse_error", "failed"] + error: str | None = None + error_code: str | None = None # 失败/parse_error 时的错误码;成功为 None +``` + +- [ ] **Step 4: 运行验证通过** + +Run: `python -m pytest tests/test_inference_types.py -v` +Expected: PASS(8 passed) + +- [ ] **Step 5: 追加 `_AI_USAGE_LOG.md`** + +范式步骤:"Agent 实现",摘要:ChatResult/StructuredResult 增加 error_code 字段(默认 None 向后兼容)。 + +- [ ] **Step 6: 提交** + +```bash +git add src/genesis/inference/types.py tests/test_inference_types.py _AI_USAGE_LOG.md +git commit -m "feat: 结果对象 error_code 字段(默认 None 向后兼容)" +``` + +--- + +### Task 3: engine 透传 error_code(同源语义) + +**Files:** +- Modify: `src/genesis/inference/engine.py` +- Test: `tests/test_inference_engine.py` + +**Interfaces:** +- Consumes: Task 1 `LLMError.error_code`;Task 2 `ChatResult.error_code` / `StructuredResult.error_code`;现有 `FakeLLMClient`(`tests/inference_helpers.py`,脚本 `("ok"|"raise_timeout"|"raise_network"|"parse_fail", content)`) +- Produces: `chat()` 失败路径 `ChatResult.error_code` 与 `error` 同源(取最后一次 LLMError);`chat_structured()` 失败/parse_error 路径同源透传 + +- [ ] **Step 1: 写失败测试** + +在 `tests/test_inference_engine.py` 的 `test_chat_all_failed_returns_failed` 附近追加(文件已 import `FakeLLMClient`/`InferenceEngine`/`Models`): + +```python +def test_chat_failed_error_code_timeout(): + # 主模型超时 + 备用也失败:error_code 与 error 同源(取最后一次异常) + client = FakeLLMClient([("raise_timeout", ""), ("raise_timeout", "")]) + eng = InferenceEngine(client=client, models=Models()) + r = eng.chat(session_id="s1", prompt=Prompt(name="p", version="v1", template="t"), variables={}) + assert r.status == "failed" + assert r.error_code == "LLM_TIMEOUT" + + +def test_chat_failed_error_code_network_last(): + # 主模型超时(第一次)、备用网络失败(最后一次)→ error_code 取最后一次 = LLM_NETWORK_ERROR + client = FakeLLMClient([("raise_timeout", ""), ("raise_network", "")]) + eng = InferenceEngine(client=client, models=Models()) + r = eng.chat(session_id="s1", prompt=Prompt(name="p", version="v1", template="t"), variables={}) + assert r.status == "failed" + assert r.error_code == "LLM_NETWORK_ERROR" + + +def test_chat_failed_error_code_not_configured(): + # 备用模型 Key 缺失(最后一次)→ LLM_NOT_CONFIGURED + client = FakeLLMClient([("raise_network", ""), ("raise_timeout", "")]) + # 用自定义异常客户端模拟 NotConfigured + class NotConfiguredClient: + def __init__(self): + self.calls = [] + def chat(self, *, model, messages, temperature, max_tokens): + self.calls.append(model) + from genesis.inference.exceptions import LLMNotConfiguredError + raise LLMNotConfiguredError("no key") + eng = InferenceEngine(client=NotConfiguredClient(), models=Models()) + r = eng.chat(session_id="s1", prompt=Prompt(name="p", version="v1", template="t"), variables={}) + assert r.status == "failed" + assert r.error_code == "LLM_NOT_CONFIGURED" + + +def test_chat_structured_parse_error_code(): + client = FakeLLMClient([("parse_fail", ""), ("parse_fail", "")]) + eng = InferenceEngine(client=client, models=Models()) + r = eng.chat_structured( + session_id="s1", prompt=Prompt(name="p", version="v1", template="提取"), + variables={}, schema={}, retry_count=1, + ) + assert r.status == "parse_error" + assert r.error_code == "LLM_PARSE_ERROR" + + +def test_chat_structured_failed_error_code_network(): + client = FakeLLMClient([("raise_network", "")]) + eng = InferenceEngine(client=client, models=Models()) + r = eng.chat_structured( + session_id="s1", prompt=Prompt(name="p", version="v1", template="提取"), + variables={}, schema={}, + ) + assert r.status == "failed" + assert r.error_code == "LLM_NETWORK_ERROR" +``` + +> 说明:`test_chat_failed_error_code_not_configured` 使用本地 NotConfiguredClient(FakeLLMClient 脚本不含该异常);文件需已 import `Prompt`(`from genesis.inference.types import Prompt`——若 Task 2 前是 `ChatMessage, Prompt`,本计划沿用 `Prompt`)。 + +- [ ] **Step 2: 运行验证失败** + +Run: `python -m pytest tests/test_inference_engine.py -v` +Expected: FAIL(新增用例中 `r.error_code` 为 `None` 或 AttributeError——取决于 engine 是否已透传;当前 engine 未透传,断言 `== "LLM_TIMEOUT"` 失败) + +- [ ] **Step 3: 实现** + +在 `src/genesis/inference/engine.py`: + +`chat()` 的失败循环增加 error_code 记录(第 103 行附近): + +```python + start = time.monotonic() + last_error: str | None = None + last_error_code: str | None = None + for idx, name in enumerate(self._model_names(model)): + try: + ... + except LLMError as exc: + last_error = str(exc) + last_error_code = exc.error_code # 同源:取最后一次失败异常 +``` + +并更新失败返回值(第 119-124 行): + +```python + return ChatResult( + text="", model=name, + prompt_version=getattr(prompt, "version", "inline"), + usage=TokenUsage(), duration_ms=int((time.monotonic() - start) * 1000), + status="failed", error=last_error, error_code=last_error_code, + ) +``` + +`chat_structured()`: + +- 循环前 `last_error` 旁加 `last_error_code: str | None = None` +- `except json.JSONDecodeError` 分支:`last_error_code = "LLM_PARSE_ERROR"` +- `except LLMError as exc` 分支的返回值:`status="failed", error=str(exc), error_code=exc.error_code` +- 循环后 parse_error 返回值:`status="parse_error", error=last_error, error_code=last_error_code` + +- [ ] **Step 4: 运行验证通过** + +Run: `python -m pytest tests/test_inference_engine.py -v` +Expected: PASS(22 passed) + +- [ ] **Step 5: 追加 `_AI_USAGE_LOG.md`** + +范式步骤:"Agent 实现",摘要:engine 失败/parse_error 路径透传 error_code(与 error 同源取最后一次异常)。 + +- [ ] **Step 6: 提交** + +```bash +git add src/genesis/inference/engine.py tests/test_inference_engine.py _AI_USAGE_LOG.md +git commit -m "feat: engine 透传 error_code(与 error 同源取最后一次异常)" +``` + +--- + +### Task 4: api-design §7 文档补丁 + +**Files:** +- Modify: `docs/api-design.md`(§7 错误码表) +- Test: 无(纯文档;回归验证全量) + +**Interfaces:** +- Consumes: Task 1 的 `LLM_NETWORK_ERROR` / `LLM_TIMEOUT` / `LLM_PARSE_ERROR` / `LLM_NOT_CONFIGURED` 错误码 +- Produces: api §7 表五条 LLM 错误码全齐,来源列与异常类型一一对应 + +- [ ] **Step 1: 修改错误码表** + +在 `docs/api-design.md` §7 表格的 `LLM_TIMEOUT` 行后插入新行: + +```markdown +| `LLM_TIMEOUT` | 502 | LLM 调用超时 | retry / skip / abort | exceptions.LLMTimeoutError | +| `LLM_NETWORK_ERROR` | 502 | 网络失败/5xx 重试耗尽 | retry / skip / abort | exceptions.LLMNetworkError | +``` + +(保留既有 `LLM_NOT_CONFIGURED` / `LLM_PARSE_ERROR` 行不动;`INTERNAL_ERROR` 行保留为 API 层兜底,来源列无需改。) + +- [ ] **Step 2: 追加 `_AI_USAGE_LOG.md`** + +范式步骤:"Agent 实现",摘要:api-design §7 补 LLM_NETWORK_ERROR 行(对齐异常树 error_code)。 + +- [ ] **Step 3: 全量回归** + +Run: `python -m pytest -v` +Expected: PASS(≥ 124 passed / 100.00% 覆盖,fail_under=99 达标) + +- [ ] **Step 4: 提交** + +```bash +git add docs/api-design.md _AI_USAGE_LOG.md +git commit -m "docs: api §7 补 LLM_NETWORK_ERROR 行(对齐异常树错误码)" +``` + +--- + +## Self-Review + +**1. Spec 覆盖**: +- §3.1 异常树 error_code → Task 1 ✓ +- §3.2 结果对象字段 → Task 2 ✓ +- §3.3 engine 同源透传 → Task 3(含「主模型超时+备用网络失败→取最后一次」测试)✓ +- §3.4 语义表 INTERNAL_ERROR 兜底 → 文档保留(Task 4 说明)✓ +- §3.5 api §7 补行 → Task 4 ✓ +- §4 测试策略 → Task 1(errors)/ Task 2(types)/ Task 3(engine 五用例)✓ +- §5 验收 1-6 → 各任务 Step 4 + Task 4 Step 3 全量回归 ✓ + +**2. 占位符检查**:无 TBD/TODO;每步含完整代码与预期输出。✓ + +**3. 类型一致性**: +- `error_code` 字段名在 types.py / engine.py / 测试一致 ✓ +- `FakeLLMClient` 脚本状态(raise_timeout/raise_network/parse_fail)与既有 helper 一致 ✓ +- `Models()` 类(`primary`/`fallback`)在 engine 测试已定义,Task 3 直接复用 ✓ +- 错误码字符串常量(`LLM_TIMEOUT` 等)与 api §7 表格一致 ✓ + +**4. 依赖顺序**:Task 1(异常常量)→ Task 2(types 字段,不依赖常量)→ Task 3(engine 透传,依赖 1+2)→ Task 4(文档)。✓ + +## 提交消息汇总 + +- Task 1: `feat: 异常树 error_code 类属性(对齐 api-design §7 错误码)` +- Task 2: `feat: 结果对象 error_code 字段(默认 None 向后兼容)` +- Task 3: `feat: engine 透传 error_code(与 error 同源取最后一次异常)` +- Task 4: `docs: api §7 补 LLM_NETWORK_ERROR 行(对齐异常树错误码)` \ No newline at end of file