1. 大模型推理可观测性到底在解决什么问题1.1 从一次线上故障说起去年冬天我帮一个团队排查他们内部问答机器人的问题。用户反馈很简单最近回答变慢了有时候等十几秒才出结果偶尔还会直接超时。团队第一反应是“模型太大GPU不够”准备申请加卡。我让他们先把每次推理的耗时和Token消耗打出来看看结果发现一个很有意思的现象P99延迟飙到了14秒但GPU利用率只有40%出头。真正的问题出在请求排队和上下文拼接上——有些用户的历史对话被无限制地塞进prompt单次输入Token从平均800涨到了6000多推理引擎的批处理队列被这些超长请求堵死后面的短请求只能干等。这件事让我更加确信一个判断大模型应用上线之后最容易被忽视、却最能决定用户体验的不是模型本身的能力而是你有没有把每一次推理的Token消耗和延迟看清楚。这就是大模型可观测性要解决的核心问题。所谓可观测性落到大模型推理这个场景里说白了就是三件事每一次推理花了多少Token、花了多长时间、这些数字在什么维度上发生了变化。听起来简单但真正做起来从埋点位置的选择、指标口径的定义到采样策略、存储成本、告警阈值每一步都有坑。我见过太多团队把精力全砸在模型选型和微调上上线后却连“哪个接口最费Token”都答不上来出了问题只能靠猜。这篇文章适合三类人看正在做大模型应用开发、需要给推理链路加监控的工程师负责大模型服务稳定性、需要定位延迟和成本问题的运维同学以及做AI产品、想搞清楚“钱到底花在哪”的负责人。我会把整套思路、埋点方法、指标设计、排查技巧都摊开讲尽量让你看完就能照着搭一套。1.2 为什么传统APM在大模型场景下不够用很多团队第一反应是我们已经有APM了直接接进去不就行了我试过结论是传统APM能覆盖一部分但远远不够。传统APM擅长的是HTTP请求级别的监控——接口耗时、错误率、QPS。但大模型推理的耗时结构和普通接口完全不同。一次推理的延迟可以拆成好几段请求排队等待、prompt预处理Tokenization、首Token生成时间TTFT、后续Token的逐字生成时间、以及后处理。传统APM只能告诉你“这个接口花了8秒”但没法告诉你这8秒里有多少是排队、多少是生成、首Token等了多久。而恰恰是这些细分指标决定了你该优化哪里。Token消耗更是传统APM的盲区。普通接口的“成本”基本是固定的一次请求消耗多少CPU、多少内存波动不大。但大模型推理的成本和输入输出长度强相关同样一个接口输入100个Token和输入5000个Token成本能差几十倍。你不把Token单独拎出来统计成本核算就是一笔糊涂账。还有一个关键差异是流式输出。大模型应用普遍用SSE或WebSocket做流式返回用户看到的是字一个个蹦出来。这种情况下“请求结束”和“用户看到完整回答”是两个时间点传统APM按请求结束时间算延迟会严重低估用户的真实等待感受。你必须单独记录首Token时间和Token生成速率才能反映真实体验。所以我的建议是传统APM继续用负责接口级别的健康度大模型推理的可观测性单独做一层专注Token和延迟的细粒度追踪。两层配合才能既看到宏观又看到微观。2. 核心指标设计追踪什么、怎么定义口径2.1 Token维度的四个核心指标Token是大模型推理的成本单位也是很多性能问题的根源。我一般会盯四个指标输入Token数prompt_tokens每次请求送进模型的Token总量。这个数字直接决定预处理耗时和显存占用。我见过最夸张的案例一个客服机器人因为把整个知识库塞进system prompt单次输入稳定在12000 Token以上成本高得离谱后来改成检索增强输入直接降到1500以内。输出Token数completion_tokens模型生成的Token总量。这个决定了生成阶段的耗时也是计费的主要部分。总Token数total_tokens输入加输出成本核算的基础。Token生成速率tokens_per_second输出Token数除以生成耗时。这个指标能反映推理引擎的真实吞吐能力也是判断“是模型慢还是排队慢”的关键。这里有个口径问题必须提前定清楚Token数到底按谁的计数算不同模型的分词器tokenizer切出来的Token数是不一样的。同一个中文句子有的模型切成20个Token有的切成35个。如果你用A模型的分词器去统计B模型的消耗数字会对不上。我的做法是统计口径以实际调用的推理引擎返回的usage字段为准不要自己用tiktoken之类的库去估算除非引擎不返回。自己估算只能用于预算不能用于对账。2.2 延迟维度的五个关键分段延迟这块我强烈建议不要只记一个总耗时而是拆成五段指标含义为什么重要排队时间queue_time请求进入队列到开始处理判断是否GPU资源不足或批处理积压预处理时间prefill_timeprompt Tokenization和KV Cache构建输入Token多时这段会明显变长首Token时间TTFT从开始处理到第一个Token输出用户感知“卡不卡”的核心指标生成时间decode_time首Token到最后一个Token反映逐字生成速度总耗时total_latency端到端完整时间对外SLA的统计口径这五段里TTFT是最容易被忽视但最重要的。用户对延迟的感知不是线性的等第一个字出来之前的那段时间焦虑感最强。实测下来TTFT控制在1秒以内用户基本无感超过3秒就会觉得“这AI是不是死了”超过8秒大概率直接关页面。而生成阶段哪怕每秒只出10个字只要字在动用户耐心就会好很多。所以做告警的时候我会给TTFT单独设一条线而不是只看总耗时。总耗时可能因为输出很长而偏高但TTFT高才是真正的体验杀手。2.3 成本与效率的衍生指标有了基础指标可以再算几个衍生指标用于横向对比和优化决策单次请求平均成本总Token数乘以单价按模型、按接口、按用户维度聚合。Token有效利用率输出Token数除以总Token数。这个比值太低说明输入里塞了太多没用的上下文有优化空间。缓存命中率如果用了KV Cache或prompt缓存命中率直接决定成本和延迟。命中率高预处理时间能砍掉一大半。每美元Token产出成本除以有效输出Token衡量“钱花得值不值”。这些衍生指标不用一开始就全上但单次请求平均成本和Token有效利用率这两个我建议从第一天就统计。它们能帮你快速发现“哪个功能在烧钱”。3. 埋点实操在推理链路的哪些位置下钩子3.1 埋点位置的选择逻辑埋点位置决定了你能拿到什么数据。我的原则是在推理引擎的入口和出口各埋一个点在应用层再埋一个点三个点交叉验证。应用层入口请求刚到达你的服务还没进推理引擎。这里记录请求ID、用户ID、接口名、原始prompt长度、时间戳。这个点的作用是拿到“用户视角”的起点。推理引擎入口请求真正提交给推理引擎比如vLLM、TGI、LocalAI等的那一刻。这里记录排队开始时间、实际送入的prompt Token数。推理引擎出口引擎返回结果。这里拿到usage字段输入输出Token数、首Token时间、生成结束时间。这是最权威的数据源。应用层出口结果返回给用户。这里记录端到端总耗时和引擎出口的数据对比差值就是你的应用层开销序列化、网络传输等。三个点的时间戳一减就能把延迟拆得清清楚楚。我一般会在日志里用统一的trace_id串起来方便后续关联查询。3.2 用OpenTelemetry做标准化埋点如果你的团队已经在用OpenTelemetryOTel那太好了直接复用它做埋点不用另起炉灶。OTel的Span模型天然适合表达“一次推理”这种有开始有结束、还带属性的操作。下面是一个Python示例展示怎么在调用推理接口时打Spanfrom opentelemetry import trace from opentelemetry.trace import Status, StatusCode import time tracer trace.get_tracer(llm.inference) def call_llm(prompt, model_name, user_id): with tracer.start_as_current_span(llm.inference) as span: span.set_attribute(llm.model, model_name) span.set_attribute(llm.user_id, user_id) span.set_attribute(llm.prompt_length, len(prompt)) start time.time() first_token_time None output_tokens 0 try: # 假设这里是流式调用 for chunk in stream_inference(prompt, model_name): if first_token_time is None: first_token_time time.time() span.set_attribute(llm.ttft_ms, (first_token_time - start) * 1000) output_tokens 1 end time.time() span.set_attribute(llm.output_tokens, output_tokens) span.set_attribute(llm.total_latency_ms, (end - start) * 1000) span.set_attribute(llm.tokens_per_second, output_tokens / (end - first_token_time)) span.set_status(Status(StatusCode.OK)) except Exception as e: span.set_status(Status(StatusCode.ERROR, str(e))) span.record_exception(e) raise这段代码的关键点TTFT在收到第一个chunk时就记录不要等全部结束。很多团队图省事等整个流式返回结束才统一算那样TTFT就丢了。另外llm.prompt_length这里用的是字符数实际应该用引擎返回的prompt_tokens我这里只是示意。3.3 日志字段设计一次推理该记哪些信息Span适合做指标聚合但排查具体问题时还是得靠结构化日志。我设计日志字段的习惯是能唯一标识一次请求的、能定位问题的、能用于聚合的三类字段都要有。下面是我常用的一套字段用JSON格式输出方便后续用日志系统检索{ trace_id: a1b2c3d4-..., request_id: req-20240115-001, user_id: u_12345, session_id: s_67890, model_name: qwen-7b-chat, endpoint: /v1/chat/completions, prompt_tokens: 1523, completion_tokens: 287, total_tokens: 1810, queue_time_ms: 45, prefill_time_ms: 320, ttft_ms: 680, decode_time_ms: 2100, total_latency_ms: 3145, tokens_per_second: 136.7, cache_hit: true, status: success, timestamp: 2024-01-15T10:23:45.123Z }这套字段里trace_id和request_id用于关联prompt_tokens和completion_tokens用于成本核算几个时间字段用于延迟分析cache_hit用于评估缓存效果。字段不要贪多但上面这些是底线。注意日志里绝对不要记录完整的prompt和输出内容尤其是涉及用户隐私的场景。只记长度和统计值就够了。如果确实需要采样记录内容用于调试一定要做脱敏并且设置很短的保留期。4. 数据采集与存储别让可观测性本身变成负担4.1 采样策略全量还是抽样大模型推理的日志量可能非常大。一个中等规模的应用每天几十万次推理如果每次都打完整日志存储成本很快就上来了。所以采样策略必须提前想清楚。我的做法是分层采样指标Metrics全量聚合Token数、延迟这些数值型指标用计数器Counter和直方图Histogram全量聚合不存原始日志。聚合后的数据量很小成本可控。日志Logs按需采样正常请求按1%到5%采样存详细日志错误请求、超时请求、Token异常高的请求100%全存。链路Traces头部采样用OTel的头部采样对慢请求和错误请求提高采样率。这样既能保证问题可排查又不会让存储爆炸。我见过一个团队不做采样三个月日志存了20TB光存储费用就够买好几张卡了。4.2 存储选型时序库加日志库的组合指标和日志的查询模式不一样存储也要分开指标数据用Prometheus或VictoriaMetrics这类时序数据库。它们对Counter和Histogram的支持好聚合查询快适合做看板和告警。日志数据用Elasticsearch、Loki或ClickHouse。Loki轻量、和Prometheus生态配合好ClickHouse查询快、压缩率高适合日志量大的场景。链路数据用Jaeger或Tempo和OTel无缝对接。如果团队规模不大不想维护太多组件我推荐Prometheus Loki Grafana这套组合部署简单社区成熟够用很久。等数据量真的上来了再考虑换ClickHouse。4.3 成本控制可观测性不能比推理还贵这里有个很现实的矛盾可观测性本身也要消耗资源。如果监控系统的成本超过了它帮你省下的钱那就本末倒置了。几个控制成本的经验指标聚合粒度不要太细按分钟聚合就够了没必要按秒。按秒聚合数据量会大60倍但排查问题时分钟级完全够用。日志保留期分级详细日志保留7天聚合指标保留90天成本报表保留1年。过期自动清理。避免高基数标签不要把user_id、request_id这种高基数字段做成指标标签否则时序库会被撑爆。这些字段放日志里指标里只用model_name、endpoint这种低基数维度。我踩过一次坑早期把user_id做成了Prometheus的label结果几万个用户直接把时序库打挂了。后来改成只在日志里记user_id指标里按用户分群聚合问题就解决了。5. 可视化与告警让数据真正发挥作用5.1 看板设计三块看板覆盖不同视角数据存下来不是目的能看懂才有用。我一般会做三块看板第一块成本看板。核心是Token消耗和费用。按模型、按接口、按天聚合展示总Token数、总成本、单次请求平均成本、Token有效利用率。这块看板给产品和负责人看回答“钱花在哪了”。第二块性能看板。核心是延迟。展示TTFT的P50/P95/P99、总耗时的P50/P95/P99、Token生成速率、排队时间。这块给工程师和运维看回答“慢在哪了”。第三块健康度看板。核心是错误率和异常。展示请求成功率、超时率、错误码分布、缓存命中率。这块给值班同学看回答“现在有没有问题”。三块看板不用做得很花哨Grafana拉几个图就够。关键是指标口径要统一别成本看板和性能看板对同一个请求的Token数算出来不一样那就乱套了。5.2 告警阈值怎么定从基线到动态告警阈值定太松问题漏报定太紧天天误报最后没人看。我的经验是先跑两周基线再定阈值。具体做法新服务上线后先只采集不告警观察两周。看TTFT的P95大概在什么范围总耗时的P99是多少Token数的分布如何。然后按基线的1.5到2倍设阈值。比如基线TTFT P95是800毫秒那告警线设在1.5秒左右。几个我必设的告警TTFT P95超过阈值持续5分钟说明用户体验在恶化可能是排队积压或模型异常。错误率超过1%持续3分钟说明服务有问题需要立即介入。单次请求Token数超过阈值说明有异常长的输入可能是prompt拼接出了问题或者有人在滥用。Token生成速率骤降说明推理引擎可能降频或资源争抢。阈值不要设太多五六个核心的就够。告警太多等于没有告警。5.3 从指标到根因一个排查实例光有告警不够还得能快速定位根因。我拿一个真实案例走一遍。某天下午告警响了TTFT P95从800毫秒涨到了3.2秒。值班同学先看性能看板发现总耗时也涨了但Token生成速率正常。这说明问题不在生成阶段而在生成之前。接着看排队时间发现queue_time从平均50毫秒涨到了1.8秒。排队时间涨说明请求在引擎入口积压了。再看请求量QPS没有明显变化。那为什么排队变长这时候去看Token分布发现prompt_tokens的P99从2000涨到了8000。有少量超长请求混进来了。超长请求的预处理时间长占着引擎的批处理槽位把后面的短请求堵住了。根因找到了某个上游功能改了prompt拼接逻辑把历史对话全量塞进去了。修复方式很简单加个滑动窗口只保留最近N轮对话。改完TTFT立刻回落。这个排查过程之所以能这么快就是因为延迟被拆成了分段Token被单独统计了。如果只有一个总耗时你根本不知道问题出在排队还是生成只能瞎猜。6. 常见问题与避坑指南6.1 流式场景下延迟统计的坑流式输出下最容易犯的错是用请求结束时间算延迟。用户看到第一个字的时间和请求真正结束的时间可能差好几秒。你按结束时间算TTFT就丢了用户体验的恶化你根本发现不了。正确做法是在收到第一个chunk时记录TTFT在收到最后一个chunk时记录总耗时两个都存。看板上前者看体验后者看资源占用。还有一个坑是客户端和服务端时间不一致。如果你的TTFT是在客户端测的服务端也在测两边对不上。我的建议是以服务端为准客户端的数据只做参考。因为服务端能排除网络波动数据更稳定。6.2 Token计数不一致怎么排查前面提过不同分词器切出来的Token数不一样。如果你发现日志里的Token数和账单对不上按这个顺序排查确认统计来源是引擎返回的usage还是自己用分词器估的以引擎返回为准。确认模型版本同一个模型的不同版本分词器可能变过。日志里要记模型版本号。确认是否包含特殊Token有些引擎的usage不包含system prompt的Token有些包含。看引擎文档确认。确认是否有重试一次请求如果重试了Token会重复计算。日志里要标记重试次数。我遇到过一次对账差异最后发现是引擎在prompt超长时自动截断了但usage返回的是截断前的Token数。这种细节只能靠仔细核对引擎文档和实际返回。6.3 高并发下的数据丢失问题高并发时如果日志是同步写的会拖慢推理主流程。我见过一个服务因为日志写磁盘太慢QPS一高就超时。解决办法是异步写日志推理主流程只把日志对象丢进内存队列后台线程慢慢消费写盘。队列满了就丢弃低优先级日志比如正常请求的采样日志保证错误日志不丢。用Python的话可以用queue.Queue加一个后台消费线程或者直接用logging.handlers.QueueHandler。关键是主流程不能阻塞在日志上。6.4 常见问题速查表现象可能原因排查方向TTFT高但生成速率正常排队积压或预处理慢看queue_time和prompt_tokens分布总耗时高但TTFT正常输出Token太多看completion_tokens分布考虑限制max_tokensToken数对不上账单统计口径不一致核对引擎usage字段和分词器版本日志量暴涨采样策略失效或异常请求多检查采样配置看是否有异常长prompt告警频繁误报阈值太紧或基线未校准重新跑基线放宽阈值缓存命中率低缓存key设计不合理检查缓存key是否包含易变字段7. 我踩过的几个真实坑7.1 别在推理主线程里做重活早期我图方便在推理返回后直接在主线程里算指标、拼日志、发到远端。结果QPS一上200延迟就抖得厉害。后来把指标聚合和日志发送全改成异步主线程只负责把数据丢进队列延迟立刻稳了。这个坑的本质是可观测性代码不能影响被观测的业务。观测是旁路不是主路。任何在主流程里做的统计、序列化、网络发送都要评估它的耗时。超过1毫秒的一律异步化。7.2 指标标签的基数陷阱前面提过user_id的坑这里再强调一次。Prometheus这类时序库每个唯一的标签组合就是一条时间线。你把user_id做成标签一万个用户就是一万条时间线内存直接爆。正确的做法是指标标签只用低基数维度比如model_name几个到几十个、endpoint几个到几十个、status成功/失败。高基数维度user_id、session_id、request_id只放日志和链路里需要按用户分析时从日志里聚合。7.3 采样率设太高等于没采样有段时间我把采样率设成50%想着数据全一点好排查。结果存储成本翻了好几倍查询也变慢真正出问题时在海量日志里找半天。后来降到5%配合错误请求全采反而更好用。采样这件事关键不是采多少而是采得巧。正常请求采一点点做基线异常请求全采做排查这个组合比均匀高采样有效得多。7.4 告警要有人看否则就是噪音我见过太多团队告警配了一堆但没人看或者看了也不处理。时间一长告警就成了背景噪音真出问题时反而被忽略。我的建议是告警数量控制在个位数每条告警都要有明确的处理人 and 处理动作。配一条告警前先问自己这条响了我具体要做什么如果答不上来就别配。宁可少配几条也要保证每条都有用。8. 后续可以怎么扩展这套基础的可观测性搭起来之后还有几个方向可以继续深挖。第一个方向是A/B实验的可观测性。如果你在对比不同模型、不同prompt策略的效果可以把实验分组做成指标标签直接在看板上对比不同组的Token消耗、延迟和成本。这样选型决策就有数据支撑不用拍脑袋。第二个方向是异常检测。现在阈值是人工设的未来可以用历史数据做基线自动检测异常。比如用滑动窗口算移动平均偏离超过几个标准差就告警。这样能适应流量的自然波动减少误报。第三个方向是成本归因。把Token消耗按业务线、按功能模块、按用户分群拆开做成成本报表。这样哪个功能在烧钱一目了然优化起来有的放矢。第四个方向是和微调打通。微调后的模型Token分布和延迟特性都会变。把微调版本号做成标签对比微调前后的指标能直观看到微调带来的收益和代价。这些扩展不用一次做完先把基础的Token和延迟追踪跑通再按需加。可观测性这东西够用比全面重要能解决你当前最痛的问题就是好方案。最后分享一个我自己的习惯每次上线新功能我都会先问一句“这个功能的Token消耗和延迟我能看到吗”如果看不到就先别上把埋点补上再说。这个习惯帮我省了很多事后排查的麻烦。