💻 源码
import re
from typing import Any

from backend.extract_schema import EXTRACT_FIELDS

CATEGORY_SIGNALS: dict[str, list[str]] = {
    "公司公告": ["公告", "披露", "发布", "宣布", "称", "表示"],
    "市场行情": ["指数", "涨跌", "板块", "收盘", "开盘", "股价", "A股", "港股"],
    "宏观经济": ["GDP", "通胀", "就业", "PMI", "宏观", "CPI", "PPI", "利率"],
    "投资建议": ["建议", "评级", "目标价", "买入", "增持", "推荐", "看好", "上调"],
    "风险提示": ["风险", "警告", "下滑", "亏损", "承压", "不确定", "波动", "拖累", "审慎"],
    "财报解读": ["财报", "业绩", "净利润", "营收", "季报", "年报", "同比", "毛利"],
    "政策监管": ["监管", "证监会", "央行", "政策", "条例", "罚款", "合规", "立案"],
}

CONTRAST_MARKERS = ("但", "然而", "同时", "尽管", "虽然", "却", "不过", "另一方面")
INDIRECT_MARKERS = (
    "该公司",
    "该企业",
    "该机构",
    "其",
    "后者",
    "前者",
    "上述",
    "前述",
    "头部",
    "龙头",
    "某",
    "相关主体",
)


def _active_category_signals(text: str) -> dict[str, int]:
    counts: dict[str, int] = {}
    for category, keywords in CATEGORY_SIGNALS.items():
        total = sum(text.count(keyword) for keyword in keywords)
        if total:
            counts[category] = total
    return counts


def _count_filled_fields(result: dict[str, list[Any]]) -> int:
    return sum(1 for field in EXTRACT_FIELDS if result.get(field))


def _char_bigrams(text: str) -> set[str]:
    chars = re.sub(r"[^\u4e00-\u9fff]", "", text)
    if len(chars) < 2:
        return set()
    return {chars[index : index + 2] for index in range(len(chars) - 1)}


def _shared_keywords(text_a: str, text_b: str, keywords: tuple[str, ...]) -> int:
    return sum(1 for keyword in keywords if keyword in text_a and keyword in text_b)


def _token_overlap_ratio(text_a: str, text_b: str) -> float:
    tokens_a = _char_bigrams(text_a)
    tokens_b = _char_bigrams(text_b)
    if not tokens_a or not tokens_b:
        return 0.0
    return len(tokens_a & tokens_b) / len(tokens_a | tokens_b)


def _contains_number(text: str) -> bool:
    return bool(re.search(r"\d", text))


def validate_hard_sample(
    task: str,
    sample: dict[str, Any],
    *,
    expected_category: str | None = None,
    expected_result: dict[str, list[Any]] | None = None,
    expected_is_match: bool | None = None,
) -> None:
    if task == "classify":
        text = str(sample.get("text", "")).strip()
        category = expected_category or str(sample.get("expected_category", "")).strip()
        if len(text) < 60:
            raise ValueError("困难分类样本至少 60 字,需包含多重信息点")
        if not any(marker in text for marker in CONTRAST_MARKERS):
            raise ValueError("困难分类样本需含转折结构(如“但/然而/同时/尽管”)")

        signals = _active_category_signals(text)
        active = [name for name, count in signals.items() if count > 0]
        if len(active) < 2:
            raise ValueError(
                "困难分类样本需同时出现至少 2 类表面信号词,制造分类歧义"
            )
        if category in active and len(active) < 3 and signals.get(category, 0) >= 2:
            # Single dominant category with only one other weak signal is too easy.
            other_total = sum(count for name, count in signals.items() if name != category)
            if other_total <= 1:
                raise ValueError(
                    "困难分类样本的歧义不足,需让多个类别都具备合理表面依据"
                )
        return

    if task == "extract":
        text = str(sample.get("text", "")).strip()
        result = expected_result or sample.get("expected_result") or {}
        if len(text) < 80:
            raise ValueError("困难抽取样本至少 80 字,需多层信息嵌套")
        if _count_filled_fields(result) < 5:
            raise ValueError("困难抽取样本 expected_result 至少 5 个字段非空")

        has_indirect = any(marker in text for marker in INDIRECT_MARKERS)
        entities = result.get("主体机构", [])
        explicit_entity = any(entity and entity in text for entity in entities)
        if entities and explicit_entity:
            raise ValueError(
                "困难抽取样本的主体机构不应在文本中完整直述,需用指代或上下文推断"
            )
        if not has_indirect and not entities:
            raise ValueError("困难抽取样本需使用指代表达或隐藏主体名称")

        conditional = any(token in text for token in ("若", "如果", "一旦", "可能", "或将在"))
        if not conditional:
            raise ValueError("困难抽取样本需含条件/推测表达(若、可能、或将在等)")
        return

    if task == "match":
        text_a = str(sample.get("text_a", "")).strip()
        text_b = str(sample.get("text_b", "")).strip()
        is_match = expected_is_match
        if is_match is None:
            is_match = sample.get("expected_is_match")

        if len(text_a) < 45 or len(text_b) < 45:
            raise ValueError("困难匹配样本每段文本至少 45 字")

        overlap = _token_overlap_ratio(text_a, text_b)
        shared = _shared_keywords(
            text_a,
            text_b,
            ("公司", "股份", "公告", "披露", "实施", "计划", "股东", "不超过"),
        )
        if is_match is False:
            if overlap < 0.18 and shared < 3:
                raise ValueError(
                    "困难不匹配样本需高度相关(同主题/同主体),表面词重叠不足"
                )
            if overlap > 0.55:
                raise ValueError(
                    "困难不匹配样本不能只是简单改写,需存在关键事实差异"
                )
            if _contains_number(text_a) and _contains_number(text_b):
                nums_a = set(re.findall(r"\d+\.?\d*", text_a))
                nums_b = set(re.findall(r"\d+\.?\d*", text_b))
                if nums_a == nums_b and nums_a:
                    raise ValueError(
                        "困难不匹配样本若同时含数字,数字不能完全相同"
                    )
        else:
            if overlap > 0.55:
                raise ValueError(
                    "困难匹配样本表面表述应明显不同,不能仅靠同义替换"
                )
            if overlap < 0.12 and shared < 2:
                raise ValueError(
                    "困难匹配样本虽表述不同,但仍需保留足够共同语义线索"
                )
        return

    raise ValueError(f"未知任务:{task}")