最近大模型圈的新版本消息几乎是一波接一波。像 DeepSeek V4 Pro、Grok 4.6、Opus 4.8 这类名字频繁出现在开发者社区里,很多人关心的问题很直接:这些模型在真实开发任务里到底谁更能打,而不是只看官方发布会里精心设计的 Demo。
但真要回答这个问题,不能只转载别人的结论,更不能相信几张随手截图的“跑分”。靠谱的做法是搭一套自己的评测闭环:设计测试集,统一调用不同模型,用相同 Prompt、相同参数去跑,最后收集结果、统计正确率、分析错误类型。这套流程不仅能用来横向比较模型,也能用于日常回归测试——新版本发布后,只要把旧题集重新跑一遍,就能判断升级是否值得。
这篇文章会围绕这个目标展开。如果你正准备给团队做模型选型,或者想验证某个新模型是否值得接入,可以按下面的步骤落地一套可复用的评测方案。文章包含完整的测试集设计思路、Python 评测脚本、配置文件示例以及我在实际评测中踩过的典型问题,新手也能照着跑。
1. 为什么需要自己的“硬核评测”
1.1 榜单跑分与真实业务之间存在明显差距
很多人习惯直接看公开榜单排名,但榜单分数高不代表在业务里好用。原因大致有几点。
第一,公开数据集有被模型“记住”的风险。模型训练时会尽量多读互联网文本,如果某个公开评测集大量出现在训练语料里,模型在测试时给出的答案可能不是来自推理能力,而是来自记忆。这会导致分数虚高,无法反映真实水平。
第二,公开题目偏向标准化问答,真实业务场景却是复杂、模糊、带约束的。例如研发团队需要模型完成“修复某个函数并补充单元测试”“根据接口文档生成调用代码”这类任务,公开榜单很少覆盖这种细节。
第三,不同服务商对同一评测集的接入方式不同,比如上下文长度、输出上限、默认参数都有差异。如果不去控制这些变量,最后比较出的差异可能是服务配置差异,而不是模型能力差异。
所以,自建评测的意义不是推翻公开跑分,而是建立一套贴近自身业务的参考系。哪怕测试集只有一两百道题,只要题目来自真实工作场景,结论就比泛泛的舆情判断更有价值。
1.2 一次有价值的模型评测要回答哪些问题
不同的团队,评测目标不同。在开始之前,先想清楚要回答以下哪些问题:
- 同一个任务换一个模型,是否能减少人工修改成本?
- 模型的输出格式是否稳定,能否直接写入后续流程?
- 面对边界输入或者歧义问题,模型会“装懂”还是诚实承认不确定?
- 中文场景、代码注释场景、专业术语场景是否存在明显短板?
- 单次请求是否可接受,并发量上来后是否容易出现限流和超时?
- 输出内容的合规性和安全边界是否符合生产要求?
把这些问题拆开,再映射到具体的测试题类型,评测才不会变成盲目的“为了跑分而跑分”。
1.3 评测不是一个一次性的活动
建议把评测当成一个长期的脚手架。最简单的方式是维护一份本地测试集,包含不同难度和不同类别的题目;每来一个新模型或一个新版本,就运行一遍同一份测试集,记录结果变化。时间久了,这份测试集会成为团队非常重要的数据资产。
2. 评测维度与测试集设计
2.1 面向开发任务的评测维度
为了贴近真实研发场景,我一般把测试题目分成几个维度。每个维度对应一类常见工作:
| 评测维度 | 典型任务 | 评判重点 |
|---|---|---|
| 代码生成 | 根据需求描述写出完整函数 | 正确性、可读性、依赖处理 |
| 代码修复 | 给定有 bug 的代码,要求定位并修改 | 是否准确找到根因、有没有引入新问题 |
| 逻辑推理 | 分析条件关系、判断必然结论 | 推理连贯性、是否被无关信息干扰 |
| 数学计算 | 完成基础数学推导或数值计算 | 结果精确性和过程合理性 |
| 指令跟随 | 严格按格式要求输出 | 格式遵守程度、是否漏项 |
| 鲁棒性 | 输入包含噪声、歧义或极端情况 | 是否仍然给出可用结果或拒绝风险请求 |
不建议把“知识问答”作为唯一核心维度。今天的模型普遍“见多识广”,一般知识问题很难拉开差距,反而是代码正确性、边界处理和输出格式稳定性更容易暴露问题。
2.2 测试集数量与被测题目的选择
建立测试集时可以分阶段:
- 探针集:30 到 60 道题,覆盖上述所有维度,用于快速判断模型基础能力。
- 小规模评测集:200 到 300 道题,用于对比多个模型,并统计分类正确率。
- 完整回归集:500 到 1000 道题,用于项目上线前的模型选型或版本升级评估。
题目来源可以来自真实 Bug、公共算法题改写、内部接口文档示例、日常开发对话记录等。需要注意的是,如果题目来自内部代码,必须先做脱敏处理,别把密钥、业务数据和用户隐私直接塞进请求。
2.3 推荐使用 JSONL 格式管理测试集
一份结构化的数据集应该把题目、选项、答案、分类都放在一起,方便程序读取。推荐使用 JSONL 格式,每行一个 JSON 对象。字段越标准,后续接入评测脚本越简单。下面是示例字段:
{ "id": "code-001", "category": "code", "type": "choice", "difficulty": "easy", "question": "...", "options": ["A. ...", "B. ...", "C. ...", "D. ..."], "answer": "B", "explanation": "..." }其中type可以取choice表示选择题,也可以取open表示开放性生成题。选择题适合程序自动判分,开放题需要人工或辅助模型判分。建议刚开始做评测时,以选择题和可验证的代码题为主,留少量开放题做人工抽测。
2.4 评测指标不能只看正确率
正确率是最直观的指标,但不能代表全部。还有几个指标在实际评测中很有价值:
- 格式合规率:模型返回是否能被程序直接解析。
- 空答率:模型是否经常拒绝回答,甚至没有给出合理原因。
- 输出长度:是否废话太多,影响下游处理速度。
- 首次可用率:生成结果是否需要大量修改才能使用。
这些指标无法单靠“对错”衡量,但它们在工程接入时非常重要。
3. 环境准备与模型接入
3.1 统一通过兼容接口完成接入
很多模型服务商都提供了与 OpenAI Chat Completions 类似的接入方式。只要替换base_url、api_key和model三个参数,就可以用同一套 Python 代码调用不同服务商的大模型。
标题中提到的 DeepSeek V4 Pro、Grok 4.6、Opus 4.8,我会在后面的配置示例中作为不同 provider 出现。不过要特别说明:不同平台对 model 的命名规则并不统一,例如有的平台写deepseek-v4-pro,有的平台可能要求写成DeepSeek-V4-Pro-xxx。配置时一定要以服务商 API 文档为准。
部分服务商的模型名可能尚未正式开放,或者你的账号没有对应调用权限。遇到这种情况,只需要把配置里的model换成服务商实际可用的模型 id 即可,评测代码不需要改动。
3.2 Python 环境依赖
推荐使用 Python 3.10 或更高版本。评测脚本依赖两个主要库:
openai:用于调用兼容接口的 Chat Completion。pyyaml:用于解析 YAML 配置文件。
安装命令如下:
pip install "openai>=1.30" "PyYAML>=6.0"如果你只需要比较非常简单的文本输出,也可以直接用requests发送 HTTP 请求。不过使用openaiSDK 代码更短,也更便于后续扩展流式输出和超时重试。
3.3 项目目录结构
建议目录结构如下:
llm-eval/ ├── config.yaml ├── dataset/ │ └── sample.jsonl ├── eval_runner.py ├── results/ └── logs/其中:
config.yaml存放各模型的接入配置。dataset存放测试集中的 JSONL 文件。eval_runner.py是评测入口脚本。results保存最终的统计结果。logs保存请求响应原始日志。
这样划分以后,新增模型、新增题目、查看结果都可以模块化处理,不会把代码和数据集混在一起。
4. 完整评测脚本实现
4.1 编写配置文件
下面是一个基础配置示例。注意这里的接口地址均为示例,请替换成各服务商实际的接口域名。
# config.yaml providers: deepseek: base_url: "https://api.example.com/v1" api_key_env: "DEEPSEEK_API_KEY" model: "deepseek-v4-pro" temperature: 0 max_tokens: 2048 grok: base_url: "https://api.grok.example.com/v1" api_key_env: "GROK_API_KEY" model: "grok-4.6" temperature: 0 max_tokens: 2048 opus: base_url: "https://api.opus.example.com/v1" api_key_env: "OPUS_API_KEY" model: "opus-4.8" temperature: 0 max_tokens: 2048 request: timeout: 60 max_retries: 2字段含义如下:
base_url:服务商的接口地址,必须与模型服务商一致。api_key_env:API Key 所在的环境变量名,避免直接把密钥写进代码库。model:服务商侧的模型 id,以实际可成功调用为准。temperature:采样温度。评测客观题时建议设为 0,降低随机性。max_tokens:模型单次回复的最大 token 数。
这段配置中,三个模型名只是演示用的占位值。如果模型 id 写错,调用阶段通常会报“model not found”。正式评测前先做探活请求,确认返回结果中的模型名,是最省时间的做法。
4.2 编写评测入口脚本
下面给出一个尽量精简但可运行的评测脚本。核心逻辑分为三步:
- 读取 JSONL 测试集。
- 按配置构造请求,并发调用不同模型。
- 保存原始响应,并对选择题做简单自动判分。
文件路径:llm-eval/eval_runner.py
import json import os import re import time import uuid from concurrent.futures import ThreadPoolExecutor, as_completed import yaml from openai import OpenAI def load_config(path: str) -> dict: with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f) def load_dataset(path: str) -> list[dict]: samples = [] with open(path, "r", encoding="utf-8") as f: for line in f: line = line.strip() if line: samples.append(json.loads(line)) return samples def build_messages(sample: dict) -> list[dict]: question = sample["question"] if sample.get("type") == "choice" and sample.get("options"): options_text = "\n".join(sample["options"]) user_content = f"{question}\n请从选项中选出正确的一项,只输出选项字母。\n{options_text}" else: user_content = question return [ { "role": "system", "content": "You are a helpful assistant participating in an offline evaluation.", }, {"role": "user", "content": user_content}, ] def chat_once( client: OpenAI, model: str, messages: list[dict], temperature: float = 0, max_tokens: int = 2048, timeout: int = 60, ) -> str: resp = client.chat.completions.create( model=model, messages=messages, temperature=temperature, max_tokens=max_tokens, timeout=timeout, ) # 部分推理模型会把思维链内容放在 reasoning_content 字段 return resp.choices[0].message.content or "" def extract_choice(text: str) -> str: # 优先匹配独立的 A/B/C/D match = re.search(r"\b([A-D])\b", text.upper()) if match: return match.group(1) # 如果没有单个字母,尝试匹配 “答案是 X” 之类表达 match = re.search(r"答案是?\s*([A-D])", text) if match: return match.group(1) return "" def judge_sample(sample: dict, response: str) -> dict: if sample.get("type") == "choice": predicted = extract_choice(response) return { "predicted": predicted, "expected": sample.get("answer", ""), "is_correct": predicted == sample.get("answer", ""), } return { "predicted": response, "expected": sample.get("answer", ""), "is_correct": None, } def run_provider( provider_name: str, provider_cfg: dict, samples: list[dict], request_cfg: dict, ): client = OpenAI( base_url=provider_cfg["base_url"], api_key=os.environ.get(provider_cfg["api_key_env"], "not-set"), ) records = [] for sample in samples: question_id = sample["id"] messages = build_messages(sample) start_time = time.time() response = "" error = "" try: response = chat_once( client=client, model=provider_cfg["model"], messages=messages, temperature=provider_cfg.get("temperature", 0), max_tokens=provider_cfg.get("max_tokens", 2048), timeout=request_cfg.get("timeout", 60), ) except Exception as exc: error = str(exc) latency_ms = int((time.time() - start_time) * 1000) judge_result = judge_sample(sample, response) record = { "record_id": uuid.uuid4().hex, "provider": provider_name, "model": provider_cfg["model"], "question_id": question_id, "category": sample.get("category", ""), "difficulty": sample.get("difficulty", ""), "type": sample.get("type", ""), "response": response, "error": error, "latency_ms": latency_ms, **judge_result, } records.append(record) return records def main(): config = load_config("config.yaml") providers = config["providers"] request_cfg = config.get("request", {}) samples = load_dataset("dataset/sample.jsonl") all_records = [] with ThreadPoolExecutor(max_workers=len(providers)) as executor: future_map = {} for provider_name, provider_cfg in providers.items(): future = executor.submit( run_provider, provider_name, provider_cfg, samples, request_cfg, ) future_map[future] = provider_name for future in as_completed(future_map): provider_name = future_map[future] try: records = future.result() all_records.extend(records) print(f"[{provider_name}] 完成 {len(records)} 条评测") except Exception as exc: print(f"[{provider_name}] 评测失败: {exc}") os.makedirs("results", exist_ok=True) os.makedirs("logs", exist_ok=True) result_path = "results/result.jsonl" with open(result_path, "w", encoding="utf-8") as f: for record in all_records: f.write(json.dumps(record, ensure_ascii=False) + "\n") print(f"原始结果已保存到 {result_path}") if __name__ == "__main__": main()这段代码包含的是最核心的评测链路。第一次跑通时不要嫌它简单,先把闭环跑起来,后续再逐步加入统计报表、重试策略和更复杂的判分逻辑。
4.3 脚本中几个需要理解的要点
代码里用ThreadPoolExecutor让多个模型并行跑,这样三四个模型几乎同时完成,不用串行等待。不过并行只是把任务提交到线程池,如果请求量特别大,仍然可能被服务商限流,所以生产级评测还需要在请求层做限速控制。当前示例适合小规模测试集,跑两三百道题问题不大。
extract_choice是从模型回复里提取选项字母。大模型经常会回复“正确答案是 B:xxx……”,所以不能简单比对整段字符串。这里使用正则先提取独立的 A-D 字母,如果匹配不到,再找“答案是 X”这类表达。这个函数在真实评测中非常关键——不少模型能力问题其实是判分逻辑写得不够健壮,把正确答案误判成错误。
对于代码生成类开放题,judge_sample不会做语义判断,返回的is_correct为None。这类记录会保存在result.jsonl中,可以后续导入到表格里做人工复看。如果要做全自动判分,需要额外写运行代码测试用例的逻辑。
4.4 输出结果与统计逻辑
运行结束后会生成results/result.jsonl,每一行是一条独立的请求记录,包含:
- provider、model:来源模型。
- question_id、category、difficulty:方便分类统计。
- response、error:模型原始返回和调用异常。
- predicted、expected、is_correct:自动判分结果。
统计时可以直接用pandas读取 JSONL,按分类算平均正确率。如果不方便使用pandas,也可以用 Python 内置函数手动统计。核心统计逻辑类似:
import json correct_count = 0 total_count = 0 category_stats = {} with open("results/result.jsonl", "r", encoding="utf-8") as f: for line in f: record = json.loads(line) if record.get("is_correct") is None: continue total_count += 1 category = record.get("category", "unknown") category_stats.setdefault(category, [0, 0]) if record["is_correct"]: correct_count += 1 category_stats[category][0] += 1 category_stats[category][1] += 1 print(f"选择题整体正确率: {correct_count / total_count:.2%}") for category, (correct, total) in category_stats.items(): print(f"{category}: {correct}/{total} = {correct / total:.2%}")这样可以快速得到三个模型的整体表现和分类表现。对技术选型来说,分类正确率往往比整体正确率更有参考价值,因为不同团队对代码、数学、逻辑的侧重点完全不同。
5. 运行一个最小示例
5.1 先做一次探活请求
在跑完整测试集之前,务必要先确认模型 id 和接口地址可用。可以用下面这段代码快速验证:
from openai import OpenAI import os client = OpenAI( base_url="https://api.example.com/v1", api_key=os.environ["DEEPSEEK_API_KEY"], ) resp = client.chat.completions.create( model="deepseek-v4-pro", messages=[{"role": "user", "content": "你好,请只回复当前模型名称。"}], max_tokens=100, ) print(resp.model) print(resp.choices[0].message.content)如果配置没问题,控制台会打印模型返回,并且能看到响应对象里的 model 字段。如果这里报错,那就先别继续跑大批量评测,优先排查网络、API Key 和模型名。
5.2 准备一份样例测试集
新建dataset/sample.jsonl,放入几道有代表性的题目。这里给出十个样例,覆盖代码、逻辑、数学三个方向,你可以直接用于测试脚本:
{"id":"code-001","category":"code","type":"choice","question":"以下 Python 代码输出什么?\nprint(type(1 / 2).__name__)","options":["A. int","B. float","C. str","D. complex"],"answer":"B"} {"id":"code-002","category":"code","type":"choice","question":"下列哪个关键字用于定义 Python 匿名函数?","options":["A. lambda","B. def","C. func","D. arrow"],"answer":"A"} {"id":"code-003","category":"code","type":"choice","question":"以下代码执行后列表 nums 的结果是什么?\nnums = [1, 2, 3]\nnums.append(nums.pop(0))","options":["A. [1, 2, 3]","B. [2, 3, 1]","C. [3, 1, 2]","D. [1, 3, 2]"],"answer":"B"} {"id":"logic-001","category":"logic","type":"choice","question":"如果所有的 A 都是 B,并且所有的 B 都是 C,那么以下哪项一定为真?","options":["A. 所有的 C 都是 A","B. 所有的 C 都是 B","C. 所有的 A 都是 C","D. 无法确定"],"answer":"C"} {"id":"logic-002","category":"logic","type":"choice","question":"小明比小红大 3 岁,小红比小刚大 5 岁。下列哪个判断一定为真?","options":["A. 小明比小刚大 8 岁","B. 小明比小刚大 2 岁","C. 小刚比小明大 2 岁","D. 无法确定"],"answer":"A"} {"id":"logic-003","category":"logic","type":"choice","question":"如果今天下雨,那么地面会湿。已知地面没有湿。由此可以推出?","options":["A. 今天一定下雨","B. 今天一定没下雨","C. 今天可能下雨","D. 无法判断是否有雨"],"answer":"B"} {"id":"math-001","category":"math","type":"choice","question":"一个商品原价 200 元,先降价 10%,再涨价 10%,最后价格是多少元?","options":["A. 200 元","B. 198 元","C. 202 元","D. 190 元"],"answer":"B"} {"id":"math-002","category":"math","type":"choice","question":"解方程:2x + 5 = 17,x 等于多少?","options":["A. 5","B. 6","C. 11","D. 12"],"answer":"B"} {"id":"math-003","category":"math","type":"choice","question":"一个等腰三角形的顶角是 80 度,它的一个底角是多少度?","options":["A. 40 度","B. 50 度","C. 60 度","D. 80 度"],"answer":"B"}这里特意加了一些不算难、但容易粗心答错的题目。比如“先降价再涨价”并不是回到原价,这类题目可以很好测试模型是否真正做了数值计算,而不仅是匹配记忆。
5.3 执行评测命令
在llm-eval目录下执行:
export DEEPSEEK_API_KEY="你的密钥" export GROK_API_KEY="你的密钥" export OPUS_API_KEY="你的密钥" python eval_runner.py如果只想跑某一个模型,可以临时把config.yaml中其余 provider 删掉或者注释掉。跑完以后,控制台会显示每个模型的完成数量,并提示结果文件保存路径。
评测完成不代表结束。打开result.jsonl,至少要做三件事:
- 查看每个模型的选择题正确率。
- 随机抽几条 code 开放题,人工判断模型生成质量。
- 检查
error字段,区分哪些题目没有拿到有效响应。
6. 如何看懂评测结果
6.1 按分类统计正确率
整体正确率只能说明模型在一个混合测试集上的总体情况,真正有价值的是分类口径。比如模型 A 的代码题正确率很高,但逻辑题偏低;模型 B 各项均衡但代码生成偏啰嗦。如果团队业务以代码为主,显然应该更看重代码维度。
分类正确率的统计方式在上一节已经给出。建议把结果整理成一张横向对比表,例如:
| 模型 | 代码生成 | 逻辑推理 | 数学计算 | 格式合规率 |
|---|---|---|---|---|
| 模型 A | 高 | 中 | 高 | 高 |
| 模型 B | 中 | 高 | 高 | 中 |
| 模型 C | 低 | 中 | 低 | 高 |
这里我用“高中低”代替具体数值,因为真实数值会随你的测试集和模型版本而变化。把表头固定下来,具体数字每次跑完自动更新,这是比较务实的做法。
6.2 大模型评测时容易忽略的细节
只看正确率会错过很多细节。建议每轮评测都抽看错误记录,关注以下问题:
- 模型面对不会做的题,是给了一个明确的错误答案,还是在答案里包含多句解释?
- 错误答案通常错在哪个环节?是推理步骤错了,还是最后输出的字母与前面文字矛盾?
- 模型是否经常不按“答案格式要求”输出,导致判分逻辑无法识别?
- 是否有某些题目所有模型都答错?如果出现这种情况,先检查是不是题目本身表述有歧义,不要急着认为是模型能力问题。
我曾经在一轮评测中发现某个模型总体准确率不低,但错误集中在“要求只输出选项字母却输出完整解释”的格式问题上。这说明它的指令跟随能力存在问题,如果下游代码期望严格 JSON,这类模型即使单题正确率高,接入成本也更高。
6.3 官方跑分与自测结果不一致的原因
自测结果和官方报告不一致很正常,原因包括:
- Prompt 不同:官方评测通常会使用精心设计的评估模板,普通业务 Prompt 未必能激发模型全部分能力。
- 数据范围不同:官方评测集覆盖通用知识,你的测试集只覆盖某几类任务。
- 版本不同:某些“预览版”和正式版能力可能有差别,接口返回的模型也许并不是发布版本。
- 参数不同:温度、上下文长度、采样策略都会影响结果。
- 数据污染概率不同:你的私有测试集不在训练语料里,更能反映真实推理能力。
所以当自测结果和公开宣传有差距时,不必急着下结论,先确认上述变量是否被控制。只要自己的评测条件一致,这个结果对选型的参考价值就高于公开榜单。
7. 常见问题与排查思路
7.1 接入阶段报错
接入不同模型时,最容易出现问题的是 API Key、接口地址和模型名。下面的表格是几个高频报错:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 401 Unauthorized | API Key 不正确 | 检查环境变量和密钥前缀 |
| Model not found | 模型 id 写错或账号无权限 | 翻看服务商模型列表,逐个试可用 id |
| 404 Not Found | base_url 路径不对 | 确认是否包含/v1后缀 |
| Connection Timeout | 网络访问不通 | 检查网络策略和接口超时设置 |
| Rate Limit Exceeded | 请求频率过高 | 降低并发或增加重试等待间隔 |
这类问题大多数靠“变量隔离法”解决:先用一个最小请求测试,确保单条调用成功,再放开到批量测试。不要带着网络问题去分析模型正确率,那样只会浪费更多时间。
7.2 评测过程结果波动
很多模型并不会因为temperature=0就完全确定。服务商可能在负载变化时给出不同的采样结果,部分推理模型还会自行调整输出。结果波动通常表现为同一道题跑两次,答案一成一败。
排查建议如下:
- 每个用例至少跑 2 到 3 次,取多数结果。
- 尽量同一时间段完成横向对比,避免服务商版本动态更新影响比较。
- 固定系统提示词和用户问题模板,避免人在中途修改 Prompt。
- 如果题目是开放生成,尽量拆成多个子任务分别评分,降低主观性。
7.3 判断题判分不准确
模型经常输出“正确答案是 B,因为……”,如果判分函数只匹配字符串B,很容易受到干扰。更麻烦的是模型可能输出“选 B”后又补充“不过 A 也有一点点道理”,导致文本中出现多个选项字母。要把判分逻辑设计得尽量保守,只提取最明确的选项,并把无法识别的答案单独归档,留给人工处理。
8. 评测工程的最佳实践
8.1 把 Prompt 与参数当成代码来管理
评测有一个天然缺陷:如果 Prompt 不冻结,两次评测之间的差异就无法归因于模型能力。我的习惯是把系统提示词、用户模板、温度、max_tokens 都纳入版本管理。任何一次 Prompt 修改,都要在测试集版本号上打标记,比如v20250601-r2,保证过几天回看时能还原当时的评测条件。
8.2 保存完整请求日志
除了结果文件之外,原始请求日志也应保留。每一条记录建议保存以下信息:
{ "record_id": "唯一编号", "provider": "模型来源", "model": "模型id", "question_id": "题目编号", "prompt_messages": "完整请求体", "response": "完整返回体", "latency_ms": "延迟", "error": "异常信息", "eval_timestamp": "评测时间" }有了完整日志,后续如果发现评测结果有问题,可以回溯到具体请求分析原因,而不是靠“我记得当时用了一个什么 Prompt”这种回忆式排查。同时,日志可以作为模型行为变化的长期证据,对线上问题排查也很有价值。
8.3 判分策略要分流处理
对于选择题和判断题,程序化判分足够;但对于代码生成题、方案设计题,完全依赖程序判分容易失真。我推荐分流处理:
- 客观题:程序自动判分。
- 可运行代码题:编写单元测试用例验证输出。
- 开放式主观题:小批量人工评分,或使用 LLM-as-a-Judge 辅助评分,但需要人工抽检。
- 无法判断的记录:统一标记为
needs_review,不要默认算错误。
这种分流能把自动化程度和可信度都保持在较高水平。
8.4 注意密钥权限和费用边界
调用大模型 API 时,应遵循最小权限原则:
- 使用独立 API Key,只开通目标模型权限,不要使用长期保存的管理员密钥。
- 代码仓库中避免提交真实密钥,配置文件里只写环境变量名。
- 批量评测前先估算 token 消耗,开放题、长上下文题目往往会显著增加费用。
- 生产环境中的评测请求不要携带真实业务敏感数据,避免把内部信息发送到外部模型服务。
8.5 建立回归评测节奏
大模型服务迭代频繁,既有能力升级也可能出现回退。因此建议建立固定评测节奏:
- 新版本发布时跑一遍完整回归集。
- 线上模型服务出现异常波动时跑探针集。
- 每个季度更新一次测试集,加入新的真实问题,淘汰过于简单或已被模型记住的问题。
这样每次模型升级时,“要不要换”“换了风险多大”就不再是拍脑袋决定,而是有一份报告可以参考。
9. 从一次评测到长期评测能力
如果只跑一次评测,很难形成真正的选型依据。更推荐的做法是把这个评测脚本放在团队的公共代码仓库里,让算法、后端和测试同学都能往测试集中添加真实问题。每来一个新模型,就直接运行同一套脚本“上强度”,短期可以判断模型差异,长期则可以积累一份团队内部的模型能力变化曲线。
下一步可以继续完善的方向包括:接入更多兼容接口的模型网关、支持流式响应、增加自定义代码执行沙箱、可视化展示历史评测结果,以及把评测任务接入 CI/CD,让模型选型评估变成每次迭代都能触发的自动化流程。
评测这件事没有一劳永逸,关键是先把最小闭环跑通,并让测试集持续保鲜。希望这一套流程,能帮你从“围观新模型发布会”进阶为“用真实任务验证模型能力”。如果这篇文章对你有帮助,可以收藏备用,后续有新模型发布时,直接用同一套脚本跑出属于自己的第一手测试结论。