这篇能做出什么
先看结果。跟着做完,你会得到一个能跑起来的小工具,它做三件事:
1. 分档:把每天遇到的编码任务分成「琐碎」「常规」「硬骨头」三类,前两类默认交给 Sonnet 档位的模型,第三类才动用 Opus 档位。
2. 省重复:把项目规范、代码风格、常用约定这些几乎不变的文字固定成提示前缀,开启提示缓存,同一会话里反复调用时不用重复付全价。
3. 能回退:Sonnet 试了一两轮不收敛,自动整理上下文、升级到 Opus,而不是让人手动重写一遍需求。
最后再加一张日志表,能看出这一天里钱花在哪个档、缓存命中了几次。数据不用猜。
前置条件清单
- 一个可用的 Anthropic API Key,放进环境变量
ANTHROPIC_API_KEY,不要写进代码。 - Python 环境,具体最低版本以官方文档当前说明为准。
- 安装官方 SDK:
```bash
pip install anthropic
```
版本以官方文档当前版本为准,不建议在依赖里锁死某个具体小版本。
- 一个你熟悉的小仓库,最好自带测试命令(
pytest、npm test之类都行)。有验收标准,分档才有意义。 - 心里清楚三个概念:token、system 提示、流式输出。
先明确 Sonnet 和 Opus 的定位差
Anthropic 的模型命名习惯里,Haiku 偏轻快,Sonnet 是均衡主力,Opus 是能力档位更高的一档。落到实际体验上,两者通常表现为:
| 维度 | Sonnet 档 | Opus 档 |
|---|---|---|
| 单位 token 成本 | 更低 | 更高 |
| 首字延迟、输出速度 | 通常更快 | 通常更慢 |
| 适合的任务 | 需求清晰、验收标准明确 | 需求模糊、需要拆解和判断 |
| 典型失败模式 | 硬扛难题,来回改 | 小任务上也慢、也贵 |
具体价格、上下文长度、缓存读写倍率会随版本调整,以官方页面为准。这篇教程的做法是:不背数字,把模型名和分档规则做成配置,价格变了只改配置不动逻辑。
一句话判断口诀:能不能一句话说清「输入什么、输出什么、怎么算做完」?能,走 Sonnet;不能,走 Opus。
第一步:跑通最小调用
先确认钥匙能用,再谈分层。
```python
hello.py
import anthropic
client = anthropic.Anthropic() # 自动读取 ANTHROPIC_API_KEY
resp = client.messages.create(
model="把这里换成官方文档里的 Sonnet 模型名",
max_tokens=512,
messages=[{"role": "user", "content": "用一句话解释幂等"}],
)
print(resp.content[0].text)
print(resp.usage)
```
把模型名字符串集中管理,别散落在十几个文件里:
```python
config.py
import os
MODELS = {
"fast": os.environ.get("MODEL_FAST", "替换为官方文档中的 Sonnet 模型名"),
"deep": os.environ.get("MODEL_DEEP", "替换为官方文档中的 Opus 模型名"),
}
```
以后官方换名字,改环境变量就行。
第二步:给任务分档
不要凭感觉,先列一张自己的表。可以参考这个模板,按你的实际工作改:
| 档位 | 典型任务 | 默认模型 |
|---|---|---|
| 琐碎 | 变量改名、补注释、写正则、格式化报错信息、加一行日志 | fast |
| 常规 | 按接口写实现、补单元测试、小 bug 修复、写 SQL、脚本加参数 | fast |
| 硬骨头 | 跨模块重构、性能问题定位、并发与竞态、模糊需求拆解、线上故障根因 | 先用 fast 探路,不收敛转 deep |
判断时看两个信号:
- 涉及文件数:改动落在 1~2 个文件、接口不变,基本是常规档;要动公共接口或跨越 3 个以上文件,先按硬骨头对待。
- 验收标准:能写出一条断言或一个测试命令的,走 fast;需要人来判断「这样设计合不合理」的,走 deep。
第三步:写出路由函数
```python
router.py
from config import MODELS
HARD_KEYWORDS = ("重构", "竞态", "死锁", "性能", "架构", "定位", "设计", "为什么")
def pick_tier(task: str, files_touched: int = 1, force_deep: bool = False) -> str:
if force_deep:
return "deep"
if files_touched >= 3:
return "deep"
if any(w in task for w in HARD_KEYWORDS):
return "deep"
return "fast"
```
关键词判断很粗糙,但足够撑起第一版。用一段时间后,把判断错的例子记下来,再考虑换成一个轻量分类调用。
第四步:把提示缓存用起来
缓存的原理不复杂:前缀逐字符一致,才可能命中。所以消息结构要按「稳定 → 易变」排序:
```
[ 固定角色设定 ] → [ 项目规范、代码风格、常用约定 ] → [ 本次任务与代码片段 ]
```
在稳定部分的末尾打缓存断点:
```python
ask.py
import anthropic
from config import MODELS
client = anthropic.Anthropic()
STABLE_RULES = """你是一名资深工程师。回答遵守以下要求:
1. 只输出可运行的代码或必要的说明,不要寒暄。
2. 改动尽量小,不顺手重构无关代码。
3. 涉及删改已有行为时,明确指出影响范围。"""
REPO_CONVENTIONS = """本仓库约定:
- 缩进 4 空格,字符串用双引号
- 新增函数必须配一个最小单元测试
- 数据库访问统一走 repository 层,不在业务函数里写 SQL
- 错误日志必须带 trace_id
(这里是示例,替换成你项目的真实约定)"""
def ask(task: str, context: str = "", tier: str = "fast", max_tokens: int = 4096):
resp = client.messages.create(
model=MODELS[tier],
max_tokens=max_tokens,
system=[
{"type": "text", "text": STABLE_RULES},
{
"type": "text",
"text": REPO_CONVENTIONS,
"cache_control": {"type": "ephemeral"}, # 字段用法以官方文档为准
},
],
messages=[{"role": "user", "content": f"{context}\n\n任务:{task}"}],
)
return resp
```
缓存到底有没有生效,看返回的用量字段:
```python
u = resp.usage
print("输入:", u.input_tokens, "输出:", u.output_tokens)
print("缓存写入:", getattr(u, "cache_creation_input_tokens", 0))
print("缓存读取:", getattr(u, "cache_read_input_tokens", 0))
```
cache_read_input_tokens 一直是 0,就说明前缀不稳定,回去查第四步开头的规则。注意缓存通常对提示长度有最低门槛,太短的内容不会缓存,具体门槛以官方文档为准。
第五步:难题回退到 Opus
回退要有触发条件,不能靠心情。可以这样定:
```python
def looks_done(text: str) -> bool:
if not text or len(text) < 40:
return False
unsure = ("需要更多信息", "无法确定", "可能有多种", "取决于")
return not any(s in text for s in unsure)
def summarize(text: str, limit: int = 400) -> str:
return text[:limit].replace("\n", " ")
def solve(task: str, context: str = "", max_rounds: int = 2, escalate: bool = True):
attempts = []
for _ in range(max_rounds):
resp = ask(task, context, tier="fast")
out = resp.content[0].text
if looks_done(out):
return out
attempts.append(summarize(out))
if not escalate:
return out
升级时不要丢掉全部历史,也不要原样丢过去
brief = "\n".join(f"- 第{i+1}轮尝试摘要:{a}" for i, a in enumerate(attempts))
escalated = (
f"{context}\n\n原始任务:{task}\n\n"
f"已尝试但未通过的思路:\n{brief}\n\n"
"请直接给出可执行的方案,并说明为什么前面的思路不成立。"
)
return ask(escalated, context="", tier="deep").content[0].text
```
关键点:升级时传的是摘要加失败现象,不是把两轮完整对话原样塞回去。完整历史又贵又容易让新模型被前面的错误思路带偏。
第六步:记账,别凭感觉
把每次调用写进 JSONL,一周后就能看出分档是否合理。
```python
log.py
import json, time
def log_call(tier, model, usage, task):
row = {
"ts": time.time(),
"tier": tier,
"model": model,
"task": task[:80],
"input": usage.input_tokens,
"output": usage.output_tokens,
"cache_write": getattr(usage, "cache_creation_input_tokens", 0),
"cache_read": getattr(usage, "cache_read_input_tokens", 0),
}
with open("calls.jsonl", "a", encoding="utf-8") as f:
f.write(json.dumps(row, ensure_ascii=False) + "\n")
```
看两个比例就够:fast 档占比是不是在七成以上;缓存读取占输入 token 的比例是不是在稳步上升。第一个低了说明分档太保守,第二个低了说明前缀没稳定。
第七步:接进日常工作流
包一个小命令行,日常就不用写代码了:
```bash
用法示意
ask "把 foo.py 里的 print 换成 logging,保持行为不变"
ask --deep "订单超时重复扣款,帮我定位可能的原因"
ask --files 4 "把 config 模块从 dict 改成 dataclass"
```
把 REPO_CONVENTIONS 那份文本放进仓库,纳入版本管理。团队里所有人跑同一份前缀,缓存才容易在共享场景下复用。
常见坑与排错
缓存命中率一直是 0。 九成是前缀里混了易变内容:时间戳、git status 输出、随机排序的文件列表、每次不同的模型名。把这些挪到用户消息里,别放 system。
以为开了缓存就一定省钱。 缓存写入本身是有成本的。同一个前缀只用一次,反而更贵。适合的场景是「一个会话里反复用」或「团队一天内多次用」。用一两次的一次性任务,直接关掉更划算。
把所有任务都丢给 Opus。 琐碎任务上它慢、贵,收益有限。改个变量名不值得动用最高档。
难题硬扛 Sonnet。 来回改五轮,累计 token 可能超过直接上一次 Opus。回退轮数设成 2 就是防这个。
模型名写死十几处。 集中到一个配置或环境变量里,官方调价换名时只改一处。
上下文无限增长。 每轮把已完成部分压成两行摘要,旧原文丢掉。上下文越长,每一轮都越贵。
重试没上限。 遇到限流或服务端错误用指数退避,并设最大次数,避免失败时疯狂重试。
排错按这个顺序走:模型名对不对 → system 前缀是不是逐字符稳定 → 用量里有没有 cache_read → 分档规则合不合理。四步走完,绝大多数问题都能定位。
下一步建议
1. 翻出过去两周的 20 个真实任务,逐个标一列「Sonnet 能否一轮过」。这张表就是你的分档规则的真实依据。
2. 给回退加个复盘记录:哪些任务升级到了 Opus,下次直接把这类任务默认归到 deep 档。
3. 试试把缓存断点前挪或后挪一格,看 cache_read 的变化。
4. 等分档规则稳定了,再考虑用一个轻量模型自动判档,人工只做兜底。
5. 官方文档里的模型名和定价页收进收藏夹,价格与模型名以官方页面当前内容为准,配置随时可改。
