AgentDepthAgent 工程学习指南
输入概念或问题,检索正文和待编写目录。
第一次学习?从 L0 总览开始 →

成本、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处理。

计划和草稿模型后并行查询库存与政策,最终模型等待两者完成;较慢的库存决定关键路径,最终调用输出用量缺失
图1:并行分支取较晚完成的一条;费用则需要每次调用完整的输入与输出用量。

保存完整程序并运行

CALLS 预置用量,TASKS 预置耗时和依赖,LATENCIES 预置整单延迟。程序计算这些记录,不实际等待或访问模型。费用使用 Decimal 的十进制数;关键路径通过递归计算依赖完成时刻。

在 PowerShell 创建并进入文件夹:

Bash
mkdir l7-cost
cd l7-cost

把下面完整代码保存为该文件夹的 cost_lab.py。数据已经写在程序中,直接保存即可运行。

Python
"""从固定调用记录计算用量、已知费用、并发关键路径和分位数。"""

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,再运行:

Bash
ls
python -X utf8 cost_lab.py

程序输出:

TXT
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,确认两个文件都在当前目录,运行:

Bash
ls
python -X utf8 -m unittest -v test_cost_lab.py
完整测试:test_cost_lab.py
Python
"""检查缺失用量、精确计价、并发时间与分位数定义。"""

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上升而均值基本不变,你会先看哪些请求?如果一次重试成功但首次调用也产生计费,整单费用应如何归集?

4 道练习 · 客观题规则评分 · 其余题目待批改

正在读取本地记录…

不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。

练习 1 / 4 · v1.1.0

选择题 · 并行步骤各耗时2秒和3秒,全部调用时长之和与用户等待时间怎样区分

并行步骤各耗时2秒和3秒,全部调用时长之和与用户等待时间怎样区分?

对应知识点:L7-cost

选择一个答案
练习 2 / 4 · v1.1.0

简答题 · 解释机制

一笔模型调用费用需要哪些输入?

对应知识点:L7-cost

练习 3 / 4 · v1.1.0

代码题 · 核对给定数据

输入1000Token单价每百万2元,输出500Token单价每百万8元,计算费用。

对应知识点:L7-cost

练习 4 / 4 · v1.1.0

场景题 · 分析具体变化

某次调用缺少用量字段,应怎样汇总账单?

对应知识点:L7-cost

图片查看器
100%

文章配图 · 按 Esc 关闭