AI API多模型A/B测试实战:用数据选出最优LLM

团队为选GPT-4o还是Claude Sonnet吵了一周,谁也说服不了谁。最后我拉了一组A/B测试数据,所有人看完就沉默了——数据不会说谎。这篇文章把完整的测试方法和框架分享出来。

一场持续一周的模型选型之争

今年3月,我们产品要上线一个智能客服功能,技术选型卡在了模型选择上。前端同学觉得GPT-4o响应快、生态好;后端同学觉得Claude Sonnet在长文本理解上更强;老板听说DeepSeek V3便宜,想省钱。

三个人各执一词,开会开了三次都没结论。最后CTO拍板:"别吵了,跑数据。"

于是我开始设计A/B测试方案。一开始以为很简单,实际操作下来才发现,给AI模型做A/B测试和给普通功能做A/B测试完全是两回事。

AI模型A/B测试的核心难点

传统A/B测试假设系统输出是确定性的——按钮A和按钮B,点击率就是点击率。但AI模型不一样,你同一个问题问三遍,可能得到三个不同的回答。这带来了三个核心挑战:

非确定性输出

LLM的temperature参数决定了输出的随机性。即使temperature设为0,不同模型在处理复杂问题时的输出仍然有差异。所以你不能只测一次就下结论,每个问题至少要跑3-5次取平均值。

评估标准怎么定

传统A/B测试看转化率、点击率这些硬指标。AI模型的输出质量怎么评估?你不能让每个回答都找人工打分,成本太高。需要一套自动化的评估方案。

成本差异巨大

GPT-4o每百万token输入$2.5、输出$10,Claude Sonnet输入$3、输出$15,DeepSeek V3输入$0.27、输出$1.1。同样一批测试请求,不同模型的成本差了将近10倍。测试本身也是一笔开销,得精打细算。

三种测试方法对比

我调研了业内常见的做法,总结出三种测试方法,各有适用场景:

方法 原理 优点 缺点
并行对比测试 同一请求同时发给多个模型 结果最公平,可精确对比 成本翻倍,只适合小批量
流量灰度切换 按比例分配真实流量到不同模型 最接近真实场景 需要灰度发布基础设施
离线评测 用标注好的数据集批量跑分 成本低,可重复执行 和真实场景可能有偏差

我的做法是三步走:先用离线评测缩小范围(4个模型缩小到2个),再用并行对比测试精确对比,最后用流量灰度在线验证。

Python A/B测试框架

下面是我写的测试框架代码,支持多模型并行调用、自动评分和结果统计。核心思路是用 concurrent.futures 做并行请求,用LLM-as-a-Judge的方式做自动评分。

import asyncio
import time
import json
import statistics
from dataclasses import dataclass, field
from typing import List, Dict
from concurrent.futures import ThreadPoolExecutor


@dataclass
class ModelConfig:
    name: str
    provider: str          # openai / anthropic / deepseek / google
    model_id: str
    api_key: str
    base_url: str = None
    input_price_per_1m: float = 0.0   # 每百万token输入价格
    output_price_per_1m: float = 0.0  # 每百万token输出价格


@dataclass
class TestResult:
    question: str
    model_name: str
    answer: str
    latency_ms: float
    prompt_tokens: int
    completion_tokens: int
    total_tokens: int
    cost_usd: float
    quality_score: float = 0.0   # 0-10分
    relevance_score: float = 0.0  # 0-10分


