拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

斯坦福大学 CS336 Lecture 12 笔记:语言模型 Evaluation 从零搭建评测流水线,把 endpoint 改到 TaoToken

斯坦福大学 CS336 Lecture 12 笔记:语言模型 Evaluation 从零搭建评测流水线,把 endpoint 改到 TaoToken

1. 从 CS336 Lecture 12 出发:语言模型 Evaluation 到底在评什么

斯坦福 CS336 Spring 2025 的第十二讲把 Evaluation 单独拎出来讲,本身就说明了一件事:评测不是训练完顺手跑个脚本那么简单。Lecture 12 的目录从 What you see、How to think about evaluation,一路铺到 Perplexity、Knowledge benchmarks、Instruction following、Agent benchmarks、Pure reasoning、Safety、Realism、Validity,最后落在 What are we evaluating。这条线索其实在提醒我们:语言模型 Evaluation 是一套需要自己动手搭起来的流水线,而不是一个现成的分数。

如果你已经跟完前十一讲,手里有一个能跑通前向传播的小模型,或者至少有一套能调用远程模型的脚本,那么这一讲最值得亲手复现的,就是三类评测的最小闭环:困惑度(Perplexity)、下游任务准确率、生成质量。前者衡量模型对 token 序列的概率分配,中间那类看模型在选择题或短答上的表现,最后一类则要处理开放式输出。三者的输入构造、调用方式、结果解读完全不同,但都可以用同一套 endpoint 配置串起来。

我试过在本地把这三类评测写成三个独立脚本,共用一份配置。踩过的坑主要集中在两处:一是困惑度需要模型返回 logprob,很多接口默认不给你;二是下游任务和生成质量对 temperature、max_tokens 的敏感度差异很大,混用同一组参数会让结果没法比较。所以这篇笔记不会只讲概念,而是把可复制的配置、数据集加载参数、逐项验证动作都写出来,并且说明怎么把请求 endpoint 统一改到 TaoToken 的 Key/API 通道,让三类评测在同一个调用入口下稳定跑完。官网入口在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end ,后面配置里会反复用到它的 API 地址。

先说清楚适用对象:你需要有 Python 环境,能装 requests 或 openai 这类客户端库,能读 JSONL 数据集,最好还跑过一次前十一讲里的训练或推理脚本。不需要你有 GPU 集群,因为评测阶段大部分算力花在调用模型上,本地只负责数据加载、结果比对和指标计算。Lecture 12 里提到的 MMLU、GPQA、IFEval、SWEBench 这些基准,我们不会全部复现,而是各取一个最小切片,保证你能在自己的机器上跑通,再按同样模式扩展。

为什么强调“最小闭环”?因为评测最容易失控的地方不是指标公式,而是数据泄漏、提示词漂移、参数不一致。Lecture 12 专门讲了 train-test overlap 和 dataset quality,这些问题在你手工搭流水线时同样会出现。比如你从网上下载了一份 MMLU 子集,如果不检查它是否和模型训练数据重叠,算出来的准确率就没有意义。再比如你调 perplexity 时用了和训练时不同的 tokenizer,得到的数值根本不可比。所以下面的每一步都会带上验证动作,确保你拿到的不只是一个数字,而是一个能解释的数字。

2. 前置准备:把 TaoToken 的 Key 和 endpoint 配好

在写评测脚本之前,先把调用通道固定下来。Lecture 12 反复提到“如何调用语言模型”会显著影响评测结果,所以我们要做的第一件事就是让所有评测脚本走同一个 endpoint、同一套鉴权,避免因为通道不同引入额外变量。TaoToken 提供统一的 Key/API 通道,官网在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end ,API 基址是 https://taotoken.net/api ,这两个地址在配置里会分别用到。

你需要先拿到一个 API Key。登录后在控制台创建,具体入口是 https://taotoken.net/console ,创建完在 API Keys 页面复制,地址是 https://taotoken.net/api-keys 。这个 Key 后面会写进环境变量,不要硬编码在脚本里。如果你还没决定用哪个模型,可以先在模型对话页面试一下,地址是 https://taotoken.net/model-chat ,确认通道能正常返回再进入评测环节。

配置方式我推荐用环境变量加一个 JSON 配置文件,这样三类评测脚本可以共用。先设置环境变量:

export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"

然后建一个eval_config.json,把评测相关的参数集中管理:

{ "base_url": "https://taotoken.net/api", "api_key_env": "TAOTOKEN_API_KEY", "models": { "target": "claude-sonnet-4-5", "judge": "gpt-4o-mini" }, "perplexity": { "dataset": "data/wiki_valid.jsonl", "max_samples": 200, "max_tokens": 1 }, "downstream": { "dataset": "data/mmlu_mini.jsonl", "max_samples": 100, "temperature": 0.0, "max_tokens": 8 }, "generation": { "prompts": "data/gen_prompts.jsonl", "max_samples": 50, "temperature": 0.7, "max_tokens": 512 } }

