成本、Token 与延迟分析
正在读取学习记录…
成本分析把一次任务的调用用量、计价参数和费用对应起来;延迟分析把用户等待的总时间与各步骤耗时对应起来。Token 是模型处理文本时的计量单位,输入与输出常按不同单价计费。一个请求调用两次模型,不能只拿最后一次的用量算整单费用。
例如模型调用用了1000个输入 token、200个输出 token,示例单价分别为每百万1美元和3美元。费用为 (1000×1 + 200×3) / 1,000,000 = 0.0016 美元。单价是预置计算参数;更换真实服务时需要传入其实际计价规则。
延迟还有另一个容易绕进去的地方:同时查库存200ms和查政策120ms,用户等的是较慢的200ms。把两项相加会把并发的好处算没了,像把两位同时切菜的厨师当成一个人排队切菜。
ms表示毫秒,1000ms等于1秒,200ms就是0.2秒。后面的时长、均值和分位数都以ms为单位;费用中的USD表示美元,两种单位分别核算。
费用合计与等待合计使用不同规则
固定任务图是计划40ms→草稿模型300ms→库存/政策并行→最终模型180ms。关键路径是决定最早完成时间的依赖链,长度为 40 + 300 + max(200,120) + 180 = 720ms。各操作时长相加为840ms,包含相互重叠的时间。
程序还有10条固定请求总延迟,以及两次模型的用量记录。最后一次模型的输出 token 缺失,用 None 表示。缺失值会保留为未知,不得当成0处理。
保存完整程序并运行
CALLS 预置用量,TASKS 预置耗时和依赖,LATENCIES 预置整单延迟。程序计算这些记录,不实际等待或访问模型。费用使用 Decimal 的十进制数;关键路径通过递归计算依赖完成时刻。
在 PowerShell 创建并进入文件夹:
mkdir l7-cost
cd l7-cost把下面完整代码保存为该文件夹的 cost_lab.py。数据已经写在程序中,直接保存即可运行。
"""从固定调用记录计算用量、已知费用、并发关键路径和分位数。"""
from decimal import Decimal
from math import ceil
# 示例计价参数:每百万输入 1 美元,每百万输出 3 美元。
INPUT_RATE = Decimal("1")
OUTPUT_RATE = Decimal("3")
CALLS = [
{"id": "draft", "input": 1000, "output": 200},
{"id": "final", "input": 600, "output": None},
]
TASKS = {
"plan": {"ms": 40, "deps": []},
"draft": {"ms": 300, "deps": ["plan"]},
"stock": {"ms": 200, "deps": ["draft"]},
"policy": {"ms": 120, "deps": ["draft"]},
"final": {"ms": 180, "deps": ["stock", "policy"]},
}
LATENCIES = [400, 500, 600, 700, 800, 900, 1000, 1200, 1500, 3000]
def usage_cost(calls):
known = Decimal("0")
missing = []
totals = {"input": 0, "output": 0}
for call in calls:
for field in totals:
value = call[field]
if value is not None:
if type(value) is not int or value < 0:
raise ValueError("usage must be a nonnegative integer")
totals[field] += value
# 缺用量的调用不按零收费;完整费用总额仍未知。
if call["input"] is None or call["output"] is None:
missing.append(call["id"])
else:
known += (Decimal(call["input"]) * INPUT_RATE
+ Decimal(call["output"]) * OUTPUT_RATE) / Decimal(1_000_000)
return known, missing, totals
def critical_path(tasks):
memo = {}
visiting = set()
def finish(name):
if name in visiting:
raise ValueError("dependency cycle")
if name in memo:
return memo[name]
task = tasks[name]
if type(task["ms"]) is not int or task["ms"] < 0:
raise ValueError("invalid duration")
visiting.add(name)
# 各依赖可并行执行,开始时间取最晚的依赖完成时间。
choices = [finish(dep) for dep in task["deps"]]
prior_ms, prior_path = max(choices, key=lambda row: row[0], default=(0, []))
visiting.remove(name)
memo[name] = (prior_ms + task["ms"], prior_path + [name])
return memo[name]
return max((finish(name) for name in tasks), key=lambda row: row[0])
def percentile(values, p):
# nearest-rank:先排序,再取第 ceil(p*n) 个,序号从 1 开始。
if not values or not 0 < p <= 1:
raise ValueError("invalid percentile")
ordered = sorted(values)
return ordered[ceil(p * len(ordered)) - 1]
def main():
known, missing, totals = usage_cost(CALLS)
wall_ms, path = critical_path(TASKS)
print(f"known_input={totals['input']} known_output={totals['output']}")
total = f"{known:.6f}" if not missing else "unknown"
print(f"complete_call_cost_usd={known:.6f} missing_usage={','.join(missing)} total={total}")
print(f"sum_task_ms={sum(t['ms'] for t in TASKS.values())} critical_path_ms={wall_ms}")
print(f"critical_path={' -> '.join(path)}")
print(f"latency_n={len(LATENCIES)} mean_ms={sum(LATENCIES) / len(LATENCIES):.1f} "
f"p50_ms={percentile(LATENCIES, .50)} p95_ms={percentile(LATENCIES, .95)}")
if __name__ == "__main__":
main()保存后列出文件,确认能看到 cost_lab.py,再运行:
ls
python -X utf8 cost_lab.py程序输出:
known_input=1600 known_output=200
complete_call_cost_usd=0.001600 missing_usage=final total=unknown
sum_task_ms=840 critical_path_ms=720
critical_path=plan -> draft -> stock -> final
latency_n=10 mean_ms=1060.0 p50_ms=800 p95_ms=3000逐项读懂结果
known_input=1600 是1000+600;known_output=200 只包含草稿调用已知的200。它们是已记录用量的合计,输出总 token 仍不完整。
complete_call_cost_usd=0.001600 只包含草稿调用的完整费用。missing_usage=final 指明缺字段的调用,total=unknown 保留整单费用未知。程序没有把 final已知的600输入 token 与未知输出混成一个“总费用”。
sum_task_ms=840 是五项相加;critical_path_ms=720 是依赖图的最早完成时刻。下一行列出决定它的路径 plan → draft → stock → final。较短的政策分支完成后仍要等库存返回。
最后一行的 latency_n=10 是固定延迟样本量。均值为10600/10=1060ms。p50与p95采用 nearest-rank(最近秩)规则:排序后取第 ceil(p×n) 个值,序号从1开始。p50取第5个得到800ms;p95取第10个得到3000ms。这个p50与偶数样本“两中间值平均”的中位数850ms不同,报告必须说明定义。
其中n是样本数,p是要查看的累计比例,p50用0.50、p95用0.95;ceil表示向上取整。因此0.95×10=9.5向上取第10个,得到3000ms,也就是3秒。这里的p指分位位置,与统计检验的p值含义不同。
改一处,再看对应结果
把 CALLS 中 final 的 "output": None 改为 "output": 100,运行 python -X utf8 cost_lab.py。已知输出变300,完整费用小计变 0.002500,缺失列表变空。总额也变成 total=0.002500,因为代码先检查缺失列表,再决定能否报告完整总额。
恢复原数据后,把 TASKS["policy"] 对应的 "ms": 120 改为250并重跑。关键路径改走政策,时间变770ms;库存仍为200ms,不能因为它曾经较慢就永远选它。恢复120后运行测试。
均值、长尾与账单怎样各自使用
均值适合算总等待负担,分位数描述排序位置。这里10条样本的p95等于最大值,尾部估计很不稳定;报告应保留样本数和分位算法。比较两个版本时,要对齐时间窗口、人群、成功/失败范围和请求单位。不能把成功请求的p95与包含超时的p95直接比较。
关键路径计算假定依赖之外的工作可以同时开始,且没有资源队列。在真实服务中,限流等待、队列、退避和网络开销都要记录进 trace,才能解释整单时间。span 之间的重叠关系可结合 OTel 的 span 时间字段观察。
费用还要按调用汇总重试、缓存计价和额外工具收费。供应商没返回用量时可另存估计值与估计来源,但应与账单用量分开;缺失占比下降才意味着观测更完整。输入 token 为0与字段没出现,是两个不同事实。
保存并运行测试
7 项测试覆盖缺失保留、补齐对账、并发关键路径、较慢分支切换、依赖环拒绝、nearest-rank分位数和非法用量。
把下面代码保存为同一文件夹的 test_cost_lab.py,确认两个文件都在当前目录,运行:
ls
python -X utf8 -m unittest -v test_cost_lab.py完整测试:test_cost_lab.py
"""检查缺失用量、精确计价、并发时间与分位数定义。"""
import sys
import unittest
from decimal import Decimal
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import cost_lab as lab
class CostTests(unittest.TestCase):
def test_missing_usage_preserves_unknown(self):
known, missing, totals = lab.usage_cost(lab.CALLS)
self.assertEqual(known, Decimal("0.0016"))
self.assertEqual(missing, ["final"])
self.assertEqual(totals, {"input": 1600, "output": 200})
def test_complete_final_usage_can_be_reconciled(self):
calls = [dict(c) for c in lab.CALLS]
calls[1]["output"] = 100
known, missing, _ = lab.usage_cost(calls)
self.assertEqual(known, Decimal("0.0025"))
self.assertEqual(missing, [])
def test_parallel_path_is_not_sum(self):
ms, path = lab.critical_path(lab.TASKS)
self.assertEqual(ms, 720)
self.assertEqual(path, ["plan", "draft", "stock", "final"])
def test_longer_policy_changes_critical_path(self):
tasks = {k: dict(v) for k, v in lab.TASKS.items()}
tasks["policy"]["ms"] = 250
self.assertEqual(lab.critical_path(tasks),
(770, ["plan", "draft", "policy", "final"]))
def test_cycle_is_rejected(self):
with self.assertRaises(ValueError):
lab.critical_path({"a": {"ms": 1, "deps": ["a"]}})
def test_nearest_rank_percentiles(self):
self.assertEqual(lab.percentile(lab.LATENCIES, .50), 800)
self.assertEqual(lab.percentile(lab.LATENCIES, .95), 3000)
self.assertEqual(lab.percentile([9], .50), 9)
def test_invalid_inputs(self):
for usage in (-1, True, 1.2):
with self.assertRaises(ValueError):
lab.usage_cost([{"id": "x", "input": usage, "output": 1}])
with self.assertRaises(ValueError):
lab.percentile([], .95)
if __name__ == "__main__":
unittest.main()继续查文档
阅读 Python Decimal 的快速入门,比较从字符串创建十进制数与从浮点数创建的差别。再读 OTel 的 Span 字段,明确总请求与子操作时间的关系。
课后练习展开习题收起习题
如果p95上升而均值基本不变,你会先看哪些请求?如果一次重试成功但首次调用也产生计费,整单费用应如何归集?
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · 并行步骤各耗时2秒和3秒,全部调用时长之和与用户等待时间怎样区分
并行步骤各耗时2秒和3秒,全部调用时长之和与用户等待时间怎样区分?
对应知识点:L7-cost
简答题 · 解释机制
一笔模型调用费用需要哪些输入?
对应知识点:L7-cost
代码题 · 核对给定数据
输入1000Token单价每百万2元,输出500Token单价每百万8元,计算费用。
对应知识点:L7-cost
场景题 · 分析具体变化
某次调用缺少用量字段,应怎样汇总账单?
对应知识点:L7-cost
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。