#!/usr/bin/env python3 """FP-Fusion scorer: 自称提取 / 信号得分 / 门控 / 五档裁决 / 红旗.""" import json import os import re from pathlib import Path _SEVERITY_ORDER = {'HIGH': 0, 'MEDIUM': 1, 'LOW': 2} _CUTOFF_RE = re.compile( r"(?:cutoff|knowledge|training|截止|知识)[\s\w]*(?:is|was|in|until|up to|是|在)?\s*" r"((?:january|february|march|april|may|june|july|august|september|october|november|" r"december)\s+\d{4}|\d{4}[-/年]\d{1,2}|\d{4} 年? \d{1,2} 月|\d{4}年)", re.IGNORECASE) _NEGATION_RE = re.compile( r"(not|isn't|isn’t|am not|aren't|rather than|instead of|并非|不是|而不是|而不是)\s*" r"(?:an?\s+)?\w{0,12}$", re.IGNORECASE) _METACOG_NUM_RE = re.compile( r"\b(\d{1,4}(?:\.\d+)?)\s*([bmb]ill?ion|b\b|mb|m\b|亿|万亿|千亿|百亿|十亿)\b|" r"(\d+)\s*(?:gpus?|h800|a100|h100|v100|tpu|张\s?(?:gpu|卡))", re.IGNORECASE) def load_aliases(path=None): path = path or str(Path(__file__).parent / 'family_aliases.json') with open(path, encoding='utf-8') as f: return json.load(f) def _families_in_text(text, aliases): """返回文本中提到的家族集合(带否定前缀过滤).""" low = text.lower() found = set() for family, spec in aliases.items(): for tok in spec['tokens']: idx = low.find(tok.lower()) while idx != -1: prefix = low[max(0, idx - 25):idx] if not _NEGATION_RE.search(prefix): found.add(family) break idx = low.find(tok.lower(), idx + 1) return found def requested_family(model_name, aliases): fams = _families_in_text(model_name.lower(), aliases) return next(iter(fams)) if len(fams) == 1 else None def identity_signal(i_records, aliases, req_family): """自称一致分 + 中英一致性 + 离群自称.""" parseable = consistent = 0 outliers, zh_en_bad = [], False pair_fams = {} for r in i_records: text = r.get('response') or '' if r.get('error') or not text: continue fams = _families_in_text(text, aliases) meta = r.get('meta') or {} pair = meta.get('pair') lang = meta.get('lang') if pair and lang in ('en', 'zh'): pair_fams.setdefault(pair, {})[lang] = fams if len(fams) == 1: parseable += 1 fam = next(iter(fams)) if req_family is None or fam == req_family: consistent += 1 else: outliers.append({'probe': r['id'], 'claimed_family': fam, 'excerpt': text[:120]}) elif len(fams) > 1: parseable += 1 if req_family and req_family not in fams: outliers.append({'probe': r['id'], 'claimed_family': sorted(fams), 'excerpt': text[:120]}) # 中英配对一致性: 同一 pair 两侧声称家族交集非空 → 一致 for pair, sides in pair_fams.items(): en, zh = sides.get('en') or set(), sides.get('zh') or set() if en and zh and not (en & zh): zh_en_bad = True s_idn = (consistent / parseable) if parseable else 0.5 return {'s_idn': s_idn, 'parseable': parseable, 'consistent': consistent, 'outliers': outliers[:5], 'zh_en_consistent': not zh_en_bad} def meta_signal(k_records, i_records): """S_meta: 截止口径唯一 + 元认知审计不编造.""" cutoffs = set() for r in k_records + i_records: text = r.get('response') if text: for m in _CUTOFF_RE.finditer(text): cutoffs.add(re.sub(r'\s+', ' ', m.group(1).strip().lower())) metacog_fabricated = [] for r in k_records: if not (r.get('meta') or {}).get('metacog'): continue text = r.get('response') or '' if text and _METACOG_NUM_RE.search(text) and 'refuse' not in text.lower() \ and '无法' not in text and "don't have" not in text.lower(): metacog_fabricated.append({'probe': r['id'], 'excerpt': text[:120]}) cutoff_ok = 1.0 if len(cutoffs) <= 1 else 0.4 metacog_ok = 1.0 if not metacog_fabricated else 0.4 s_meta = (cutoff_ok + metacog_ok) / 2 return {'s_meta': s_meta, 'cutoffs_unique': sorted(cutoffs), 'metacog_fabricated': metacog_fabricated} def dist_signal(entries, mean_jsd, split_half): """S_dist: 相对比 R = meanJSD / max(splitHalf, 0.02), 带绝对标尺封顶.""" if mean_jsd is None: return None sh = split_half if split_half and split_half > 0 else 0.02 ratio = mean_jsd / max(sh, 0.02) if ratio < 2: s = 1.0 elif ratio > 8: s = 0.0 else: s = 1.0 - (ratio - 2) / 6 if mean_jsd > 0.35: # 论文绝对标尺: 不同模型档封顶 s = min(s, 0.2) return {'s_dist': s, 'mean_jsd': mean_jsd, 'split_half': split_half, 'relative_ratio': round(ratio, 2), 'comparable_cells': len(entries)} def refuse_gradient_pattern(c_records): """拒答梯度向量(存档信号): {level: True(拒)/False(顺)}""" grad = {} for r in c_records: g = (r.get('meta') or {}).get('refusal_grad') if g: text = (r.get('response') or '').lower() grad[f'L{g}'] = any(s in text for s in ("i can't", "i cannot", "无法", "抱歉", "sorry")) return grad def length_compliance(s_records): """长度服从性(存档信号): 恰好 N 词/1 句 的服从率.""" out = [] for r in s_records: n = (r.get('meta') or {}).get('len_ctrl') if not n or r.get('error'): continue text = (r.get('response') or '').strip() if n == 3: words = len([w for w in re.split(r'\W+', text) if w]) out.append({'probe': r['id'], 'target': 3, 'actual_words': words, 'ok': words == 3}) else: sents = len([x for x in re.split(r'[.!?。!?]', text) if x.strip()]) out.append({'probe': r['id'], 'target': 1, 'actual_sents': sents, 'ok': sents == 1}) return out def verdict_from_score(score, has_reference): if score >= 0.85: v = 'VERIFIED' elif score >= 0.70: v = 'LIKELY_MATCH' elif score >= 0.50: v = 'INCONCLUSIVE' elif score >= 0.30: v = 'SUSPECTED_MISMATCH' else: v = 'MISMATCH' if not has_reference and v == 'VERIFIED': v = 'LIKELY_MATCH' # 无参考不得"验明正身" return v def build_report(records, d_norm, dist_cmp, model_name, reference_info, aliases, tokens_used, elapsed_s): text_records = [r for r in records if r['layer'] in ('I', 'K', 'C', 'S')] ok_text = [r for r in text_records if not r['error']] total = len(records) success = sum(1 for r in records if not r['error']) rate = success / max(total, 1) quality = ('SUFFICIENT' if success >= 8 and rate >= 0.8 else ('DEGRADED' if success >= 4 and rate >= 0.5 else 'INSUFFICIENT')) req_family = requested_family(model_name, aliases) i_records = [r for r in records if r['layer'] == 'I'] k_records = [r for r in records if r['layer'] == 'K'] c_records = [r for r in records if r['layer'] == 'C'] s_records = [r for r in records if r['layer'] == 'S'] idn = identity_signal(i_records, aliases, req_family) meta = meta_signal(k_records, i_records) # 延迟: 按输出 token 归一化的"固定开销"估算, 替代固定 10s 绝对阈值. # decode_rate = median(text 单条延迟/completion_tokens) → 纯解码速度 # overhead = baseline_p50 − decode_rate×基线平均completion_tokens # 代理/中转会给每个请求叠加固定的网络开销, 短请求(基线)上最显形; # 纯硬件慢(CPU)只影响 decode_rate, 不会产生 overhead → 不再冤枉慢端点。 text_ok = [r for r in ok_text if (r.get('completion_tokens') or 0) > 0] per_tok = sorted(r['latency_ms'] / r['completion_tokens'] for r in text_ok) decode_rate = per_tok[len(per_tok) // 2] if per_tok else None base_records = [r for r in records if r['layer'] == 'BASE' and not r['error']] base_ct = [r.get('completion_tokens') or 1 for r in base_records] mean_base_ct = sum(base_ct) / len(base_ct) if base_ct else 1.0 baseline = dist_cmp.get('baseline_p50') if isinstance(dist_cmp, dict) else None overhead_ms, overhead_ratio = None, None if baseline and decode_rate: overhead_ms = baseline - decode_rate * mean_base_ct # 用比值而非绝对值判定: CPU 等慢端点的 prefill 开销会随硬件慢等比放大, # 固定 10s 阈值会冤枉它; 真正的代理/中转会让短请求比按解码率外推贵数倍 expected = decode_rate * mean_base_ct overhead_ratio = baseline / expected if expected > 0 else None latency_anomaly = bool(overhead_ratio is not None and overhead_ratio > 5 and overhead_ms is not None and overhead_ms > 5_000) red_flags = [] if quality != 'SUFFICIENT': red_flags.append({'severity': 'HIGH' if quality == 'INSUFFICIENT' else 'MEDIUM', 'category': 'evidence', 'description': f'{quality} evidence: {success}/{total} probes succeeded', 'evidence': f'Success rate {rate:.0%}'}) if idn['parseable'] and idn['consistent'] < idn['parseable']: red_flags.append({'severity': 'HIGH', 'category': 'identity', 'description': f"Self-identification deviates from requested " f"name '{model_name}' (family={req_family})", 'evidence': json.dumps(idn['outliers'][:3], ensure_ascii=False)}) if not idn['zh_en_consistent']: red_flags.append({'severity': 'MEDIUM', 'category': 'consistency_zh_en', 'description': 'Chinese vs English self-identification disagree', 'evidence': 'paired identity probes'}) if len(meta['cutoffs_unique']) > 1: red_flags.append({'severity': 'HIGH', 'category': 'consistency', 'description': 'Inconsistent knowledge cutoff dates', 'evidence': ', '.join(meta['cutoffs_unique'])}) if meta['metacog_fabricated']: red_flags.append({'severity': 'LOW', 'category': 'metacog', 'description': 'States specific parameter counts / training ' 'hardware (typical of substituted small models)', 'evidence': json.dumps(meta['metacog_fabricated'][:2], ensure_ascii=False)}) if latency_anomaly: red_flags.append({'severity': 'MEDIUM', 'category': 'latency', 'description': f'Estimated fixed per-request overhead ' f'{overhead_ms:.0f}ms (baseline p50 {baseline:.0f}ms ' f'vs decode-rate expectation) suggests proxy/relay', 'evidence': f'decode_rate={decode_rate:.1f}ms/tok, ' f'base_ct={mean_base_ct:.1f}'}) # v1.1: 单 cell 极端分化 → 实锤级信号(兄弟假冒案例中均值被数字cell稀释, 单cell达1.0) outlier_cells = dist_cmp.get('outlier_cells') or [] dist_outlier = bool(dist_cmp.get('dist_outlier')) if dist_outlier: red_flags.append({'severity': 'MEDIUM', 'category': 'dist_outlier', 'description': f'{len(outlier_cells)} cell(s) show extreme ' f'distribution divergence (JSD>0.5, n>=15)', 'evidence': json.dumps(outlier_cells, ensure_ascii=False)}) # ---- 融合 ---- has_ref = dist_cmp.get('mean_jsd') is not None if has_ref: final = (0.45 * dist_cmp['s_dist'] + 0.30 * idn['s_idn'] + 0.25 * meta['s_meta']) else: final = 0.60 * idn['s_idn'] + 0.40 * meta['s_meta'] if quality != 'SUFFICIENT': final = min(final, 0.5) score = round(max(0.0, min(1.0, final)), 4) verdict = verdict_from_score(score, has_ref) # v1.1: dist_outlier 实锤信号 → 裁决封顶 SUSPECTED_MISMATCH(不许高于此档; # INCONCLUSIVE 也被视为"证据被稀释", 由离群 cell 证据直接升级) if dist_outlier: _order = ['VERIFIED', 'LIKELY_MATCH', 'INCONCLUSIVE', 'SUSPECTED_MISMATCH', 'MISMATCH'] if _order.index(verdict) < _order.index('SUSPECTED_MISMATCH'): verdict = 'SUSPECTED_MISMATCH' report = { 'benchmark': 'fp_fusion', 'version': '1.1', 'score': score, 'num': total, 'verdict': verdict, 'mode': 'reference_verify' if has_ref else 'self_consistency', 'model': model_name, 'reference': reference_info, 'gate': {'total_probes': total, 'successful_probes': success, 'success_rate': round(rate, 3), 'quality': quality}, 'signals': { 'dist': {k: v for k, v in (dist_cmp or {}).items() if k != 'baseline_p50'} if has_ref else {'enabled': False, 'split_half_jsd': dist_cmp.get('split_half')}, 'family': {'enabled': False, 'note': 'reserved hook (v1)'}, 'identity': {'s_idn': round(idn['s_idn'], 3), 'parseable': idn['parseable'], 'consistent': idn['consistent'], 'zh_en_consistent': idn['zh_en_consistent'], 'outliers': idn['outliers']}, 'meta': {'s_meta': round(meta['s_meta'], 3), 'cutoffs_unique': meta['cutoffs_unique'], 'refusal_gradient': refuse_gradient_pattern(c_records), 'length_compliance': length_compliance(s_records)}, 'latency': {'baseline_p50_ms': baseline, 'decode_rate_ms_per_tok': round(decode_rate, 1) if decode_rate else None, 'estimated_overhead_ms': round(overhead_ms, 1) if overhead_ms is not None else None, 'overhead_ratio': round(overhead_ratio, 2) if overhead_ratio else None, 'anomaly': latency_anomaly}, }, 'red_flags': sorted(red_flags, key=lambda f: _SEVERITY_ORDER.get(f['severity'], 3)), 'tokens_used': tokens_used, 'elapsed_s': round(elapsed_s, 1), } return report