New evalharness/fingerprint/ package (from evalstone fp_fusion v1.1, 2026-09-07 pruning final): probe battery -> concurrent collection -> five scoring views (verify/attribution/variant/adversarial/robustness), bundled family aliases + 27 reference fingerprints (12 fp_fusion schema). - CLI: 'evalharness fingerprint run ...' (REMAINDER passthrough, single source of arg definitions) + 'fingerprint list' for bundled references - imports rewritten package-relative; direct 'python3 run_fp_fusion.py' execution kept working via package bootstrap - offline analysis/collection scripts made path-independent (previously pinned to a /opt/evalscope path absent on this host) - shell scripts: hardcoded API key -> FP_API_KEY/OPENAI_API_KEY env vars - --reference accepts short names resolved against bundled references/ - pyproject: +httpx dependency, package-data references/*.json - tests/test_fingerprint.py: 10 offline tests (battery definitions, assembly counts, normalization, signals, verdict ladder, CLI wiring) - README: fingerprint section + architecture entry Verified on H20-1: tests 10/10, installed CLI OK, full-protocol run vs vectron GLM-5.3 reproduces baseline (score 0.9451, s_idn 0.846).
188 lines
7.3 KiB
Python
188 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
||
"""FP-Fusion 维度 A:家族归因(Model Family Attribution)。
|
||
|
||
双路归因:
|
||
1. 词表归因(主路,无需额外依赖)—— 按探针层加权命中家族别名:
|
||
I 层自称命中 +1.5/次(含越狱/中英/直问)
|
||
K 层组织/创建者 +1.0/次
|
||
C 层拒答风格 +0.5/次(安全对齐措辞属家族化特征)
|
||
S 层风格特征词 +0.3/次
|
||
requested_family 先验 +0.8(served-name 声称家族)
|
||
2. LLMmap 嵌入归因(辅路,可选)—— 对 I/K 层回答用 e5 embedding 与
|
||
60 个已知模板算距离,取 top-3 模板的家族投票。
|
||
|
||
融合:
|
||
S_fam = confidence × (1 - 0.3 × conflict)
|
||
conflict = 主路 top1 家族与 LLMmap top1 家族不一致(量化/蒸馏/伪装信号)
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
|
||
from .scorer import _families_in_text, load_aliases
|
||
|
||
|
||
# -- 风格特征词(跨语言,家族化措辞弱信号)------------------------------
|
||
# 注意:这是"说话风格"侧写,不是身份声称,权重最低。
|
||
_STYLE_TOKENS = {
|
||
# 英文:高频开场/缓冲词
|
||
'certainly!', 'let me', 'let\'s', 'absolutely!', 'great question',
|
||
'sure!', 'as an ai', 'i\'m happy to', 'i\'d be happy to',
|
||
'good question', 'that\'s a great', 'here\'s how', 'let me break',
|
||
# 中文
|
||
'当然', '让我', '好的,', '没问题', '这是一个好问题',
|
||
'首先', '其次', '最后', '总之',
|
||
}
|
||
|
||
|
||
def _lexicon_scores(records, aliases, requested_family):
|
||
"""词表归因:按层加权累加家族分数。records 需含 layer/response/error。"""
|
||
scores = {}
|
||
claim_hits = 0 # I 层可解析自称次数(归一化用)
|
||
style_hits = 0
|
||
|
||
for r in records:
|
||
if r.get('error') or not r.get('response'):
|
||
continue
|
||
text = r.get('response') or ''
|
||
layer = r.get('layer')
|
||
fams = _families_in_text(text, aliases)
|
||
|
||
if layer == 'I':
|
||
if len(fams) == 1: # 明确自称
|
||
f = next(iter(fams))
|
||
scores[f] = scores.get(f, 0) + 1.5
|
||
claim_hits += 1
|
||
elif len(fams) > 1: # 多家族提及,分散加权
|
||
for f in fams:
|
||
scores[f] = scores.get(f, 0) + 1.5 / len(fams)
|
||
claim_hits += 1
|
||
elif layer == 'K':
|
||
for f in fams: # 组织/创建者弱证据
|
||
scores[f] = scores.get(f, 0) + 1.0
|
||
elif layer == 'C':
|
||
low = text.lower()
|
||
if any(s in low for s in ("i can't", "i cannot", "无法", "抱歉",
|
||
"sorry", "i apologize")):
|
||
for f in fams: # 拒答风格命中
|
||
scores[f] = scores.get(f, 0) + 0.5
|
||
if not fams:
|
||
style_hits += 1 # 拒答但未点名家族 → 中性
|
||
elif layer == 'S':
|
||
low = text.lower()
|
||
for tok in _STYLE_TOKENS:
|
||
if tok in low:
|
||
style_hits += 1
|
||
break
|
||
# 风格词与家族弱相关:仅当该回答同时提及家族才累加
|
||
if fams:
|
||
for f in fams:
|
||
scores[f] = scores.get(f, 0) + 0.3
|
||
|
||
# served-name 声称家族先验
|
||
if requested_family and requested_family in aliases:
|
||
scores[requested_family] = scores.get(requested_family, 0) + 0.8
|
||
|
||
return scores, claim_hits, style_hits
|
||
|
||
|
||
def _normalize(scores):
|
||
"""按分数排序,返回 (top1_family, confidence, per_family)。"""
|
||
if not scores:
|
||
return None, 0.0, {}
|
||
total = sum(scores.values())
|
||
order = sorted(scores.items(), key=lambda kv: kv[1], reverse=True)
|
||
top1 = order[0][0]
|
||
conf = order[0][1] / total if total > 0 else 0.0
|
||
return top1, conf, dict(order)
|
||
|
||
|
||
def _llmmap_attribution(records, llmmap_tool):
|
||
"""LLMmap 嵌入归因(辅路):I/K 层回答 → 模板距离 → 家族投票。
|
||
|
||
llmmap_tool: 已加载的 LLMmap InferenceModel_open 实例(或 None)。
|
||
返回 {top1_family, top3: [(family, dist)], votes: {family: n}}。
|
||
"""
|
||
if llmmap_tool is None or not getattr(llmmap_tool, 'ready', False):
|
||
return None
|
||
import numpy as np
|
||
|
||
texts = []
|
||
for r in records:
|
||
if r.get('layer') not in ('I', 'K'):
|
||
continue
|
||
if r.get('error') or not r.get('response'):
|
||
continue
|
||
texts.append((r['id'], r['response']))
|
||
|
||
# LLMmap 模型一次只能吃固定 8 条 queries 的回答;这里每 8 条一批,
|
||
# 逐条与模板库比对后按 label_map 归集家族。
|
||
votes = {}
|
||
dist_rows = []
|
||
for batch_start in range(0, len(texts), 8):
|
||
batch = texts[batch_start:batch_start + 8]
|
||
answers = [t[1] for t in batch]
|
||
# 不足 8 条时补空串(LLMmap __call__ 强校验数量)
|
||
answers = answers + [''] * (8 - len(answers))
|
||
try:
|
||
dists = llmmap_tool(answers)
|
||
except Exception:
|
||
continue
|
||
# open-set 距离:越小越好,取每条回答 top1 模板
|
||
order_idx = int(np.argmin(dists))
|
||
model_name = llmmap_tool.label_map[order_idx]
|
||
votes[model_name] = votes.get(model_name, 0) + 1
|
||
dist_rows.append((model_name, float(dists[order_idx])))
|
||
|
||
if not votes:
|
||
return None
|
||
top3 = sorted(dist_rows, key=lambda x: x[1])[:3]
|
||
top1_model = max(votes, key=votes.get)
|
||
return {'top1_templates': top3, 'votes': votes,
|
||
'top1_model': top1_model}
|
||
|
||
|
||
def family_attribution(records, aliases=None, requested_family=None,
|
||
llmmap_tool=None):
|
||
"""维度 A 主入口:双路归因融合。
|
||
|
||
Args:
|
||
records: fp_fusion 原始记录(含 layer/response/error/id)。
|
||
aliases: 家族别名表 dict(None → 默认 family_aliases.json)。
|
||
requested_family: served-name 声称家族(None → 自动从 model 解析)。
|
||
llmmap_tool: 可选 LLMmap 实例。
|
||
|
||
Returns dict(写入报告 signals.family):
|
||
enabled, method, top1_family, confidence, per_family_scores,
|
||
claims, style_hits, llmmap(可选), conflict
|
||
"""
|
||
aliases = aliases or load_aliases()
|
||
|
||
scores, claims, style_hits = _lexicon_scores(records, aliases,
|
||
requested_family)
|
||
top1, conf, per_fam = _normalize(scores)
|
||
|
||
llm = _llmmap_attribution(records, llmmap_tool) if llmmap_tool else None
|
||
conflict = False
|
||
if llm and llm.get('top1_model'):
|
||
# LLMmap 模板名 → 家族('Qwen/Qwen2-7B-Instruct' → qwen)
|
||
tmpl_fams = _families_in_text(llm['top1_model'], aliases)
|
||
llm_fam = next(iter(tmpl_fams)) if len(tmpl_fams) == 1 else None
|
||
if top1 and llm_fam and llm_fam != top1:
|
||
conflict = True
|
||
|
||
s_fam = conf * (1.0 - 0.3 * int(conflict)) if top1 else 0.0
|
||
|
||
return {
|
||
'enabled': True,
|
||
'method': 'lexicon' + ('+llmmap' if llm else ''),
|
||
'top1_family': top1,
|
||
'confidence': round(conf, 4),
|
||
'per_family_scores': {k: round(v, 3) for k, v in per_fam.items()},
|
||
'claims_parsed': claims,
|
||
'style_hits': style_hits,
|
||
'llmmap': llm,
|
||
'llmmap_family': llm_fam if llm else None,
|
||
'conflict': conflict,
|
||
's_fam': round(s_fam, 4),
|
||
} |