这里有几个点要注意。base_url用 API 地址,不带任何查询参数;api_key_env指向环境变量名,脚本运行时再读取,避免 Key 进版本库。models.target是被评测模型,models.judge是生成质量评测里可能用到的裁判模型,两者可以不同。困惑度那组参数里max_tokens设为 1,是因为我们只需要模型对给定序列的 logprob,不需要它继续生成。

如果你用的是 OpenAI 兼容客户端,初始化方式大致是这样:

import os from openai import OpenAI client = OpenAI( api_key=os.environ["TAOTOKEN_API_KEY"], base_url=os.environ["TAOTOKEN_BASE_URL"], )

这段代码本身不区分评测类型,三类脚本都可以复用。Lecture 12 里提到“你如何调用这个语言模型”是评测框架的四个基础问题之一,把这一步固定下来,后面换数据集、换指标时就不会因为通道差异导致结果漂移。如果你后续要做长期编码或 Agent 类评测,可以了解 Coding Plan,地址是 https://taotoken.net/coding-plan ,它更适合持续性的调用场景;接入细节可以看文档 https://taotoken.net/doc 。

3. 可复制配置:三类评测脚本的完整参数

这一节把三类评测的脚本骨架写出来,每一段都能直接跑。先约定数据格式:所有数据集都用 JSONL,每行一个样本,字段名在脚本里显式读取,避免隐式约定。

3.1 困惑度评测脚本

困惑度需要模型返回 token 级别的 logprob。不同接口对 logprob 的支持程度不一样,所以脚本里要先做一次能力探测,再决定是否继续。

