# 估算模型 这份文档说明读数条上的数字是怎么来的,以及它在哪里会不准。 ## 一、先看清楚成本由什么构成 拿一个真实会话的累计数据做例子(DeepSeek-V4.1-Flash,空闲时段): | 桶 | token 数 | 单价(元/百万) | 金额 | 占比 | |---|---|---|---|---| | 缓存读 | 182,673,664 | 0.02 | ¥3.65 | 37.5% | | 未缓存输入 | 4,603,456 | 1 | ¥4.60 | 47.3% | | 输出 | 368,734 | 4 | ¥1.47 | 15.1% | | 合计 | 187,645,854 | | ¥9.73 | | 三个桶的价格差得很远:输出单价是缓存命中价的 200 倍,未命中价是命中价的 50 倍。 所以有两件事不能凭直觉判断: **一、token 数量占比不等于成本占比。** 输出只占全部 token 的 0.2%,却占 15% 的钱。 反过来,缓存读占了 97% 的 token,只占 37% 的钱。看 token 数会得出错误的优化方向。 **二、单步成本里,输出是一项主要开销。** 看会话中某一步的实际分桶: | 桶 | token | 金额 | 占比 | |---|---|---|---| | 缓存读 | 564,480 | ¥0.0113 | 64% | | 输出 | 1,546 | ¥0.0062 | 35% | | 未缓存 | 144 | ¥0.0001 | 1% | 一步只产出一千多个 token,就要花掉这一步三分之一的开销。而输出长度是最难预知的量, 这就是读数条给区间而不是单点值的直接原因。 ## 二、为什么成本会随会话变贵 每一次请求都要把**整个上下文**重新发一遍。上下文随着会话推进只增不减, 于是单步的输入量持续上升。 一天 600 多步的会话里,累计缓存读 1.83 亿 token,平均每步 30 万; 而会话末尾单步的上下文是 56 万。平均值约为末尾值的一半, 符合"上下文近似线性增长、累计量按平方增长"的形状。 这解释了一个现象:**会话开头的每一步都很便宜,越到后面越贵,而总账涨得比步数快得多。** 读数条把"下一步"单列出来,就是为了让这个边际成本可见。 ## 三、估算公式 ``` 下一步输入 = projectedTokens (按本会话实测命中率拆成 命中 / 未命中 两桶) 下一步输出 = outputTokens ÷ steps,取 [0.5×, 1.5×] 作为区间 单步成本 = 命中×命中价 + 未命中×未命中价 + 输出×输出价 一轮成本 = 单步成本 × 每轮步数区间 ``` 各量的来源: | 量 | 来源 | 说明 | |---|---|---| | 命中率 | `cacheRead ÷ (cacheRead + uncached + cacheWrite)` | 本会话实测,随会话收敛 | | 下一步输入 | `contextPressure.projectedTokens` | 宿主定义它就是为了回答"下一次请求要发多少" | | 每步平均输出 | `tokenUsage.outputTokens ÷ sessionStats.steps` | 本会话实测均值 | | 每轮步数 | `sessionStats.steps ÷ turns` | 取 [0.6×, 1.5×] 作为区间 | 三项里两项来自本会话的实测统计,只有输出长度是靠带宽覆盖的。 ## 四、字段口径 数据全部来自客户端的三个投影,字段名以宿主实现为准: **`tokenUsage`**(`dsh-token-meter`) 线上视图就是状态里的 `totals`,即**会话累计**的四个扁平桶: ```js { cacheReadTokens, cacheWriteTokens, uncachedInputTokens, outputTokens } ``` **`contextPressure`**(`dsh-token-meter`) 线上视图只有三个字段,用于计算上下文占用: ```js { contextWindow?, pressureTokens?, projectedTokens? } ``` 占用率按 `(projectedTokens ?? pressureTokens) ÷ contextWindow` 计算, 与宿主自带的上下文圆环同一套口径,两个读数不会互相矛盾。 **`sessionStats`**(`dsh-session-stats`) 取 `turns` 与 `steps` 用于估算每轮步数。 ## 五、价格 内置价目(元 / 百万 tokens),来自 DeepSeek 官方文档,查证日期 2026-10-12: | 模型 | 缓存命中 空闲 | 缓存命中 高峰 | 未命中 空闲 | 未命中 高峰 | 输出 空闲 | 输出 高峰 | |---|---|---|---|---|---|---| | `deepseek-flash` | 0.02 | 0.04 | 1 | 2 | 4 | 8 | | `deepseek-v4-pro` | 0.15 | 0.30 | 4.5 | 9.0 | 13.5 | 27.0 | 高峰时段为北京时间周一至周五(不含法定节假日)9:00–12:00 与 14:00–18:00, 其余时段为空闲,空闲价是高峰价的一半。 价格会调整,内置值只是一份快照。来源: https://api-docs.deepseek.com/zh-cn/quick_start/pricing/ ## 六、已知偏差 **会低估的情况** - 会话刚开始、命中率还没收敛时,用 0 命中率估算反而偏高;命中率一旦稳定就准了 - 如果某一步的输出远长于历史均值(比如一次性生成大文件),实际值会冲出区间上界 **会高估的情况** - 上一轮的输出成为下一轮上下文的一部分,如果这部分未命中缓存,实际比估算更贵; 反之若整段命中,则比估算便宜 - 峰谷切换的瞬间:跨过时段边界的那一轮,实际按两档分别计费,读数条只按当前档显示 **算不到的** - 法定节假日没有内置。每年的假期安排会调整,写死会过期,所以只按周末与时段判断 - 模型名写死为 `deepseek-flash`。客户端投影里没有当前模型名,换模型需要同步改 `src/component.js` 里的 `MODEL` 常量 ## 七、为什么不做更复杂的模型 可以收集每一步的历史、拟合上下文增长曲线、做更精细的回归。没有这么做,原因是: 单步成本的方差主要来自输出长度,而输出长度本质上不可预知——模型要写多长, 在它写出来之前没有可靠信号。把输入侧算得很精细,对总区间的收窄贡献有限, 却会让模型难以解释、也难以验证。 现在的做法只依赖三项可观测的实测统计,每一项都能在读数条的展开面板里对上账。