首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >用 Python 给大模型跑分:dataclass、异步重试和代理配置一个不少

用 Python 给大模型跑分:dataclass、异步重试和代理配置一个不少

原创
作者头像
小白学大数据
发布2026-09-22 17:04:23
发布2026-09-22 17:04:23
180
举报

看到这个标题我先笑了一下。鬼故事这三个字用得诚实,因为它确实就是个鬼故事。DeepSeek 4.1、Opus 5、GPT 5.6,这几个版本号我一个都核实不了。真要按照发布节奏,它们大概率还没出,或者出了但我不知道。拿一个没法验证的对比当结论抛出来,和村口大爷说隔壁老王中彩票了是一个性质:传得热闹,查无此事。

但这事值得认真写一写,不是因为谁输谁赢,而是因为太多人真的会信这种标题,然后拿它当选型依据。我干爬虫和数据采集这些年,最怕的就是用别人喂的结论代替自己跑的数据。模型强弱这东西,你不自己搭一套评测跑一遍,永远是在别人嘴里听结果。

我也理解为什么这种标题有人信。每天都有新模型、新榜单砸过来,谁有功夫一个个验证。标题把复杂的取舍压成一个字"强",读着省事。省事的东西传播快,这是人性,不是你的问题。

为什么 headline 上的对比基本不能信

同一个模型在不同榜单上的名次能差出十条街。原因不复杂,但标题从不会告诉你。

测试集不一样,结论就跟着变。有人用 MMLU,有人用 GSM8K,有人自己攒了一堆业务题。模型 A 在数学上吊打 B,换到代码生成可能反过来。你只看到一个总分,总分背后的构成全被吃掉了。

提示词敏感度这事儿被严重低估。同一个问题,加一句"请一步步思考"和不加,同一个模型的得分能差好几个点。很多榜单的 prompt 是固定模板,换个人写就是另一个结果。这种脆弱性不会写在标题里。

还有测试集污染。训练数据里要是混进了评测题,那分数就是开卷考试,看着漂亮,落地就露馅。这块各家都没法自证清白,只能看第三方复现。我一般只信那种公开了题目和打分脚本的评测,其余当八卦看了。

再说句不好听的,评测本身就是门生意。每家实验室发新模型都要配一份漂亮榜单,挑自己擅长的题、调自己顺手的 prompt,发出来就是新闻。你看到的标题越夸张,背后越可能是精心选过的子集。这不是阴谋论,是发布会的基本操作。所以看到"碾压""超越"这种词,先默认它只在一小块定制数据上成立。

所以我的铁律只有一条:任何模型对比,先问"用的什么题、怎么问的、跑了几遍"。答不上来,当故事听就行。

自己搭一套最小可用的评测管线

与其信标题,不如花一个下午把评测跑起来。下面这套东西我自己在用,核心就三块:配置用 dataclass 收口,请求用异步加重试,日志全程留痕。代码能直接跑,依赖就 httpx 一个。

代码语言:javascript
复制
from dataclasses import dataclass
from typing import AsyncIterator
import httpx
import logging
import asyncio
import random

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
)
logger = logging.getLogger("llm_bench")


@dataclass
class ModelConfig:
    name: str
    api_url: str
    api_key: str
    # 走亿牛云代理时填这里,海外模型 API 从国内机器直连经常卡在握手阶段
    proxy: str | None = None
    timeout: float = 30.0
    max_retries: int = 3


@dataclass
class BenchResult:
    model: str
    question_id: str
    answer: str
    latency_ms: int
    ok: bool = True
    error: str | None = None


async def call_model(
    client: httpx.AsyncClient,
    cfg: ModelConfig,
    question: str,
    qid: str,
) -> BenchResult:
    payload = {
        "model": cfg.name,
        "messages": [{"role": "user", "content": question}],
        "temperature": 0,
    }
    for attempt in range(cfg.max_retries):
        try:
            resp = await client.post(
                cfg.api_url,
                json=payload,
                headers={"Authorization": f"Bearer {cfg.api_key}"},
                timeout=cfg.timeout,
            )
            resp.raise_for_status()
            data = resp.json()
            text = data["choices"][0]["message"]["content"]
            return BenchResult(
                model=cfg.name,
                question_id=qid,
                answer=text,
                latency_ms=int(resp.elapsed.total_seconds() * 1000),
            )
        except Exception as e:
            wait = (2 ** attempt) + random.random()
            logger.warning("模型 %s 第 %d 次失败: %s,%.1fs 后重试", cfg.name, attempt + 1, e, wait)
            await asyncio.sleep(wait)
    return BenchResult(cfg.name, qid, "", 0, ok=False, error="retries exhausted")


async def run_bench(
    configs: list[ModelConfig],
    questions: list[tuple[str, str]],
) -> AsyncIterator[BenchResult]:
    limits = httpx.Limits(max_connections=8)
    for cfg in configs:
        async with httpx.AsyncClient(proxy=cfg.proxy, limits=limits) as client:
            for qid, q in questions:
                yield await call_model(client, cfg, q, qid)


