1. 从直播里的两个数字说起:为什么我用 pytest 去量 Token
小米把 mimo-v2.6-pro 和 flash 两个模型的 Agentic RL 后训练过程放到直播里跑,这件事本身挺有意思:mimo-v2.6-pro 在 DeepSWE 上拿到 62 分,低于 DeepSeek-v4.1-flash 的 74.2,原作者也明确说这还处在训练早期。更抓眼球的是作者当场推算,这套后训练每秒大约烧掉 10 美元。但我盯着这行数字看了半天,结论是:它和你的项目账单没有半毛钱关系。直播间的成本口径是「集群满负载跑训练」,而你在本地写的评测脚本,花的是「推理调用 token」的钱,两者不是一个量级,也不是一套计费方式。
真正该关心的,是你自己的评测脚本跑一轮要消耗多少 token。我这边在做 Agentic 编码能力的横向评测时,pytest 一开始直接打默认的 provider 地址,结果第一次全量跑就撞了一墙 401 和 404:有的用例拿的是没配对的 key,有的把 base_url 写成了带/v1又重复拼接的地址,还有的客户端默认走了海外直连,超时重试三次才失败。排查完我把整条评测链路统一换成 TaoToken 入口,Key 在官网申请,地址取自 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=mimo_eval_intro ,请求的 Base URL 统一写成https://taotoken.net/api。改完之后,pytest 不仅能稳定跑完,还能顺手把每个模型的 prompt_tokens / completion_tokens 记下来,做成可断言的消耗基线。
这篇文章就围绕这条链路展开:怎么把 TaoToken 接进 pytest 评测夹具,怎么让 mimo-v2.6-pro 与 flash 在同一批用例上跑出可比的 token 数据,以及当脚本报错时先查哪几个地方。文中出现的 62 分、74.2 分、每秒约 10 美元,都是原作者直播里的口径,我不会把它们当作你的成本依据,只会把它们当作「值得复现的对照样本」。
2. 把评测链路接到 TaoToken:Key、Base URL 与三个客户端
2.1 先拿 Key,再决定地址怎么写
评测脚本要跑真实调用,第一步是拿到可用的凭证。进入 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=mimo_eval_key_setup ,在控制台创建 API Key,把返回值整串复制下来。它只会完整显示一次,建议直接落到本机环境变量里,不要提交进 Git:
# 本地开发环境,写入 shell 配置或 .env(注意 .env 要进 .gitignore) export TAOTOKEN_API_KEY="YOUR_API_KEY" export TAOTOKEN_BASE_URL="https://taotoken.net/api"这里有个容易踩的点:TAOTOKEN_BASE_URL的值就是https://taotoken.net/api,不要在末尾自己补/,也不要手工加/v1。OpenAI 兼容客户端的行为不统一,有的会内部拼一次路径,你再加一层就会变成/api/v1/v1/chat/completions,返回 404。脚本里统一从环境变量读,报错时只改环境变量一个地方,比在十几个测试文件里搜字符串靠谱得多。
2.2 pytest 侧的最小接入
我倾向把客户端构造收进 fixture,而不是每个用例各自 new 一个。这样超时、重试、base_url 只有一处定义,换供应商时改动面最小:
# tests/conftest.py import os import time from dataclasses import dataclass, field, asdict import pytest from openai import OpenAI BASE_URL = os.getenv("TAOTOKEN_BASE_URL", "https://taotoken.net/api") API_KEY = os.getenv("TAOTOKEN_API_KEY", "YOUR_API_KEY") @dataclass class UsageRecord: model: str case: str prompt_tokens: int completion_tokens: int latency_s: float @property def total_tokens(self) -> int: return self.prompt_tokens + self.completion_tokens @dataclass class UsageBook: records: list = field(default_factory=list) def add(self, rec: UsageRecord) -> None: self.records.append(rec) def by_model(self, model: str) -> list: return [r for r in self.records if r.model == model] def total_tokens(self, model: str) -> int: return sum(r.total_tokens for r in self.by_model(model)) def to_json(self) -> str: import json return json.dumps([asdict(r) for r in self.records], ensure_ascii=False, indent=2) @pytest.fixture(scope="session") def client(): if API_KEY == "YOUR_API_KEY": pytest.skip("未设置 TAOTOKEN_API_KEY,跳过需要真实调用的用例") return OpenAI(base_url=BASE_URL, api_key=API_KEY, timeout=120, max_retries=2) @pytest.fixture(scope="session") def usage_book(): return UsageBook() def call_and_record(client, usage_book, model, case, prompt, **kwargs): start = time.perf_counter() resp = client.chat.completions.create( model=model, messages=[{"role": "user", "content": prompt}], **kwargs, ) latency = time.perf_counter() - start u = resp.usage usage_book.add( UsageRecord( model=model, case=case, prompt_tokens=u.prompt_tokens, completion_tokens=u.completion_tokens, latency_s=round(latency, 3), ) ) return resppytest.skip这行是有意加上的。评测脚本常常要在 CI 里跑一遍「不花钱的干跑」,如果 key 缺失就直接 fail,CI 会被无谓地刷红;skip 更符合预期。
2.3 Claude Code 的 settings.json
如果你希望 Claude Code 也走同一条通道做对照实验,配置写在用户级或项目级settings.json的环境变量段:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "YOUR_API_KEY", "ANTHROPIC_MODEL": "mimo-v2.6-pro", "ANTHROPIC_SMALL_FAST_MODEL": "mimo-v2.6-flash" } }注意ANTHROPIC_*这套变量只属于 Claude Code 生态,别把它套到 Codex 上,两者读的字段完全不同。这里的ANTHROPIC_MODEL我临时指向 pro,是为了让交互式会话和 pytest 里的主力模型保持一致,方便对照。完整的字段说明和更多环境变量组合,可以看 https://taotoken.net/doc/ClaudeCodeAnthropic?utm_source=taotoken_aicg_blog_end&utm_content=mimo_eval_cc 。
2.4 Codex 的 config.toml
Codex 走的是 TOML,写 provider 段:
model = "mimo-v2.6-pro" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "chat"env_key写的是环境变量名,不是 key 本身,这一点很多人第一次会写错。wire_api保持chat即可,除非你明确要接其他协议形态。
2.5 CC Switch 的三件套
用 CC Switch 做多供应商切换时,把它当成三件套来填,缺一不可:
- 供应商地址:
https://taotoken.net/api - API Key:填你创建出来的
YOUR_API_KEY对应值 - 默认模型:
mimo-v2.6-pro(对照测试时再切一次到 flash)
三件套填完记得在切换后重启终端会话,否则老的进程还在用上一次的环境变量,你会看到「明明改了配置还是打老地址」的假故障。
3. pytest 评测夹具:把两个模型的调用量记成可断言的数据
3.1 用例设计:同一批任务、两个模型、一份账本
Agentic 评测的任务集通常包含「读代码、定位问题、生成补丁、解释改动」几类。为了对比,我把同一批 prompt 参数化到两个模型上,避免因为任务不同导致 token 差异失真:
# tests/test_mimo_token_budget.py import pytest from conftest import call_and_record MODELS = [ pytest.param("mimo-v2.6-pro", id="pro"), pytest.param("mimo-v2.6-flash", id="flash"), ] TASKS = [ { "id": "locate_regression", "prompt": "下面这段 Python 函数在并发调用时会偶发返回 None,指出可疑行并说明理由,只给结论和最小修复片段。", }, { "id": "patch_generation", "prompt": "给定一个 pytest 断言失败堆栈,输出能通过该用例的最小补丁,不要重写整个模块。", }, { "id": "diff_explain", "prompt": "解释这段 diff 对缓存命中率的影响,控制在 200 字以内。", }, ] @pytest.mark.parametrize("model", MODELS) @pytest.mark.parametrize("task", TASKS, ids=[t["id"] for t in TASKS]) def test_agentic_task(client, usage_book, model, task): resp = call_and_record( client, usage_book, model=model, case=task["id"], prompt=task["prompt"], temperature=0.2, max_tokens=1024, ) text = resp.choices[0].message.content or "" assert text.strip(), f"{model} 在 {task['id']} 上返回空内容"这里故意没有断言「返回内容是否完全正确」。原因是这类生成式任务的正确性判定应该交给一套独立的规则或人工复核,硬塞进同一个用例会让 token 基线被失败重试污染。评测脚本的职责是「稳定拿到可比数据」,不是「顺便把模型分数算完」。
3.2 会话级断言:调用量与消耗区间
pytest 的执行顺序里,会话级 fixture 在所有用例结束后才销毁,正好可以挂一个 teardown 断言:
# tests/test_mimo_token_budget.py(续) import pytest @pytest.fixture(scope="session", autouse=True) def assert_and_dump_usage(usage_book, request): yield # 1) 先落盘,失败也能看到现场数据 with open("usage_report.json", "w", encoding="utf-8") as f: f.write(usage_book.to_json()) pro_total = usage_book.total_tokens("mimo-v2.6-pro") flash_total = usage_book.total_tokens("mimo-v2.6-flash") # 2) 两个模型都必须真实产生过调用 assert pro_total > 0, "mimo-v2.6-pro 没有产生任何 token 记录,检查 fixture 是否被跳过" assert flash_total > 0, "mimo-v2.6-flash 没有产生任何 token 记录" # 3) 示例阈值:flash 的总量不应显著超过 pro # 具体倍数请按你自己的任务集重新标定,这里只给断言骨架 assert flash_total <= pro_total * 1.2, ( f"flash 消耗异常偏高 pro={pro_total} flash={flash_total}" ) # 4) 单次请求的 prompt 长度要有上限,防止误把大文件整段塞进去 for rec in usage_book.records: assert rec.prompt_tokens <= 8000, f"{rec.case} prompt 过长: {rec.prompt_tokens}" assert rec.latency_s <= 120, f"{rec.case} 延迟异常: {rec.latency_s}s"第 3 条的倍数不是从直播里的分数推出来的,是我在自己的任务集上跑几轮后标定的。评测基线的正确用法就是这样:先在稳定环境下采几次数据,再把「当前水位」写成断言,之后任何一次超标都会让 CI 亮红灯,而不是等到月底看账单才发现。
3.3 跑起来的命令
# 只跑 token 记账相关用例 python -m pytest tests/test_mimo_token_budget.py -v # 输出更详细的 token 统计,方便观察 python -m pytest tests/ -v -s --tb=short跑完后当前目录会多出usage_report.json,里面每个用例、每个模型的 prompt/completion/延迟都是逐条落盘的。这份文件比任何一句「大概很贵」都实在,因为你可以拿它去乘单价,算出真实成本。
4. 双模型对照:62 分 / 74.2 分之外,脚本还能告诉你什么
4.1 分差只是在评测口径下的一个切片
直播里给出的 DeepSWE 62 与 74.2 是分差,不是能力上限,原作者也说了训练还在早期。把它放到评测脚本里看,你能得到的额外信息是:两个模型在同一批任务上的 token 结构并不一样。常见形态有这么几种:
- pro 倾向输出更长的推理链,completion_tokens 明显更高,但一次过的比例高,重试少;
- flash 单次输出短,首轮通过率略低,需要多轮对话补上下文,prompt_tokens 会随轮次累积;
- 两者的 prompt_tokens 在首轮几乎相同,差异从第二轮开始拉开。
这几种形态用肉眼看对话很难分辨,但落到usage_report.json里就是一列列数字。我建议在评测报告里固定输出一张对照表,字段包括:用例数、prompt 合计、completion 合计、平均延迟、平均重试次数。分数只写一行,token 结构写一整块。
4.2 用 token 口径复算「每秒多少钱」
直播里「每秒约 10 美元」这个数字,是作者对训练阶段的估算,前面的「每秒」指的是训练集群在跑,不是推理请求的响应时间。两者千万别混。如果你用评测脚本的数据去套这个数,出来的结论一定是错的。
正确的复算方式很朴素:从usage_report.json里取一轮评测的总 token,乘上对应模型的单价,再除以这轮评测的耗时,得到的是「每评测秒花费」。这个口径才有工程意义,因为它直接对应你跑一次回归测试要花多少钱:
# tools/cost_estimate.py import json # 单价占位:换成你实际使用的价格口径,单位统一为 元 / 千 token PRICE = { "mimo-v2.6-pro": {"prompt": 0.0, "completion": 0.0}, "mimo-v2.6-flash": {"prompt": 0.0, "completion": 0.0}, } with open("usage_report.json", encoding="utf-8") as f: records = json.load(f) total_cost = 0.0 total_latency = 0.0 for r in records: p = PRICE.get(r["model"]) if not p: continue cost = ( r["prompt_tokens"] / 1000 * p["prompt"] + r["completion_tokens"] / 1000 * p["completion"] ) total_cost += cost total_latency += r["latency_s"] print(f"本轮成本: {total_cost:.4f}") print(f"本轮耗时: {total_latency:.1f}s") if total_latency > 0: print(f"每评测秒成本: {total_cost / total_latency:.6f}")把单价留成占位而不是写死,是有意的:价格会变,评测脚本不该跟着价格一起改。你需要最新的价格与模型清单时,去 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=mimo_eval_cost 核对一遍再填,比在旧脚本里翻注释靠谱。
4.3 分数与成本要一起看
一个容易踩的坑是只看分数不看成本。假设 flash 在某类任务上分数低一点,但它的 completion_tokens 只有 pro 的一半、延迟也更短,那么在「大批量回归」这种场景下,flash 反而可能是更合适的那一个。评测脚本的价值就在这里:它把「分数」和「消耗」放在同一份数据里,让你做取舍时手上有依据,而不是凭印象。
5. 报错排障:pytest 跑评测最常见的几类故障
5.1 认证类
| 现象 | 常见原因 | 处理 |
|---|---|---|
| 401 Unauthorized | TAOTOKEN_API_KEY没设置或仍是YOUR_API_KEY | 检查环境变量是否被 shell 会话继承 |
| 401 但 key 看起来没问题 | 复制时带了首尾空白或换行 | print(repr(os.getenv(...)))看一眼 |
| 403 | key 被禁用或权限不足 | 到控制台确认 key 状态 |
5.2 地址类
| 现象 | 常见原因 | 处理 |
|---|---|---|
| 404 Not Found | base_url 被重复拼接了/v1 | 统一使用https://taotoken.net/api |
404 且路径显示为/api/v1/chat/completions | 客户端自行补了路径 | 保持 base 不变,不要再手工加段 |
| DNS / 连接超时 | 网络出口不稳定 | 换网络环境后重试,或调大timeout |
调试时最有用的一招,是在 fixture 里把最终生效的地址打出来:
@pytest.fixture(scope="session") def client(): print(f"[debug] base_url={BASE_URL}") if API_KEY == "YOUR_API_KEY": pytest.skip("未设置 TAOTOKEN_API_KEY,跳过真实调用") return OpenAI(base_url=BASE_URL, api_key=API_KEY, timeout=120, max_retries=2)配-s跑一次,地址一目了然,比猜快得多。更多接入层面的问题,可以先过一遍 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=mimo_eval_debug 里的说明。
5.3 模型名与参数类
模型名写错是最隐蔽的一类:不会立刻报错,而是返回一个兜底模型的结果,导致你测的根本不是想测的那个。防御办法是在用例里顺手校验返回体里的 model 字段:
def test_model_name_echoed(client, usage_book): resp = call_and_record( client, usage_book, model="mimo-v2.6-flash", case="model_echo", prompt="回答 OK 即可。", max_tokens=8, ) assert resp.model, "返回体缺少 model 字段"5.4 流式与超时
流式请求在 pytest 里有个典型问题:断言在流结束前就执行,拿到的是半截内容。如果开启stream=True,务必先把 chunk 收完再断言,并在收流过程中累计 usage(部分实现只在最后一个 chunk 给出 usage)。超时则建议按任务类型分档:解释类任务 30s,补丁生成类 120s,不要把全局 timeout 调成一个很大的值来掩盖问题。
6. 把评测脚本固化进 CI,别让 token 悄悄涨
6.1 一个最小可用的执行脚本
#!/usr/bin/env bash set -euo pipefail export TAOTOKEN_BASE_URL="https://taotoken.net/api" # CI 里通过 secrets 注入,不要写进仓库 export TAOTOKEN_API_KEY="${TAOTOKEN_API_KEY:?missing key}" python -m pytest tests/test_mimo_token_budget.py -v --tb=short6.2 门禁怎么设
我一般设两道:
- 硬门禁:token 总量超过基线 1.2 倍直接失败,防止有人误把整个仓库塞进 prompt;
- 软提示:延迟中位数上涨超过 30% 只打 warning,不阻断合并,因为延迟受网络波动影响更大。
基线值从usage_report.json里自动读,不要手写常量,否则每一次任务集调整都要手工改数字,很快就没有人愿意维护了。
6.3 和直播数字保持距离
回到最开始那两条数字:mimo-v2.6-pro 的 DeepSWE 62、DeepSeek-v4.1-flash 的 74.2,以及作者推算的每秒约 10 美元。它们是有价值的行业信号,值得记录在选题清单里,但它们不是你的验收标准。你的验收标准是你自己的 pytest 用例、你自己的usage_report.json、你自己标定出来的那条阈值线。把这三样东西跑通,任何新模型出来你都能在半小时内得出属于自己的结论。
如果你还没开始跑,建议按这个顺序走:先去模型对话页把两个模型各问一轮,感受一下输出长度和风格差异;再把评测脚本接上,拿同一批任务跑一次对照;如果发现日常交互里用得比评测多,可以看看 Coding Plan 这类更适合高频使用的方案;接着在控制台创建一把专门给 CI 用的 key,和生产 key 分开;最后照 Claude Code 文档把交互式会话也配到同一条链路上,让评测数据和日常使用数据能对得上。
- 模型对话入口:https://taotoken.net/models/detail/chat?utm_source=taotoken_aicg_blog_end&utm_content=mimo_eval_chat
- Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=mimo_eval_plan
- 创建 API Key:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=mimo_eval_keys
- Claude Code 配置文档:https://taotoken.net/doc/ClaudeCodeAnthropic?utm_source=taotoken_aicg_blog_end&utm_content=mimo_eval_cc
评测这件事,最怕的不是模型分数不好看,而是你根本不知道自己花了多少。把 token 记账做进 pytest,是成本最低的一种确定性。