feat(inference): chat_structured 解析重试走降级链(T2 架构审查整改)

- Issue2: 解析重试按 primary→fallback 顺序尝试,不再硬编码首选模型
- Issue10: 提取 names 局部变量,删除 4 处重复 _model_names(None)[0] 调用
- LLMError 不再 early return,继续降级链;全部失败按 last_was_parse_error 区分 parse_error/failed
- 新增 2 用例(解析/网络失败降级 fallback),同步更新 7 个既有用例至降级链语义
- 全量 165 passed / 100.00% 覆盖(941 stmts/242 br),fail_under=99 达标
This commit is contained in:
lhl
2026-08-12 09:36:10 +08:00
parent 25fc472d9b
commit cf9600e437
3 changed files with 97 additions and 54 deletions
+41 -42
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
import json
import time
from typing import Any, Callable
from typing import Any, Callable, Literal
import jsonschema
@@ -144,59 +144,58 @@ class InferenceEngine:
schema_hint = json.dumps(schema, ensure_ascii=False) if schema else ""
base_rendered = rendered + (f'\n\n请输出符合以下 JSON Schema 的 JSON{schema_hint}' if schema_hint else "")
names = self._model_names(None) # 降级链:解析重试也按 primary→fallback 顺序(T2/Issue10
start = time.monotonic()
attempts = 0
last_raw = ""
last_error: str | None = None
last_error_code: str | None = None
last_was_parse_error = False
while attempts <= retry_count:
attempts += 1
try:
text, usage = self._call(
model=self._model_names(None)[0], # 解析重试用首选模型
rendered=base_rendered,
temperature=0.0, max_tokens=4096,
)
last_raw = text
data = json.loads(text)
if schema:
# 真 schema 校验:不合 schema 时按解析失败重试(T1)
jsonschema.validate(instance=data, schema=schema)
return StructuredResult(
data=data, raw_text=text, parse_attempts=attempts,
model=self._model_names(None)[0],
prompt_version=getattr(prompt, "version", "inline"),
usage=usage,
duration_ms=int((time.monotonic() - start) * 1000),
# fallback 语义保留给模型降级;解析重试成功仍为 ok
status="ok",
)
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 jsonschema.ValidationError as exc:
last_error = f"校验失败: {exc.message}"
last_error_code = "LLM_PARSE_ERROR"
# 带校验错误信息重试
base_rendered = base_rendered + f"\n\n上次校验失败:{exc.message}。请重新输出符合 JSON Schema 的 JSON。"
except LLMError as exc:
return StructuredResult(
data={}, raw_text="", parse_attempts=attempts,
model=self._model_names(None)[0],
prompt_version=getattr(prompt, "version", "inline"),
usage=TokenUsage(),
duration_ms=int((time.monotonic() - start) * 1000),
status="failed", error=str(exc), error_code=exc.error_code,
)
for idx, name in enumerate(names):
try:
text, usage = self._call(
model=name,
rendered=base_rendered,
temperature=0.0, max_tokens=4096,
)
last_raw = text
data = json.loads(text)
if schema:
# 真 schema 校验:不合 schema 时按解析失败重试(T1)
jsonschema.validate(instance=data, schema=schema)
return StructuredResult(
data=data, raw_text=text, parse_attempts=attempts,
model=name,
prompt_version=getattr(prompt, "version", "inline"),
usage=usage,
duration_ms=int((time.monotonic() - start) * 1000),
# 首选模型成功为 ok;降级链模型成功为 fallback
status="ok" if idx == 0 else "fallback",
)
except (json.JSONDecodeError, jsonschema.ValidationError) as exc:
last_error = f"解析/校验失败: {exc}"
last_error_code = "LLM_PARSE_ERROR"
last_was_parse_error = True
# 带错误信息继续降级链(备用模型重试时可见)
base_rendered = base_rendered + f"\n\n上次失败:{last_error}。请重新输出合法 JSON。"
except LLMError as exc:
last_error = str(exc)
last_error_code = exc.error_code
last_was_parse_error = False
# 继续降级链尝试下一模型
if last_was_parse_error:
status: Literal["ok", "fallback", "parse_error", "failed"] = "parse_error"
else:
status = "failed"
return StructuredResult(
data={}, raw_text=last_raw, parse_attempts=attempts,
model=self._model_names(None)[0],
model=names[0],
prompt_version=getattr(prompt, "version", "inline"),
usage=TokenUsage(),
duration_ms=int((time.monotonic() - start) * 1000),
status="parse_error", error=last_error, error_code=last_error_code,
status=status, error=last_error, error_code=last_error_code,
)