跳到主内容
快讯直播
AI智模界
教程

上手 Claude Fable 5.2:API 接入与单位任务成本实测

这篇教程带你从零把 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% 的通过样本看看风格和边界处理,把明显不合格的补成新的测试用例,反哺任务集。

最后,把上面那张汇总表贴在团队文档里,注明测量日期、任务集版本和单价来源。以后有人问"该用哪个档位",直接把表给他看,而不是给一句感觉。

AI 生成本文由 AI 基于公开信息自动生成,仅供参考。