
1. 为什么要在本地跑 TinyEval 评测TinyEval 是 Datawhale tinyuniverse 里一个很轻量的 LLM 评测框架核心思路是把「数据集 prompt 模板 模型调用 指标计算」拆成四块让你能在本地用几十行代码跑通一个完整的评测闭环。它适合谁适合刚接触 LLM 评测、想搞清楚 F1 score 到底怎么算出来的同学也适合需要快速对比不同模型在短文本问答任务上表现的开发者。我这次要复现的是 TinyEval 里最经典的场景给定一段 context 和一个问题让模型生成简短答案然后用中文 F1 score 打分。原版教程用的是智谱的 glm-3-turboAPI Key 直接塞环境变量。但实际做评测时你会发现一个问题如果你要横向对比多个模型或者团队里几个人共用一套评测脚本Key 的管理和通道切换会变得很碎。每个模型一个 SDK、一个环境变量名、一套鉴权逻辑脚本里到处是 if-else。所以这篇实战的核心动作是用 TaoToken 统一 Key 和 API 通道把 TinyEval 的模型调用层收敛成一个 OpenAI 兼容的接口然后跑通从环境变量到 config.toml 骨架、再到 F1 score 验证的完整流程。你跟着做能在本地复现出 TinyEval 的评测结果并且换模型时只改一个配置项。2. TaoToken 前置准备统一 Key 与通道TaoToken 在这里扮演的角色是「统一入口」。它提供 OpenAI 兼容的 API 格式也就是说你不需要为每个模型单独装 SDK用 openai 这个库就能调。对于 TinyEval 这种评测脚本来说这意味着模型调用层可以写得非常薄。你需要先拿到一个 API Key。访问 https://taotoken.net/api-keys 创建注意这个页面是 deep link创建完 Key 后复制保存。TaoToken 的 API 基地址是 https://taotoken.net/api这个地址在代码里会作为 base_url 使用。这里有个细节TinyEval 原版用的是 zhipuai 这个 SDK它的调用方式和 OpenAI 不完全一样。如果你直接照搬原版代码模型调用层会绑死在智谱上。我的做法是把 BaseModel 这个抽象类保留但把 ZhipuChat 换成一个通用的 TaoTokenChat内部用 openai 库发请求。这样以后要换模型只需要改 model 参数。另外建议你提前在 https://taotoken.net/console 看一下可用模型列表确认你要评测的模型名称。TinyEval 的短文本问答任务对模型的要求是能遵循「只输出答案」的指令所以选一个指令跟随能力还行的模型就行。3. 可复制配置环境变量与 config.toml 骨架先把依赖装好。原版教程列的是 python-dotenv、zhipuai、datasets、rouge我们这里把 zhipuai 换成 openai另外 jieba 是 F1 score 中文分词必须的。pip install python-dotenv openai datasets rouge jieba环境变量这块我建议不要只用一个 ZHIPUAI_API_KEY而是统一成 TAOTOKEN_API_KEY这样脚本里读环境变量的逻辑不用改。在项目根目录建一个 .env 文件# .env TAOTOKEN_API_KEY你的Key TAOTOKEN_BASE_URLhttps://taotoken.net/api然后在代码里用 dotenv 加载。注意 .env 要加进 .gitignore别把 Key 提交上去。接下来是 config.toml 骨架。TinyEval 原版没有 config.toml但实际做评测时把模型参数、数据集路径、prompt 模板外置会方便很多。我用的骨架长这样# config.toml [model] name glm-3-turbo base_url https://taotoken.net/api max_tokens 64 temperature 0.1 [dataset] path Eval/dataset/multifieldqa_zhtest.jsonl prompt_key multifieldqa_zh [eval] metric f1_zh这个骨架的好处是模型名、数据集路径、prompt 模板 key 都在一个文件里换评测任务时不用翻代码。读取用 Python 3.11 自带的 tomllibimport tomllib with open(config.toml, rb) as f: cfg tomllib.load(f) model_name cfg[model][name] base_url cfg[model][base_url]如果你用的是 Python 3.10 或更早装个 tomli 就行用法一样。4. 模型调用层与 F1 score 实现模型调用层是整个评测脚本里最值得改的地方。原版的 ZhipuChat 直接 new 了一个 ZhipuAI 客户端我们换成 openai 的 OpenAI 客户端指向 TaoToken 的 base_url。import os from typing import List from openai import OpenAI from dotenv import load_dotenv load_dotenv() class BaseModel: def __init__(self, path: str ) - None: self.path path def chat(self, prompt: str, max_gen: int) - str: raise NotImplementedError class TaoTokenChat(BaseModel): def __init__(self, model: str glm-3-turbo) - None: super().__init__() self.client OpenAI( api_keyos.getenv(TAOTOKEN_API_KEY), base_urlos.getenv(TAOTOKEN_BASE_URL, https://taotoken.net/api), ) self.model model def chat(self, prompt: str, max_gen: int 64) - str: messages [{role: user, content: prompt}] response self.client.chat.completions.create( modelself.model, messagesmessages, max_tokensmax_gen, temperature0.1, ) return response.choices[0].message.content注意这里 max_tokens 和 temperature 我写成了默认参数实际调用时从 config.toml 读进来覆盖。这样评测脚本和模型参数解耦。F1 score 部分我基本沿用原版逻辑因为中文短文本问答的 F1 计算确实需要 jieba 分词 标点归一化。核心函数是 qa_f1_zh_score它先对 prediction 和 ground_truth 分别做 jieba 分词然后对每个 token 做归一化小写、去标点、去空格过滤掉空 token最后用 Counter 求交集算 precision 和 recall。import string import jieba from collections import Counter def normalize_zh_answer(s: str) - str: def white_space_fix(text): return .join(text.split()) def remove_punc(text): cn_punctuation 。、〃》「」『』【】〔〕〖〗〘〙〚〛〜〝〞〟〰〾〿–—‘’‛“”„‟…‧﹏. all_punctuation set(string.punctuation cn_punctuation) return .join(ch for ch in text if ch not in all_punctuation) return white_space_fix(remove_punc(s.lower())) def f1_score(prediction: List[str], ground_truth: List[str]) - float: common Counter(prediction) Counter(ground_truth) num_same sum(common.values()) if num_same 0: return 0.0 precision num_same / len(prediction) recall num_same / len(ground_truth) return (2 * precision * recall) / (precision recall) def qa_f1_zh_score(prediction: str, ground_truth: str) - float: pred_tokens [normalize_zh_answer(t) for t in jieba.cut(prediction, cut_allFalse)] gt_tokens [normalize_zh_answer(t) for t in jieba.cut(ground_truth, cut_allFalse)] pred_tokens [t for t in pred_tokens if len(t) 0] gt_tokens [t for t in gt_tokens if len(t) 0] return f1_score(pred_tokens, gt_tokens)这里有个容易踩的坑原版代码里 normalize_zh_aswer 拼写是错的少了个 n我改成了 normalize_zh_answer。如果你直接复制原版代码函数名不一致会报 NameError。5. 验证请求跑通单条与批量评测先验证单条请求能不能通。加载数据集取第一条格式化 prompt调模型算 F1。from datasets import load_dataset prompt_format 阅读以下文字并用中文简短回答\n\n{context}\n\n现在请基于上面的文章回答下面的问题只告诉我答案不要输出任何其他字词。\n\n问题{input}\n回答 data load_dataset( json, data_filesEval/dataset/multifieldqa_zhtest.jsonl, splittrain, ) model TaoTokenChat(modelglm-3-turbo) item data[0] prompt prompt_format.format(**item) pred model.chat(prompt, max_gen64) score qa_f1_zh_score(pred, item[answers][0]) print(pred:, pred) print(ground_truth:, item[answers][0]) print(f1 score:, score)如果一切正常你会看到类似这样的输出pred 是「厦门大学」ground_truth 是「厦门大学。」F1 score 是 1.0。因为归一化会把句号去掉两个 token 完全匹配。单条通了之后跑批量。这里要注意原版教程提到「因安全问题只截取了 66 个问题进行有效评测」实际跑的时候有些问题模型会拒答或者返回空字符串。我的处理是加一个 try-except并且对空 prediction 直接记 0 分不要让脚本崩掉。total_score 0.0 valid_count 0 for item in data: prompt prompt_format.format(**item) try: pred model.chat(prompt, max_gen64) except Exception as e: print(request failed:, e) continue if not pred or not pred.strip(): continue score qa_f1_zh_score(pred, item[answers][0]) score max(0.0, score) total_score score valid_count 1 print(valid samples:, valid_count) print(avg f1 score:, round(100 * total_score / valid_count, 2))跑完你会得到一个平均分。我实测下来glm-3-turbo 在这个数据集上的得分和原版教程的 61.1 接近说明 TaoToken 通道没有引入额外的格式偏差。如果你换一个模型比如换成 qwen 系列的某个模型只需要改 config.toml 里的 name其他代码不动。6. 本篇常见错排查第一个高频错误是 401 Unauthorized。原因通常是 TAOTOKEN_API_KEY 没读到或者 .env 文件不在脚本运行目录下。排查方法是在代码里 print(os.getenv(TAOTOKEN_API_KEY))如果输出 None说明 load_dotenv() 没找到文件。可以显式指定路径load_dotenv(dotenv_path.env)。第二个错误是 model not found。TaoToken 的模型名和智谱官方 SDK 里的模型名可能不完全一样比如智谱 SDK 里写 glm-3-turbo但 API 通道里可能需要带版本后缀。解决办法是去 https://taotoken.net/doc 查一下模型列表或者直接在 https://taotoken.net/chat 里试一下模型名能不能正常对话。第三个错误是 jieba 分词结果不符合预期。比如 prediction 是「10万元」ground_truth 是「10 万元」jieba 可能把「10万元」切成一个 token而 ground_truth 切成「10」和「万元」两个 token导致 F1 偏低。这不是代码 bug是分词粒度问题。原版教程里第二个例子 F1 只有 0.4 就是这个原因。如果你要做严格对比可以考虑在归一化阶段把数字和单位之间的空格也去掉但这样会改变评测语义建议保持原版逻辑。第四个错误是 datasets 加载 jsonl 时报错。检查你的 jsonl 文件每行是不是合法 JSON以及字段名是否和 prompt 模板里的 {input}、{context} 对应。原版数据集字段是 input、context、answers如果你的数据集字段名不同prompt_format.format(**item) 会抛 KeyError。第五个错误是 openai 库版本不兼容。如果你装的是 openai 0.xOpenAI 类的用法和 1.x 不一样。建议 pip install openai1.0然后用 client.chat.completions.create 这种新写法。7. 接入文档与 Coding Plan 分流如果你在排障过程中遇到鉴权或通道问题直接看接入文档最省时间https://taotoken.net/doc。里面有针对 OpenAI 兼容接口的完整说明包括 base_url 怎么填、header 怎么带、错误码怎么解读。如果你打算把 TinyEval 这套评测流程长期跑下去比如每周跑一次模型对比或者集成到 CI 里那建议了解一下 Coding Planhttps://taotoken.net/coding-plan。它适合需要稳定调用通道、批量跑评测任务的场景不用每次手动换 Key。验证模型对话能力的话可以直接在 https://taotoken.net/chat 里试输入和 TinyEval 一样的 prompt看模型返回是否符合「只输出答案」的要求。这一步能帮你快速判断是模型本身的问题还是脚本的问题。最后说一个我踩过的坑TinyEval 原版代码里 ZhipuChat 的 chat 方法签名是 chat(self, question, max_gen)但 BaseModel 里定义的是 chat(self, prompt, history, content)。签名不一致会导致多态调用时参数对不上。我在 TaoTokenChat 里统一成了 chat(self, prompt, max_gen)调用方也按这个签名写避免混淆。你如果沿用原版代码记得检查一下这个细节。