长上下文、缓存、成本与维护
历史增长为何会改变账单和答案
每轮发送的历史构成新的输入。只在 Python 列表尾追加消息会让输入持续增长;删掉旧消息又可能删除尚未配对的工具请求。因此裁剪单位应为完整轮次或完整工具往返,而非按消息数量随意切片。摘要要保留来源、时间和未解决事项,原始证据在外部存储,摘要不能充当唯一审计记录。
缓存复用稳定前缀的计算,不是缓存最终答案。把每轮时间戳插在前缀开头,会让之后的相同内容失去复用机会。下面是沿用第一章环境的实际请求,缓存是否命中取决于模型、最小长度、前缀一致性与 TTL;短例子不承诺产生缓存读取。
import os
from anthropic import Anthropic
c=Anthropic(timeout=30,max_retries=0)
r=c.messages.create(model=os.environ['ANTHROPIC_MODEL'],max_tokens=100,
system=[{'type':'text','text':'依据资料回答;没有依据时说明缺失。',
'cache_control':{'type':'ephemeral'}}],
messages=[{'role':'user','content':'没有提供资料时你应怎么做?'}])
print(r.usage.model_dump())先观察 cache_creation_input_tokens 与 cache_read_input_tokens,再判断写入或命中,不通过第二次“感觉更快”得出结论。价格计算区分普通输入、缓存写入、缓存读取与输出,缓存不取消上下文窗口限制。
为什么未知字段要保留而未知行为要拒绝
SDK/模型会新增块类型或 stop_reason。展示层可以保留原始块元数据供排查;执行层遇到不认识的工具或终态应停止提交。把 else: pass 放进执行流可能悄悄丢掉必要状态。老 Text Completions 的单字符串 prompt 与 Messages 的内容块不能仅替换 URL;维护旧系统先记录实际请求/响应形状,再映射角色、停止信号和计量字段。
模型能力评估应分开文本、视觉、上下文、工具和结构化输出,不用“同一家 API”推断所有模型支持同一参数。比较模型时固定问题和评分规则:依据是否引用正确、工具参数是否有效、异常是否可恢复、总 token 和延迟是否满足预算。
保留旧 Text Completions 输入的可维护适配器
旧存档可能包含 \n\nHuman:、\n\nAssistant: 分隔,不能把整个prompt直接当Messages用户消息,否则旧助手回答也被用户角色包裹。下面legacy.py只支持明确的单轮存档合同;多轮或分隔符出现在资料正文中时拒绝并要求人工解析,避免静默错配。无需调用已不可用的旧模型。
def adapt_legacy(record):
prefix='\n\nHuman: ';suffix='\n\nAssistant:'
prompt=record['prompt']
if not prompt.startswith(prefix) or not prompt.endswith(suffix):raise ValueError('unknown legacy layout')
text=prompt[len(prefix):-len(suffix)]
if '\n\nHuman:' in text or '\n\nAssistant:' in text:raise ValueError('ambiguous history')
return {'messages':[{'role':'user','content':text}],
'max_tokens':record['max_tokens_to_sample']}
def legacy_result(record):
if record['stop_reason']!='stop_sequence':raise ValueError('legacy generation incomplete')
return record['completion']
r={'prompt':'\n\nHuman: hello\n\nAssistant:','max_tokens_to_sample':100}
assert adapt_legacy(r)=={'messages':[{'role':'user','content':'hello'}],'max_tokens':100}
assert legacy_result({'completion':'OK','stop_reason':'stop_sequence'})=='OK'
for old in ({**r,'prompt':'hello'},{**r,'prompt':'\n\nHuman: first\n\nAssistant: old\n\nHuman: next\n\nAssistant:'}):
try:adapt_legacy(old)
except ValueError:pass
else:raise AssertionError('ambiguous legacy accepted')
try:legacy_result({'completion':'OK','stop_reason':'max_tokens'})
except ValueError:pass
else:raise AssertionError('truncation accepted')
print('legacy input and stop mapping passed')这里锁住的是实际旧请求/响应形状,现代在线请求用 messages.create(model=..., **adapt_legacy(record));旧stop_sequence不能直接当现代end_turn枚举,分别归一化为应用完成状态。带自定义停止序列的旧业务还须核对停止点是否构成完整业务答案,不因字符串非空就入库。
确定性计算缓存用量
保存为 usage.py,Python 标准库即可运行。这里输入的是固定的教学价格,不是现行价格表。
def cost(usage,price):
return sum(usage.get(k,0)*price[k] for k in price)/1_000_000
usage={'input':100,'cache_write':1000,'cache_read':2000,'output':50}
price={'input':3,'cache_write':3.75,'cache_read':0.3,'output':15}
value=cost(usage,price)
assert abs(value-0.0054)<1e-12
print(value)这里每类 token 互斥。把缓存 token 再加进普通输入会重复计费;SDK 字段含义必须按官方 usage 定义归一化后再计算。多租户缓存键需要隔离资料权限,不能因为摘要相同就跨用户复用私有结果。
429 与服务过载可有限退避;参数、权限与模型不可用先修配置。读取超时并不保证服务端停止生成。日志保留 request ID、stop_reason、usage 和 SDK 版本,移除密钥与原始私密文件。在线调用均未计费运行,本地配对与计量逻辑可以独立复现。
练习
使用正文价格,令 cache_read 从 2000 增为 3000,其他不变。计算新增成本并断言;说明为什么命中缓存不意味着免费。
先记录自己的输出与解释,再核对本章答案。