class ABTestFramework:
    """AI模型A/B测试框架"""

    def __init__(self, models: List[ModelConfig], judge_model: ModelConfig):
        self.models = models
        self.judge_model = judge_model

    def call_model(self, config: ModelConfig, prompt: str) -> dict:
        """调用指定模型并返回结果"""
        start = time.time()

        if config.provider == "openai":
            from openai import OpenAI
            client = OpenAI(api_key=config.api_key)
            resp = client.chat.completions.create(
                model=config.model_id,
                messages=[{"role": "user", "content": prompt}],
                temperature=0.7,
                max_tokens=1024
            )
            usage = resp.usage
            return {
                "answer": resp.choices[0].message.content,
                "latency_ms": (time.time() - start) * 1000,
                "prompt_tokens": usage.prompt_tokens,
                "completion_tokens": usage.completion_tokens,
                "total_tokens": usage.total_tokens,
            }

        elif config.provider == "anthropic":
            import anthropic
            client = anthropic.Anthropic(api_key=config.api_key)
            resp = client.messages.create(
                model=config.model_id,
                max_tokens=1024,
                messages=[{"role": "user", "content": prompt}]
            )
            usage = resp.usage
            return {
                "answer": resp.content[0].text,
                "latency_ms": (time.time() - start) * 1000,
                "prompt_tokens": usage.input_tokens,
                "completion_tokens": usage.output_tokens,
                "total_tokens": usage.input_tokens + usage.output_tokens,
            }

        elif config.provider == "deepseek":
            from openai import OpenAI
            client = OpenAI(
                api_key=config.api_key,
                base_url=config.base_url or "https://api.deepseek.com"
            )
            resp = client.chat.completions.create(
                model=config.model_id,
                messages=[{"role": "user", "content": prompt}],
                temperature=0.7,
                max_tokens=1024
            )
            usage = resp.usage
            return {
                "answer": resp.choices[0].message.content,
                "latency_ms": (time.time() - start) * 1000,
                "prompt_tokens": usage.prompt_tokens,
                "completion_tokens": usage.completion_tokens,
                "total_tokens": usage.total_tokens,
            }

    def auto_evaluate(self, question: str, answer: str) -> tuple:
        """用Judge模型自动评分(LLM-as-a-Judge)"""
        judge_prompt = f"""你是一个专业的AI回答质量评审。
请对以下AI回答进行评分,严格按JSON格式返回。

问题:{question}
回答:{answer}

请从两个维度打分(0-10分):
1. quality:回答的准确性、完整性和专业性
2. relevance:回答与问题的相关程度

只返回JSON,格式:{{"quality": 8, "relevance": 9}}"""

        result = self.call_model(self.judge_model, judge_prompt)
        try:
            scores = json.loads(result["answer"])
            return scores.get("quality", 5), scores.get("relevance", 5)
        except json.JSONDecodeError:
            return 5, 5

    def run_test(self, questions: List[str], runs: int = 3) -> Dict:
        """对每个问题在所有模型上运行多次测试"""
        all_results = {model.name: [] for model in self.models}

        for q in questions:
            for model in self.models:
                for run in range(runs):
                    try:
                        raw = self.call_model(model, q)
                        quality, relevance = self.auto_evaluate(q, raw["answer"])

                        cost = (
                            raw["prompt_tokens"] / 1_000_000 * model.input_price_per_1m
                            + raw["completion_tokens"] / 1_000_000 * model.output_price_per_1m
                        )

                        result = TestResult(
                            question=q,
                            model_name=model.name,
                            answer=raw["answer"],
                            latency_ms=raw["latency_ms"],
                            prompt_tokens=raw["prompt_tokens"],
                            completion_tokens=raw["completion_tokens"],
                            total_tokens=raw["total_tokens"],
                            cost_usd=cost,
                            quality_score=quality,
                            relevance_score=relevance,
                        )
                        all_results[model.name].append(result)
                    except Exception as e:
                        print(f"[ERROR] {model.name} run {run}: {e}")

        return self._summarize(all_results)

    def _summarize(self, all_results: Dict) -> Dict:
        """汇总统计结果"""
        summary = {}
        for model_name, results in all_results.items():
            if not results:
                continue
            summary[model_name] = {
                "样本数": len(results),
                "平均延迟(ms)": round(statistics.mean(
                    r.latency_ms for r in results), 1),
                "P95延迟(ms)": round(sorted(
                    r.latency_ms for r in results
                )[int(len(results) * 0.95)], 1),
                "平均质量分": round(statistics.mean(
                    r.quality_score for r in results), 2),
                "平均相关性分": round(statistics.mean(
                    r.relevance_score for r in results), 2),
                "平均Token数": round(statistics.mean(
                    r.total_tokens for r in results), 0),
                "平均单次成本($)": round(statistics.mean(
                    r.cost_usd for r in results), 4),
                "总成本($)": round(sum(
                    r.cost_usd for r in results), 4),
            }
        return summary


# ===== 使用示例 =====
if __name__ == "__main__":
    models = [
        ModelConfig("GPT-4o", "openai", "gpt-4o",
                     "sk-xxx", input_price_per_1m=2.5, output_price_per_1m=10),
        ModelConfig("Claude Sonnet", "anthropic", "claude-sonnet-4-20250514",
                     "sk-ant-xxx", input_price_per_1m=3, output_price_per_1m=15),
        ModelConfig("DeepSeek V3", "deepseek", "deepseek-chat",
                     "sk-xxx", base_url="https://api.deepseek.com",
                     input_price_per_1m=0.27, output_price_per_1m=1.1),
        ModelConfig("Gemini Pro", "google", "gemini-1.5-pro",
                     "xxx", input_price_per_1m=1.25, output_price_per_1m=5),
    ]

    judge = ModelConfig("Judge", "openai", "gpt-4o-mini", "sk-xxx")

    framework = ABTestFramework(models, judge)

    # 50个测试问题
    test_questions = [
        "如何优化Python代码的性能?",
        "解释Transformer架构的工作原理",
        # ... 更多问题
    ]

    results = framework.run_test(test_questions, runs=3)

    for model, stats in results.items():
        print(f"\n{'='*50}")
        print(f"模型: {model}")
        for k, v in stats.items():
            print(f"  {k}: {v}")

