1. 推理成本为什么总比账单上看到的贵单位 Token 成本这件事很多人第一反应是「卡时 × 单价 ÷ 产出 token 数」。这个算法不算错但它把太多变量压进了一个平均值里。你拿这个数字去做优化决策方向经常是反的——以为瓶颈在算力结果加卡之后成本没降以为批处理开大就能摊薄结果尾延迟炸了用户开始重试无效 token 反而更多。我试过在一个 70B 量级的自建推理服务上做成本拆解最后发现真正决定单位 Token 成本的是五个因子的乘积关系硬件摊销、能耗、显存占用、批处理效率、尾延迟惩罚。其中显存占用不直接出现在公式里但它通过「能同时跑多少请求」间接主导了批处理效率而批处理效率又直接决定分母端的产出规模。尾延迟则是最容易被漏掉的一项——它不出现在任何账单上表现为「利用率只有 30%」而不是「我们为延迟花了多少钱」。这篇面向自建推理 API 的开发者交付三样东西一张可复制的成本拆解表、一套能直接跑的压测配置、以及验证「批处理 vs 显存」权衡的具体动作。核心检索词是推理 Token 成本、显存占用、批处理效率、尾延迟适合已经在跑 vLLM / TGI / SGLang 这类推理引擎、想把单位成本压下来的同学。如果你还在选型阶段也可以先看拆解逻辑再决定要不要自建。先说结论显存占用往往比算力更决定成本。推理服务的吞吐上限通常不是算力耗尽而是显存耗尽——权重占一部分KV Cache 占剩下的绝大部分剩下的空间才决定能同时处理多少个请求。KV Cache 的占用随并发请求数与上下文长度线性增长一个长上下文请求的 KV Cache 就可能吃掉数 GB此时算力可能还有大量空闲。所以优化顺序应该是先解决系统级的浪费调度、复用、分池再解决局部的效率算子。2. TaoToken 前置把成本模型落到可调用的 API 上要把上面这套成本模型跑通你需要一个能稳定调用、能观测 token 消耗、能切换模型档位的推理入口。TaoToken 在这里的角色是「统一接入层」它把不同模型、不同档位的推理服务收敛到一套 OpenAI 兼容的 API 上你可以在同一套压测脚本里切换模型 ID对比单位 Token 成本而不需要为每个模型改一遍客户端代码。官网入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 这个地址不加 UTM 参数直接用于配置。你需要先拿到一个 API Key再去控制台确认可用模型列表。拿 Key 的路径是登录后进控制台在 API Keys 页面创建一个新 Key。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite API Keys 页面是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。创建时建议按用途命名比如cost-bench方便后面在压测里区分调用来源。模型对话的调试入口在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 你可以先在网页上发几条请求确认模型 ID 和返回格式再写进压测脚本。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有完整的请求参数说明。如果你后面要做长期编码或 Agent 类任务可以看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。Claude Code 相关的接入说明在 https://taotoken.net/claudecode-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecode-anthropicutm_campaignrewrite 。这里要强调一点TaoToken 是合规的 API 接入服务不是任何形式的灰色中转。你用它做压测和成本核算得到的是真实的 token 消耗数据可以直接对应到账单。这一点对成本建模很关键——如果计量口径不透明后面所有拆解都是空中楼阁。前置准备清单项目值说明Base URLhttps://taotoken.net/api不加 UTMAPI Key控制台创建建议按用途命名Model ID从模型列表确认压测时用于切换档位计量口径输入/输出 token 分别计成本拆解的基础拿到这三件套Base URL Key Model ID之后就可以进入配置环节了。3. 可复制配置压测脚本与成本拆解表这一节给你两份可直接复制的东西一份是压测用的 Python 脚本一份是成本拆解表。脚本用 OpenAI 兼容接口改一下 Base URL 和 Key 就能跑。先看压测脚本。它的设计目标是固定并发数逐步拉长上下文长度记录每个请求的输入 token、输出 token、首 token 延迟TTFT和总延迟最后算出单位 Token 成本。import time import json import statistics from concurrent.futures import ThreadPoolExecutor, as_completed from openai import OpenAI client OpenAI( base_urlhttps://taotoken.net/api, api_keysk-your-key-here, ) MODEL_ID your-model-id CONCURRENCY 8 CONTEXT_LENS [512, 2048, 8192, 16384] def build_prompt(target_tokens): # 粗略按 1 token ≈ 4 字符构造实际以返回 usage 为准 base 请对以下内容做摘要并给出三个关键点。 filler 推理成本由显存占用、批处理效率和尾延迟共同决定。 * (target_tokens // 20) return base filler def one_request(ctx_len): prompt build_prompt(ctx_len) start time.time() first_token_time None chunks [] stream client.chat.completions.create( modelMODEL_ID, messages[{role: user, content: prompt}], max_tokens256, streamTrue, ) for chunk in stream: if first_token_time is None: first_token_time time.time() delta chunk.choices[0].delta.content if delta: chunks.append(delta) end time.time() return { ctx_target: ctx_len, ttft: (first_token_time - start) if first_token_time else None, total_latency: end - start, output_chars: len(.join(chunks)), } def run_batch(ctx_len): results [] with ThreadPoolExecutor(max_workersCONCURRENCY) as pool: futures [pool.submit(one_request, ctx_len) for _ in range(CONCURRENCY)] for f in as_completed(futures): results.append(f.result()) return results if __name__ __main__: summary [] for ctx in CONTEXT_LENS: rs run_batch(ctx) ttfts [r[ttft] for r in rs if r[ttft]] lats [r[total_latency] for r in rs] summary.append({ ctx_target: ctx, ttft_p50: round(statistics.median(ttfts), 3), ttft_p99: round(sorted(ttfts)[int(len(ttfts) * 0.99) - 1], 3), lat_p50: round(statistics.median(lats), 3), lat_p99: round(sorted(lats)[int(len(lats) * 0.99) - 1], 3), }) print(json.dumps(summary, ensure_asciiFalse, indent2))跑之前把api_key和MODEL_ID换成你自己的。这个脚本的关键点是它同时记录 TTFT 和总延迟并且按上下文长度分档。TTFT 反映 prefill 阶段的压力总延迟反映 decode 阶段的压力两者分开看才能判断瓶颈在显存还是算力。再看成本拆解表。这张表是你做核算的模板建议直接抄进 Excel 或 Notion因子计算方式典型影响幅度可控性优化优先级硬件摊销卡时单价 ÷ 总产出 token采购期决定中中能耗功耗 × 时长 × 电价 ÷ 产出占总成本 20%–30%低低显存占用通过批大小放大数倍数倍差异高最高批处理效率直接影响吞吐直接高高尾延迟惩罚为压 P99 牺牲的利用率隐性但显著中高单位 Token 成本的近似公式可以写成单位 token 成本 ≈ (硬件摊销 能耗) / 总产出 token × 1 / 批处理效率 尾延迟惩罚显存占用没有直接出现在公式里但它通过「能同时跑多少请求」决定了批处理效率的上限。可并发请求数的估算方式是可并发请求数 ≈ (总显存 − 权重 − 系统预留) / 单请求 KV Cache单请求 KV Cache 随上下文长度线性增长。这就是为什么长上下文场景下并发数会被压到个位数而算力还有空闲。如果你用的是 vLLM可以在启动参数里直接控制显存利用率和批大小上限python -m vllm.entrypoints.openai.api_server \ --model your-model-path \ --gpu-memory-utilization 0.90 \ --max-num-seqs 64 \ --max-model-len 16384 \ --enable-prefix-caching \ --port 8000--gpu-memory-utilization控制 KV Cache 能占多少显存--max-num-seqs控制最大并发序列数--enable-prefix-caching开启前缀复用。这三个参数是显存与批处理权衡的核心旋钮。跑压测时固定其中两个只调一个才能看出单个因子的影响。如果你用 Claude Code 做开发侧的调用配置三件套是{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-your-key-here, ANTHROPIC_MODEL: your-model-id } }这段配置写进 Claude Code 的 settings 文件即可。Base URL、Key、Model ID 三件套缺一不可少任何一个都会在启动时报错。4. 验证请求怎么确认批处理与显存的权衡点配置跑通之后下一步是验证。验证的目标不是「服务能返回」而是「在什么批大小下单位 Token 成本最低同时尾延迟还能接受」。这是一个多目标问题需要分步做。第一步固定上下文长度只调并发数。用上面的压测脚本把CONTEXT_LENS改成单一值比如 4096然后把CONCURRENCY从 1 逐步拉到 32。记录每一档的 TTFT P50、TTFT P99、总延迟 P50、总延迟 P99以及吞吐每秒完成的请求数。你会看到一条典型的曲线并发数从 1 到 8吞吐快速上升TTFT 上升缓慢从 8 到 16吞吐上升变缓TTFT 开始明显抬头超过 16 之后吞吐几乎不再增长但 P99 延迟快速恶化。这个拐点就是「批处理效率饱和点」。第二步固定并发数只调上下文长度。把CONCURRENCY固定在饱和点附近把CONTEXT_LENS从 512 拉到 16384。你会看到 TTFT 随上下文长度近似线性上升因为 prefill 的计算量与上下文长度成正比。同时由于 KV Cache 占用增加实际能并发的请求数会下降表现为总延迟的 P99 明显恶化。第三步计算单位 Token 成本。用压测得到的吞吐和延迟数据代入成本拆解表。这里的关键是不要只看平均延迟要看 P99。因为 P99 决定了你需要预留多少冗余容量而冗余容量直接推高硬件摊销。一个实测的观察在 4096 上下文、8 并发的配置下吞吐大约是 1 并发的 6 倍而 P99 延迟只上升了 40%。这个区间是「性价比甜点」。当并发拉到 16吞吐只比 8 并发高 20%但 P99 延迟翻了一倍。多出来的那部分延迟意味着你需要为突发流量预留更多容量单位成本反而上升。验证请求是否成功的具体动作curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer sk-your-key-here \ -H Content-Type: application/json \ -d { model: your-model-id, messages: [{role: user, content: 用一句话解释尾延迟对推理成本的影响}], max_tokens: 128 }返回里会带usage字段包含prompt_tokens和completion_tokens。这两个数字是你核算成本的原始输入。把每次压测的 usage 累加除以总耗时就是实际的 token 吞吐率。如果你在压测中看到 TTFT 异常高先检查是不是--max-model-len设得太大导致 KV Cache 预留过多、实际可用并发被压缩。如果是把--max-model-len调到略大于你的实际最大上下文即可不要盲目设成模型的理论上限。批处理与显存的权衡本质是一个「用延迟换吞吐」的交易。你要找的是那个交易比率最优的点而不是吞吐最大或延迟最小的点。这个点会随模型、硬件、负载分布变化所以需要定期重测不能一次定死。5. 常见报错排查401、local proxy failed、reading choices、OAuth压测和接入过程中有几类报错出现频率最高。这一节按报错原文对照排查每条都给出可执行的动作。401 Unauthorized。这是最常见的一类。报错原文通常是{error: {message: Invalid API key, type: invalid_request_error}}。排查顺序先确认 Key 有没有复制完整前后有没有多余空格再确认请求头是Authorization: Bearer sk-xxx不是x-api-key最后确认 Key 没有过期或被删除。如果你在环境变量里存 Key注意有些 shell 会把换行符带进去用echo -n $API_KEY | wc -c检查长度是否符合预期。local proxy failed / connection refused。这类报错通常出现在你本地配了代理但代理没有正确转发到https://taotoken.net/api。排查动作先确认 Base URL 写的是https://taotoken.net/api不是https://taotoken.net/api/v1路径重复会导致 404不是这个报错但容易混再确认本地没有残留的HTTP_PROXY/HTTPS_PROXY环境变量指向一个已经关掉的端口。用curl -v https://taotoken.net/api/v1/models -H Authorization: Bearer sk-xxx直接测如果 curl 通而你的脚本不通问题在脚本的客户端配置。reading choices 相关报错。典型原文是KeyError: choices或TypeError: NoneType object is not subscriptable出现在解析返回时。原因通常是请求本身失败了返回体是错误结构但你的代码直接去取response.choices[0]。修复动作在解析前先判断response里有没有error字段或者用response.get(choices)做空值保护。流式请求下还要注意最后一个 chunk 的choices可能是空数组直接索引会报错。OAuth / 认证相关报错。如果你用 Claude Code 或类似工具接入报错可能是OAuth token expired或authentication failed。这类工具通常有两套认证一套是工具自身的登录态一套是 API Key。接入 TaoToken 时要确保工具走的是 API Key 路径而不是它自己的 OAuth 流程。配置里显式写ANTHROPIC_BASE_URL、ANTHROPIC_API_KEY、ANTHROPIC_MODEL三件套能覆盖大部分认证问题。如果工具同时支持两种认证优先用 API Key因为它的计量口径更清晰便于成本核算。显存相关报错。如果你在自建推理侧看到CUDA out of memory排查顺序先降--gpu-memory-utilization从 0.90 降到 0.85再降--max-num-seqs最后降--max-model-len。这三个参数对显存的影响是叠加的一次只调一个才能定位是哪个参数超了。如果降完还 OOM检查是不是有残留进程占着显存用nvidia-smi确认。尾延迟异常排查。如果 P99 延迟远高于 P50先看是不是批内请求的输出长度差异太大。短请求被长请求拖住是尾延迟的主要来源。解决方向是连续批处理请求级调度而不是简单调大批大小。如果你用的推理引擎支持--enable-chunked-prefill打开它能把长 prefill 拆成多个 chunk减少对 decode 请求的阻塞。排查的一个通用原则先确认请求本身是否成功看 HTTP 状态码和返回体结构再确认计量数据是否合理看 usage 字段最后才看性能指标。顺序反了容易在错误的数据上做优化。6. 把成本模型变成日常动作成本优化不是一次性项目。模型会换、硬件会换、负载分布会变上个月的最优批大小这个月可能就不是了。所以比某一次优化成果更重要的是建立一套度量与迭代机制。具体动作可以拆成三步。第一步每周跑一次固定压测用同一套脚本、同一组上下文长度记录吞吐和 P99 延迟。第二步把压测数据代入成本拆解表算出单位 Token 成本和上周对比。第三步如果成本上升超过 10%就按「显存 → 批处理 → 尾延迟」的顺序排查而不是直接加卡。请求分级是另一个值得尽早做的动作。把长请求和短请求分到不同的实例组避免长请求占满显存导致短请求排队。混部时少量长请求就能把整机的并发能力拖垮。分池之后短请求组的批处理效率会明显提升长请求组则可以专门优化 KV Cache 复用。KV Cache 复用也值得单独提。多轮对话和 Agent 调用里有大量重复前缀开启前缀缓存prefix caching能直接省掉这部分显存和对应的 prefill 计算。在 vLLM 里就是--enable-prefix-caching在 SGLang 里是 RadixAttention。这类优化的效果在长上下文、多轮场景下最显著。最后说一个核算上的坑不要把研发调用的算力算进生产。开发、评测、实验消耗的算力常常和生产混在一起导致单位成本虚高进而误导定价决策。建议在 API Key 层面就做隔离生产用一个 Key研发用另一个账单分开看。如果你想把成本模型和实际调用对齐可以从模型对话入口先发几条请求确认计量口径https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。确认之后再去 API Keys 页面创建压测专用的 Keyhttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。完整的参数说明和接入细节在文档里https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。长期做编码或 Agent 类任务的话Coding Plan 的计量方式更适合做成本基线https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。Claude Code 的接入配置参考https://taotoken.net/claudecode-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecode-anthropicutm_campaignrewrite 。成本优化的终点不是某个数字而是一套能持续回答「钱花在哪了」的机制。有了这套机制每次模型升级、硬件换代、负载变化你都能快速判断该调哪个旋钮。