feat(evolution): diagnose.py metrics + attribution (Stage 1, #8)
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,508 @@
|
|||||||
|
"""core/evolution/diagnose.py 单元测试。
|
||||||
|
|
||||||
|
覆盖:
|
||||||
|
- 7 个规则指标(空输入、边界、典型值)
|
||||||
|
- extract_json_from_response(三策略 + 拒绝非 dict + 垃圾输入)
|
||||||
|
- attribute_error 瀑布(4 条路径)
|
||||||
|
- question_soft_score(空→None)
|
||||||
|
- aggregate_soft(跳过 None、全 None→None)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from core.evolution.diagnose import (
|
||||||
|
_trigrams,
|
||||||
|
aggregate_soft,
|
||||||
|
attribute_error,
|
||||||
|
calc_budget_usage,
|
||||||
|
calc_confidence_calibration,
|
||||||
|
calc_format_compliance,
|
||||||
|
calc_level_jump_pattern,
|
||||||
|
calc_repeat_visit_rate,
|
||||||
|
calc_search_keyword_repetition,
|
||||||
|
calc_tool_usage,
|
||||||
|
extract_json_from_response,
|
||||||
|
extract_rule_metrics,
|
||||||
|
question_soft_score,
|
||||||
|
)
|
||||||
|
from core.evolution.types import (
|
||||||
|
QuestionMetrics,
|
||||||
|
SpanMetrics,
|
||||||
|
)
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# 工厂函数
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def _make_qm(
|
||||||
|
question_id: str = "q1",
|
||||||
|
video_id: str = "v1",
|
||||||
|
task_type: str = "Action Reasoning",
|
||||||
|
correct: bool = False,
|
||||||
|
format_compliance: float = 1.0,
|
||||||
|
budget_usage: float = 0.5,
|
||||||
|
confidence_calibration: str = "calibrated",
|
||||||
|
repeat_visit_rate: float = 0.0,
|
||||||
|
search_keyword_repetition: float = 0.0,
|
||||||
|
level_jump_pattern: str = "",
|
||||||
|
tool_usage: dict[str, int] | None = None,
|
||||||
|
span_metrics: list[SpanMetrics] | None = None,
|
||||||
|
missed_nodes: list[str] | None = None,
|
||||||
|
skill_adherence: list[Any] | None = None,
|
||||||
|
confirmation_bias: bool | None = None,
|
||||||
|
evidence_sufficient: bool | None = None,
|
||||||
|
degraded: bool = False,
|
||||||
|
) -> QuestionMetrics:
|
||||||
|
"""构造 QuestionMetrics,非关键字段使用合理默认值。"""
|
||||||
|
return QuestionMetrics(
|
||||||
|
question_id=question_id,
|
||||||
|
video_id=video_id,
|
||||||
|
task_type=task_type,
|
||||||
|
correct=correct,
|
||||||
|
format_compliance=format_compliance,
|
||||||
|
budget_usage=budget_usage,
|
||||||
|
confidence_calibration=confidence_calibration,
|
||||||
|
repeat_visit_rate=repeat_visit_rate,
|
||||||
|
search_keyword_repetition=search_keyword_repetition,
|
||||||
|
level_jump_pattern=level_jump_pattern,
|
||||||
|
tool_usage=tool_usage or {},
|
||||||
|
span_metrics=span_metrics or [],
|
||||||
|
missed_nodes=missed_nodes or [],
|
||||||
|
skill_adherence=skill_adherence or [],
|
||||||
|
confirmation_bias=confirmation_bias,
|
||||||
|
evidence_sufficient=evidence_sufficient,
|
||||||
|
degraded=degraded,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_span(
|
||||||
|
step: int = 0,
|
||||||
|
tool_name: str = "view_node",
|
||||||
|
extraction_completeness: float = 0.8,
|
||||||
|
hallucination_rate: float = 0.1,
|
||||||
|
) -> SpanMetrics:
|
||||||
|
"""构造 SpanMetrics 快捷工厂。"""
|
||||||
|
return SpanMetrics(
|
||||||
|
step=step,
|
||||||
|
tool_name=tool_name,
|
||||||
|
extraction_completeness=extraction_completeness,
|
||||||
|
hallucination_rate=hallucination_rate,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# A. 规则指标测试
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestCalcFormatCompliance:
|
||||||
|
"""calc_format_compliance 测试。"""
|
||||||
|
|
||||||
|
def test_empty_returns_one(self) -> None:
|
||||||
|
"""空列表返回 1.0。"""
|
||||||
|
assert calc_format_compliance([]) == 1.0
|
||||||
|
|
||||||
|
def test_all_compliant(self) -> None:
|
||||||
|
"""全部合规返回 1.0。"""
|
||||||
|
raw = json.dumps({"reflect": {}, "plan": {}, "action": {}})
|
||||||
|
assert calc_format_compliance([raw, raw]) == 1.0
|
||||||
|
|
||||||
|
def test_none_compliant(self) -> None:
|
||||||
|
"""全部不合规返回 0.0。"""
|
||||||
|
assert calc_format_compliance(["not json", '{"foo": 1}']) == 0.0
|
||||||
|
|
||||||
|
def test_partial_compliance(self) -> None:
|
||||||
|
"""部分合规返回正确比例。"""
|
||||||
|
good = json.dumps({"reflect": {}, "plan": {}, "action": {}})
|
||||||
|
bad = json.dumps({"reflect": {}, "plan": {}})
|
||||||
|
assert calc_format_compliance([good, bad]) == 0.5
|
||||||
|
|
||||||
|
|
||||||
|
class TestCalcBudgetUsage:
|
||||||
|
"""calc_budget_usage 测试。"""
|
||||||
|
|
||||||
|
def test_typical(self) -> None:
|
||||||
|
"""典型值。"""
|
||||||
|
assert calc_budget_usage(5, 10) == 0.5
|
||||||
|
|
||||||
|
def test_full_budget(self) -> None:
|
||||||
|
"""用满预算。"""
|
||||||
|
assert calc_budget_usage(10, 10) == 1.0
|
||||||
|
|
||||||
|
def test_zero_steps(self) -> None:
|
||||||
|
"""未使用步数。"""
|
||||||
|
assert calc_budget_usage(0, 10) == 0.0
|
||||||
|
|
||||||
|
def test_zero_max_steps_raises(self) -> None:
|
||||||
|
"""max_steps=0 应抛出 ZeroDivisionError(P5: 不掩盖错误)。"""
|
||||||
|
with pytest.raises(ZeroDivisionError):
|
||||||
|
calc_budget_usage(5, 0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCalcConfidenceCalibration:
|
||||||
|
"""calc_confidence_calibration 测试。"""
|
||||||
|
|
||||||
|
def test_high_conf_wrong(self) -> None:
|
||||||
|
"""高置信度答错。"""
|
||||||
|
assert calc_confidence_calibration(0.7, correct=False) == "high_conf_wrong"
|
||||||
|
assert calc_confidence_calibration(0.9, correct=False) == "high_conf_wrong"
|
||||||
|
|
||||||
|
def test_low_conf_right(self) -> None:
|
||||||
|
"""低置信度答对。"""
|
||||||
|
assert calc_confidence_calibration(0.3, correct=True) == "low_conf_right"
|
||||||
|
assert calc_confidence_calibration(0.49, correct=True) == "low_conf_right"
|
||||||
|
|
||||||
|
def test_calibrated(self) -> None:
|
||||||
|
"""正常校准。"""
|
||||||
|
assert calc_confidence_calibration(0.5, correct=True) == "calibrated"
|
||||||
|
assert calc_confidence_calibration(0.7, correct=True) == "calibrated"
|
||||||
|
assert calc_confidence_calibration(0.3, correct=False) == "calibrated"
|
||||||
|
|
||||||
|
def test_boundary_high(self) -> None:
|
||||||
|
"""边界值: 0.7 答错。"""
|
||||||
|
assert calc_confidence_calibration(0.7, correct=False) == "high_conf_wrong"
|
||||||
|
|
||||||
|
def test_boundary_low(self) -> None:
|
||||||
|
"""边界值: 0.5 答对不算 low_conf_right。"""
|
||||||
|
assert calc_confidence_calibration(0.5, correct=True) == "calibrated"
|
||||||
|
|
||||||
|
|
||||||
|
class TestCalcRepeatVisitRate:
|
||||||
|
"""calc_repeat_visit_rate 测试。"""
|
||||||
|
|
||||||
|
def test_empty(self) -> None:
|
||||||
|
"""空列表返回 0.0。"""
|
||||||
|
assert calc_repeat_visit_rate([]) == 0.0
|
||||||
|
|
||||||
|
def test_no_repeats(self) -> None:
|
||||||
|
"""无重复。"""
|
||||||
|
assert calc_repeat_visit_rate(["a", "b", "c"]) == 0.0
|
||||||
|
|
||||||
|
def test_all_same(self) -> None:
|
||||||
|
"""全重复。"""
|
||||||
|
rate = calc_repeat_visit_rate(["a", "a", "a"])
|
||||||
|
assert abs(rate - (1 - 1 / 3)) < 1e-9
|
||||||
|
|
||||||
|
def test_partial_repeats(self) -> None:
|
||||||
|
"""部分重复。"""
|
||||||
|
rate = calc_repeat_visit_rate(["a", "b", "a"])
|
||||||
|
assert abs(rate - (1 - 2 / 3)) < 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
class TestTrigrams:
|
||||||
|
"""_trigrams 辅助函数测试。"""
|
||||||
|
|
||||||
|
def test_short_string(self) -> None:
|
||||||
|
"""不足 3 字符返回空集。"""
|
||||||
|
assert _trigrams("ab") == set()
|
||||||
|
assert _trigrams("") == set()
|
||||||
|
|
||||||
|
def test_exact_three(self) -> None:
|
||||||
|
"""恰好 3 字符。"""
|
||||||
|
assert _trigrams("abc") == {"abc"}
|
||||||
|
|
||||||
|
def test_longer(self) -> None:
|
||||||
|
"""多字符。"""
|
||||||
|
assert _trigrams("abcd") == {"abc", "bcd"}
|
||||||
|
|
||||||
|
|
||||||
|
class TestCalcSearchKeywordRepetition:
|
||||||
|
"""calc_search_keyword_repetition 测试。"""
|
||||||
|
|
||||||
|
def test_single_query(self) -> None:
|
||||||
|
"""单条查询返回 0.0。"""
|
||||||
|
assert calc_search_keyword_repetition(["hello"]) == 0.0
|
||||||
|
|
||||||
|
def test_empty(self) -> None:
|
||||||
|
"""空列表返回 0.0。"""
|
||||||
|
assert calc_search_keyword_repetition([]) == 0.0
|
||||||
|
|
||||||
|
def test_identical_queries(self) -> None:
|
||||||
|
"""完全相同的连续查询,Jaccard=1.0。"""
|
||||||
|
assert calc_search_keyword_repetition(["hello world", "hello world"]) == 1.0
|
||||||
|
|
||||||
|
def test_disjoint_queries(self) -> None:
|
||||||
|
"""完全不同的查询,Jaccard 接近 0。"""
|
||||||
|
score = calc_search_keyword_repetition(["aaa", "zzz"])
|
||||||
|
assert score == 0.0
|
||||||
|
|
||||||
|
def test_takes_max_across_pairs(self) -> None:
|
||||||
|
"""取连续对的最大值。"""
|
||||||
|
score = calc_search_keyword_repetition(["aaa", "zzz", "zzz"])
|
||||||
|
assert score == 1.0 # 第二对完全相同
|
||||||
|
|
||||||
|
|
||||||
|
class TestCalcLevelJumpPattern:
|
||||||
|
"""calc_level_jump_pattern 测试。"""
|
||||||
|
|
||||||
|
def test_empty(self) -> None:
|
||||||
|
"""空列表返回空字符串。"""
|
||||||
|
assert calc_level_jump_pattern([]) == ""
|
||||||
|
|
||||||
|
def test_typical(self) -> None:
|
||||||
|
"""典型节点 ID 序列。"""
|
||||||
|
result = calc_level_jump_pattern(["vid_L1_001", "vid_L2_003", "vid_L3_005"])
|
||||||
|
assert result == "L1→L2→L3"
|
||||||
|
|
||||||
|
def test_no_match(self) -> None:
|
||||||
|
"""无匹配的节点 ID 被跳过。"""
|
||||||
|
assert calc_level_jump_pattern(["no_level_here"]) == ""
|
||||||
|
|
||||||
|
def test_mixed(self) -> None:
|
||||||
|
"""混合匹配和非匹配。"""
|
||||||
|
result = calc_level_jump_pattern(["vid_L2_001", "bad_id", "vid_L1_003"])
|
||||||
|
assert result == "L2→L1"
|
||||||
|
|
||||||
|
|
||||||
|
class TestCalcToolUsage:
|
||||||
|
"""calc_tool_usage 测试。"""
|
||||||
|
|
||||||
|
def test_empty(self) -> None:
|
||||||
|
"""空列表返回空字典。"""
|
||||||
|
assert calc_tool_usage([]) == {}
|
||||||
|
|
||||||
|
def test_counts(self) -> None:
|
||||||
|
"""正确计数。"""
|
||||||
|
result = calc_tool_usage(["view_node", "search_similar", "view_node"])
|
||||||
|
assert result == {"view_node": 2, "search_similar": 1}
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtractRuleMetrics:
|
||||||
|
"""extract_rule_metrics 测试。"""
|
||||||
|
|
||||||
|
def test_basic_extraction(self) -> None:
|
||||||
|
"""基本规则指标提取。"""
|
||||||
|
prediction = {
|
||||||
|
"steps_json": [
|
||||||
|
{
|
||||||
|
"tool_call": {
|
||||||
|
"tool": "view_node",
|
||||||
|
"args": {"node_id": "vid_L1_001"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tool_call": {
|
||||||
|
"tool": "search_similar",
|
||||||
|
"args": {"query": "test query"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"correct": True,
|
||||||
|
}
|
||||||
|
result = extract_rule_metrics(prediction, [], max_steps=10)
|
||||||
|
assert result["budget_usage"] == 0.2
|
||||||
|
assert result["tool_usage"] == {"view_node": 1, "search_similar": 1}
|
||||||
|
assert "L1" in result["level_jump_pattern"]
|
||||||
|
|
||||||
|
def test_empty_prediction(self) -> None:
|
||||||
|
"""空预测。"""
|
||||||
|
result = extract_rule_metrics({}, [], max_steps=10)
|
||||||
|
assert result["format_compliance"] == 1.0
|
||||||
|
assert result["budget_usage"] == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# B. JSON 提取测试
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtractJsonFromResponse:
|
||||||
|
"""extract_json_from_response 测试。"""
|
||||||
|
|
||||||
|
def test_fenced_block(self) -> None:
|
||||||
|
"""从 markdown 代码块提取。"""
|
||||||
|
raw = '```json\n{"key": "value"}\n```'
|
||||||
|
assert extract_json_from_response(raw) == {"key": "value"}
|
||||||
|
|
||||||
|
def test_fenced_block_no_json_tag(self) -> None:
|
||||||
|
"""从无 json 标签的代码块提取。"""
|
||||||
|
raw = '```\n{"key": "value"}\n```'
|
||||||
|
assert extract_json_from_response(raw) == {"key": "value"}
|
||||||
|
|
||||||
|
def test_outermost_braces(self) -> None:
|
||||||
|
"""从最外层花括号提取。"""
|
||||||
|
raw = 'Some text before {"result": 42} and after'
|
||||||
|
assert extract_json_from_response(raw) == {"result": 42}
|
||||||
|
|
||||||
|
def test_non_dict_raises(self) -> None:
|
||||||
|
"""非 dict 类型抛出 ValueError。"""
|
||||||
|
raw = "[1, 2, 3]"
|
||||||
|
with pytest.raises(ValueError, match="无法从 LLM 回复中提取 JSON"):
|
||||||
|
extract_json_from_response(raw)
|
||||||
|
|
||||||
|
def test_garbage_raises(self) -> None:
|
||||||
|
"""完全无法解析的输入抛出 ValueError。"""
|
||||||
|
with pytest.raises(ValueError, match="无法从 LLM 回复中提取 JSON"):
|
||||||
|
extract_json_from_response("this is not json at all !!!")
|
||||||
|
|
||||||
|
def test_nested_braces(self) -> None:
|
||||||
|
"""嵌套花括号正确处理。"""
|
||||||
|
inner = {"nested": {"deep": True}}
|
||||||
|
raw = f"Result: {json.dumps(inner)}"
|
||||||
|
assert extract_json_from_response(raw) == inner
|
||||||
|
|
||||||
|
def test_fenced_block_non_dict_falls_through(self) -> None:
|
||||||
|
"""代码块中是列表时,回退到后续策略。"""
|
||||||
|
raw = '```json\n[1,2,3]\n``` {"fallback": true}'
|
||||||
|
result = extract_json_from_response(raw)
|
||||||
|
assert result == {"fallback": True}
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# C. question_soft_score / aggregate_soft 测试
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestQuestionSoftScore:
|
||||||
|
"""question_soft_score 测试。"""
|
||||||
|
|
||||||
|
def test_empty_returns_none(self) -> None:
|
||||||
|
"""空 span 列表返回 None。"""
|
||||||
|
assert question_soft_score([]) is None
|
||||||
|
|
||||||
|
def test_single_span(self) -> None:
|
||||||
|
"""单个 span 的计算。"""
|
||||||
|
span = _make_span(extraction_completeness=0.8, hallucination_rate=0.2)
|
||||||
|
score = question_soft_score([span])
|
||||||
|
# (0.8 + (1.0 - 0.2)) / 2 = (0.8 + 0.8) / 2 = 0.8
|
||||||
|
assert score is not None
|
||||||
|
assert abs(score - 0.8) < 1e-9
|
||||||
|
|
||||||
|
def test_multiple_spans(self) -> None:
|
||||||
|
"""多个 span 取均值。"""
|
||||||
|
span1 = _make_span(extraction_completeness=1.0, hallucination_rate=0.0)
|
||||||
|
span2 = _make_span(extraction_completeness=0.6, hallucination_rate=0.4)
|
||||||
|
score = question_soft_score([span1, span2])
|
||||||
|
# span1: (1.0 + 1.0) / 2 = 1.0
|
||||||
|
# span2: (0.6 + 0.6) / 2 = 0.6
|
||||||
|
# mean: (1.0 + 0.6) / 2 = 0.8
|
||||||
|
assert score is not None
|
||||||
|
assert abs(score - 0.8) < 1e-9
|
||||||
|
|
||||||
|
def test_perfect_span(self) -> None:
|
||||||
|
"""完美 span。"""
|
||||||
|
span = _make_span(extraction_completeness=1.0, hallucination_rate=0.0)
|
||||||
|
score = question_soft_score([span])
|
||||||
|
assert score == 1.0
|
||||||
|
|
||||||
|
|
||||||
|
class TestAggregateSoft:
|
||||||
|
"""aggregate_soft 测试。"""
|
||||||
|
|
||||||
|
def test_all_none_returns_none(self) -> None:
|
||||||
|
"""全部 None 返回 None。"""
|
||||||
|
assert aggregate_soft([None, None, None]) is None
|
||||||
|
|
||||||
|
def test_empty_returns_none(self) -> None:
|
||||||
|
"""空列表返回 None。"""
|
||||||
|
assert aggregate_soft([]) is None
|
||||||
|
|
||||||
|
def test_skip_none(self) -> None:
|
||||||
|
"""跳过 None 计算均值。"""
|
||||||
|
result = aggregate_soft([0.8, None, 0.6])
|
||||||
|
assert result is not None
|
||||||
|
assert abs(result - 0.7) < 1e-9
|
||||||
|
|
||||||
|
def test_all_valid(self) -> None:
|
||||||
|
"""全部有效。"""
|
||||||
|
result = aggregate_soft([0.5, 0.7, 0.9])
|
||||||
|
assert result is not None
|
||||||
|
assert abs(result - 0.7) < 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# D. attribute_error 瀑布测试
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestAttributeError:
|
||||||
|
"""attribute_error 瀑布规则测试。"""
|
||||||
|
|
||||||
|
def test_extraction_failure_low_completeness(self) -> None:
|
||||||
|
"""avg completeness < 0.5 → extraction_failure。"""
|
||||||
|
qm = _make_qm(
|
||||||
|
span_metrics=[
|
||||||
|
_make_span(extraction_completeness=0.3, hallucination_rate=0.1),
|
||||||
|
_make_span(extraction_completeness=0.4, hallucination_rate=0.1),
|
||||||
|
],
|
||||||
|
missed_nodes=["node_1"], # 有遗漏,但 extraction 优先
|
||||||
|
)
|
||||||
|
result = attribute_error(qm)
|
||||||
|
assert result.error_type == "extraction_failure"
|
||||||
|
assert result.question_id == "q1"
|
||||||
|
|
||||||
|
def test_extraction_failure_high_hallucination(self) -> None:
|
||||||
|
"""max hallucination > 0.5 → extraction_failure。"""
|
||||||
|
qm = _make_qm(
|
||||||
|
span_metrics=[
|
||||||
|
_make_span(extraction_completeness=0.9, hallucination_rate=0.6),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
result = attribute_error(qm)
|
||||||
|
assert result.error_type == "extraction_failure"
|
||||||
|
|
||||||
|
def test_search_failure(self) -> None:
|
||||||
|
"""有遗漏节点 → search_failure。"""
|
||||||
|
qm = _make_qm(
|
||||||
|
span_metrics=[
|
||||||
|
_make_span(extraction_completeness=0.8, hallucination_rate=0.1),
|
||||||
|
],
|
||||||
|
missed_nodes=["node_1", "node_2"],
|
||||||
|
)
|
||||||
|
result = attribute_error(qm)
|
||||||
|
assert result.error_type == "search_failure"
|
||||||
|
|
||||||
|
def test_reasoning_failure(self) -> None:
|
||||||
|
"""evidence_sufficient=True → reasoning_failure。"""
|
||||||
|
qm = _make_qm(
|
||||||
|
span_metrics=[
|
||||||
|
_make_span(extraction_completeness=0.8, hallucination_rate=0.1),
|
||||||
|
],
|
||||||
|
missed_nodes=[],
|
||||||
|
evidence_sufficient=True,
|
||||||
|
)
|
||||||
|
result = attribute_error(qm)
|
||||||
|
assert result.error_type == "reasoning_failure"
|
||||||
|
|
||||||
|
def test_mixed_evidence_none(self) -> None:
|
||||||
|
"""evidence_sufficient=None → mixed。"""
|
||||||
|
qm = _make_qm(
|
||||||
|
span_metrics=[
|
||||||
|
_make_span(extraction_completeness=0.8, hallucination_rate=0.1),
|
||||||
|
],
|
||||||
|
missed_nodes=[],
|
||||||
|
evidence_sufficient=None,
|
||||||
|
)
|
||||||
|
result = attribute_error(qm)
|
||||||
|
assert result.error_type == "mixed"
|
||||||
|
|
||||||
|
def test_mixed_evidence_false(self) -> None:
|
||||||
|
"""evidence_sufficient=False → mixed。"""
|
||||||
|
qm = _make_qm(
|
||||||
|
span_metrics=[
|
||||||
|
_make_span(extraction_completeness=0.8, hallucination_rate=0.1),
|
||||||
|
],
|
||||||
|
missed_nodes=[],
|
||||||
|
evidence_sufficient=False,
|
||||||
|
)
|
||||||
|
result = attribute_error(qm)
|
||||||
|
assert result.error_type == "mixed"
|
||||||
|
|
||||||
|
def test_no_spans_extraction(self) -> None:
|
||||||
|
"""无 span 时 avg_completeness=0 (< 0.5) → extraction_failure。"""
|
||||||
|
qm = _make_qm(span_metrics=[], missed_nodes=["node_x"])
|
||||||
|
result = attribute_error(qm)
|
||||||
|
# _mean([]) = 0.0 < 0.5 → extraction_failure
|
||||||
|
assert result.error_type == "extraction_failure"
|
||||||
|
|
||||||
|
def test_reasoning_failure_type_is_none(self) -> None:
|
||||||
|
"""attribute_error 不设 reasoning_failure_type(由后续阶段补充)。"""
|
||||||
|
qm = _make_qm(evidence_sufficient=True)
|
||||||
|
result = attribute_error(qm)
|
||||||
|
assert result.reasoning_failure_type is None
|
||||||
Reference in New Issue
Block a user