337 lines
13 KiB
Python

#!/usr/bin/env python3
"""正文五维盲评量表的离线回归测试。"""
from __future__ import annotations
import pathlib
import sys
import unittest
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
SCRIPT_DIR = PROJECT_ROOT / "muse" / "lifecycle" / "quality" / "skills" / "judge" / "评估内容质量" / "scripts"
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
from writer_rubric import ( # noqa: E402
COMMON_DIMENSIONS,
DIMENSIONS,
RUBRIC_POLICY_VERSION,
RUBRIC_PROFILE,
SCENARIO_DIMENSION,
SCENARIO_TYPES,
adjudicate_reviews,
adjudicate_structured_reviews,
deblind_reports,
rubric_for_scenario,
validate_blind_pair,
validate_report,
validate_structured_report,
)
def scorecard(score: float = 8.0) -> dict[str, dict[str, object]]:
"""构造包含四类证据归因的合法五维评分。"""
source_types = ("fine_outline", "historical_prose", "judge_inference")
return {
dimension: {
"score": score,
"evidence": [
{
"sourceType": source_types[index % len(source_types)],
"sourceRef": f"source:{dimension}",
"excerpt": f"证据-{dimension}",
}
],
}
for index, dimension in enumerate(DIMENSIONS)
}
def report(
reviewer_id: str,
scores: dict[str, dict[str, object]] | None = None,
*,
order: tuple[str, ...] = ("blind-1", "blind-2", "blind-3"),
) -> dict[str, object]:
"""构造不携带真实臂名的单评委报告。"""
return {
"profile": RUBRIC_PROFILE,
"reviewerId": reviewer_id,
"sampleId": "deep-space-489",
"blindCandidateId": "blind-1",
"candidateOrder": list(order),
"scores": scores or scorecard(),
}
class WriterRubricValidationTest(unittest.TestCase):
"""验证量表结构与盲评纪律。"""
def test_five_dimensions_use_zero_to_ten_half_steps(self):
self.assertEqual(len(DIMENSIONS), 5)
self.assertEqual(validate_report(report("judge-1", scorecard(0.0))), [])
self.assertEqual(validate_report(report("judge-1", scorecard(10.0))), [])
bad = scorecard(8.0)
bad[DIMENSIONS[0]]["score"] = 8.25
self.assertTrue(any("0.5" in error for error in validate_report(report("judge-1", bad))))
def test_policy_combines_common_dimensions_with_registered_scenario_profile(self):
expected_bands = ["9-10", "7-8.5", "5-6.5", "3-4.5", "0-2.5"]
display_names: set[str] = set()
for scenario in SCENARIO_TYPES:
policy = rubric_for_scenario(scenario)
self.assertEqual(policy["policyVersion"], RUBRIC_POLICY_VERSION)
self.assertEqual(policy["scenarioType"], scenario)
self.assertEqual(policy["dimensions"], list(DIMENSIONS))
self.assertEqual(
[item["dimensionId"] for item in policy["commonDimensions"]],
list(COMMON_DIMENSIONS),
)
for item in policy["commonDimensions"]:
self.assertEqual(
[anchor["scoreBand"] for anchor in item["anchors"]],
expected_bands,
)
scenario_dimension = policy["scenarioDimension"]
self.assertEqual(scenario_dimension["dimensionId"], SCENARIO_DIMENSION)
self.assertEqual(
[anchor["scoreBand"] for anchor in scenario_dimension["anchors"]],
expected_bands,
)
self.assertGreaterEqual(len(scenario_dimension["criteria"]), 4)
display_names.add(scenario_dimension["displayName"])
self.assertEqual(len(display_names), len(SCENARIO_TYPES))
with self.assertRaisesRegex(ValueError, "未登记场景类型"):
rubric_for_scenario("unknown")
def test_each_dimension_requires_typed_evidence(self):
bad = scorecard()
bad[DIMENSIONS[2]]["evidence"] = []
self.assertTrue(any("证据" in error for error in validate_report(report("judge-1", bad))))
bad = scorecard()
bad[DIMENSIONS[3]]["evidence"][0]["sourceType"] = "target_answer"
self.assertTrue(any("来源类型" in error for error in validate_report(report("judge-1", bad))))
hidden_card = scorecard()
hidden_card[DIMENSIONS[0]]["evidence"][0]["sourceType"] = "card_index"
self.assertTrue(
any("来源类型" in error for error in validate_report(report("judge-1", hidden_card)))
)
def test_pair_must_be_independent_blind_and_reverse_ordered(self):
first = report("judge-1")
second = report("judge-2", order=("blind-3", "blind-2", "blind-1"))
self.assertEqual(validate_blind_pair(first, second), [])
same_order = report("judge-2")
self.assertTrue(any("反序" in error for error in validate_blind_pair(first, same_order)))
different_candidate = report(
"judge-2", order=("blind-3", "blind-2", "blind-1")
)
different_candidate["blindCandidateId"] = "blind-2"
self.assertTrue(
any("同一盲化候选" in error for error in validate_blind_pair(first, different_candidate))
)
leaked = report("judge-2", order=("blind-3", "blind-2", "blind-1"))
leaked["arm"] = "C"
self.assertTrue(any("去盲" in error for error in validate_blind_pair(first, leaked)))
def test_deblind_uses_preregistered_mapping_not_position(self):
reports = [
report("judge-1"),
{**report("judge-1"), "blindCandidateId": "blind-3"},
]
mapping = {"blind-1": "C", "blind-2": "A", "blind-3": "B"}
deblinded = deblind_reports(reports, mapping)
self.assertEqual([item["arm"] for item in deblinded], ["C", "B"])
class WriterRubricAdjudicationTest(unittest.TestCase):
"""验证双评稳定性与最多一次第三评委仲裁。"""
def test_gap_above_half_requires_third_reviewer_once(self):
first = report("judge-1", scorecard(8.0))
second_scores = scorecard(8.0)
second_scores[DIMENSIONS[0]]["score"] = 7.0
second = report("judge-2", second_scores, order=("blind-3", "blind-2", "blind-1"))
result = adjudicate_reviews(first, second)
self.assertEqual(result["status"], "needs_third_reviewer")
self.assertEqual(result["unstableDimensions"], [DIMENSIONS[0]])
def test_third_reviewer_uses_median_when_any_stable_pair_exists(self):
first = report("judge-1", scorecard(8.0))
second_scores = scorecard(8.0)
second_scores[DIMENSIONS[1]]["score"] = 6.5
second = report("judge-2", second_scores, order=("blind-3", "blind-2", "blind-1"))
third_scores = scorecard(8.0)
third_scores[DIMENSIONS[1]]["score"] = 7.5
third = report("judge-3", third_scores)
result = adjudicate_reviews(first, second, third)
self.assertEqual(result["status"], "adjudicated_report")
self.assertEqual(result["scores"][DIMENSIONS[1]], 7.5)
def test_third_reviewer_must_bind_same_candidate_and_candidate_set(self):
"""第三评不得用另一候选或漂移后的候选集合参与仲裁。"""
first = report("judge-1", scorecard(8.0))
second_scores = scorecard(8.0)
second_scores[DIMENSIONS[1]]["score"] = 6.5
second = report("judge-2", second_scores, order=("blind-3", "blind-2", "blind-1"))
different_candidate = report("judge-3", scorecard(7.5))
different_candidate["blindCandidateId"] = "blind-2"
candidate_result = adjudicate_reviews(first, second, different_candidate)
self.assertEqual(candidate_result["status"], "invalid_report")
self.assertTrue(any("同一盲化候选" in error for error in candidate_result["errors"]))
different_set = report(
"judge-3",
scorecard(7.5),
order=("blind-1", "blind-2", "blind-4"),
)
set_result = adjudicate_reviews(first, second, different_set)
self.assertEqual(set_result["status"], "invalid_report")
self.assertTrue(any("相同的盲化候选集合" in error for error in set_result["errors"]))
def test_no_stable_pair_marks_sample_invalid(self):
first = report("judge-1", scorecard(8.0))
second_scores = scorecard(8.0)
second_scores[DIMENSIONS[4]]["score"] = 6.5
second = report("judge-2", second_scores, order=("blind-3", "blind-2", "blind-1"))
third_scores = scorecard(8.0)
third_scores[DIMENSIONS[4]]["score"] = 9.5
third = report("judge-3", third_scores)
result = adjudicate_reviews(first, second, third)
self.assertEqual(result["status"], "invalid_unstable")
self.assertEqual(result["unstableDimensions"], [DIMENSIONS[4]])
self.assertNotIn("winner", result)
def structured_report(
reviewer_id: str,
*,
order: tuple[str, ...] = ("blind-1", "blind-2"),
score: float = 8.0,
verdict: str = "pass",
) -> dict[str, object]:
"""构造已完成输入绑定后的 v3 结构化报告,专测 rubric 稳定仲裁。"""
candidate_hashes = {
"blind-1": "sha256:" + "1" * 64,
"blind-2": "sha256:" + "2" * 64,
}
return {
"schemaVersion": "blind-judge-report-v4",
"runId": "run-1",
"sampleId": "sample-1",
"scenario": "battle",
"rubricPolicyVersion": RUBRIC_POLICY_VERSION,
"reviewerInvocationId": reviewer_id,
"blindInputSha256": "sha256:" + "3" * 64,
"oracleTruthPackSha256": "sha256:" + "4" * 64,
"candidateOrder": list(order),
"candidateScores": [
{
"blindCandidateId": blind_id,
"candidateSha256": candidate_hashes[blind_id],
"scores": scorecard(score),
}
for blind_id in order
],
"dimensionPreferences": [
{
"dimension": dimension,
"orderedCandidateIds": list(order),
"reason": "共同 rubric 下的匿名偏好顺序",
}
for dimension in DIMENSIONS
],
"oracleAssertionVerdicts": [
{
"assertionId": "assertion-1",
"candidateSha256": candidate_hashes[blind_id],
"verdict": verdict,
"reason": "共同 oracle 断言核对结果",
"evidenceRefs": [
{"sourceType": "oracle_assertion", "sourceRef": "assertion-1"}
],
}
for blind_id in order
],
"hardConstraintVerdicts": [
{
"constraintId": "constraint-1",
"candidateSha256": candidate_hashes[blind_id],
"verdict": "pass",
"reason": "共同细纲硬约束核对结果",
"evidenceRefs": [
{
"sourceType": "preregistered_constraint",
"sourceRef": "constraint-1",
}
],
}
for blind_id in order
],
"modelReceiptSha256": "sha256:" + "5" * 64,
"status": "completed",
"reportSha256": "sha256:" + "6" * 64,
}
class WriterStructuredRubricAdjudicationTest(unittest.TestCase):
"""验证 v3 报告把逐 verdict 分歧纳入第三评委触发和最终裁决。"""
def test_structured_report_rejects_unknown_and_extra_field(self):
unknown = structured_report("reviewer-1", verdict="unknown")
self.assertTrue(any("unknown" in error for error in validate_structured_report(unknown)))
leaked = structured_report("reviewer-1")
leaked["arm"] = "C"
self.assertTrue(any("额外字段" in error for error in validate_structured_report(leaked)))
def test_verdict_disagreement_requires_and_uses_third_reviewer(self):
first = structured_report("reviewer-1")
second = structured_report(
"reviewer-2",
order=("blind-2", "blind-1"),
verdict="fail",
)
pending = adjudicate_structured_reviews(first, second)
self.assertEqual(pending["status"], "needs_third_reviewer")
self.assertTrue(pending["unstableOracleVerdicts"])
third = structured_report("reviewer-3", verdict="pass")
resolved = adjudicate_structured_reviews(first, second, third)
self.assertEqual(resolved["status"], "adjudicated_report")
self.assertEqual(resolved["oracleAssertionVerdicts"][0]["verdict"], "pass")
def test_third_reviewer_cannot_hide_score_instability(self):
first = structured_report("reviewer-1", score=8.0)
second = structured_report(
"reviewer-2",
order=("blind-2", "blind-1"),
score=6.5,
)
third = structured_report("reviewer-3", score=9.5)
result = adjudicate_structured_reviews(first, second, third)
self.assertEqual(result["status"], "failed_judge_unstable")
self.assertTrue(result["unstableScores"])
if __name__ == "__main__":
unittest.main()