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}")