models = [
    ModelConfig(
        name="deepseek-4.1",
        api_url="https://api.deepseek.com/v1/chat/completions",
        api_key="YOUR_KEY",
        proxy="http://user:pass@proxy.yiniu.cn:7788",
    ),
    ModelConfig(
        name="opus-5",
        api_url="https://api.anthropic.com/v1/messages",
        api_key="YOUR_KEY",
    ),
    ModelConfig(
        name="gpt-5.6",
        api_url="https://api.openai.com/v1/chat/completions",
        api_key="YOUR_KEY",
        proxy="http://user:pass@proxy.yiniu.cn:7788",
    ),
]

questions = [("q1", "用 Python 写一个快速排序,并解释时间复杂度。")]

proxy 字段我留了位置,因为从国内服务器直接打 OpenAI 和 Anthropic 的接口,十次有八次卡在握手阶段。我一般用亿牛云代理的出海线路做转发,延迟稳,也不会因为 IP 被风控整批失败。这不是广告,是踩了无数次坑之后的选择。

重试用的是指数退避加一点随机抖动,避免一堆请求在同一秒同时重试把接口打爆。temperature 设 0,评测要的是可复现,不是创意。日志用标准库就够,跑完翻一眼 WARNING 就知道哪家的接口在抽风。

跑出来的结果我建议落库而不是打印完拉倒。建一张 results 表,字段就 model、question_id、answer、latency_ms、ok,后面用 SQL 直接 group by 出各维度得分,比在内存里现算方便太多。评测这东西会反复跑,能查历史比能跑一次值钱。

还有个细节:把这次跑的模型版本、题目哈希、温度、代理线路全写进一张 meta 表。三个月后你忘了当时用的哪版 API,没有记录就只能重跑。可复现不是学术要求,是怕自己坑自己。

题从哪来,比模型重要

很多人卡在第一步:拿什么题去测。我的做法是从真实业务里挖,而不是去背公开榜单。

比如你做的是客服问答,就把过去半年的用户提问日志导出来,按意图分个类,挑三百条有代表性的。你做的是代码生成,就攒一堆内部真实的 bug 单和重构需求。这种题竞争对手拿不到,模型在公开榜上刷分的套路对你没用,测出来的才是你关心的东西。

采集这些日志本身就是个爬虫活。脱敏之后批量入库,再随机抽一部分人工标答案,剩下交给模型跑,最后比对。这套流程跑顺了,你换任何新模型都能在半小时内出一份只属于你业务的榜单。

题量不用贪多,三百条覆盖主要场景就够出趋势了。关键是去重和分层,别让某一类问题占了大半,那样总分就被它带偏。我一般先按业务意图分桶,每个桶里随机抽固定比例,保证报告里每类都有代表性。

让模型当裁判,坑也不少

分数总不能全靠人手标,量大了扛不住。常见的办法是拿一个强模型当裁判,把标准答案和待评答案一起喂进去让它打分。这招能用,但有两个坑要记着。

裁判模型有偏好。它普遍偏爱长得长、说得满的答案,哪怕内容更水。所以光看总分容易把啰嗦的模型捧上去。我的处理是让它打分项分,每条标准单独给 0 到 1 的分,再加权,比直接要个总分稳。

裁判也会累。同一对答案换不同顺序喂进去,分数能飘。我习惯每对答案跑两次,顺序颠倒一次,取平均。成本上去了,但比被随机性忽悠强。

怎么读结果才算诚实

跑完别急着宣布谁第一。先看三件事。

方差。同一道题连跑五次,如果四次对一次错,那这个"对"是侥幸还是真懂,你心里得打个问号。我习惯每个 question 跑三遍取众数,单点结果不进报告。

分维度。数学、代码、长文本、中文理解,分开算分。总分第一的模型可能在你最关心的那个维度垫底。选型看的是你自己的短板,不是别人的总分。

成本。很多对比把价格和速度吃了。一个模型强两个点但贵十倍慢五倍,对你未必划算。我会在结果表里把每千 token 价格和 p99 延迟一起列出来,让读者自己权衡。

顺带提一句样本量。你只测了二十道题,那点差距放在统计上根本不显著,两个模型其实半斤八两。题目过百再谈谁领先,不然所谓的强弱只是噪声。我会在报告里附上置信区间,差没出区间的一律写"持平"。

真实结论往往很无聊

说回开头那个标题。等这几个版本真的出了,你大概率会看到一堆互相矛盾的榜单,有的说 DeepSeek 强,有的说 Opus 强,有的说 GPT 强。它们可能都是对的,因为测的不是同一套东西。

我做数据采集这么久,最大的教训就是别拿别人的结论当自己的依据。鬼故事听个乐就行,真要选型,把上面那套脚本改改你自己的业务题,跑一轮。数据比任何标题都靠谱,包括我这篇。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 为什么 headline 上的对比基本不能信
  • 自己搭一套最小可用的评测管线
  • 题从哪来,比模型重要
  • 让模型当裁判,坑也不少
  • 怎么读结果才算诚实
  • 真实结论往往很无聊
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档