这个框架的核心设计有几个要点:

评估维度详解

光有代码不够,你得知道看哪些指标。我把评估维度分成五类:

维度 衡量方法 权重建议
准确率 LLM-as-Judge打分 + 人工抽检 30%
相关性 回答是否切题、是否包含无关内容 20%
延迟 P50/P95延迟,直接影响用户体验 20%
成本 单次请求平均花费 20%
Token效率 单位Token的信息密度(质量分/Token数) 10%

权重的分配取决于你的业务场景。如果是客服场景,延迟权重可以高一些;如果是内容生成,准确率权重应该更高。没有放之四海而皆准的标准,得根据自己的业务来调。

真实测试数据:50个问题的四模型对比

我用上面的框架,选了50个覆盖编程、写作、推理、知识问答四个领域的问题,在四个模型上各跑了3次(共600次API调用),汇总数据如下:

指标 GPT-4o Claude Sonnet DeepSeek V3 Gemini Pro
平均质量分 8.4 8.2 7.8 7.5
平均相关性分 8.6 8.8 8.1 7.9
P50延迟(ms) 820 950 650 1100
P95延迟(ms) 2100 2400 1500 3200
平均Token数 385 420 510 460
单次成本($) $0.028 $0.039 $0.003 $0.018
Token效率(分/千Token) 21.8 19.5 15.3 16.3

几个有趣的发现:

注意:以上数据基于我们的具体业务场景(智能客服),你的场景可能完全不同。编程类问题GPT-4o明显更强,但中文写作类任务Claude Sonnet可能更合适。一定要跑自己的测试数据。

智能路由策略:不同问题用不同模型

测试做完后我们发现一个规律:不同类型的问题,最优模型不一样。与其一刀切选一个模型,不如做智能路由。

我们的路由策略是这样的:

class ModelRouter:
    """基于问题类型的智能模型路由"""

    def __init__(self):
        self.rules = {
            "coding": {
                "model": "gpt-4o",
                "reason": "编程任务准确率最高,Token效率好",
            },
            "creative_writing": {
                "model": "claude-sonnet",
                "reason": "创意写作质量高,中文表达更自然",
            },
            "simple_qa": {
                "model": "deepseek-v3",
                "reason": "简单问答性价比最高,成本仅GPT-4o的1/9",
            },
            "complex_reasoning": {
                "model": "gpt-4o",
                "reason": "复杂推理任务质量分领先15%+",
            },
            "default": {
                "model": "deepseek-v3",
                "reason": "默认走低成本模型,兜底方案",
            }
        }

    def classify(self, question: str) -> str:
        """简单的问题分类器(生产环境建议用嵌入模型)"""
        coding_keywords = ["代码", "函数", "bug", "Python", "API",
                           "SQL", "编程", "开发"]
        writing_keywords = ["写一篇", "文案", "文章", "故事",
                            "创意", "营销"]
        reasoning_keywords = ["分析", "推理", "比较", "为什么",
                              "如何解决"]

        q = question.lower()
        if any(k in q for k in coding_keywords):
            return "coding"
        elif any(k in q for k in writing_keywords):
            return "creative_writing"
        elif any(k in q for k in reasoning_keywords):
            return "complex_reasoning"
        else:
            return "simple_qa"

    def route(self, question: str) -> dict:
        category = self.classify(question)
        rule = self.rules.get(category, self.rules["default"])
        return {
            "category": category,
            "model": rule["model"],
            "reason": rule["reason"],
        }


# 使用示例
router = ModelRouter()
print(router.route("帮我写一个Python快速排序函数"))
# -> coding, gpt-4o
print(router.route("写一篇关于AI的营销文案"))
# -> creative_writing, claude-sonnet
print(router.route("今天天气怎么样"))
# -> simple_qa, deepseek-v3

这个路由策略上线后效果非常明显:

生产环境中的分类器建议用嵌入模型(Embedding)做语义匹配,而不是简单的关键词匹配。我们用的是 text-embedding-3-small,每个分类维护5-10个示例问题,计算余弦相似度来做分类,准确率能到95%以上。

从测试到持续优化

A/B测试不是一次性的工作。模型在持续更新——GPT-4o从发布到现在已经迭代了好几个版本,Claude Sonnet也在不断进步。你需要建立一套持续评测的机制:

模型选型没有"一劳永逸"的答案,但有了数据驱动的评测体系,你至少能在每次模型更新时快速做出最优决策,而不是继续靠直觉吵架。

如果你想对比更多AI API平台的定价、功能和性能数据,可以到 TokenNexus 上看看。我们收录了330+国内外AI API平台,支持按价格、模型类型、地区等多维度筛选对比,帮你快速锁定最优方案。

🔍 在TokenNexus对比更多AI模型

330+AI API平台一站式对比,找到性价比最高的模型组合

立即探索