我去年接手了一个内部AI客服项目,每个月Claude API账单稳定在2600块上下,一查后台,全是重复上下文在烧钱。后来花了两周时间整理调用链路、改缓存策略、拆消息结构,月底账单直接掉到800以内。今天把这套完整路径摊开讲,不吹不黑,每一步都有对应改动代码和实测数字。
这篇文章适合谁看:自己接了大模型API、公司内部在做Agent、或者写自动化脚本重度依赖Claude的人。核心思路其实通用于所有按token计费的大模型API,只是Claude的计费边界更清晰,拿它举例最直观。
1. 先算清楚钱到底烧在哪四个地方
很多人的第一反应是砍调用频率,我一开始也这么干,结果业务方天天来问为什么机器人变傻了。后来把账单拉出来对了一遍,才发现真正的问题根本不在调用次数,而是每次调用的token构成极其浪费。
1.1 固定开销陷阱:system prompt被反复计价
我当时的systm prompt写了两千多token,里面塞了大量业务规则、兜底话术、CRM字段说明。每条用户消息进来,这些内容原封不动跟着请求发一遍。一天几千次请求,光这部分每天就要烧掉几百万token,月账单里至少四分之一的钱花在没人看的规则文本上。
这就像你每天打车去上班,但每次都在车上放一块两公斤的大石头,司机按里程算钱,你按总重付油费,石头其实一动不动。优化第一步不是换车,是把这块石头拿下来。
1.2 模型选型过度:屠龙刀切菜
我们早期所有请求都走最贵的那个大模型,包括“把用户地址里的省市区拆出来”这种纯规则的活儿。这类任务是典型的小模型舒适区,拿最强的模型跑就是纯粹的按token烧钱。账单爆掉之前,我从没认真统计过不同任务的难度分布,这是第二个坑。
1.3 对话历史的指数放大效应
每轮多轮对话我们都会把完整历史带上,用户聊到第30轮,前面29轮的原文全都要重新计费。这里还有个容易忽略的细节——每条历史消息都会附带角色标记和时间戳,它们不是免费的。
1.4 实时请求拦截了降本机会
大量后台数据处理任务,比如批量打分、历史工单归类、凌晨跑数据报表,全都在用实时接口请求。实时接口的价格是异步批量接口的两倍,而这些任务的容忍度是“几分钟后拿到结果就可以”,完全没必要付实时溢价。
注意:下面的优化顺序很重要。如果不做1.1和1.3,直接切批量,等于把浪费乘以两倍再打包,白省。
2. 第一刀:给首轮请求加上提示词缓存
Claude API有一个缓存机制,可以把固定的前缀内容先存起来,后续相同前缀的请求只要付一个极低的缓存读取价。这个机制是官方支持的,不是巧劲,具体原理这里不展开,你只需要知道它最适合“每次请求前几段内容一模一样”的场景。
2.1 加缓存的正确姿势
初始化客户端时直接在请求体里带上缓存控制字段,官方SDK这样写就能生效:
import anthropic client = anthropic.Anthropic(api_key="your-api-key") response = client.messages.create( model="claude-sonnet-4-something", # 按你的实际模型来 max_tokens=1024, system=[ { "type": "text", "text": "你是一个电商客服助手,负责售后规则解答…… (这里原本有两千字规则)", "cache_control": {"type": "ephemeral"} } ], messages=[...] )加了cache_control之后,第一次请求会先把system前缀写入缓存,之后5分钟内的相同前缀请求,直接按缓存读取计费。官方给的价格是写入比正常贵一点点,读取大概只有原价的十分之一。这一刀下去,system prompt的固定开销直接降到原来的一折,账单肉眼可见往下掉。
2.2 缓存命中的隐藏条件
缓存命中有个隐性前提:前缀必须逐字节一致。也就是说,如果你在system prompt里动态拼了当前时间、随机ID或者用户昵称,这一整段前缀都会失配,缓存直接报废。
我当时踩了这个坑:system prompt末尾拼了当前会话ID: xxx,结果每个会话一个前缀,缓存几乎零命中。后面把这种动态字段全部挪到消息数组里,让system部分彻底静默,命中率才从个位数拉到了90%以上。
2.3 实测数字前后对比
优化后跑了一周,system prompt相关的费用从每天160块左右降到20块不到。缓存写入有少量额外成本,但总体算下来,这刀省了差不多四成。
提示:缓存时间窗是5分钟(ephemeral)。如果你的业务经常有超过5分钟的间隔,缓存会失效重写。对客服这种高频并发场景效果最好,对每天只调几次的低频脚本需要配合下文第4节的批量思路。
3. 第二刀:按任务难度给请求分线路
现在大模型API一般都有多个型号,区分度主要在推理深度和成本。我的调整思路很简单:只有需要复杂推理、多步规划、长文理解的请求才走最强型号,其余全部降级到轻量型号。
3.1 任务难度分级清单
我把业务里的调用场景列了一张表,每个场景标注了难度和允许延迟:
| 场景 | 原模型 | 调整后 | 理由 |
|---|---|---|---|
| 售后规则问答 | 最强 | 轻量 | 规则固定,检索兜底,无需深度推理 |
| 用户情绪识别 | 最强 | 轻量 | 单标签分类,小模型足够 |
| 多轮意图推断 | 最强 | 最强 | 上下文长,需要综合判断 |
| 工单摘要生成 | 最强 | 轻量 | 摘要模板化,思路固定 |
| 复杂投诉升级判断 | 最强 | 最强 | 涉及多文档比对,不能降级 |
| 地址字段清洗 | 最强 | 轻量 | 纯抽取任务,规则可覆盖 |
3.2 在代码里做请求路由
做法是在客户端包一层路由器,根据场景直接换model字段:
model_map = { "complex": "claude-opus-x", # 最强型号 "default": "claude-sonnet-x", # 性价比型号 "light": "claude-haiku-x", # 轻量型号 } def pick_model(task_type: str, retry_count: int = 0): # 重试超过2次的请求,说明轻量模型解决不了,升级线路 if retry_count >= 2: return model_map["complex"] return model_map.get(task_type, model_map["default"])3.3 两个容易忽略的细节
第一个细节是重试升级机制。轻量模型偶尔确实会答偏,我给它加了“重试超过N次自动转最强模型”的兜底逻辑,这样不会因为降级而牺牲掉最复杂的那部分请求。
第二个细节是temperature和max_tokens要跟着调。同一个轻量模型,把temperature从0.7降到0.3,输出会更稳定,很多原本需要重试的情况就少了。max_tokens也按场景卡死,不让它放飞自我生成大段废话。
4. 第三刀:重构消息结构,把“带历史”改成“带记忆”
多轮对话的历史携带是最隐蔽的烧钱点。对话越聊越长,每轮请求的token量就线性增长。核心解法不是什么高深技巧,就是砍历史长度、做结构化压缩、把临时内容挪出长期上下文。
4.1 定长窗口+动态摘要
我的方案是双轨制:最近6轮原始消息完整保留,超过6轮的部分抽取成摘要,摘要每10轮更新一次。这样既保留近期上下文细节,又不让早期的原始文本反复烧钱。
def compress_history(messages, max_round=6): if len(messages) <= max_round: return messages recent = messages[-max_round:] old = messages[:-max_round] summary_prompt = f"用三句话概括以下对话较早部分的关键信息:{old}" summary = client.messages.create(model="claude-haiku-x", max_tokens=200, messages=[{"role": "user", "content": summary_prompt}]) return [{"role": "user", "content": f"对话历史摘要:{summary.content[0].text}"}] + recent4.2 这个方案为什么省得明显
假设单轮对话平均600 token,第n轮的请求token数大约是600×n。定长窗口之后,每轮都是固定的6轮加一份200 token摘要,请求长度不再随对话轮数线性增长。
我实测了一个30轮的长会话,改动前最后几轮请求在1.8万token上下,改动后稳定在4000token左右。长会话越多的业务,这刀收益越猛。专门跑长对话评测的团队,这刀可能直接砍掉一半账单。
4.3 摘要这个动作本身也要花钱,别忽略
摘要生成用的是轻量模型,单次也就几十token的量级,整体成本可以接受。但要注意不能无限摘要,我的上限是十条,再多就丢弃最早的那批。业务方如果追问早期信息,后台有原始会话归档,API调用链上不保留。
注意:消息数组里的角色标记(role字段)、工具调用结果(tool_result)也参与计费。工具调用轮数多的Agent应用,尽量把中间工具结果压缩成“结论+证据摘要”两部分,而不是原封不动回传JSON。
5. 第四刀:能异步的请求全部切批量模式
前面提到了批量接口,这是背靠背省钱的大头。Claude的批量API走的是独立队列,官方价格是实时请求的一半,适合所有不要求秒回的批量任务。
5.1 哪些任务适合走批量
我梳理了一下,以下任务全部切了过去:
- 每天凌晨的会话数据打标签(几百条,十分钟内跑完,完全没有实时压力)
- 工单历史归档摘要(跑一次半小时,反正人要第二天才看)
- 批量拒信改写(同时几百封,后台队列慢慢发)
- 内部知识库的自动纠错检查(每天一次,中午前出报告就行)
5.2 批量请求的代码形态
批量接口不是直接换一个调用方式,而是先把请求写成一个JSONL文件,上传之后异步轮询结果:
import json, time from anthropic import Anthropic client = Anthropic(api_key="your-api-key") requests = [ { "custom_id": f"task-{i}", "params": { "model": "claude-haiku-x", "max_tokens": 2048, "messages": [{"role": "user", "content": prompt}] } } for i, prompt in enumerate(prompts) ] with open("batch_input.jsonl", "w") as f: for req in requests: f.write(json.dumps(req) + "\n") resp = client.messages.batches.create(file=open("batch_input.jsonl", "rb")) batch_id = resp.id # 轮询结果 while True: batch = client.messages.batches.retrieve(batch_id) if batch.processing_status == "ended": break time.sleep(30)批量请求还能和白天的缓存策略共存——因为批量任务通常是一小时内集中提交,前缀一致,缓存命中率反而比实时分散请求更高。缓存加上批量折扣,两项叠加,这批任务的成本大概只有原来的三分之一。
5.3 批量模式的时间成本要说清楚
批量不是即时的。官方给的是“一小时内完成”,我实际跑下来大多在10到40分钟之间。所以它只适合延迟容忍度高的任务,不能用来替代用户实时对话。这个判断错了的话,省下来的钱会被客诉骂回去。
6. 账单复盘:怎么持续盯紧每一笔token
省一次钱不难,难的是让费用一直保持在低位。后面我搭了一个最简成本监控,不用买什么额外监控平台,直接抓API回传的usage字段落库就行。
6.1 把usage字段记录成日志
每次调用完,SDK返回里都带token明细。我加了一个装饰器,统一把信息打进本地数据库:
def log_usage(func): def wrapper(*args, **kwargs): resp = func(*args, **kwargs) usage = resp.usage print(f"input_tokens={usage.input_tokens}, " f"cache_read_input_tokens={usage.cache_read_input_tokens}, " f"output_tokens={usage.output_tokens}, " f"model={resp.model}") return resp return wrapper这个日志是后续所有决策的依据。如果只盯总金额,你永远不知道钱花在哪一步;盯着分项token数,一周就能看出哪个模块在漏。
6.2 每周看三个比例
我每周固定看三组数据,超过阈值就要动手:
- 缓存读取占比:低于60%说明缓存策略没吃透,回去查前缀一致性。
- 最强模型调用占比:超过15%说明路由分级没执行到位,有场景在偷跑高配。
- 平均单次请求token量:持续走高说明窗口压缩失效,有人在偷偷塞大段历史。
6.3 踩过的漏记坑
有段时间日志里token总和跟账单对不上,排查半天才发现是批量任务的回调结果里也带了usage,但我只记了实时接口的日志。批量任务跑了一周,全部费用成了账单里的“幽灵token”。后来把批量结果遍历的时候也补上统计,金额才重新对上。
7. 常见问题与避坑清单
整个优化过程中踩过的坑不少,挑几个高频的整理成清单,你照着排掉,基本不会重蹈覆辙。
7.1 缓存写入费比想象中贵
缓存生效前,第一次写入是按正常价的1.25倍计费的。如果你每天只有几十次调用,且间隔经常超过5分钟,缓存可能越省越亏。省钱的前提是高频、前缀固定、间隔短。低频脚本不要硬套缓存。
7.2 降级模型导致返工重试
我见过有人为了省钱把所有请求切成最轻量模型,结果复杂任务反复返工,起夜看了三遍结果,最后还是换回高配。省钱的账要算全,重试本身也花钱。我的经验是:先跑一周分级路由,看轻量模型的失败率,超过5%就调整分类边界。
7.3 批量任务静默失败
批量接口偶尔部分任务失败,不会报错弹窗,只会静静放在结果文件里。轮询结束后一定要检查每个custom_id的结果状态,失败的任务要么重投要么走实时兜底,不能稀里糊涂隔了一周才发现。
7.4 多轮对话摘要失真
摘要压缩很省token,但早期信息被过度概括后确实会丢细节。比如用户第2轮说“我喜欢红色”,第20轮再问他偏好,摘要可能只剩“用户有偏好”这种废信息。关键偏好类信息需要在摘要生成前单独抽出来作为“持久字段”,不走普通摘要。
8. 经验总结与后续优化方向
几次大改下来,账单从2600降到了700多,月省1800块是实打实的数字。四刀砍完,其实还有几个方向我留着二期再动:
- 进一步压system prompt本身,现在还剩1500 token的规则文本,部分话术可以直接挪到业务代码里做模板拼接,不进prompt。
- 对轻量模型的输出加一层schema校验,强制结构化JSON输出,省掉因为解析失败而重试的token。
- 接入更细的配额管理,给每个业务方单独建token配额,按周冻结,倒逼业务侧自己精简调用逻辑。
最后再分享一个个人体会:优化API成本,本质上不是让大家少用AI,而是把每一笔token花在刀刃上。同一个功能,用最强模型跑一遍和用轻量模型配合缓存跑一遍,用户体验差距往往不大,但费用能差好几倍。这中间的差价就是你的优化空间,也是CTO最喜欢的汇报素材。上面这套方法对OpenAI、Google的同类API同样适用,计费模型略有差异,但思路是通用的。