← 全部极客知识库

大模型通识 / 大模型应用开发入门

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命令未实机执行。