这篇教程带你从零把 Claude Fable 5.2 接进自己的代码里,再用同一批编码任务把它和 Opus 5.5 放在一起量一遍:成功率多少、token 花在哪、每完成一个任务到底要多少钱。跑完之后你会得到一张可以拿去做决策的表,以及一套"什么时候用高档位、什么时候降到低档位"的规则。文中代码可以直接照抄,价格、模型 ID 这类会变的东西一律从官方定价页和控制台的模型列表里取,以官方文档当前版本为准。
需要提前说清楚一件事:不同时间点、不同账号、不同任务集跑出来的数字一定不一样。这篇给的是量法,不是结论。任何"某模型比某模型强 X%"的说法,只要不是你自己在真实任务上跑出来的,都不该用来做预算。
这篇能做出什么
完成下面八步,你手上会有四样东西:
第一,一个能跑通的最小调用,同时有 curl 和 Python 两个版本,方便你在任何环境里先确认网络和鉴权没问题。
第二,一个评测器。它读一份任务清单,对每个任务调用指定模型,把模型产出的代码落盘并用测试判定对错,全过程记录 token 用量,最后写成 JSONL 日志。
第三,一张汇总表。字段包括成功率、平均输入/输出 token、总花费、每任务成本和每"成功"任务成本。最后这个指标才是真正该看的东西,原因下面会展开。
第四,一个价格敏感性脚本。把单价乘以任意系数(比如 0.8,对应降价 20%),一键重算,看结论会不会翻转,以及翻转之后你该把哪些任务迁到哪一档。
前置条件清单
- 一个 Anthropic API Key,在控制台创建,配好余额或额度上限。
- Python 3.9 以上(具体版本以官方 SDK 的安装要求为准),能
pip install anthropic。 - 官方定价页上两个模型的输入、输出单价,抄进环境变量。注意区分"每千 token"和"每百万 token",单位错一位,后面所有结论都是错的。
- 一个已经有 pytest 的仓库,或者你愿意新建一个。评测的"对错"必须由测试判定,不能靠肉眼。
- 20 条左右的编码任务。太少了方差大,太多了第一次跑不完。建议覆盖三类:单函数实现、跨文件修改、带边界条件的解析逻辑。
- 一个预算上限。跑批之前先手算一遍最坏情况:任务数 × 单任务最大 token × 单价 × 并发重试次数。
步骤一:确认档位口径,把名字变成变量
在写任何代码之前,先在控制台的模型列表里确认两件事:你想要的那个档位的准确模型 ID 是什么,以及它是否支持扩展思考这类可调参数。
不要在两个脚本里各写一遍模型名字。统一放进环境变量:
```bash
.env 单价从官方定价页抄,单位:每百万 token;模型 ID 从控制台模型列表抄
ANTHROPIC_API_KEY=sk-ant-...
MODEL_HIGH= # 高档位,例如 Opus 5.5 对应的 ID
MODEL_LOW= # 待评估档位,例如 Fable 5.2 对应的 ID
PRICE_IN_HIGH=
PRICE_OUT_HIGH=
PRICE_IN_LOW=
PRICE_OUT_LOW=
MAX_CONCURRENCY=4
```
档位选择通常不是二选一,而是三层:旗舰档负责复杂重构和长上下文推理;中间档负责绝大多数日常编码;轻量档负责格式化、抽取、改写这类结构清晰的任务。同一模型内部往往还有"思考预算"这种软档位,如果该模型支持,可以通过请求参数控制,具体可用性和预算上限以官方文档为准。软档位的好处是不用换模型就能调整"想多久",坏处是它同样计入输出 token。
步骤二:跑通最小调用
先用 curl 确认鉴权和网络,排除 SDK 层的问题:
```bash
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "'"$MODEL_LOW"'",
"max_tokens": 1024,
"temperature": 0,
"messages": [
{"role": "user", "content": "用 Python 写一个线程安全的 LRU 缓存,只返回代码"}
]
}'
```
请求头里的版本号以官方文档为准,不同时期可能新增。返回体里除了内容,重点看 usage:里面有输入 token 和输出 token,如果命中了提示缓存,还会有缓存写入和缓存读取两个单独字段。这几个数字是后面所有成本计算的唯一来源,不要自己用字符数估算。
再用 Python SDK 做同样的事:
```python
import os
from anthropic import Anthropic
client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
model = os.environ["MODEL_LOW"]
resp = client.messages.create(
model=model,
max_tokens=2048,
temperature=0,
system="你是一名资深后端工程师。只返回代码,不要解释,不要 Markdown 围栏。",
messages=[{"role": "user", "content": "实现一个带过期时间的 KV 存储,只返回代码"}],
)
print("stop_reason:", resp.stop_reason)
print("usage:", resp.usage)
print(resp.content[0].text[:500])
```
stop_reason 一定要打印。如果它是 max_tokens,说明输出被截断了,这次结果不能算模型的失败,只能算你的配置问题。评测里把截断算成失败,会系统性低估高档位模型的成功率。
步骤三:把 token 计量对齐
正式跑批之前,先用计数接口把任务清单过一遍,估出输入侧的量级:
```python
r = client.messages.count_tokens(
model=model,
messages=[{"role": "user", "content": "你的任务描述"}],
)
print(r.input_tokens)
```
这一步能提前发现两类问题:某条任务的提示词长得离谱,或者你把整个仓库都塞进了上下文。顺带把提示缓存也用上——固定的系统提示、固定的项目骨架、固定的风格约束,都可以标记为可缓存。缓存写入通常比普通输入贵,缓存读取通常比普通输入便宜,具体倍率以官方定价页为准。所以在成本脚本里,缓存写入、缓存读取、普通输入要分成三个价格变量,不能合并成一个。
```python
COST = (
normal_in / 1e6 * PRICE_IN
+ cache_write / 1e6 * PRICE_CACHE_WRITE
+ cache_read / 1e6 * PRICE_CACHE_READ
+ out / 1e6 * PRICE_OUT
)
```
步骤四:把任务写成可判定的清单
一个任务一条 JSON,放在 tasks.jsonl 里:
```json
{"id": "parse-001", "prompt": "实现 parse_duration(s),支持 1h30m / 90s / 2d 三种写法,非法输入抛 ValueError。只返回代码。", "test": "from solution import parse_duration\nimport pytest\n\ndef test_ok():\n assert parse_duration('90s') == 90\n assert parse_duration('1h30m') == 5400\n\ndef test_bad():\n with pytest.raises(ValueError):\n parse_duration('abc')\n"}
```
写清单时守住两条:每条任务必须有一个能自动跑的 test;测试文件本身要先在一个"已知正确解"上跑通,确认测试没写错。测试写错是这类评测里最隐蔽的坑,它会让你得出完全相反的结论。
步骤五:写跑批脚本
下面这个脚本负责调用、重试、落盘、判定。存成 runner.py:
```python
import json, os, re, subprocess, tempfile, time, sys
from pathlib import Path
from anthropic import Anthropic, APIStatusError
client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
MODEL = os.environ["MODEL_LOW"]
LOG = Path("runs") / f"{MODEL}.jsonl"
LOG.parent.mkdir(exist_ok=True)
FENCE = re.compile(r"``[a-zA-Z0-9]*\n(.*?)``", re.S)
def extract_code(text: str) -> str:
m = FENCE.search(text)
return (m.group(1) if m else text).strip()
def call(prompt: str, max_tokens: int = 4096, retries: int = 5):
last = None
for i in range(retries):
try:
return client.messages.create(
model=MODEL,
max_tokens=max_tokens,
temperature=0,
messages=[{"role": "user", "content": prompt}],
)
except APIStatusError as e:
last = e
if e.status_code in (429, 529) and i < retries - 1:
try:
wait = float(e.response.headers.get("retry-after", 2 ** i))
except Exception:
wait = 2 ** i
time.sleep(wait)
continue
raise
raise last
def run_tests(code: str, test_code: str, timeout: int = 120) -> bool:
with tempfile.TemporaryDirectory() as d:
d = Path(d)
(d / "solution.py").write_text(code, encoding="utf-8")
(d / "test_solution.py").write_text(test_code, encoding="utf-8")
try:
r = subprocess.run(
[sys.executable, "-m", "pytest", "-q", "test_solution.py"],
cwd=d, capture_output=True, text=True, timeout=timeout,
)
return r.returncode == 0
except subprocess.TimeoutExpired:
return False
for line in Path("tasks.jsonl").read_text(encoding="utf-8").splitlines():
task = json.loads(line)
rec = {"id": task["id"], "model": MODEL}
try:
resp = call(task["prompt"])
u = resp.usage
rec["input_tokens"] = getattr(u, "input_tokens", 0)
rec["output_tokens"] = getattr(u, "output_tokens", 0)
rec["cache_write"] = getattr(u, "cache_creation_input_tokens", 0) or 0
rec["cache_read"] = getattr(u, "cache_read_input_tokens", 0) or 0
rec["truncated"] = (resp.stop_reason == "max_tokens")
rec["success"] = (not rec["truncated"]) and run_tests(
extract_code(resp.content[0].text), task["test"]
)
except Exception as e:
rec.update({"input_tokens": 0, "output_tokens": 0,
"cache_write": 0, "cache_read": 0,
"truncated": False, "success": False,
"error": f"{type(e).__name__}: {e}"})
with LOG.open("a", encoding="utf-8") as f:
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
print(rec["id"], rec["success"])
```
跑的时候把两个模型各来一遍:
```bash
export $(grep -v '^#' .env | xargs)
MODEL_LOW=$MODEL_LOW python runner.py
MODEL_LOW=$MODEL_HIGH python runner.py # 换成高档位,日志文件名跟着变
```
注意失败调用也要写进日志。错误分支里把 token 记成 0 是一种简化,但如果你的重试策略在某些错误下已经产生了计费(比如流式请求中途断开),就老老实实记实际用量。漏算失败成本,会让便宜但爱失败的档位显得比实际更划算。
步骤六:算单位任务成本
这是整篇的核心。很多人比价只比单价,但真正决定预算的是"每成功完成一个任务花多少钱":
```text
每成功任务成本 = 总花费 / 成功任务数
= (单任务平均花费) / 成功率
```
单价降一半、成功率掉三成,实际成本反而可能上升。把汇总写成脚本 summarize.py:
```python
import json, os, sys
from pathlib import Path
def summarize(path, p_in, p_w, p_r, p_out):
rows = [json.loads(l) for l in Path(path).read_text(encoding="utf-8").splitlines()]
n = len(rows)
ok = sum(1 for r in rows if r["success"])
tin = sum(r["input_tokens"] for r in rows)
tw = sum(r["cache_write"] for r in rows)
tr = sum(r["cache_read"] for r in rows)
tout = sum(r["output_tokens"] for r in rows)
cost = (tin/1e6*p_in + tw/1e6*p_w + tr/1e6*p_r + tout/1e6*p_out)
return {
"model": rows[0]["model"],
"tasks": n,
"success_rate": round(ok/n, 3) if n else 0,
"avg_in": round(tin/n, 1) if n else 0,
"avg_out": round(tout/n, 1) if n else 0,
"total_cost": round(cost, 4),
"cost_per_task": round(cost/n, 4) if n else 0,
"cost_per_success": round(cost/ok, 4) if ok else None,
}
if __name__ == "__main__":
p = {k: float(os.environ[k]) for k in
["PRICE_IN", "PRICE_CACHE_WRITE", "PRICE_CACHE_READ", "PRICE_OUT"]}
for f in sys.argv[1:]:
print(json.dumps(summarize(f, **p), ensure_ascii=False))
```
把结果填进这张表,行是你的模型,列含义如下:
| 指标 | 含义 | 怎么读 |
|---|---|---|
| success_rate | 测试通过的样本占比 | 低于 0.7 时,先看是测试问题还是提示词问题 |
| avg_in / avg_out | 单任务平均输入与输出 token | 输出 token 通常是成本大头,重点关注 |
| cost_per_task | 含失败在内的平均花费 | 用来估月度预算 |
| cost_per_success | 总花费 ÷ 成功数 | 用来做档位决策,这是主指标 |
同一批任务跑三遍,取中位数,因为模型输出有随机性。三遍之间成功率相差超过 0.15,说明任务集太小或者测试太脆,先把这两件事修掉再谈结论。
步骤七:价格敏感性,看结论会不会翻转
价格一变,之前的排序可能就翻了。写个小函数,把三个输入价和输出价同时乘一个系数,重算上表:
```python
def rescale(price_in, price_w, price_r, price_out, factor):
return (price_in*factor, price_w*factor, price_r*factor, price_out*factor)
```
以"某档位降价 20%"为例,把 factor 设成 0.8,只作用在被降价的那个档位上,然后重跑 summarize。你要观察的是两件事:cost_per_success 的排名有没有变;如果没变,差距从多少缩到多少。
如果原本低档位的 cost_per_success 只比高档位高一点点,降价后大概率会反超,这时候迁移是明确划算的。如果原本差着数倍,降价 20% 通常不足以翻盘,别急着迁。判断依据永远是这张表,不是直觉。
步骤八:迁移与降级策略
有了数字之后,按任务类型切分,而不是一刀切。
直接降到低档位的任务:输入输出结构固定、有明确测试、上下文短于几千 token 的。这类任务占了日常编码的多数,成功率差异通常最小,省下来的钱最多。
留在高档位的任务:跨多个文件的改动、需要理解隐含约束的重构、长上下文里的定位问题。这类任务一旦失败,返工成本远超 token 差价。
走"升级链"的任务:先让低档位跑,测试不过再升级到高档位重跑。它的期望成本是:
```text
期望成本 = C_low + (1 - s_low) × C_high
综合成功率 = s_low + (1 - s_low) × s_high
```
其中 C_low、C_high 是单次调用成本,s_low、s_high 是各自的成功率。把实测值代进去,如果综合成功率能接近单用高档位、期望成本又明显更低,这条链就值得上。反过来,如果 s_low 只有三成,你等于有七成概率白付一次低档位的钱,这时候直接用高档位更省。
别忘了缓存。固定的系统提示、风格约束、项目骨架打进缓存之后,重复调用的输入侧成本会显著下降。缓存对高频小任务的效果比低频大任务明显得多。
常见坑与排错
鉴权失败(401)。多半是 .env 没加载。export $(grep -v '^#' .env | xargs) 这行别漏,或者用 dotenv 库显式加载。
限流(429)与过载(529)。读响应头里的 retry-after 再退避,别用固定间隔猛冲。并发调到 2 到 4 起步,稳了再加。
把截断当成失败。stop_reason == "max_tokens" 时先怀疑 max_tokens 设小了,尤其是输出里带大段代码的任务。评测前先用几条样例校准这个值。
代码围栏没剥干净。模型经常返回带 ``` ` `` 的代码块,直接写进 solution.py 会让测试报语法错误。上面脚本里的 extract_code` 就是干这个的。
测试本身有 bug。先用一份你自己写的正确解跑一遍全部测试,确认全绿。这一步花十分钟,能省掉后面几小时的困惑。
只跑一遍就下结论。temperature=0 也不保证完全确定性。至少三遍,看方差。
漏算失败成本。失败的调用照样计费,日志里必须记。
单位搞错。每千 token 和每百万 token 差三个数量级,代进公式前先确认定价页的单位。
缓存 token 混算。缓存写入和缓存读取的单价与普通输入不同,三个变量分开,以官方定价页为准。
下一步建议
把任务集从 20 条扩到 100 条左右,让它尽量贴近你团队真实的提交分布——如果七成工作是写小函数,任务集里就该有七成是小函数。任务集跑偏,结论也会跑偏。
把汇总脚本接进 CI,每次模型或提示词有改动就跑一轮,观察 cost_per_success 的漂移。模型会更新,价格会调整,一次性结论的保鲜期很短。
再补一层人工抽检。测试通过不等于代码能维护,抽 10% 的通过样本看看风格和边界处理,把明显不合格的补成新的测试用例,反哺任务集。
最后,把上面那张汇总表贴在团队文档里,注明测量日期、任务集版本和单价来源。以后有人问"该用哪个档位",直接把表给他看,而不是给一句感觉。