import json import math import os from openai import OpenAI client = OpenAI( api_key=os.environ["TAOTOKEN_API_KEY"], base_url=os.environ["TAOTOKEN_BASE_URL"], ) def load_jsonl(path, limit=None): rows = [] with open(path, "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue rows.append(json.loads(line)) if limit and len(rows) >= limit: break return rows def perplexity_one(text, model): resp = client.chat.completions.create( model=model, messages=[{"role": "user", "content": text}], max_tokens=1, temperature=0.0, logprobs=True, top_logprobs=1, ) choice = resp.choices[0] if not choice.logprobs or not choice.logprobs.content: raise RuntimeError("endpoint 未返回 logprobs,无法计算困惑度") logprob_sum = sum(t.logprob for t in choice.logprobs.content) n_tokens = len(choice.logprobs.content) return math.exp(-logprob_sum / max(n_tokens, 1)), n_tokens def run_perplexity(cfg): rows = load_jsonl(cfg["perplexity"]["dataset"], cfg["perplexity"]["max_samples"]) model = cfg["models"]["target"] scores = [] for i, row in enumerate(rows): text = row["text"] try: ppl, n = perplexity_one(text, model) scores.append(ppl) print(f"[{i+1}/{len(rows)}] tokens={n} ppl={ppl:.3f}") except Exception as e: print(f"[{i+1}/{len(rows)}] skipped: {e}") if scores: avg = sum(scores) / len(scores) print(f"average perplexity = {avg:.3f} over {len(scores)} samples") return scores

这段脚本的关键在于logprobs=True和top_logprobs=1。如果接口不支持,会直接抛错,而不是给你一个看似合理的数字。Lecture 12 特别提醒过,困惑度评估依赖模型提供者返回有效的概率分布,所以这个探测步骤不能省。数据集方面,data/wiki_valid.jsonl每行形如{"text": "..."},建议用和训练时相同的 tokenizer 预处理,否则困惑度不可比。

3.2 下游任务评测脚本

下游任务用 MMLU 风格的选择题做最小切片。数据集每行包含question、choices、answer三个字段。

def build_mmlu_prompt(row): lines = [row["question"]] for idx, c in enumerate(row["choices"]): lines.append(f"{chr(65+idx)}. {c}") lines.append("Answer with a single letter.") return "\n".join(lines) def run_downstream(cfg): rows = load_jsonl(cfg["downstream"]["dataset"], cfg["downstream"]["max_samples"]) model = cfg["models"]["target"] correct = 0 total = 0 for i, row in enumerate(rows): prompt = build_mmlu_prompt(row) resp = client.chat.completions.create( model=model, messages=[{"role": "user", "content": prompt}], temperature=cfg["downstream"]["temperature"], max_tokens=cfg["downstream"]["max_tokens"], ) pred = resp.choices[0].message.content.strip()[:1].upper() gold = row["answer"].strip().upper() total += 1 if pred == gold: correct += 1 print(f"[{i+1}/{len(rows)}] pred={pred} gold={gold}") acc = correct / max(total, 1) print(f"accuracy = {acc:.4f} ({correct}/{total})") return acc

temperature=0.0是为了让结果可复现,max_tokens=8足够模型输出一个字母加少量解释。如果你发现模型经常输出多余内容,可以在 prompt 里再强调一次“只输出字母”,或者在解析时用正则提取第一个 A-D 字符。Lecture 12 提到提示词选择会带来显著波动,所以这一组参数要固定下来,不要在不同模型之间随意改。

3.3 生成质量评测脚本

生成质量最难自动化,这里用一个简化方案:先让目标模型生成回答,再用裁判模型按固定维度打分。数据集每行包含prompt和reference(可选)。

JUDGE_TEMPLATE = """You are evaluating an answer. Prompt: {prompt} Answer: {answer} Score from 1 to 5 on relevance, coherence, and helpfulness. Return JSON: {{"relevance": int, "coherence": int, "helpfulness": int}} """ def run_generation(cfg): rows = load_jsonl(cfg["generation"]["prompts"], cfg["generation"]["max_samples"]) target = cfg["models"]["target"] judge = cfg["models"]["judge"] results = [] for i, row in enumerate(rows): gen = client.chat.completions.create( model=target, messages=[{"role": "user", "content": row["prompt"]}], temperature=cfg["generation"]["temperature"], max_tokens=cfg["generation"]["max_tokens"], ) answer = gen.choices[0].message.content judge_prompt = JUDGE_TEMPLATE.format(prompt=row["prompt"], answer=answer) jr = client.chat.completions.create( model=judge, messages=[{"role": "user", "content": judge_prompt}], temperature=0.0, max_tokens=128, ) results.append({"prompt": row["prompt"], "answer": answer, "judge": jr.choices[0].message.content}) print(f"[{i+1}/{len(rows)}] judged") with open("gen_results.jsonl", "w", encoding="utf-8") as f: for r in results: f.write(json.dumps(r, ensure_ascii=False) + "\n") return results

裁判模型用gpt-4o-mini这类成本较低的模型即可,重点是评分维度固定、输出格式固定。Lecture 12 里 AlpacaEval 和 WildBench 都用了 LLM-as-judge,同时也指出了长度偏差等问题,所以这里把三个维度分开打分,方便你后续做长度校正或人工抽检。

4. 验证请求:从单条样本到批量跑通

配置写完之后,不要一上来就跑全量。先用单条样本验证通道和返回格式,再逐步放大。Lecture 12 的框架里,“如何调用语言模型”和“如何评估输出”是分开的两步,验证阶段也要分开做。

第一步,验证困惑度接口是否返回 logprobs。用一条短文本:

ppl, n = perplexity_one("The capital of France is", "claude-sonnet-4-5") print(ppl, n)

如果这一步报endpoint 未返回 logprobs,说明当前模型或通道不支持该能力,需要换模型或改用其他困惑度近似方案。不要跳过这个检查,否则批量跑完才发现全是无效值。

第二步,验证下游任务解析是否正确。用一条 MMLU 样本:

row = {"question": "Which is a prime number?", "choices": ["4", "6", "7", "9"], "answer": "C"} print(build_mmlu_prompt(row))

确认输出里选项编号和答案字母对应正确。然后跑单条:

resp = client.chat.completions.create( model="claude-sonnet-4-5", messages=[{"role": "user", "content": build_mmlu_prompt(row)}], temperature=0.0, max_tokens=8, ) print(resp.choices[0].message.content)

如果模型返回的是“C. 7”而不是“C”,解析逻辑要相应调整。这一步的验证动作是:打印原始返回,人工确认一次,再决定解析规则。

第三步,验证生成质量链路。先跑一条 prompt,确认目标模型有输出、裁判模型返回的是可解析 JSON。如果裁判返回带 markdown 代码块,需要在解析前去掉围栏。验证通过后,再把max_samples从 1 调到 10,观察耗时和错误率,最后才跑全量。

批量跑的时候建议加一个简单的重试和日志:

import time def call_with_retry(fn, retries=3, wait=2): for attempt in range(retries): try: return fn() except Exception as e: print(f"attempt {attempt+1} failed: {e}") time.sleep(wait) raise RuntimeError("all retries failed")

把三类评测的调用都包一层重试,避免个别网络抖动导致整批中断。跑完之后,你会得到三个结果:平均困惑度、下游准确率、生成质量评分分布。Lecture 12 强调“如何解读结果”和“评估对象是什么”,所以拿到数字后要问自己:这个困惑度是在哪个数据集上算的?这个准确率对应的提示词模板是什么?生成评分和人工判断是否一致?这些问题不回答,数字就只是数字。

5. 常见报错排查:401、local proxy failed、reading choices、OAuth

评测脚本跑不起来,多数问题集中在鉴权和返回解析上。下面按真实报错逐条排查。

401 Unauthorized。最常见的原因是 Key 没读到或写错了。先确认环境变量:

echo $TAOTOKEN_API_KEY

如果为空,说明 export 没生效,或者你在新的 shell 里没重新加载。另一个原因是base_url写成了带路径的地址,比如多加了/v1,导致鉴权头没被正确识别。正确写法是https://taotoken.net/api,不要自己拼路径。如果确认 Key 和环境变量都没问题,去 API Keys 页面重新生成一个,地址是 https://taotoken.net/api-keys ,排除 Key 被禁用或过期的情况。

local proxy failed。这个报错通常出现在客户端尝试走本地代理但代理没启动时。检查你的环境变量里有没有HTTP_PROXY、HTTPS_PROXY或ALL_PROXY,如果有但代理服务没开,就会失败。评测脚本建议显式禁用代理:

import os os.environ.pop("HTTP_PROXY", None) os.environ.pop("HTTPS_PROXY", None) os.environ.pop("ALL_PROXY", None)

或者在初始化客户端时传入http_client并设置trust_env=False。这一步能排除大部分本地网络配置带来的干扰。

reading choices 报错。典型信息是KeyError: 'choices'或IndexError: list index out of range。原因通常是接口返回了错误结构,比如{"error": {...}},而脚本直接去读resp.choices。排查方法是先打印原始返回:

resp = client.chat.completions.create(...) print(resp.model_dump_json(indent=2))

如果看到error字段,按错误信息处理;如果choices为空列表,可能是max_tokens太小或模型被内容策略拦截。把max_tokens调大一点,或者换一条 prompt 再试。

OAuth 相关报错。如果你用的是某些 CLI 工具或 IDE 插件,可能会遇到 OAuth 流程失败。这类工具通常有自己的登录态,和 API Key 是两套机制。排查时先确认你是在用 API Key 调用,而不是走 OAuth。如果工具强制 OAuth,检查系统时间是否准确,时间偏差过大会导致 token 校验失败。对于评测脚本,建议直接用 API Key,避免引入 OAuth 环节。

logprobs 相关报错。如果困惑度脚本报logprobs is not supported,说明当前模型不支持返回 logprob。解决办法是换一个支持该能力的模型,或者改用“计算给定答案的似然”这类近似方法。Lecture 12 提到困惑度评估容易被搞砸,所以遇到不支持时不要强行用其他字段凑数。

结果全为 0 或全为 1。这不是报错,但同样需要排查。常见原因是解析逻辑把模型输出截断错了,比如模型返回“The answer is C”,你取第一个字符得到“T”。解决办法是先用正则提取[A-D],再和 gold 比较。另一个原因是数据集字段名和脚本不一致,比如数据集用answer而脚本读label,导致 gold 为空。打印前几条样本的原始字段,确认键名。

6. 把评测跑稳之后:CTA 与后续扩展

三类评测跑通之后,你手里就有了一条可复用的最小流水线。Lecture 12 最后问“What are we evaluating”,这个问题在工程上对应的是:你的评测对象是模型本身,还是包含提示词、工具调用、后处理的整个系统。如果你要评估的是系统,那么 endpoint 配置、重试策略、解析规则都属于系统的一部分,换模型时这些都要重新验证。

如果你打算把评测扩展到更多基准,比如 IFEval 或 SWEBench 风格的任务,建议先把调用通道固定下来。TaoToken 的 API 入口是 https://taotoken.net/api ,接入文档在 https://taotoken.net/doc ,里面有不同客户端的配置示例。需要管理多个 Key 或查看用量时,控制台在 https://taotoken.net/console ,API Keys 页面在 https://taotoken.net/api-keys 。如果评测涉及大量重复调用,可以了解 Coding Plan,地址是 https://taotoken.net/coding-plan ,它更适合长期、批量的调用场景。想先对比不同模型的输出风格,可以在模型对话页面手动试几条 prompt,地址是 https://taotoken.net/model-chat 。

最后给一个实用建议:把每次评测的配置、数据集版本、模型名、结果文件放在同一个目录下,用时间戳命名。Lecture 12 里提到的 train-test overlap 和 dataset quality 问题,在你自己搭流水线时同样存在。只有把配置和结果绑定,后续才能解释“为什么这次分数比上次高”。评测不是跑一次就结束的事,而是一条需要持续维护的流水线。

返回列表