15 KiB
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.tomlfail_under = 99(当前基线 119 passed / 100.00%),新增分支需足量测试 - 全量回归:
python -m pytest -v;聚焦:python -m pytest tests/<file> -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 已引入):
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 中为每个类加类属性:
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: 提交
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/StructuredResultdataclass(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 追加:
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 字段后追加:
@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: 提交
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 2ChatResult.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):
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 脚本不含该异常);文件需已 importPrompt(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 行附近):
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 行):
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: 提交
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 行后插入新行:
| `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: 提交
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 行(对齐异常树错误码)