大模型通识 / 大模型应用开发入门
9. 算清费用:用量、上下文和缓存
原文备份 · 第 11 / 19 篇 · 查看极客时间原知识库 ↗
本篇目录 · 10 节
来源:大模型应用开发入门
上一课的助手终于能在429之后稍等,再次尝试。有了最终答案,你可能顺手打开最后一份响应,看到输出只有几百个Token,便觉得这次任务“就花了这么一点”。但前面的失败、重复送入的笔记、输入中的缓存部分,都还没有进入这笔计算。
本篇给项目增加一个本地费用查看工具:读取已有卡片记录,按明确选择的价格计算已知用量;发现未知的尝试,就把任务总估算留空。读完后,你还应能解释为什么聊天越长可能越贵,以及为什么缓存命中不等于免费回答。
前置是第08篇的独立代码快照。仍用 Python 3.11+、openai==3.24.0、DeepSeek 官方 API;资料核实于 2026年10月4日。本篇全部必做操作都离线,不需要Key或充值。价格是当日资料快照,样例用量由作者构造,不是实际账单。
一、先分清三个数字
你会遇到三个看起来相近、实际上不同的数字:
| 数字 | 从哪里来 | 能回答什么 |
|---|---|---|
| 本次响应的用量 | API返回的usage | 这份响应报告了多少输入、输出Token |
| 本地任务费用估算 | 已保存的各次用量 × 指定单价 | 按这些记录和价格假设,任务大约花多少 |
| 账户实际扣费 | 服务平台账户记录 | 平台实际怎样扣费,包括本地遗漏的调用 |
本课做第二项。它不能凭最后一份成功响应还原丢失的用量,也无法知道你在另一台电脑上发了哪些请求。
Token也不等于汉字。文字、代码和标点经模型的分词方式转换后,数量才确定。len(text) 得到的是Python字符串长度,不能直接乘单价当账单;“大概一个汉字几个Token”的经验值也只适合粗略规划。
我们优先使用API的用量字段,缺少必要数据时明确说明,避免用一个看起来精确的小数掩盖猜测。
二、读懂usage,先别急着乘价格
当前 Chat Completions 文档将输入拆成命中与未命中两部分,并提供输出总量与思考细分。total_tokens 是输入加输出,不是第三份额外消费。DeepSeek:Chat Completions用量字段
以下是人为构造的算术样例:
{
"prompt_tokens": 1000,
"prompt_cache_hit_tokens": 800,
"prompt_cache_miss_tokens": 200,
"completion_tokens": 200,
"total_tokens": 1200,
"completion_tokens_details": {
"reasoning_tokens": 50
}
}
输入1000,其中800命中缓存、200没有命中。输出200,其中50用于思考。因此计费计算使用800、200、200这三项,而不是再把1200或50加进去。
输入1000 = 缓存命中800 + 未命中200
输出200 = 思考细分50 + 其余输出部分
总用量1200 = 输入1000 + 输出200
如果只看到 prompt_tokens: 1000,却没有缓存拆分,本课工具不会默认这1000全部未命中,更不会默认全部命中。它将该次标为不能完整估算。你可以另做最坏情形预算,但那应标明“假设全部未命中”,不能冒充实际用量结算。
程序同时检查数字为非负整数、两种输入之和等于输入总数,以及存在时的总Token关系。引用错误的卡片也可能带着完全合法的usage:内容质量和调用费用是两个不同维度。
三、单价带日期,也带时段
截至核实日,官方中文价格页中 deepseek-flash 的人民币单价如下,单位都是元 / 百万Token,不是元 / 千Token。DeepSeek:模型与价格
| 类型 | 空闲时段 off_peak | 高峰时段 peak |
|---|---|---|
| 输入:缓存命中 | 0.02 | 0.04 |
| 输入:缓存未命中 | 1 | 2 |
| 输出 | 4 | 8 |
该页定义的高峰是北京时间周一至周五、不含中国法定节假日的9:00—12:00及14:00—18:00;其他时间属于空闲时段。价格可能调整,实际使用前要重新核实。
这带来一个容易遗漏的选择:价格与调用发生时的规则有关,不能看现在电脑上的时间,就给上周的请求套价格。本课不内置节假日库,也不猜跨时段长请求的计价边界,因此要求你显式指定 --period off_peak 或 --period peak。报告会显示 assumed_period,提醒这是你选择的假设。
遇到多个日期、多次价格调整或横跨时段的任务,应先查对应平台规则,再分别核对。这个小工具按所选同一组单价估算一次记录中的各次尝试,适合教学和小项目检查,不承担自动账单对账。
prices.example.json 保存模型、币种、每百万单位、核实日期、来源,以及上述两组单价。换模型时不能只改请求配置,还要准备匹配的价格表。文件中的单价使用 "0.02" 这样的字符串,交给Python的 Decimal 做十进制计算,减少二进制浮点表示带来的小误差。Python:decimal
四、先用纸笔算一遍,再让代码算
对第二节的样例,选择空闲时段:
命中输入:800 × 0.02 / 1,000,000 = 0.000016元
未命中输入:200 × 1 / 1,000,000 = 0.000200元
输出:200 × 4 / 1,000,000 = 0.000800元
合计:0.001016元
如果同样1000输入、200输出都没有缓存命中,估算为 0.001800元。差额来自输入部分,输出费用没有因为输入缓存而消失。
不要把这些极小单次金额先四舍五入成两位小数再累加,否则每次都可能显示0.00。本工具保留十进制小数,最后汇总时再决定展示精度;它没有实现平台内部的扣费舍入规则。
配套 costs.py 在确认用量字段一致后执行这段计算:
amount = (Decimal(hit) * rates["hit"] + Decimal(miss) * rates["miss"]
+ Decimal(output) * rates["output"]) / Decimal(1000000)
return amount
公式不复杂,困难在于输入是否完整、单位是否相同、价格是否适用。代码前面的检查,比把这两行写得更短更有价值。
五、运行独立快照,验证五种情况
下载 lesson-09代码包,解压后进入 lesson-09 目录。它完整保留第08篇的卡片、聊天、校验、重试与报告入口,只增加 costs.py、价格样例与 fixtures/cost-cases.json。不会修改旧快照。
macOS / Linux首次准备与离线演示:
python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
cp config.example.json config.json
.venv/bin/python cards.py --dry-run --thinking off --budget 1024 --attempts 1
.venv/bin/python costs.py --prices prices.example.json --period off_peak --demo
Windows PowerShell:
py -3 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
Copy-Item config.example.json config.json
.\.venv\Scripts\python.exe cards.py --dry-run --thinking off --budget 1024 --attempts 1
.\.venv\Scripts\python.exe costs.py --prices prices.example.json --period off_peak --demo
已有配置不要覆盖;Windows没有 py 时使用已安装Python对应的 python。安装依赖需要网络,两个演示命令本身不联网。环境准备见第02篇。
预演仍显示两条消息和一次尝试上限。费用演示先打印价格来源与 assumed_period: off_peak,随后输出五个任务:
| 样例标签 | 已知部分小计 / 元 | 任务估算 / 元 | 原因 |
|---|---|---|---|
| cold | 0.001800 | 0.001800 | 输入1000全未命中,输出200 |
| cache_hit | 0.001016 | 0.001016 | 输入800命中、200未命中,输出200 |
| retry_unknown | 0.001016 | null | 首次失败用量未知,第二次与上一行相同 |
| bad_answer | 0.001016 | 0.001016 | 卡片校验失败,但收到了完整用量 |
| missing_breakdown | 0 | null | 缺少缓存拆分,没有可完整计价的尝试 |
小数末尾的零可能被省略,例如0.0018与0.001800相同。最后应出现“可展示任务5;无法估算文件0;排除模拟0”。这里“可展示”包括能展示未知原因,不等于五个任务都有完整价格。
独立验证时,打开 fixtures/cost-cases.json,找到 cache_hit,检查它的逐次usage是否与第二节相同,再用纸笔结果核对 task_estimate_cny。将命令的 off_peak 换成 peak 再运行,该样例应为 0.002032,其余完整估算也翻倍,未知仍为null。
此演示读取固定样例,不写入 records,也不会调用 cards.py 生成新答案。样例里的50个思考Token只是验证细分不重复计算,不能推断你的真实卡片会产生相同用量。
六、任务总成本为什么可能必须留空
打开 retry_unknown 的结果,你会看到类似以下关键字段:
{
"known_subtotal_cny": "0.001016",
"unknown_attempts": 1,
"task_estimate_cny": null
}
意思是:一份可计价的响应按所选价格算出0.001016元,另外一次没有充分数据。程序没有证据宣布整个任务只花这么多。
对于第08篇起的卡片结果,费用工具只从 result.attempts 读取每次用量。顶层 result.usage 是最后正常响应的重复记录,不再额外累加。否则一次成功可能被算两次。
任务最终是 validation_failed 或 response_rejected,也不抹掉已收到的用量;只有 received 尝试且必要usage合法,才进入已知小计。started、失败、缺字段和矛盾字段都会留下不能估算的原因。小计0仅表示没有任何已知金额可累加,不能读成免费。
task_estimate_cny 有值,只能说明这份本地记录中的全部尝试都能按所选价格计算。它仍不保证文件完整覆盖账户活动,不考虑未记录请求或平台优惠。程序不会将文件夹里的数字自动合并成账户总账。
七、查看自己的已有记录
完成离线练习后,可以只读已有文件,不必为了这课重新付费调用。
若第09篇目录已有真实卡片结果,在当前目录运行:
.venv/bin/python costs.py --prices prices.example.json --period off_peak
PowerShell对应:
.\.venv\Scripts\python.exe costs.py --prices prices.example.json --period off_peak
仍要根据要核对的调用选择时段。默认读取脚本旁的 records;没有目录会明确提示没有记录,不显示“账户支出为0”。本地的 cards.py --demo、--demo-retry 记录被自动排除,它们不具有真实用量。
如果保留了相邻第08篇的记录,可直接只读那个目录,无需复制:
.venv/bin/python costs.py --prices prices.example.json --period off_peak --records ../lesson-08/records
PowerShell:
.\.venv\Scripts\python.exe costs.py --prices prices.example.json --period off_peak --records ..\lesson-08\records
这些相对路径假设两个解压目录并列;否则替换为自己的路径,含空格时加引号。--prices 与 --records 显式路径相对当前终端目录解析,省略 --records 才使用脚本旁目录。
工具不读Key、不访问平台,也不改已有记录。没有result的request会显示在 requests_without_result 中,提醒该任务没有进入估算。旧聊天记录或第07篇及以前缺少attempt的记录不自动猜测转换;坏文件和模型不匹配也会明确列出,存在此类文件时退出码为1。
核对平台扣费时,应让模型、调用时间范围、币种和价格口径一致。若不一致,先查是否遗漏失败请求、其他入口或跨时段调用,不要为了让数字好看而把差额塞进一个“修正系数”。
八、上下文越长,为什么输入可能越来越多
多轮聊天不是每次只发最新一句。我们的 main.py 会把system消息、此前保留的问答和最新问题一起发送。第1轮答案在第2轮成为输入,以后还可能被反复发送。
下面只按人为假设的Token数解释增长,不是对本课文字的分词实测。假设固定说明100,每条新问题50,每条答案100:
| 调用轮次 | 输入组成 | 输入Token假设值 |
|---|---|---|
| 第1轮 | 说明 + 问题1 | 150 |
| 第2轮 | 说明 + 问题1 + 答案1 + 问题2 | 300 |
| 第3轮 | 再加答案2与问题3 | 450 |
三次累计输入900,而不是最后一次看到的450。部分内容即使命中缓存,仍占输入上下文,并按命中价格计量;长上下文还会影响可容纳材料和处理负担。
本项目已有两道简单控制:卡片每次只发送两条独立消息,不把上次卡片继续叠进来;聊天最多6轮,单次输入最多2000字符。这些是教学限制,字符数不是精确Token限额。用 cards.py --dry-run 查看messages,确认其中没有无关的历史答案,是现在就能做的成本检查。
长会话需要新主题时,可先用聊天入口的 /history 查看,再用 /reset 清空内存历史。重置不会删除磁盘记录,也不撤销已产生费用。将来若自动摘要旧历史,要同时考虑摘要调用本身的费用,以及遗漏日期、约束或否定条件的风险。
控制成本的顺序可以很朴素:不发送无关材料,复习卡维持短答案,输出预算够用即可,重试保留上限。别单纯把预算压到极低,让答案反复截断再重做。
九、缓存帮助重复输入,但不会替你保存会话
当前DeepSeek上下文缓存默认启用,匹配的是已建立的完整前缀单元。公共前缀可以在请求之间被识别并建立缓存;命中是尽力而为,不能保证每次成功。缓存复用输入计算,输出仍要生成。DeepSeek:上下文缓存
对本项目,固定任务说明和笔记在前、变化的问题在后,是可以尝试的输入组织方式。study_task.py 已先放notes,再放questions;但这不意味着第二次换题时,所有相同文字就必然获得命中折扣。要看实际usage,不能自己按字符串重复率填缓存Token。
相反,若每次把当前时间、随机编号写在最前面,原本稳定的开头也会变化。可以把纯本地追踪ID保留在记录文件里,只把模型确实需要的信息送入消息。本课的UUID用于文件名,没有为了跟踪任务把它塞进提示词。
“缓存存在”也不表示下次可以省略笔记。应用仍要发送需要的上下文,由服务判断复用哪些计算;缓存不是我们以后要做的会话数据库,更不是直接返回上一份卡片的答案缓存。
这里不安排连续发很多次付费请求“刷命中率”。先理解输入顺序,待正常使用积累记录后再观察命中字段。不同样本长度、任务和时间不能随意混在一起,得出缓存节省比例或模型速度结论。
十、两个小练习与排查方向
**练习一:**输入1000,其中命中800;输出200,内含思考50。按空闲时段计费,能否用 (1000×1 + 250×4)/1000000?
不能。输入没有区分缓存,输出又把思考重复加了一次。应使用第二节三项拆分,结果0.001016元。
**练习二:**一次任务有3次attempt,前两次失败没有usage,第三次算出0.001016元。再多给最终金额保留几位小数,能否让总账更准确?
不能。缺的是数据,不是小数位数。已知小计可以保留,未知尝试数应为2,任务总估算仍应为null。
| 症状 | 下一步检查 |
|---|---|
| 没有可估算记录 | 目录是否正确;是否只有模拟记录;是否从未真实调用 |
| 缺少缓存拆分 | 打开原始SDK响应核对字段;不自行假定全命中或全未命中 |
| 数字大了很多 | 单价单位是否把百万误读成千,或忘记除以一百万 |
| 任务总估算为null | 查看逐次reason与unknown_attempts,别仅看最终成功 |
| 模型不匹配 | 核对请求与返回模型,再准备适用价格文件,不强行套价 |
| 总是没有命中 | 检查稳定前缀;也可能尚未建立或已失效,不能据此断言接口坏了 |
| 估算与扣费不同 | 检查时段、价格日期、其他请求及本地遗漏,再核对平台记录 |
现在助手保留了用量、每次尝试和明确价格假设,你能解释一份估算的来源,也能知道哪些地方暂时算不出来。下一篇将让模型提出受控的工具调用;那时一次用户任务可能包含多次模型请求,费用与执行边界都需要继续保留。
上一篇:错误与有限重试 · 本部分大纲 · 代码包 · 运行说明
验证范围:32项离线测试通过,包含第08篇回归与费用算术、未知用量、错误价格、重复计数和只读行为检查。Python3.11.8实际运行;无真实API或账户账单核验,Windows/Linux命令未实机执行。