大模型推理可观测性实战:Token追踪、延迟拆解与成本归因 1. 从一次线上事故说起为什么大模型推理必须做可观测性去年冬天我负责的一个智能客服系统突然出现大面积超时。用户反馈回答要等十几秒但监控面板上GPU利用率只有40%显存占用也正常。运维团队查了半天网络、查了负载均衡都没发现问题。最后我把推理服务的日志翻出来按请求维度重新聚合才发现问题出在一个不起眼的环节某个上游业务方在高峰期把max_tokens参数从512悄悄改成了4096导致单次推理的输出Token数量暴涨虽然GPU没跑满但每个请求的排队时间被拉长了三倍。这件事给我最大的教训是大模型推理的性能问题往往不藏在GPU指标里而藏在每一次请求的Token消耗和延迟分布里。传统的APM工具能告诉你服务响应慢但它不知道慢是因为输入太长、输出太多、还是排队太久。你需要的是专门针对大模型推理链路的可观测性体系。这篇内容就是把我这两年在大模型推理可观测性上踩过的坑、搭过的方案、调过的参数完整地梳理一遍。核心围绕三件事Token消耗怎么追踪、延迟怎么拆解、数据怎么用起来。适合正在做或准备做大模型服务部署的工程师、SRE、以及需要为推理成本负责的技术负责人。不管你是用vLLM、LocalAI还是自己写的推理引擎这套思路都能直接套用。2. 大模型可观测性和传统服务监控的本质差异2.1 传统监控的三个盲区做后端监控出身的同学第一反应可能是接Prometheus、配Grafana、看QPS和P99延迟。这套东西在大模型场景下会失效原因有三个。第一请求成本极度不均匀。一个传统HTTP接口每次请求消耗的资源基本恒定P99延迟能反映真实体验。但大模型推理不一样输入10个Token和输入10000个Token计算量差几个数量级输出50个Token和输出2000个Token时间差几十倍。你用统一的P99去衡量长请求会把短请求的体验完全掩盖掉。第二延迟的构成完全不同。传统服务的延迟主要是网络数据库业务逻辑大模型推理的延迟要拆成排队时间、Prefill时间、Decode时间、输出传输时间。这四段的优化手段完全不一样混在一起看等于没看。第三Token是真正的成本单位。GPU小时数只是表象真正决定成本的是输入Token和输出Token的数量。输出Token的生成成本通常是输入的5到10倍因为Decode阶段是逐Token串行生成的如果不区分统计成本核算会严重失真。2.2 可观测性要回答的四个问题我总结下来一套合格的大模型可观测性体系必须能回答这四个问题这次请求花了多少Token输入多少、输出多少、缓存命中多少。时间花在哪了排队等了多久、Prefill用了多久、每个输出Token的平均生成时间是多少。谁在用哪个业务方、哪个用户、哪个模型版本消耗了多少资源。异常在哪哪些请求触发了截断、哪些请求超时、哪些请求的输出质量异常。这四个问题对应四类指标Token指标、延迟指标、维度标签、异常事件。下面逐个拆。2.3 一个关键认知Token是一等公民很多团队做可观测性时把Token当成一个附属字段日志里打印一下就完事。这是最大的误区。Token应该和延迟一样是一等公民指标。原因很简单延迟影响体验Token影响成本而成本决定这个服务能不能长期跑下去。我在实际项目里会把Token指标拆成至少六个维度prompt_tokens输入、completion_tokens输出、total_tokens总计、cached_tokens缓存命中、reasoning_tokens推理模型的思考Token、truncated是否被截断。这六个字段缺一个成本分析就会出偏差。3. Token追踪的落地从日志字段到成本归因3.1 推理引擎原生返回的Token字段怎么用主流推理引擎在返回结果时都会带上Token统计。以OpenAI兼容接口为例响应体里通常有这样一个结构{ usage: { prompt_tokens: 128, completion_tokens: 256, total_tokens: 384 } }vLLM、LocalAI、TGI这些引擎都遵循这个约定。但要注意不同引擎的字段命名和统计口径有细微差异。比如有些引擎在流式返回时usage字段只在最后一个chunk里出现有些引擎在开启Prefix Caching后prompt_tokens会把缓存命中的部分也算进去导致成本虚高。我的做法是在网关层做一次归一化不管底层引擎返回什么格式统一转换成内部标准结构同时补上cached_tokens字段。如果引擎不返回缓存命中数就通过对比prompt_tokens和实际计费Token来推算。3.2 流式场景下的Token统计陷阱流式输出SSE是可观测性最容易翻车的地方。因为Token是逐个吐出来的你没法在请求结束时一次性拿到usage。常见的错误做法是在客户端数chunk数量然后乘以一个系数估算Token数。这个估算误差能到30%以上因为一个chunk可能包含多个Token也可能只包含半个Token多字节字符被拆分。正确的做法有两种。第一种是依赖引擎的最终usage在流式响应的最后一个chunk里引擎会带上完整的usage信息你只需要在网关层拦截这个chunk把usage提取出来记录即可。第二种是在网关层做实时Token计数用tiktoken或引擎自带的tokenizer对每个chunk的文本增量做编码累加Token数。这种方式的好处是可以在流式过程中就实时上报不用等请求结束。我实测下来第一种方式更准第二种方式更实时。生产环境我一般两个都做实时计数用于监控大盘最终usage用于成本核算两者对不上就告警。3.3 成本归因把Token账单拆到业务方Token统计出来只是第一步真正有价值的是成本归因。我见过太多团队月底看到一笔大模型账单但不知道是哪个业务花的。要解决这个问题需要在请求入口处打上足够的维度标签。我通常会在请求头或元数据里强制要求带上这几个字段字段名说明是否必填app_id业务方标识必填user_id终端用户标识选填model_name模型名称与版本必填request_type请求类型对话/补全/嵌入必填priority优先级用于排队调度选填有了这些标签你就可以在日志系统里按app_id聚合Token消耗生成每个业务方的成本报表。更进一步可以设置配额当某个app_id的日Token消耗超过阈值时自动降级到小模型或直接限流。提示app_id一定要在网关层强制校验不能依赖业务方自觉上报。我踩过的坑就是某个业务方漏传了app_id导致这部分Token消耗成了无主账单月底对账时扯皮了很久。3.4 缓存命中的Token怎么算Prefix Caching和KV Cache复用是降低推理成本的重要手段但它会让Token统计变得复杂。举个例子一个系统提示词有2000个Token被1000个请求复用。如果没有缓存这2000个Token要算1000次有了缓存只算1次或者按缓存命中价格算。我的处理方式是在日志里同时记录prompt_tokens和cached_tokens成本计算时用(prompt_tokens - cached_tokens) * 输入单价 cached_tokens * 缓存单价 completion_tokens * 输出单价。这样算出来的成本才准确。如果引擎不返回cached_tokens可以通过对比开启缓存前后的prompt_tokens均值来估算缓存命中率。4. 延迟拆解把一次推理切成四段来看4.1 排队延迟最容易被忽视的大头很多人优化推理延迟第一反应是换更快的GPU、用更小的模型。但实际生产环境里排队延迟经常占总延迟的50%以上尤其是在高峰期。排队延迟的产生原因是推理引擎的并发处理能力有限当请求数超过并发数时后来的请求就要排队。vLLM用Continuous Batching来缓解这个问题但它不能消除排队。要观测排队延迟你需要在请求进入引擎前打一个时间戳t_enqueue在引擎开始处理时打一个时间戳t_start两者之差就是排队延迟。我在网关层会记录三个关键时间点t_request_in请求到达网关t_engine_start引擎开始处理通过引擎的回调或日志获取t_first_token第一个Token返回t_last_token最后一个Token返回由此可以算出排队延迟 t_engine_start - t_request_inPrefill延迟 t_first_token - t_engine_startDecode延迟 t_last_token - t_first_token。4.2 Prefill与Decode两种完全不同的计算模式Prefill阶段是并行处理所有输入Token计算密集延迟和输入长度近似线性相关。Decode阶段是逐Token串行生成延迟和输出长度线性相关但每个Token的生成时间受batch size影响很大。这两个阶段的优化方向完全不同。Prefill阶段适合用大batch、高算力GPUDecode阶段受限于显存带宽batch size太大会导致每个Token的生成变慢。所以观测时一定要分开统计不能混在一起。我通常会用两个指标来衡量TTFTTime To First Token从请求发出到第一个Token返回的时间反映Prefill排队的综合体验。TPOTTime Per Output Token每个输出Token的平均生成时间反映Decode阶段的效率。这两个指标是业界公认的大模型推理核心指标。TTFT决定用户等多久看到第一个字TPOT决定用户看字的速度有多快。对话场景下TTFT比TPOT更重要长文本生成场景下TPOT更关键。4.3 输出传输延迟流式场景的隐形杀手流式输出时Token从引擎生成到用户看到中间还要经过网关、网络、客户端渲染。这段延迟在局域网内可能只有几毫秒但在跨地域场景下可能达到几百毫秒。我遇到过一个案例用户反馈回答是一个字一个字蹦出来的很卡。查下来发现是网关的SSE缓冲区设置太大导致Token被攒够一批才发送。把缓冲区从8KB调到1KB后体验立刻流畅了。所以观测延迟时除了引擎内部的时间还要记录网关转发延迟和客户端接收延迟。前者可以通过在网关层打时间戳来测后者需要在客户端埋点。如果客户端不方便改至少要把网关转发延迟纳入监控。4.4 一个完整的延迟拆解示例假设一次请求的总耗时是3.2秒拆解下来可能是这样阶段耗时占比优化方向排队1.5s47%扩容、优先级调度、限流Prefill0.4s12%减少输入长度、Prefix CacheDecode1.1s34%减少输出长度、投机解码传输0.2s6%优化网络、调整缓冲区看到这个拆解优化方向就一目了然了优先解决排队问题而不是去换GPU。这就是可观测性的价值——让优化有的放矢。5. 技术选型用什么工具搭这套体系5.1 日志采集结构化是第一原则大模型推理日志必须是结构化的不能是纯文本。我推荐用JSON格式每条日志包含请求ID、时间戳、Token统计、延迟拆解、维度标签。采集工具用Fluent Bit或Vector都行前者轻量后者功能更强。关键点是日志字段要提前设计好不能边写边加。我一般会定义一个Schema所有推理服务必须按这个Schema输出日志。Schema大概长这样{ request_id: uuid, timestamp: ISO8601, app_id: string, model: string, prompt_tokens: 0, completion_tokens: 0, cached_tokens: 0, queue_ms: 0, prefill_ms: 0, decode_ms: 0, transfer_ms: 0, total_ms: 0, status: success|timeout|error, truncated: false }5.2 指标存储Prometheus还是ClickHousePrometheus适合存聚合指标比如每分钟的Token总量、P99延迟。但它的标签基数有限制如果你把request_id或user_id作为标签Prometheus会直接爆掉。所以我的方案是双写聚合指标写Prometheus用于实时监控和告警明细日志写ClickHouse或Elasticsearch用于事后分析和成本归因。ClickHouse在聚合查询上性能极好适合做Token成本报表Elasticsearch全文检索强适合做异常排查。5.3 可视化Grafana面板怎么设计Grafana面板我一般分四块总览QPS、Token吞吐量、TTFT P50/P99、TPOT P50/P99、错误率。成本按app_id分组的Token消耗趋势、成本趋势、缓存命中率。延迟四段延迟的堆叠图、按模型分组的延迟对比。异常截断请求数、超时请求数、Token异常波动告警。面板设计的原则是让看的人三秒内知道有没有问题三十秒内知道问题在哪。所以总览面板要放在最上面异常面板要能直接下钻到具体请求。5.4 告警规则别让告警变成噪音大模型服务的告警很容易变成噪音因为延迟波动天然就大。我的经验是设置动态基线告警而不是固定阈值。比如TTFT的P99用过去7天同一时段的均值乘以1.5作为阈值而不是固定写死500ms。另外告警要分级Token消耗异常、错误率飙升这类影响成本或可用性的走P0告警延迟轻微上升这类影响体验的走P1或P2发到群里就行不用半夜打电话。6. 踩坑实录那些让我加班到凌晨的可观测性问题6.1 时间戳不同步导致的延迟负数有一次我在Grafana上看到延迟拆解图里出现了负数queue_ms是-200ms。查了半天发现是网关服务器和推理引擎服务器的时间没同步NTP服务挂了。网关打的时间戳比引擎的还晚算出来自然是负数。这个坑的教训是所有参与延迟计算的服务必须强制时间同步。我现在会在部署脚本里加一步NTP校验时间偏差超过50ms就直接拒绝启动。6.2 流式请求的Token统计翻倍另一个坑是流式请求的Token统计翻倍。原因是网关层做了实时Token计数同时又把引擎返回的最终usage也累加了进去。两个数据源叠加导致统计值正好是真实值的两倍。修复方式很简单实时计数和最终usage二选一或者用最终usage覆盖实时计数。但排查这个问题花了我一整天因为一开始怀疑是引擎的bug后来才发现是网关逻辑写重了。6.3 缓存命中率虚高Prefix Caching开启后我发现缓存命中率显示90%以上但成本并没有明显下降。深入查才发现引擎返回的cached_tokens统计的是被缓存的Token数而不是本次请求实际命中缓存的Token数。这两个概念完全不同。正确的做法是在网关层对比开启缓存前后的prompt_tokens变化或者直接看引擎的缓存命中日志。不要盲目相信引擎返回的cached_tokens字段。6.4 高基数标签把Prometheus打挂前面提过user_id不能作为Prometheus标签。我有个同事不信邪把user_id加进去了结果Prometheus内存暴涨整个监控系统挂了半小时。后来改成在ClickHouse里做用户维度的分析Prometheus只保留app_id和model这两个低基数标签。注意Prometheus的标签基数建议控制在10万以内。超过这个数查询会变慢内存会暴涨。用户级别的分析一定要放到列式数据库里做。7. 从可观测性到可优化数据怎么反哺推理性能7.1 用Token分布指导模型选型有了Token统计数据你就可以做一件很有价值的事按请求特征做模型路由。比如统计发现80%的请求输入Token少于200、输出Token少于100这类请求完全可以用小模型处理成本只有大模型的十分之一。剩下20%的复杂请求再走大模型。我在一个项目里做了这个优化整体成本下降了60%而用户满意度几乎没有变化。关键是要有Token分布数据支撑否则你不敢做这个决策。7.2 用延迟拆解指导扩容策略延迟拆解数据能告诉你该扩什么。如果排队延迟占比高说明并发不够该加副本如果Prefill延迟高说明输入太长或GPU算力不够该优化输入或换卡如果Decode延迟高说明显存带宽是瓶颈该考虑量化或投机解码。我见过一个团队盲目加了三倍的GPU副本结果延迟没降多少因为瓶颈其实在网关的SSE缓冲区。这就是没有延迟拆解的后果。7.3 用异常数据驱动Prompt优化截断率truncated为true的比例是一个被低估的指标。截断率高说明max_tokens设置不合理或者Prompt设计有问题导致模型输出过长。我通过分析截断请求的输入特征发现很多请求的System Prompt里写了请详细回答导致模型倾向于生成超长输出。把这句话改成请简洁回答后截断率从15%降到了3%Token成本也跟着降了。7.4 建立Token预算和配额机制最后一步是把可观测性数据变成主动控制。我通常会给每个app_id设置日Token预算当消耗达到80%时发预警达到100%时自动降级到小模型或限流。这套机制依赖前面所有的数据积累你得先知道每个业务方正常消耗多少才能设置合理的预算。预算机制上线后我再也没有遇到过月底账单爆炸的情况。因为异常消耗在当天就会被发现和拦截而不是等到月底对账。8. 我在实际项目中的几点体会这套可观测性体系我从零搭过两次每次都有新的收获。最大的体会是不要追求一步到位先从Token统计和TTFT/TPOT两个指标做起。这两个指标能解决80%的问题剩下的20%再逐步补。另一个体会是日志Schema的设计比工具选型重要十倍。工具可以换但Schema一旦定了改起来成本极高。所以前期一定要把字段设计好尤其是维度标签宁可多留几个字段也不要后期加。还有一点可观测性本身也有成本。日志量大了存储和查询都是钱。我的做法是明细日志保留7天聚合指标保留90天超期的自动降级或删除。这样既保证了排查问题的能力又控制了成本。最后分享一个小技巧在网关层加一个采样开关对于高频的、低价值的请求比如健康检查、简单问答只记录聚合指标不记录明细日志。这样能把日志量降低一个数量级而关键请求的明细一条不少。这个开关在流量高峰期特别有用能避免日志系统被冲垮。