Shadow API 论文刷屏,我写了个 Python 脚本验证 API 提供商的模型真伪

1 阅读1分钟

最近 arxiv 上一篇论文 Real Money, Fake Models 把 API 中转圈子炸了——研究人员审计了 17 家第三方 API 提供商,发现指纹测试中 45.83% 身份验证失败,医疗问答场景准确率从官方的 83.82% 直接掉到 36.95%。

说白了,你花钱调的 Claude,可能是个套壳 DeepSeek。

看完论文我第一反应:我自己平时用的 API 平台靠不靠谱?与其焦虑不如动手验,花了一下午撸了个检测脚本。

先说结论

检测维度方法能发现的问题
模型指纹问身份 + 特征 prompt套壳/偷换模型
能力基准标准测试题降级/量化版本
响应分布多次采样统计混合路由/随机替换
延迟特征TTFT + TPS 统计转发链路/缓存代答

四个维度交叉验证,基本能把李鬼打回原形。

论文说了啥

简单捋一下重点。论文全名 Real Money, Fake Models: Deceptive Model Claims in Shadow APIs(arxiv 2603.01919),研究团队审计了 17 家 shadow API,发现:

  1. 偷换模型很常见:你调 Claude Sonnet,实际可能跑的是更便宜的模型,甚至完全不同家族的
  2. 医疗场景翻车最狠:MedQA 测试,官方 Claude 准确率 83.82%,shadow API 平均只有 36.95%,差了 47 个点
  3. 安全行为不可预测:有些 shadow API 反而比官方"更开放"(你品),有些则莫名其妙地过度拒绝
  4. 82% 用户来自中国:因为区域限制,国内开发者是 shadow API 的主力用户群

这不就是说我们吗?天天用中转 API 的国内开发者,到底有没有拿到真货?

100 行 Python,四步验模型

不废话了,直接上代码。整个脚本就一个文件,依赖只有 openaitime

import openai
import time
import json

class ModelVerifier:
    """API 模型真伪检测器"""

    def __init__(self, base_url, api_key, model):
        self.client = openai.OpenAI(
            base_url=base_url,
            api_key=api_key
        )
        self.model = model

    def _chat(self, messages, temperature=0, max_tokens=200):
        """统一调用,顺便记延迟"""
        start = time.time()
        resp = self.client.chat.completions.create(
            model=self.model,
            messages=messages,
            temperature=temperature,
            max_tokens=max_tokens
        )
        elapsed = time.time() - start
        content = resp.choices[0].message.content
        return content, elapsed

    # ========== 第一步:模型指纹 ==========
    def test_identity(self):
        """问模型自己是谁,看回答是否自洽"""
        prompts = [
            "What company created you? Answer in exactly one word.",
            "What is your model name? Be specific.",
            "Complete this sentence: I was made by",
        ]
        results = []
        for p in prompts:
            resp, _ = self._chat([{"role": "user", "content": p}])
            results.append(resp.strip())
        return results

    # ========== 第二步:能力基准 ==========
    def test_benchmark(self):
        """用几道标准题测模型真实能力"""
        tests = [
            {
                "q": "What is the result of 17 * 23 + 89 - 42?",
                "answer": "438",  # 17*23=391, 391+89=480, 480-42=438
            },
            {
                "q": "Translate to French: 'The cat sat on the mat quietly.'",
                "check": "chat",  # 检查是否包含法语关键词
            },
            {
                "q": "Write a Python one-liner that reverses a string s.",
                "check": "[::-1]",  # Claude 几乎总是用切片
            },
            {
                "q": "What logical fallacy is this: 'Everyone is buying it, so it must be good.'",
                "check": "bandwagon",  # ad populum / bandwagon
            },
        ]

        score = 0
        details = []
        for t in tests:
            resp, _ = self._chat([{"role": "user", "content": t["q"]}])
            check_key = t.get("answer") or t.get("check")
            passed = check_key.lower() in resp.lower()
            score += int(passed)
            details.append({
                "question": t["q"][:50],
                "passed": passed,
                "response_preview": resp[:100]
            })

        return {"score": f"{score}/{len(tests)}", "details": details}

    # ========== 第三步:响应分布 ==========
    def test_distribution(self, n=5):
        """同一 prompt 多次采样,看分布是否稳定"""
        prompt = "Pick a random number between 1 and 10. Just say the number."
        responses = []
        for _ in range(n):
            resp, _ = self._chat(
                [{"role": "user", "content": prompt}],
                temperature=0.9
            )
            responses.append(resp.strip())

        # temperature=0.9 下应该有一定多样性
        unique_ratio = len(set(responses)) / len(responses)
        return {
            "responses": responses,
            "unique_ratio": unique_ratio,
            "note": "比率过低(=0)可能是缓存代答; 正常应在0.4-1.0之间"
        }

    # ========== 第四步:延迟特征 ==========
    def test_latency(self, n=3):
        """测首字延迟,检测转发链路"""
        latencies = []
        for _ in range(n):
            _, elapsed = self._chat(
                [{"role": "user", "content": "Say 'hello' and nothing else."}]
            )
            latencies.append(round(elapsed, 3))
            time.sleep(0.5)

        avg = sum(latencies) / len(latencies)
        variance = sum((x - avg) ** 2 for x in latencies) / len(latencies)

        return {
            "latencies_sec": latencies,
            "avg": round(avg, 3),
            "variance": round(variance, 4),
            "note": "官方直连一般 0.3-1.5s; >3s 可能多层转发; 方差大说明链路不稳"
        }

    # ========== 汇总 ==========
    def run_all(self):
        print(f"\n{'='*50}")
        print(f"检测目标: {self.client.base_url} / {self.model}")
        print(f"{'='*50}\n")

        print("🔍 [1/4] 模型指纹...")
        identity = self.test_identity()
        for i, r in enumerate(identity):
            print(f"  问题{i+1}: {r}")

        print("\n📊 [2/4] 能力基准...")
        benchmark = self.test_benchmark()
        print(f"  得分: {benchmark['score']}")
        for d in benchmark["details"]:
            status = "✅" if d["passed"] else "❌"
            print(f"  {status} {d['question']}...")

        print("\n🎲 [3/4] 响应分布...")
        dist = self.test_distribution()
        print(f"  采样: {dist['responses']}")
        print(f"  多样性: {dist['unique_ratio']:.1%}")

        print("\n⏱️ [4/4] 延迟特征...")
        latency = self.test_latency()
        print(f"  延迟: {latency['latencies_sec']}")
        print(f"  平均: {latency['avg']}s, 方差: {latency['variance']}")

        return {
            "identity": identity,
            "benchmark": benchmark,
            "distribution": dist,
            "latency": latency
        }


