
最近在优化线上 LLM 应用时我发现团队在做成本核算时存在一个普遍的盲区大家都习惯用“每次调用多少钱”来衡量模型和路由策略的优劣却很少去追问“一个业务请求真正成功最终花了多少钱”。这个差异看似只是统计口径不同但实际上会直接改变路由策略、模型选型和成本预算。本文想围绕这个指标展开聊聊为什么 cost-per-call 会误导决策以及如何用更真实的口径——cost-per-success——来评估和持续优化 LLM 路由。这篇文章适合正在做 LLM 应用落地的开发者、算法工程师和技术负责人阅读。如果你已经接入了多个模型或者正在比较不同路由方案的性价比这篇文章能帮你建立一套更完整的成本度量体系。读完你会理解 cost-per-success 的定义和计算方式知道如何埋点统计这个指标并能用一套简单的工程框架持续优化路由成本。1. 为什么你计算的 LLM 成本往往是错的1.1 LLM 路由是什么LLM 路由LLM Routing是一个很容易被忽视却在很多生产系统中真实存在的组件。它的核心职责是在收到一个请求时决定把这个请求交给哪个大语言模型来处理。在早期团队通常只接入一个模型例如 GPT-4、Claude 或国内某家大模型 API那时候不存在路由问题。但现在的应用环境已经变了一个 LLM 应用可能同时使用两个、三个甚至五六个模型有的模型擅长推理有的模型响应速度快有的模型长文本理解能力好还有的模型在特定垂直领域效果更佳。如果让所有请求都走同一个模型要么成本不可控要么延迟不可控要么效果不理想。LLM 路由就这样成了应用性能与成本之间的调节阀。常见的路由方式有很多种按规则路由、按分类器路由、按历史反馈动态路由甚至有些团队会用强化学习来学习路由策略。路由能解决的问题也很清晰降低成本、降低延迟、提升生成质量、保障服务可用性。而在这些目标之间成本往往是最先被量化、也是最容易被错误量化的一个。1.2 cost-per-call 的经典误区“cost-per-call”也就是单次调用的成本是大多数人评估 LLM 成本时的第一反应。因为 API 供应商通常按 token 计费而一次调用消耗的 token 数量是明确的乘以单价就能得到一个数字。这个数字很直观模型 A 单次调用 0.003 美元模型 B 单次调用 0.0002 美元那当然是模型 B 便宜。但从业务视角看这种比较存在几个致命问题。第一调用成功不等于业务成功。模型可以正常返回一段文本但内容质量差、格式不符合要求、输出被业务规则拦截最终还是不能被使用。也就是说一次“成功”的调用对业务来说可能是“失败”的。第二失败会引发重试而重试会重复产生成本。如果我们选择了一个便宜但稳定性较差的模型任务成功率只有 70%那么每 10 个请求就有 3 个需要再次发起调用。重试后仍然失败的请求还需要人工兜底或升级到更贵的模型。这一系列额外成本在单单看“每次调用多少钱”时是看不到的。第三不同任务对模型能力的敏感度不一样。一个“帮我翻译一句话”的任务小模型可能已经做得很好但一个“分析财务报表并给出投资建议”的任务小模型的失败概率很高。把失败后的补偿成本摊进总成本单次调用成本再低也可能是亏的。这就是 cost-per-call 这个指标的局限性它只回答了“调用一次模型需要多少钱”没有回答“完成一笔业务需要多少钱”。而后者才是业务方真正关心的。1.3 为什么现在该看 cost-per-success随着模型种类变多、路由策略变复杂团队完全有条件用更准确的指标来评估成本。这个指标就是 cost-per-success简单说就是“让一个业务请求最终成功处理平均需要花多少钱”。之所以强调“现在”是因为过去很多团队只有单一模型没有路由环节也没有足够的可观测性数据。要统计 cost-per-success需要知道每次请求路由到了哪个模型、消耗了多少 token、是否成功、是否重试、是否人工兜底。这些信息需要系统的埋点和日志配合而不是拍脑袋能算出来的。当路由策略开始引入多个模型之后成本优化的空间变大了同时成本误判的风险也变大了。如果只看单次调用成本团队很可能会选择那条“表面最便宜”的路线结果反而把总成本推高。所以建立以 cost-per-success 为核心的度量体系是 LLM 应用走向成熟必须完成的一步。2. 核心指标拆解cost-per-call vs cost-per-success2.1 指标定义先给出三个基础定义。cost-per-call指的是模型总调用成本除以模型调用次数。它反映的是“平均每次模型调用花多少钱”。success rate指的是业务成功请求数除以业务请求总数。这里的“业务成功”需要团队先定义清楚例如输出通过格式校验、分类结果抽检准确、用户没有发起二次修改等。cost-per-success指的是为完成所有业务成功请求所付出的总成本除以业务成功请求数。总成本不仅包括模型调用成本还应该包含重试成本、人工兜底成本、后处理修复成本等。可以简单写成公式cost_per_call 模型总成本 / 模型调用总次数 success_rate 业务成功请求数 / 业务请求总数 cost_per_success (模型调用成本 重试成本 兜底成本 修复成本) / 业务成功请求数需要注意的是cost-per-success 不是一个只能计算模型成本的指标它是一个端到端的业务指标。分子里的每一项都是在真实生产链路中可能发生的额外支出。分母里的“业务成功”则是团队和业务方共同认可的成功口径。2.2 一个可复现的成本模拟我们用一组模拟数据来对比几个典型方案。假设业务场景是电商客服工单分类需要对 10000 个用户请求做文本分类。任务失败后最终失败的请求会转由人工兜底处理每条人工兜底成本按 0.05 美元估算。模型方面提供一个能力较强的大模型 XL 和一个能力较弱但便宜很多的小模型 XS具体参数如下模型单次调用成本首次成功率大模型 XL0.003 美元98%小模型 XS0.0002 美元72%现在比较四种方案方案 A全部请求调用 XL。方案 B全部请求调用 XS失败后再用小模型重试一次。方案 C规则路由简单任务走 XS复杂任务走 XL。方案 D智能路由用分类器把简单任务更准确地分流到 XS复杂任务走 XL。各方案的模型调用次数、成本、人工兜底成本如下方案首轮模型成本$重试成本$人工兜底成本$总成本$模型调用次数cost-per-call$cost-per-success$A全部 XL3001040100000.0040.004B全部 XS20.567072.56128000.005670.00726C规则路由13.23.31127.5111000.002480.00275D智能路由13.22.4621.6108000.0020.00216在这组模拟数据中方案 B 的首轮模型成本只有 2 美元表面上看是最省钱的。但到了 end-to-end 总成本它反而是最贵的因为 72% 的首次成功率意味着大量失败请求需要重试重试后仍有约 1400 个请求最终失败需要人工兜底这个成本被完全放大。这组数据可以通过一段 Python 脚本复现。# cost_per_success_demo.py # 一个简单而完整的 cost-per-call / cost-per-success 对比脚本 def evaluate( business_requests: int, model_calls: int, total_model_cost: float, llm_success_count: int, fallback_cost_per_request: float 0.05, ) - dict: 根据模拟参数估算成本指标。 business_requests: 业务请求总数 model_calls: 模型调用总次数包含重试 total_model_cost: 所有模型调用带来的总成本 llm_success_count: 不经过人工兜底、模型直接处理成功的请求数 fallback_cost_per_request: 每条最终失败请求的人工兜底成本 fallback_count business_requests - llm_success_count fallback_cost fallback_count * fallback_cost_per_request total_cost total_model_cost fallback_cost success_count llm_success_count fallback_count # 兜底后业务也视为成功 return { 总成本: total_cost, 模型调用次数: model_calls, cost_per_call: total_cost / model_calls, cost_per_success: total_cost / success_count, LLM直接成功率: llm_success_count / business_requests, } # 方案 A全部调用大模型 XL plan_a evaluate( business_requests10000, model_calls10000, total_model_cost10000 * 0.003, llm_success_countint(10000 * 0.98), ) # 方案 B全部调用小模型 XS失败后重试一次小模型 first_calls 10000 first_success int(first_calls * 0.72) first_fail first_calls - first_success retry_calls first_fail retry_success int(retry_calls * 0.50) llm_success_b first_success retry_success model_calls_b first_calls retry_calls model_cost_b model_calls_b * 0.0002 plan_b evaluate( business_requests10000, model_callsmodel_calls_b, total_model_costmodel_cost_b, llm_success_countllm_success_b, ) # 方案 C规则路由6000 个简单请求走 XS4000 个复杂请求走 XL simple_success_c int(6000 * 0.85) complex_success_c int(4000 * 0.95) failed_c 6000 - simple_success_c 4000 - complex_success_c retry_success_c int(failed_c * 0.80) model_calls_c 6000 4000 failed_c model_cost_c 6000 * 0.0002 (4000 failed_c) * 0.003 llm_success_c simple_success_c complex_success_c retry_success_c plan_c evaluate( business_requests10000, model_callsmodel_calls_c, total_model_costmodel_cost_c, llm_success_countllm_success_c, ) # 方案 D智能路由简单请求走 XS 的成功率提升到 90% simple_success_d int(6000 * 0.90) complex_success_d int(4000 * 0.95) failed_d 6000 - simple_success_d 4000 - complex_success_d retry_success_d int(failed_d * 0.85) model_calls_d 6000 4000 failed_d model_cost_d 6000 * 0.0002 (4000 failed_d) * 0.003 llm_success_d simple_success_d complex_success_d retry_success_d plan_d evaluate( business_requests10000, model_callsmodel_calls_d, total_model_costmodel_cost_d, llm_success_countllm_success_d, ) for plan in (plan_a, plan_b, plan_c, plan_d): print(plan)这段脚本用于展示对比思路实际数值可能因模型定价、任务难易度、兜底成本不同而有较大差异。你可以根据自己的线上数据替换参数。2.3 从数据中得到的结论从上面的模拟数据可以看到几个重要结论。第一只看单次调用成本会严重失真。方案 B 的单次调用成本在所有方案中最低但最终 cost-per-success 最高几乎是方案 D 的三倍多。第二模型调用的稳定性非常关键。便宜模型的首次成功率只有 72%意味着近三成请求要进入补偿流程。补偿流程不仅消耗模型成本还消耗人工成本而人工兜底往往是所有成本里最贵的。第三路由的价值不是“选便宜模型”而是“把合适的任务送给合适的模型”。方案 C 和 D 同样把 6000 个简单请求路由给 XS但因为分流准确率不同最终 cost-per-success 差异明显。智能路由看似复杂但它减少的是失败率和兜底成本。第四cost-per-call 在总成本评估中参考价值有限。真要对比路由策略一定要把重试、兜底、修复、等待延迟这些隐性成本一起算进去。3. 度量体系把 cost-per-success 落地到工程链路3.1 你需要采集哪些数据要在真实系统中统计 cost-per-success第一步是确定数据采集点。建议每个请求保留一份完整的事件记录字段包括字段说明request_id请求唯一 ID用于关联后续重试和兜底记录task_type任务类型例如分类、总结、翻译、抽取route_name路由策略名称或版本号model实际调用的模型名称prompt_tokens / completion_tokens输入输出 token 数量用于核算成本cost本次模型调用的费用first_try_success首次调用是否直接成功retry_count重试次数fallback是否最终走了人工兜底final_status最终业务是否成功duration_ms耗时便于分析延迟对成本的影响这些字段不需要一次采集完整可以先从“请求 ID、模型、token、是否成功、是否重试”开始再逐步补充业务侧的成功标记。3.2 一个轻量级指标记录脚本下面用一个简单的 Python 类来演示指标埋点和汇总。实际项目中可以把日志打到文件、Kafka 或云日志服务但这里的核心逻辑是一样的。# metrics.py import json import time class LLRouterMetrics: 轻量级 LLM 路由指标采集器。 会把每次请求的指标追加写入 JSONL 文件。 生产环境建议替换为更可靠的日志通道并做数据分区。 def __init__(self, log_path: str llm_metrics.jsonl): self.log_path log_path def record( self, request_id: str, route_name: str, model: str, cost: float, status: str, duration_ms: float, retry_count: int 0, fallback: bool False, note: str , ): entry { timestamp: time.time(), request_id: request_id, route_name: route_name, model: model, cost: cost, status: status, # success / failed / fallback duration_ms: duration_ms, retry_count: retry_count, fallback: fallback, note: note, } with open(self.log_path, a, encodingutf-8) as f: f.write(json.dumps(entry, ensure_asciiFalse) \n) def summary(self) - dict: 读取日志计算 cost-per-call 与 cost-per-success。 这里把 success 和 fallback 都视为业务成功。 fallback 表示虽然模型没有直接给出满意结果但通过人工兜底完成了业务。 它的成本已经计入 total_cost因此分母可以包含 fallback 请求。 total_cost 0.0 success_count 0 total_count 0 with open(self.log_path, r, encodingutf-8) as f: for line in f: item json.loads(line) total_cost item[cost] total_count 1 if item[status] in (success, fallback): success_count 1 return { total_requests: total_count, success_requests: success_count, total_cost: round(total_cost, 6), cost_per_call: round(total_cost / total_count, 6) if total_count else 0, cost_per_success: round(total_cost / success_count, 6) if success_count else 0, success_rate: round(success_count / total_count, 4) if total_count else 0, }这个类本身不直接承担路由逻辑但它可以被插入到路由函数返回后、业务结果判断后的任意位置用来记录这条请求的最终处理情况。只要数据积累到一定量就能按 route_name 分组统计比较不同路由策略的 cost-per-success。3.3 指标口径要与业务对齐在实际落地 cost-per-success 时最难的一步往往不是写代码而是定义“成功”。同样是客服工单分类任务“成功”可以有不同的定义模型输出的分类标签与人工标注一致算成功输出格式合法算成功用户没有在后续会话中修改系统给出的分类结果也算成功。不同定义下的 success rate 差异会非常大cost-per-success 也会随之波动。建议团队在做指标建设时先与业务方确认一个可量化的成功标准并且把它写在文档里。比较好的做法是同时维护两个口径工程口径模型调用链路是否成功返回。业务口径返回的结果是否真正被使用、被接受。cost-per-success 建议优先使用业务口径。工程口径可以用于定位链路问题但不适合用来做成本决策。否则你可能会发现 cost-per-success 被高估或低估进而影响路由策略的判断。4. 工程实战用 cost-per-success 优化 LLM 路由4.1 先做一个最小路由路由的复杂度可以分几个阶段。第一阶段可以用一个简单的启发式函数来决定把请求分给哪个模型。下面是一个最小可运行的路由示例它根据 prompt 的长度和关键词判断任务复杂度再决定调用大模型还是小模型。# llm_router.py 一个极简的 LLM 路由示例。 规则 1. prompt 长度超过阈值认为任务复杂路由到大模型。 2. 包含分析、总结、推理等关键词时也认为任务复杂。 3. 其余请求路由到小模型。 def call_large_model(prompt: str) - str: # 实际项目中替换为真实的 LLM API 调用 return f[XL] answer for: {prompt[:30]}... def call_small_model(prompt: str) - str: # 实际项目中替换为真实的 LLM API 调用 return f[XS] answer for: {prompt[:30]}... def is_complex(prompt: str, length_threshold: int 50) - bool: complex_keywords [分析, 生成方案, 总结报告, 对比, 推理] if len(prompt) length_threshold: return True return any(keyword in prompt for keyword in complex_keywords) def route(prompt: str): if is_complex(prompt): return XL, call_large_model(prompt) return XS, call_small_model(prompt) if __name__ __main__: test_prompts [ 你好我想查一下订单状态, 请帮我分析当前市场数据并生成一份总结报告, 这句话怎么翻译good morning, ] for prompt in test_prompts: model, answer route(prompt) print(fprompt: {prompt}\n - route to {model}, answer: {answer}\n)运行起来可以看到前两个 prompt 被路由到 XL第三个 prompt 被路由到 XS。这只是一个非常粗糙的最小路由但它已经具备了一个路由器的核心能力根据特征做模型选择。4.2 接入指标采集让路由可评估有了最小路由之后最重要的不是继续优化规则而是先让路由变得可以衡量。把前面定义的 LLRouterMetrics 集成到路由调用中。# router_with_metrics.py from metrics import LLRouterMetrics from llm_router import route metrics LLRouterMetrics(llm_metrics.jsonl) def handle_request(request_id: str, prompt: str): # 调用路由获取模型名和结果 model, result route(prompt) # 模拟 token 损耗这里只是示意真实项目应从 API 响应中读取 input_tokens len(prompt) output_tokens len(result) if model XL: cost (input_tokens / 1000) * 0.003 (output_tokens / 100