ruoxi_sun 09b2add673 fingerprint: integrate fp_fusion model fingerprint benchmark
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).
2026-09-11 03:52:21 +00:00

188 lines
7.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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.8served-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: 家族别名表 dictNone → 默认 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),
}