feat: engine 透传 error_code(与 error 同源取最后一次异常)
This commit is contained in:
@@ -54,3 +54,4 @@
|
||||
| 2026-08-09 11:20 | 反馈迭代 | Web UI 设计评审(v1.0→v1.1,7 项发现全修):以大赛手册+api-design 为权威基准审查 web-ui-design.md——F1 上传区补「概要设计做成说明书」独立 write_instruction file_type(api-design §2.2 连带扩展)+ 命名统一;F2 §4.1 任务队列 Redis 写死→抽象 TaskQueue(InMemory 默认/Redis/Valkey 可选);F3 新增设置页 §3.6;F4 预览路径改 ContentBlock→chapter_html+docx;F5 生成页新增规则冲突卡片(WS conflict_pending→conflicts/{id}/resolve);F6 新增会话历史页 §3.7;F7 新增 §7 无障碍与设计规范。同步 design.md §8 镜像(上传区/设置/队列/会话/冲突卡片/历史简版//history 命令);产出评审报告 docs/web-ui-design-review-v1.1.md;docs/design.md、docs/api-design.md 连带修订 | docs/web-ui-design.md, docs/web-ui-design-review-v1.1.md, docs/api-design.md, docs/design.md, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
|
||||
| 2026-08-09 11:45 | Agent 实现 | LLM 错误码对齐 Task1:异常树 error_code 类属性(LLMError 基类 None + four 子类 four 码,对齐 api-design §7),沿用现有中文 docstring 未改动。TDD RED(AttributeError: type object 'LLMError' has no attribute 'error_code')→ GREEN(聚焦 4 passed,exceptions.py 100%);聚焦单文件运行触发全库 fail-under=99 coverage exit 1 为基线行为(总覆盖 9.95%),与本次改动无关;加入 _AI_USAGE_LOG.md 提交 | src/genesis/inference/exceptions.py, tests/test_inference_errors.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
|
||||
| 2026-08-09 13:42 | Agent 实现 | LLM 错误码对齐 Task2:ChatResult/StructuredResult 增加 error_code 字段(默认 None 向后兼容)。TDD RED(AttributeError: 'ChatResult' object has no attribute 'error_code')→ GREEN(聚焦 8 passed,types.py 100%);pytest 全量 123 passed 覆盖 100.00%(799 stmts/188 br),fail_under=99 达标;提交见 git log | src/genesis/inference/types.py, tests/test_inference_types.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
|
||||
| 2026-08-09 14:02 | Agent 实现 | LLM 错误码对齐 Task3:engine 失败/parse_error 路径透传 error_code(与 error 同源取最后一次异常)。chat() 循环记 last_error_code=exc.error_code,失败返回 error_code=last_error_code;chat_structured() 三路径:JSONDecodeError 分支置 last_error_code="LLM_PARSE_ERROR"(解析耗尽→parse_error)、LLMError 分支 early return 带 error_code=exc.error_code、循环后 parse_error 用 last_error_code。TDD RED(5 failed,error_code=None)→ GREEN(聚焦 22 passed);pytest 全量 128 passed 覆盖 100.00%(803 stmts/188 br),fail_under=99 达标;提交见 git log | src/genesis/inference/engine.py, tests/test_inference_engine.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
|
||||
|
||||
@@ -101,6 +101,7 @@ class InferenceEngine:
|
||||
|
||||
start = time.monotonic()
|
||||
last_error: str | None = None
|
||||
last_error_code: str | None = None
|
||||
for idx, name in enumerate(self._model_names(model)):
|
||||
try:
|
||||
text, usage = self._call(
|
||||
@@ -115,12 +116,13 @@ class InferenceEngine:
|
||||
)
|
||||
except LLMError as exc:
|
||||
last_error = str(exc)
|
||||
last_error_code = exc.error_code # 同源:取最后一次失败异常
|
||||
|
||||
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,
|
||||
status="failed", error=last_error, error_code=last_error_code,
|
||||
)
|
||||
|
||||
def chat_structured(
|
||||
@@ -144,6 +146,7 @@ class InferenceEngine:
|
||||
attempts = 0
|
||||
last_raw = ""
|
||||
last_error: str | None = None
|
||||
last_error_code: str | None = None
|
||||
|
||||
while attempts <= retry_count:
|
||||
attempts += 1
|
||||
@@ -166,6 +169,7 @@ class InferenceEngine:
|
||||
)
|
||||
except json.JSONDecodeError as exc:
|
||||
last_error = f"JSON 解析失败: {exc}"
|
||||
last_error_code = "LLM_PARSE_ERROR"
|
||||
# 带错误信息重试
|
||||
base_rendered = base_rendered + f"\n\n上次解析失败:{exc}。请重新输出合法 JSON。"
|
||||
except LLMError as exc:
|
||||
@@ -175,7 +179,7 @@ class InferenceEngine:
|
||||
prompt_version=getattr(prompt, "version", "inline"),
|
||||
usage=TokenUsage(),
|
||||
duration_ms=int((time.monotonic() - start) * 1000),
|
||||
status="failed", error=str(exc),
|
||||
status="failed", error=str(exc), error_code=exc.error_code,
|
||||
)
|
||||
|
||||
return StructuredResult(
|
||||
@@ -184,5 +188,5 @@ class InferenceEngine:
|
||||
prompt_version=getattr(prompt, "version", "inline"),
|
||||
usage=TokenUsage(),
|
||||
duration_ms=int((time.monotonic() - start) * 1000),
|
||||
status="parse_error", error=last_error,
|
||||
status="parse_error", error=last_error, error_code=last_error_code,
|
||||
)
|
||||
@@ -81,6 +81,65 @@ def test_chat_all_failed_returns_failed():
|
||||
assert r.status == "failed" and r.error
|
||||
|
||||
|
||||
# ---------- error_code 透传(与 error 同源取最后一次异常) ----------
|
||||
|
||||
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"
|
||||
|
||||
|
||||
def test_chat_plain_string_prompt():
|
||||
eng = make_engine(FakeLLMClient([("ok", "hi")]))
|
||||
r = eng.chat(session_id="s1", prompt="直接文本", variables={})
|
||||
|
||||
Reference in New Issue
Block a user