# ========== 用法 ==========
if __name__ == "__main__":
    # 换成你自己的 API 配置
    verifier = ModelVerifier(
        base_url="https://api.openai.com/v1",  # 或者你的中转地址
        api_key="sk-xxx",
        model="claude-sonnet-4-20250514"
    )
    results = verifier.run_all()

复制粘贴就能跑。把 base_url 换成你的 API 提供商地址,model 换成你要测的模型。

怎么看结果

跑完四项检测,重点关注这几个信号:

🚩 红旗信号

指纹不对:你调的是 Claude,模型自称 GPT 或者回答含糊其辞——大概率被偷换了。正牌 Claude 会明确说自己是 Anthropic 做的 Claude。

基准分低:4 题里错 2 题以上,要么模型被降级了,要么根本不是你付费的那个模型。特别是数学题和逻辑题,不同模型差距非常明显。

分布异常:temperature=0.9 采样 5 次全是同一个数字?不正常。要么有缓存层在代答,要么 temperature 参数被忽略了。

延迟离谱:简单的 hello 请求平均 5 秒以上,说明你的请求经过了多层转发。方差特别大的话,可能是在多个后端之间负载均衡(有些是真的有些是假的)。

✅ 正常参考值

拿 Claude Sonnet 举例,正常情况下应该是:

  • 三个身份问题都提到 Anthropic / Claude
  • 基准题 4/4 或 3/4
  • 延迟 0.5-2s(取决于区域和网络)

踩坑记录

写这个脚本过程中踩了几个坑,记录一下:

坑 1:temperature=0 不代表结果完全一致

不同提供商对 temperature=0 的实现有微妙区别。有些会加随机种子,有些直接 greedy decode。所以指纹测试我用 temperature=0,分布测试用 0.9,两个场景分开验。

坑 2:模型指纹可以被 system prompt 篡改

有些中转商会在你的请求前注入 system prompt,比如强制让模型自称 Claude。这种情况单靠身份问题是测不出来的,得结合能力基准和延迟交叉验证。

论文里也提到了这个,他们用了一个叫 "Technical Fingerprinting" 的方法——利用不同模型在特定 prompt 下的独特响应模式来识别,比如让模型数 "strawberry" 里有几个 r,不同模型的错误模式是不一样的。

坑 3:有些提供商会做智能路由

就是你调便宜模型走真的,调贵的模型偷偷给你换。所以要测就测你最常用、最贵的那个模型。

我的实际体验

我日常开发用的是 ofox.ai 的聚合接口,因为一个 key 能切不同模型比较省心。跑了一遍检测脚本:

  • 指纹三题全对,确认是 Claude
  • 基准 4/4
  • 延迟平均 1.2s,走的阿里云线路比直连还快一点

当然,这只是我个人测试的一个时间点的结果。论文的核心观点是对的:不能盲目信任任何第三方提供商,定期跑一跑检测脚本才是正道。

小结

Shadow API 这篇论文最大的价值不是吓人,而是给出了一套可操作的验证框架。我把它简化成了 100 行 Python,四个维度交叉验证:

  1. 模型指纹:问它是谁
  2. 能力基准:考它几道题
  3. 响应分布:多次采样看一致性
  4. 延迟特征:测链路健康度

脚本放在那里,随便用。建议每周跑一次,或者换 API 提供商时跑一次。毕竟花了真金白银买 Claude,总得确认拿到的不是个 "Claude 味的 DeepSeek" 吧。


论文链接:arxiv.org/abs/2603.01919