
前阵子在调试一个模型网关时遇到一个非常典型的问题明明接的是某个开源模型的 API但返回结果的风格和生成质量总是对不上怀疑网关背后偷偷做了模型替换。进一步验证时发现直接在 Prompt 里问“你是什么模型”根本不可靠模型会受系统提示词、对齐策略和数据分布影响给出的回答经常和真实身份不符。这类场景多了以后我意识到一个很务实的工程问题当提示词会“说谎”时怎么给 AI 模型做指纹识别这篇文章就围绕“AI 模型指纹识别”展开整理一套不需要大规模算力、普通后端团队就能落地的识别思路。内容包括核心概念、常见识别方法、Python 代码实战、参数解析和踩坑排查适合从事模型网关、AI 安全、模型评测、数据合规的同学参考。1. 背景为什么提示词会“说谎”1.1 模型不会稳定地“自报家门”很多人第一个想到的方法就是直接在 Prompt 里询问你是什么模型请如实回答。这个做法在 demo 里看起来有效但放到真实环境里几乎不可靠。原因有几个系统提示词覆盖模型接入方可能在接口层注入了 system prompt要求模型隐藏身份、扮演特定角色模型会优先遵循系统指令。对齐训练干预当前主流模型在 RLHF 或基于人类反馈的优化阶段会刻意避免透露底层模型信息防止被用于评估或滥用。数据分布影响如果模型经过领域微调例如只学习了客服问答对它对自身来源的认知来自微调语料可能给出错误的品牌名。蒸馏和套壳很多商业 API 实际上是开源模型的微调版或蒸馏版模型本身不知道自己是谁或者被服务方改名。所以Prompt 里的“自我声明”只能当作弱信号不能作为身份判定的依据。1.2 为什么需要模型指纹识别模型指纹识别的目标不是让模型“告诉我它是什么”而是通过模型对特定输入产生的可观测响应计算出一组特征再和已知模型的指纹库比对推断模型身份。典型的应用场景包括模型网关审计确认某个 API 背后的模型和合同约定一致。模型盗窃溯源模型蒸馏、微调后仍然保留底层生成偏好可用于溯源。风控与合规识别用户是否通过套壳模型绕过合规审核。模型选型评测对比多个模型输出风格和稳定性辅助技术选型。从这个角度看模型指纹识别和传统软件指纹的思路类似只是分析对象从二进制特征变成了文本特征、概率分布和行为特征。1.3 需要澄清的概念在开始之前区分几个容易混淆的概念概念说明和模型指纹的关系模型水印Watermark在生成文本中嵌入特定无感标记用于事后证明文本来自某模型属于指纹的一种实现手段模型指纹Fingerprint通过主动或被动方式收集模型行为特征用于身份识别本文的核心提示词注入Prompt Injection通过构造输入操纵模型行为会让“自报家门”失效因此需要指纹设备指纹Device Fingerprint通过硬件和软件特征识别设备例如 goodix fingerprint usb device 相关驱动指纹和 AI 模型指纹不是同一个领域本文讨论的“指纹”是针对模型行为特征的身份识别不要和终端设备驱动指纹混淆。2. 环境准备与版本说明2.1 运行环境本文的实战代码使用 Python 编写建议版本为 Python 3.9 或更高版本。实际项目中版本需要根据现有环境调整本文重点演示识别思路。需要安装的核心库pip install requests openai numpy scipy如果要使用本地 embedding 模型做语义距离分析可以额外安装pip install sentence-transformerssentence-transformers 会下载模型权重首次运行时间较长如果只是做文本层指纹比对不是必须安装。2.2 模型接口封装不同的模型供应商接口差异较大本文用一个统一的call_llm函数封装请求逻辑。为了便于演示示例基于 OpenAI 兼容的接口格式实际使用时要根据供应商文档调整 Base URL、API Key、模型名称和请求体。import requests import json def call_llm(prompt: str, model_name: str, base_url: str, api_key: str, temperature: float 0.7, max_tokens: int 512) - dict: 调用 OpenAI 兼容格式的大模型接口。 返回 { text: str, # 生成的完整文本 logprobs: list, # 每个 token 的对数概率可选 tokens: list # token 列表可选 } headers { Authorization: fBearer {api_key}, Content-Type: application/json } payload { model: model_name, messages: [{role: user, content: prompt}], temperature: temperature, max_tokens: max_tokens } resp requests.post(f{base_url}/v1/chat/completions, headersheaders, jsonpayload) resp.raise_for_status() data resp.json() result {text: data[choices][0][message][content]} # 如果接口支持返回 logprobs则保留概率信息 if logprobs in data[choices][0] and data[choices][0][logprobs]: result[tokens] data[choices][0][logprobs][tokens] result[logprobs] data[choices][0][logprobs][token_logprobs] return result注意logprobs字段在不同供应商接口中的返回结构不完全一致有的接口叫token_logprobs有的需要通过额外参数开启所以这段代码是核心片段需要按实际接口调整。2.3 示例项目结构ai-model-fingerprint/ ├── fingerprint/ │ ├── __init__.py │ ├── api_client.py # 模型接口封装 │ ├── sentinel.py # 标志性输出检测 │ ├── distribution.py # token 概率分布对比 │ ├── stats_features.py # 文本统计特征提取 │ └── combiner.py # 多特征综合判定 ├── known_models/ # 已知模型指纹库 │ └── model_a.json ├── tests/ │ └── test_fingerprint.py └── main.py # 入口示例这种结构适合工程化落地每个识别维度独立成模块方便扩展和维护。3. 核心识别方法从四个维度给模型“打指纹”单个维度的特征很容易受到 Prompt 设计、采样参数、微调程度的影响所以实际工程中更推荐多维度交叉验证。下面从简单到复杂介绍四种可落地的指纹识别方法。3.1 方法一标志性输出测试Sentinel Test思路设计一组能触发模型固定偏好的输入观察模型的输出是否存在某种标志性文本模式。例如有些模型训练语料中有大量特定风格的代码注释、固定开头或特殊语气词。通过一组覆盖面较广的触发指令可以收集模型在不同任务下的习惯表达生成文本指纹。核心示例# 指纹库每个模型存一组“预期标志片段” MODEL_SIGNATURES { model_a: { signatures: [ 配置项说明如下, 请注意这是一个测试示例, // TODO: 请补充实现 ], description: Model A 的典型表达习惯 }, model_b: { signatures: [ Sure, here is, I cannot assist, As an AI language model ], description: Model B 的典型表达习惯 } } def sentinel_check_fingerprint(response_text: str, candidate_model: str) - float: 计算响应文本中包含候选模型标志片段的比例。 返回 0~1 之间的分数分数越高说明越匹配。 signatures MODEL_SIGNATURES[candidate_model][signatures] if not signatures: return 0.0 matched sum(1 for sig in signatures if sig.lower() in response_text.lower()) return matched / len(signatures)适用场景作为快速筛选的第一层判定尤其在候选模型池较大时可以先用标志片段过滤掉明显不匹配的模型。局限微调、蒸馏和 Prompt 语言切换都会改变表达习惯标志片段如果太短容易误判。3.2 方法二Token 概率分布对比思路同一个 Prompt 输入给不同模型即使在高 temperature 下底层模型的 token 概率分布也会有差异。对比候选模型在某段固定 Prompt 下的 top-n token 概率可以量化相似度。为什么比文本比对更可靠因为文本生成经过采样相同模型两次输出可能不同但 token 概率分布体现了模型的“偏好底数”。通过固定 temperature 0 或随机种子可以让输出更稳定便于对比。import numpy as np from scipy.spatial.distance import cosine def get_token_probability_vector(logprobs: list) - np.ndarray: 将 token 的对数概率转换为普通概率向量。 logprobs 中每个值是对数概率通常为负数。 probs np.exp(logprobs) # 归一化避免浮点误差 probs probs / np.sum(probs) return probs def compare_distribution(logprobs_a: list, logprobs_b: list) - float: 计算两个 token 概率分布的余弦相似度。 返回 0~1 之间的相似度。 if len(logprobs_a) 0 or len(logprobs_b) 0: return 0.0 vec_a get_token_probability_vector(logprobs_a) # 对齐长度取较短的序列长度 min_len min(len(vec_a), len(logprobs_b)) vec_a vec_a[:min_len] vec_b get_token_probability_vector(logprobs_b[:min_len]) sim 1 - cosine(vec_a, vec_b) return float(sim)关键参数说明logprobs必须来自同一组 token 序列否则对齐没有意义。比较时建议使用相同的 prompt、相同的max_tokens和temperature0。如果接口不返回 logprobs可以使用贪心解码多次重复采样再用文本统计近似。适用场景模型供应商开放 logprobs 字段时这种方法的区分效果较好如果接口不开放只能作为补充手段。3.3 方法三响应统计特征提取思路不从 token 级别看而是从整段文本的统计特征识别模型。不同模型的偏好会体现在平均 token 长度、句子长度。词汇丰富度TTRType-Token Ratio。标点符号使用频率。Markdown 结构使用习惯标题、列表、代码块密度。文本信息熵。import re import math from collections import Counter def compute_stats_features(text: str) - dict: 提取文本的统计特征用于构建模型指纹向量。 words re.findall(r\b\w\b, text.lower()) sentences re.split(r[。!?\.\n], text) sentences [s for s in sentences if s.strip()] total_words len(words) unique_words len(set(words)) total_chars len(text) features { avg_word_length: total_chars / total_words if total_words else 0, type_token_ratio: unique_words / total_words if total_words else 0, avg_sentence_length: total_words / len(sentences) if sentences else 0, comma_ratio: text.count() / total_chars if total_chars else 0, code_block_ratio: len(re.findall(r, text)) / total_chars if total_chars else 0, entropy: compute_entropy(words) } return features def compute_entropy(words: list) - float: 计算词序列的信息熵。 counter Counter(words) total len(words) if total 0: return 0.0 entropy 0.0 for count in counter.values(): p count / total entropy - p * math.log2(p) return entropy衡量方式先对已知模型按同一批 Prompt 生成文本提取统计特征构建指纹库然后对未知模型生成文本计算特征向量之间的欧氏距离或余弦距离。适用场景不依赖供应商接口差异只要模型能生成文本就能收集特征也是最容易落地的方式。局限统计特征对 Prompt 主题敏感。比如让模型写代码和写诗词汇丰富度差异会非常大所以提取统计特征时必须保持 Prompt 集合一致。3.4 方法四语义 Embedding 距离思路不同模型对同一语义意图的文本表达在语义空间中会形成不同的聚类。用 Sentence-BERT 或 OpenAI Embedding 将生成文本转为向量计算候选模型生成样例的质心再计算未知模型向量到质心的距离。# 如果安装了 sentence-transformers可以这样提取语义向量 try: from sentence_transformers import SentenceTransformer _encoder None def get_embedding(text: str) - list: global _encoder if _encoder is None: _encoder SentenceTransformer(paraphrase-multilingual-MiniLM-L12-v2) return _encoder.encode(text, normalize_embeddingsTrue).tolist() except ImportError: # 如果没有安装可以退化为调用文本 embedding API def get_embedding(text: str) - list: # 这里需要替换为你的 embedding API 调用逻辑 raise NotImplementedError(sentence-transformers 未安装请安装或改用 API)适用场景作为交叉验证的最后一层可以缓解文本统计特征容易被 Prompt 主题干扰的问题。注意语义向量维度较高计算质心和余弦距离时建议做归一化不同 embedding 模型生成的向量不能直接混用。4. 完整实战构建一个可运行的模型指纹识别工具下面把上面四种方法整合起来实现一个完整的指纹识别工具。这个工具会使用一批“校准 Prompt”分别让未知模型和已知模型生成文本。提取统计特征和语义特征。综合比对输出最终的相似度排名。4.1 校准 Prompt 集合校准 Prompt 要覆盖不同类型至少包含代码生成、文本改写、问答和翻译四类避免只测一个任务导致指纹偏差。CALIBRATION_PROMPTS [ 请用 Python 写一个函数判断一个字符串是否是回文。, 请把下面这句话改写成更正式的表达今天天气不错我们出去走走。, 请简单解释一下什么是局部变量和全局变量。, 请把“Good morning, nice to meet you”翻译成中文。 ]实际项目中建议准备 20 到 50 个校准 Prompt覆盖代码、日志、总结、角色扮演等常见业务场景。4.2 构建已知模型指纹库先运行如下脚本为已知模型生成指纹。import json import os from fingerprint.api_client import call_llm from fingerprint.stats_features import compute_stats_features def build_fingerprint_for_model(model_name: str, base_url: str, api_key: str, output_path: str) - None: 为单个模型构建统计特征指纹库。 每个 Prompt 生成 5 次取特征平均值降低随机性干扰。 all_features [] for prompt in CALIBRATION_PROMPTS: for _ in range(5): resp call_llm( promptprompt, model_namemodel_name, base_urlbase_url, api_keyapi_key, temperature0.3, max_tokens300 ) features compute_stats_features(resp[text]) all_features.append(features) # 对每个特征取平均生成模型指纹 avg_features {} for key in all_features[0].keys(): avg_features[key] sum(item[key] for item in all_features) / len(all_features) fingerprint_data { model: model_name, stats_features: avg_features, prompt_count: len(CALIBRATION_PROMPTS) } os.makedirs(os.path.dirname(output_path), exist_okTrue) with open(output_path, w, encodingutf-8) as f: json.dump(fingerprint_data, f, ensure_asciiFalse, indent2) print(f指纹已保存到 {output_path})输出指纹库示例{ model: model_a, stats_features: { avg_word_length: 5.12, type_token_ratio: 0.43, avg_sentence_length: 18.6, comma_ratio: 0.021, code_block_ratio: 0.003, entropy: 9.87 }, prompt_count: 4 }4.3 对未知模型生成指纹并匹配对需要识别的模型同样的流程生成指纹然后和指纹库比对。import json import math from fingerprint.api_client import call_llm from fingerprint.stats_features import compute_stats_features from fingerprint.sentinel import sentinel_check_fingerprint from fingerprint.distribution import compare_distribution def compute_unknown_fingerprint(base_url: str, api_key: str, model_name: str unknown) - dict: 生成未知模型的统计特征指纹。 all_features [] for prompt in CALIBRATION_PROMPTS: resp call_llm( promptprompt, model_namemodel_name, base_urlbase_url, api_keyapi_key, temperature0.3, max_tokens300 ) features compute_stats_features(resp[text]) all_features.append(features) avg_features {} for key in all_features[0].keys(): avg_features[key] sum(item[key] for item in all_features) / len(all_features) return avg_features def euclidean_distance(features_a: dict, features_b: dict) - float: 计算两个特征字典的欧氏距离距离越小越相似。 distance 0.0 for key in features_a: if key in features_b: distance (features_a[key] - features_b[key]) ** 2 return math.sqrt(distance) def match_model(unknown_features: dict, fingerprint_lib_path: str) - list: 匹配未知模型与指纹库中的已知模型。 返回按距离升序排列的列表。 with open(fingerprint_lib_path, r, encodingutf-8) as f: lib json.load(f) results [] for known_model in lib[models]: dist euclidean_distance(unknown_features, known_model[stats_features]) results.append({ model: known_model[model], distance: round(dist, 6) }) results.sort(keylambda x: x[distance]) return results4.4 多维度综合判定单一维度可能出现误判所以把标志性输出检测、token 分布距离、统计特征距离综合起来生成一个综合评分。def combined_score(unknown_features: dict, known_model: dict, response_text: str, logprobs: list) - dict: 综合多个维度的评分。 维度 1. 统计特征距离越小越好 2. 标志性输出匹配度越大越好 3. token 分布余弦相似度越大越好 stats_dist euclidean_distance(unknown_features, known_model[stats_features]) sentinel_score sentinel_check_fingerprint( response_text, known_model[model] ) # 归一化统计距离这里简单映射到 0~1分数越高代表越匹配 max_expected_dist 10.0 stats_score max(0.0, 1.0 - stats_dist / max_expected_dist) # 综合得分可以按需调整权重 total_score ( 0.4 * stats_score 0.3 * sentinel_score 0.3 * (logprobs or 0) ) return { model: known_model[model], total_score: round(total_score, 6), stats_score: round(stats_score, 6), sentinel_score: round(sentinel_score, 6), distance: round(stats_dist, 6) }4.5 运行与验证在main.py中调用from fingerprint.api_client import call_llm from fingerprint.combiner import combined_score def main(): # 1. 生成未知模型的统计特征指纹 unknown_features compute_unknown_fingerprint( base_urlhttps://your-api.example.com, api_keyyour-key, model_nameunknown-model ) # 2. 获取一段响应文本用于标志性输出检测 resp call_llm( promptCALIBRATION_PROMPTS[0], model_nameunknown-model, base_urlhttps://your-api.example.com, api_keyyour-key, temperature0.3, max_tokens300 ) # 3. 和已知模型指纹库匹配 matches match_model(unknown_features, known_models/fingerprint_lib.json) for match in matches[:3]: combined combined_score( unknown_featuresunknown_features, known_modelmatch, response_textresp[text], logprobsresp.get(logprobs) ) print(combined) if __name__ __main__: main()预期输出{model: model_a, total_score: 0.812, stats_score: 0.78, sentinel_score: 0.25, distance: 2.2} {model: model_b, total_score: 0.674, stats_score: 0.69, sentinel_score: 0.0, distance: 3.1}从结果可以看出综合评分结合了多个维度比单一维度更稳定。5. 常见问题与排查思路在实际部署时模型指纹识别会遇到不少干扰因素。下面列举几个出现频率较高的问题。问题现象常见原因解决思路校准 Prompt 相同但两次生成的特征差异很大temperature 过高导致采样随机性增强将 temperature 降低到 0.2 或更低并增加生成次数取平均结果匹配到错误模型准确率低指纹库中模型数量太少或特征维度不够增加校准 Prompt 数量和生成次数补充语义特征维度日志中出现fingerprint sha256 has already been taken类似报错部分指纹管理系统或模型注册中心会为模型哈希建立唯一索引重复注册相同版本会报冲突检查模型版本号或哈希值更新注册信息而不是重复插入接口不返回 logprobs 信息token 分布对比无法执行供应商接口未默认开启 logprobs 字段改用统计特征对比或通过多次采样统计生成结果分布未知模型是已知模型的微调版统计距离很接近微调保留了底层基座的大部分偏好换用更敏感的 Prompt例如代码生成、数学推理等底层能力激活明显的任务模型供应商升级版本后指纹库失效模型参数更新导致行为特征漂移建立指纹库版本管理机制定期重新采集已知模型指纹同一模型在不同地区接入点返回结果不一致供应商存在多版本灰度或多副本部署差异对每个接入点单独采样统计识别结果分布排查建议先降随机性把 temperature 调到 0用相同 Prompt 生成多次观察输出是否稳定。再看维度数如果只用统计特征遇到模型版本升级就会失效尽量增加语义特征和标志性输出维度。最后查样本量如果每个模型只有一两条生成样本指纹方差会很大至少每个 Prompt 生成 5 次以上。6. 最佳实践与工程建议6.1 指纹库需要版本管理模型更新频率很快同一个模型 A 的 v1 和 v2 版本在行为上可能差异很大。建议指纹库引入版本号字段例如{ schema_version: 2, model: model_a, model_version: 2025.06, collected_at: 2025-06-01, stats_features: {} }定期重新采集指纹并对比旧版本识别效果。如果发现识别率下降优先考虑模型升级导致的漂移。6.2 权重参数要按业务场景调整综合评分里的权重例如 0.4/0.3/0.3要视场景调整如果重点排查模型是否被替换为套壳微调版加大统计特征和语义特征的权重。如果重点判断 API 背后是哪个开源基座模型加大标志性输出和 token 分布对比的权重。如果候选模型池很大先用标志性输出粗筛再用全量特征精确匹配。6.3 注意合法性和授权边界识别模型指纹的目的是合规审计、模型选型和系统维护。在采集模型响应、构建指纹库、比对外部模型身份时应遵守只使用你有权访问的模型接口进行测试。不要使用指纹识别技术对未授权系统进行扫描或逆向。涉及用户数据时遵循最小授权原则避免将用户输入用于未授权的模型测试。在生产环境做模型替换审计时先确认替换行为是否属于合法变更流程。6.4 日志与监控指纹识别过程本身会产生大量日志建议记录采样时间、候选模型集合、校准 Prompt 集合。生成文本的哈希值例如 SHA-256用于事后比对和版本追踪。综合评分和各维度得分便于分析误判原因。指纹库版本方便定位是否因为指纹库过期导致识别错误。6.5 对“提示词说谎”保持防御心态既然提示词不可靠就不要把用户输入或系统输出中的“我是什么模型”当成可信字段。在工程上所有身份声明都应该是弱信号真正可信的是可观测的行为特征。设计网关审计时建议把指纹识别和 API Key 权限、调用来源 IP、流量配额等维度结合起来形成多层防线。7. 总结与后续学习方向这篇文章围绕“当提示词不可靠时如何给 AI 模型做指纹识别”展开核心可以概括为几点模型在 Prompt 中的“自报家门”不可信不能作为身份判定依据。指纹识别的本质是采集模型行为特征通过多维度比对定位模型身份。四种可行手段分别是标志性输出检测、token 概率分布对比、文本统计特征提取和语义 embedding 距离。工程落地时需要一套校准 Prompt、指纹库版本管理和综合评分机制不能只依赖单一维度。如果想继续深入可以关注这几个方向学习更多文本向量化方法对比不同 embedding 模型对指纹区分度的影响。熟悉不同模型供应商 API 的 logprobs 差异探索更细致的概率分布对比方法。研究模型水印技术了解如何在模型训练阶段主动嵌入可验证的指纹信息。结合模型网关日志构建立体化的模型身份审计平台。最后补一句识别模型身份这件事本质是在和“不可信声明”打交道方法没有绝对完美关键是让多个微弱信号互相验证把误判率压到可接受范围内。建议先用小规模样本把上述流程跑通再逐步扩大到完整指纹库观察不同模型之间的区分度最终形成一套适合自己业务场景的识别策略。