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:
@@ -118,7 +118,8 @@ def test_chat_failed_error_code_not_configured():
|
||||
|
||||
|
||||
def test_chat_structured_parse_error_code():
|
||||
client = FakeLLMClient([("parse_fail", ""), ("parse_fail", "")])
|
||||
# 两轮降级链(primary+fallback)均解析失败 → parse_error(T2:解析重试走降级链)
|
||||
client = FakeLLMClient([("parse_fail", ""), ("parse_fail", ""), ("parse_fail", ""), ("parse_fail", "")])
|
||||
eng = InferenceEngine(client=client, models=Models())
|
||||
r = eng.chat_structured(
|
||||
session_id="s1", prompt=Prompt(name="p", version="v1", template="提取"),
|
||||
@@ -129,11 +130,12 @@ def test_chat_structured_parse_error_code():
|
||||
|
||||
|
||||
def test_chat_structured_failed_error_code_network():
|
||||
client = FakeLLMClient([("raise_network", "")])
|
||||
# 降级链两个模型都网络失败 → failed + LLM_NETWORK_ERROR
|
||||
client = FakeLLMClient([("raise_network", ""), ("raise_network", "")])
|
||||
eng = InferenceEngine(client=client, models=Models())
|
||||
r = eng.chat_structured(
|
||||
session_id="s1", prompt=Prompt(name="p", version="v1", template="提取"),
|
||||
variables={}, schema={},
|
||||
variables={}, schema={}, retry_count=0,
|
||||
)
|
||||
assert r.status == "failed"
|
||||
assert r.error_code == "LLM_NETWORK_ERROR"
|
||||
@@ -184,17 +186,19 @@ def test_chat_structured_ok():
|
||||
|
||||
|
||||
def test_chat_structured_retry_parse():
|
||||
# 首选模型解析失败 → 降级链备用模型成功 → fallback(T2)
|
||||
client = FakeLLMClient([("parse_fail", ""), ("ok", '{"a": 2}')])
|
||||
eng = make_engine(client)
|
||||
r = eng.chat_structured(
|
||||
session_id="s1", prompt=Prompt(name="p", version="v1", template="提取"),
|
||||
variables={}, schema={},
|
||||
)
|
||||
assert r.status == "ok" and r.data == {"a": 2} and r.parse_attempts == 2
|
||||
assert r.status == "fallback" and r.data == {"a": 2} and r.parse_attempts == 1
|
||||
|
||||
|
||||
def test_chat_structured_parse_error_returns_raw():
|
||||
client = FakeLLMClient([("parse_fail", ""), ("parse_fail", "")])
|
||||
# 两轮降级链均解析失败 → parse_error,raw_text 为最后一次输出
|
||||
client = FakeLLMClient([("parse_fail", ""), ("parse_fail", ""), ("parse_fail", ""), ("parse_fail", "")])
|
||||
eng = make_engine(client)
|
||||
r = eng.chat_structured(
|
||||
session_id="s1", prompt=Prompt(name="p", version="v1", template="提取"),
|
||||
@@ -206,11 +210,12 @@ def test_chat_structured_parse_error_returns_raw():
|
||||
|
||||
|
||||
def test_chat_structured_failed_on_network():
|
||||
client = FakeLLMClient([("raise_network", "")])
|
||||
# 降级链两个模型都网络失败 → failed
|
||||
client = FakeLLMClient([("raise_network", ""), ("raise_network", "")])
|
||||
eng = make_engine(client)
|
||||
r = eng.chat_structured(
|
||||
session_id="s1", prompt=Prompt(name="p", version="v1", template="提取"),
|
||||
variables={}, schema={},
|
||||
variables={}, schema={}, retry_count=0,
|
||||
)
|
||||
assert r.status == "failed"
|
||||
|
||||
@@ -321,7 +326,7 @@ def test_chat_structured_empty_schema_no_hint():
|
||||
# ---------- T1: chat_structured 真 schema 校验(jsonschema) ----------
|
||||
|
||||
def test_chat_structured_schema_violation_retries():
|
||||
"""返回不合 schema 的 JSON 时带错误信息重试;第二次合法 → ok。"""
|
||||
"""返回不合 schema 的 JSON 时带错误信息重试;降级链备用模型成功 → fallback(T1+T2)。"""
|
||||
client = FakeLLMClient([("ok", '{"a": "not_a_number"}'), ("ok", '{"a": 2}')])
|
||||
eng = make_engine(client)
|
||||
r = eng.chat_structured(
|
||||
@@ -329,14 +334,14 @@ def test_chat_structured_schema_violation_retries():
|
||||
variables={},
|
||||
schema={"type": "object", "properties": {"a": {"type": "number"}}, "required": ["a"]},
|
||||
)
|
||||
assert r.status == "ok" and r.data == {"a": 2} and r.parse_attempts == 2
|
||||
# 第二次调用带上次校验错误信息(重试提示)
|
||||
assert r.status == "fallback" and r.data == {"a": 2} and r.parse_attempts == 1
|
||||
# 降级链第二次调用(备用模型)带上次校验错误信息(重试提示)
|
||||
assert "校验失败" in client.calls[1]["messages"][0]
|
||||
|
||||
|
||||
def test_chat_structured_schema_violation_parse_error():
|
||||
"""全部返回不合 schema 的 JSON → parse_error,error 含校验详情。"""
|
||||
client = FakeLLMClient([("ok", '{"a": "bad"}'), ("ok", '{"a": "bad"}')])
|
||||
"""两轮降级链均返回不合 schema 的 JSON → parse_error,error 含校验详情。"""
|
||||
client = FakeLLMClient([("ok", '{"a": "bad"}'), ("ok", '{"a": "bad"}'), ("ok", '{"a": "bad"}'), ("ok", '{"a": "bad"}')])
|
||||
eng = make_engine(client)
|
||||
r = eng.chat_structured(
|
||||
session_id="s1", prompt=Prompt(name="p", version="v1", template="提取"),
|
||||
@@ -361,6 +366,44 @@ def test_chat_structured_schema_valid_passes_without_retry():
|
||||
assert r.status == "ok" and r.data == {"a": 1} and r.parse_attempts == 1
|
||||
|
||||
|
||||
# ---------- T2: 解析重试降级链 + 模型名局部变量 ----------
|
||||
|
||||
def test_chat_structured_parse_retry_uses_fallback():
|
||||
"""首选模型解析失败后,重试走降级链使用备用模型(T2)。"""
|
||||
client = FakeLLMClient([
|
||||
("ok", '{"a": "bad"}'), # 首选模型:不合 schema
|
||||
("ok", '{"a": 2}'), # 备用模型:合法
|
||||
])
|
||||
eng = make_engine(client)
|
||||
r = eng.chat_structured(
|
||||
session_id="s1", prompt=Prompt(name="p", version="v1", template="提取"),
|
||||
variables={},
|
||||
schema={"type": "object", "properties": {"a": {"type": "number"}}, "required": ["a"]},
|
||||
)
|
||||
assert r.status == "fallback"
|
||||
assert r.data == {"a": 2}
|
||||
assert client.calls[0]["model"] == "deepseek-chat"
|
||||
assert client.calls[1]["model"] == "qwen-max"
|
||||
|
||||
|
||||
def test_chat_structured_network_failure_tries_fallback():
|
||||
"""首选模型网络失败时,降级链继续尝试备用模型(T2)。"""
|
||||
client = FakeLLMClient([
|
||||
("raise_network", ""), # 首选模型:网络失败
|
||||
("ok", '{"a": 3}'), # 备用模型:成功
|
||||
])
|
||||
eng = make_engine(client)
|
||||
r = eng.chat_structured(
|
||||
session_id="s1", prompt=Prompt(name="p", version="v1", template="提取"),
|
||||
variables={},
|
||||
schema={"type": "object", "properties": {"a": {"type": "number"}}, "required": ["a"]},
|
||||
)
|
||||
assert r.status == "fallback"
|
||||
assert r.data == {"a": 3}
|
||||
assert client.calls[0]["model"] == "deepseek-chat"
|
||||
assert client.calls[1]["model"] == "qwen-max"
|
||||
|
||||
|
||||
def test_chat_truncation_callback_returns_none_keeps_variables():
|
||||
"""truncate_cb 返回 None 时回退原 variables(覆盖 new_vars is None 分支)。"""
|
||||
def truncate_cb(prompt_text, variables):
|
||||
|
||||
Reference in New Issue
Block a user