上线大模型应用后,“成本/延迟”几乎都会比你想象的复杂:
不是因为公式难,而是因为你没把数据打出来。
这篇给你一个工程可落地的最小方案:
- 从请求日志(jsonl)统计 P50/P95 input/output tokens、P95 延迟
- 按价格算 单次/日/月成本
- 用 (Concurrency \approx QPS_{peak} \times Latency_{p95}) 估算 峰值并发
- 输出一份能直接贴到评审会的数字
0)前提:你至少要记录这些字段
建议你在服务端(或网关)把下面字段落日志(脱敏后):
{
"request_id": "xxx",
"ok": true,
"latency_ms": 1234,
"input_tokens": 4321,
"output_tokens": 567,
"model": "gpt-4.1-mini",
"prompt_version": "v12",
"retrieval_onoff": true,
"retry_count": 0
}
你不一定一开始就都有,但
latency_ms/input_tokens/output_tokens/ok这四个建议先齐。
1)核心公式(够用)
1.1 单次成本
[ Cost_{call} \approx \frac{T_{in}}{1000}P_{in} + \frac{T_{out}}{1000}P_{out} ]
1.2 峰值并发(容量规划最关键)
[ Concurrency \approx QPS_{peak} \times Latency_{p95}(\text{seconds}) ]
经验:再乘 1.2~1.5 的安全系数覆盖抖动与重试。
2)最小脚手架:llm_budget.py
import json
from dataclasses import dataclass
from typing import Iterable, Optional
def percentile(sorted_vals: list[int], p: float) -> Optional[int]:
if not sorted_vals:
return None
idx = int(p * (len(sorted_vals) - 1))
return sorted_vals[idx]
@dataclass
class Pricing:
# 单价:元/1k token
input_per_1k: float
output_per_1k: float
@dataclass
class BudgetInputs:
daily_calls: int
qps_peak: float
safety_factor: float = 1.3
@dataclass
class Stats:
p50_in: int
p95_in: int
p50_out: int
p95_out: int
p50_latency_ms: int
p95_latency_ms: int
def load_jsonl(path: str) -> Iterable[dict]:
with open(path, "r", encoding="utf-8") as f:
for line in f:
if line.strip():
yield json.loads(line)
def compute_stats(rows: list[dict]) -> Stats:
ins = sorted(int(r["input_tokens"]) for r in rows)
outs = sorted(int(r["output_tokens"]) for r in rows)
lats = sorted(int(r["latency_ms"]) for r in rows)
return Stats(
p50_in=percentile(ins, 0.50),
p95_in=percentile(ins, 0.95),
p50_out=percentile(outs, 0.50),
p95_out=percentile(outs, 0.95),
p50_latency_ms=percentile(lats, 0.50),
p95_latency_ms=percentile(lats, 0.95),
)
def estimate_cost_per_call(tin: int, tout: int, pricing: Pricing) -> float:
return (tin / 1000.0) * pricing.input_per_1k + (tout / 1000.0) * pricing.output_per_1k
def main(
path: str,
pricing: Pricing,
budget: BudgetInputs,
):
ok_rows = [r for r in load_jsonl(path) if r.get("ok", True)]
s = compute_stats(ok_rows)
# 用 P95 估 SLA 预算(更接近线上上限)
cost_per_call_p95 = estimate_cost_per_call(s.p95_in, s.p95_out, pricing)
cost_day = cost_per_call_p95 * budget.daily_calls
cost_month = cost_day * 30
concurrency = budget.qps_peak * (s.p95_latency_ms / 1000.0) * budget.safety_factor
print("=== stats ===")
print(s)
print("=== budget(P95) ===")
print(
{
"cost_per_call_p95": round(cost_per_call_p95, 6),
"cost_day": round(cost_day, 2),
"cost_month": round(cost_month, 2),
"concurrency_estimate": round(concurrency, 2),
}
)
if __name__ == "__main__":
main(
path="requests.jsonl",
pricing=Pricing(input_per_1k=1.0, output_per_1k=3.0), # 占位:按你的模型价格填
budget=BudgetInputs(daily_calls=1000, qps_peak=10, safety_factor=1.3),
)
3)怎么用(两步)
- 准备
requests.jsonl(每行一条请求记录) - 填价格与业务量:
input_per_1k/output_per_1k/daily_calls/qps_peak
脚本输出会给你:
- token/延迟的 P50/P95
- 按 P95 估算的单次/日/月预算
- 峰值并发估算(含安全系数)
4)最常见坑(上线后超预算的原因)
- 上下文变长:history 越叠越长、TopK 越调越大、工具返回塞了全量日志
- 重试放大:排队变长→超时变多→重试变多→排队更长
- 只看平均值:平均延迟看着还行,P95 已经炸了
5)资源区:做多模型算账/对比时,先把接入层统一
你一定会对比不同模型/不同策略(比如不同上下文预算、RAG 开关)。
如果每次对比都要换一套 SDK/鉴权,成本会很高。
更省事的方式是统一成 OpenAI 兼容入口(多数时候只改 base_url 和 api_key)。
举个例子:我会用 147ai 这类聚合入口做对比(具体模型与参数以其控制台/文档为准):
- API Base URL:
https://147ai.com - 端点:
POST /v1/chat/completions - 鉴权:
Authorization: Bearer <KEY> - 文档:
https://147api.apifox.cn/