
做模型运维最头疼的一件事就是老板突然问“这个月的推理成本为什么涨了30%”而日志系统里只有一片空白。大模型上线的第一天我就意识到传统那套面向 Web 应用的日志和监控体系根本无法回答关于 Token 消耗、生成延迟和成本分摊的任何问题。翻遍日志也只能看到“请求成功”或“请求失败”至于一次对话到底花了多少 Token、首字等待了多久、用户卡顿是用例还是推理瓶颈统统不知道。这半年我逐步把“大模型日志与可观测性”这套体系从零搭起来过程踩了不少坑这里把核心思路、落地方法和排查经验一次讲清楚。文章面向的是正在或准备将大模型接进生产环境的工程师看完可以直接照着设计自己的日志结构、采集链路和看板告警。1. 大模型可观测性到底在观测什么1.1 传统监控解决不了的新问题传统 Web 监控关注的是 QPS、响应时间、错误率这些指标放在大模型推理场景下依然存在但已经不够了。模型推理和普通接口最大的区别在于它的“响应时间”不是一个单一数字而是由排队时间、prefill预填充时间、decode逐字生成时间共同组成的复合值。同样一次成功的调用可能因为输入长度不同、模型版本不同、是否命中缓存而产生十几倍的延迟和成本差异。更麻烦的是Token 不是一个固定开销的业务字段它像一个不停跳动的计价器每一次推理都在累加成本。如果日志里没有记录 Token 消耗后续分析成本、做配额控制、向不同业务线分摊费用全都是拍脑袋。我见过不少团队上线了大模型接口结果月底对账时发现成本远超预期却查不出是哪条业务线烧掉的。这类问题不是靠增加服务器就能解决的必须从日志源头把 Token 的使用明细完整记录下来。1.2 三个观测维度指标、日志、追踪大模型可观测性同样跑不出指标、日志、追踪这三个核心件但各自的内涵发生了变化。指标用于回答“系统整体健康吗”。传统指标是 CPU、内存、请求量大模型场景则要增加每秒生成 Token 数、首 Token 延迟TTFT、输入 Token 占比、缓存命中率等专属指标。这些指标要能从原始日志中聚合出来而不是让应用进程额外暴露一堆零散的监控端点。日志用于回答“这请求到底发生了什么”。每条推理记录必须包含足够多的上下文请求 ID、用户标识、模型版本、输入输出 Token 数、各段耗时时长、采样参数、异常信息。日志既是问题排查的依据也是成本分析的事实来源。追踪用于回答“慢请求慢在哪”。大模型服务往往包含多级结构前面有网关鉴权与限流中间有推理服务后面还有向量数据库、缓存、外部工具调用。一次带工具调用的 Agent 请求可能要跨越好多个服务。分布式追踪能把一次请求的完整调用链串起来准确定位是哪个环节拖了后腿。1.3 从痛点出发的指标设计设计指标时别急着堆数量先想清楚要回答哪几个问题。我在实际项目里只保留了三类核心指标每一类都对应一个高频痛点。第一类是延迟类总耗时、TTFT、平均每 Token 生成耗时TPOT。其中 TTFT 直接决定用户体验“转圈半天”说的就是它TPOT 则影响整体流式输出速度。第二类是 Token 类输入 Token、输出 Token、总 Token、缓存 Token。这一类直接和成本挂钩按天、按客户、按模型维度聚合后就能做出成本报表。第三类是结果类成功/失败/超时/流式中断次数、重试率、限流次数。没有这类指标稳定性就是空话。指标没有命中痛点监控面板就算做了几百个图也只是心理安慰。我后来把面板精简成了三行成本、延迟、错误运营和研发各看各的十分钟就能复盘当天情况。2. 一次推理要记录哪些关键指标2.1 核心字段清单Token 与延迟日志里每次推理事件的字段不能拍脑袋定我最终沉淀了一套固定格式JSON 结构每个字段都有明确意义。基本字段包括request_id全局唯一请求 ID必须从入口网关传入贯穿所有内部服务client_id / user_id谁调的用于成本分摊model_name 和 model_version哪个模型哪次发布版本不记录等于白测prompt_tokens、completion_tokens、total_tokens模型 API 返回的原始数字latency_ms从收到请求到完成响应的时间ttft_ms从发出请求到收到第一个 Token 的时间tpot_ms平均每个输出 Token 的生成耗时stream是否流式temperature / max_tokens影响成本和时延的采样参数status成功、错误、超时、中止这套字段可以扩展但核心的这几个尽量不要省。尤其是 model_version不少团队上线新模型后延迟陡增就是因为没记录版本新旧数据混在一起完全无法对比。2.2 TTFT 与 TPOT两个容易被忽略的延迟指标总耗时会掩盖问题。一个 10 秒的请求可能前 8 秒都在等首字也可能首字 0.5 秒就出来了但生成 300 个字花了近 10 秒。这两种情况优化方向完全不同。TTFT 长基本是 prefill 阶段的计算问题或者是入口排队、网络传输慢TPOT 长则是 decode 阶段吞吐不够要考虑批处理策略、显存带宽或者模型量化优化。实测中 TTFT 的测量有个坑如果走代理网关网关自身处理时间会被算进去。最稳妥的方法是让大模型服务自己在返回的流式响应中打时间戳精确记录“请求进入推理进程”到“第一个 Token 写出”的间隔。日志记录要区分时间点不能只在出口打一次总耗时。2.3 给 Token 消耗加上成本和缓存视角Token 不仅是技术指标更是钱。日志里最好同时记录单位成本和估算费用或者在聚合阶段用 model_name 关联单价表。更实用的做法是加上 cache_tokens 字段很多模型服务支持 prompt 缓存命中的部分计费折扣很大。如果缓存命中率高成本下降非常明显。我踩过的一个教训是初期只记 total_tokens没有拆分输入和输出。后来发现综合成本一直对不上账单因为输入和输出 Token 单价不同模型厂商计费是分开算的。把 prompt_tokens、completion_tokens、cache_tokens 拆开记录之后账单终于对得上了。这一步相当于给成本管理打下了数据地基。3. 搭建日志管道从应用日志到 ELK3.1 先设计日志格式再写代码日志格式不是随缘的最好一开始就统一为 JSON。JSON 的好处是字段天然结构化Elasticsearch 可以直接解析后续做聚合查询不需要正则匹配。自研服务日志也尽量用 JSON 格式如果实在不想改默认日志库至少保证关键字段以 keyvalue 形式跟在 message 后面。我现在的做法是所有服务统一输出一行 JSON例如{ timestamp: 2025-06-18T14:23:45.123Z, level: INFO, logger: llm-gateway, request_id: 9f8a7b6c5d4e3f, client_id: finance-report, model_name: deepseek-chat, model_version: v3.20250501, prompt_tokens: 1903, completion_tokens: 452, total_tokens: 2355, cache_tokens: 1280, latency_ms: 8654, ttft_ms: 1120, tpot_ms: 17, stream: true, temperature: 0.3, max_tokens: 1024, status: success }看到这段结构就能直接回答“这次调用花了多少 Token、等了多久首字、总耗时多少”。日志不是为了给机器看的也不是为了日志平台检查而是为了未来站在故障现场的人能看懂。3.2 日志分级与敏感内容脱敏大模型日志有个特殊性请求里可能带了用户自然语言这既是排查问题的宝贵信息也可能是隐私风险。我建议明确两级策略元数据进日志原文不进日志。任何情况下不要直接记录完整用户输入记录 prompt_tokens、tokens 的哈希摘要或者输入长度就足够了如果实在需要分析问题单独配置异步的脱敏采样对敏感词做过滤后再写入。日志级别也别乱用。INFO 记录每一次推理调用摘要WARN 记录部分失败或阈值超限ERROR 记录完整异常堆栈和关联的上下文 ID。DEBUG 只在测试环境开启。生产环境一条推理一条 INFO 日志每条约 0.5KB日请求量 100 万也就约 500MB 日志量对 ELK 来说完全扛得住完全可以放开记录。3.3 日志采集Filebeat 轻量又可靠日志打出来之后需要采集进集中平台。我用的方案是 Filebeat配合 Logstash 做轻量处理和 Elasticsearch 存储。Filebeat 的好处在于占用小、部署简单天然支持多行 JSON 日志采集。如果是 Kubernetes 环境可以直接用 Filebeat DaemonSet 采集每个 Pod 的标准输出日志如果是裸机Filebeat 直接采集日志文件。一个需要注意的点是 Filebeat 默认按行读取并发送如果日志不是标准 JSON后续在 Logstash 里还要做 grok 解析。为了避免这个麻烦日志统一 JSON 输出就省下了 grok 的维护成本。下面是一个 Filebeat 配置片段采集指定路径下的日志并用 json 参数直接解析filebeat.inputs: - type: log enabled: true paths: - /data/logs/llm/*.log json.keys_under_root: true json.add_error_key: true fields: log_type: llm-inference fields_under_root: true output.elasticsearch: hosts: [http://es01:9200] index: llm-inference-%{yyyy.MM.dd}这样日志进入 Elasticsearch 时已经是结构化字段可以直接用 Kibana 或者 Grafana 做聚合查询。如果日志量特别大可以加一层 Kafka 缓冲Filebeat - Kafka - Logstash - Elasticsearch避免高峰流量直接把集群压垮。但中小团队没必要一上来就引入 Kafka省掉一层技术复杂度往往比可扩展性更重要。3.4 应用内的请求 ID 传递可观测性最隐蔽的一个坑是请求 ID 丢失。如果网关生成了一条 request_id但下游服务没有把它透传出去排查问题就会断链。这个问题的解法是约定在每个服务的请求头里携带 X-Request-ID输出日志时自动关联到当前上下文。实现方式有框架内建方案也可以单独封装一个日志过滤器。伪代码思路大致是从请求头拿 request_id没有就生成一个把 request_id 放进日志上下文请求结束时统一输出结构化日志。核心是要保证一条业务请求的所有内部调用共享同一个 ID。ID 丢失的问题不加处理后面分析日志的时候会发现按 request_id 一关联只关联到孤零零一条记录。4. 从日志到可视化与告警4.1 看板设计成本面板、延迟面板、错误面板日志最终要变成能看能问的数据。我的看板设计遵循“三块屏”原则先说结论再逐步下钻。成本面板默认展示当日 Token 总消耗、预计费用、按 client_id 分组的 Top 消费方、各模型占比。运营每天看一下这个面板就知道钱花哪了。延迟面板展示 P50/P95/P99 总延迟、TTFT、TPOT 的趋势重点看 P95平均值太容易被极端点拉高P95 更能反映大多数真实用户的体验。错误面板展示失败率、各类错误码分布、限流次数出现跌落式上升时立刻能发现。三个面板背后都是同一张索引表只是聚合维度不同。Kibana 的数据视图加三个多维分析表就能实现不需要引入额外的报表工具。如果是 Grafana 接 Prometheus则需要在日志采集端同时输出指标到 Prometheus逻辑相同只是技术栈不一样。4.2 延迟问题定位一次真实的慢请求排查举一个真实案例。某天用户反馈客户端转圈时间变长总延迟 P95 从 5 秒涨到 9 秒。我先看延迟面板发现 TTFT P95 从 1.2 秒涨到 4 秒TPOT 没变化。结论是 prefill 或者排队出了问题。再下钻到具体模型发现只有某个模型延迟飙升其他模型正常。于是看这个模型的 QPS 曲线发现高峰时段 QPS 翻倍而模型实例并没有扩容积分排队严重。最终处理方式是给这个模型增加实例数并在网关侧开启请求排队限制超出阈值的请求直接返回 429。两天后 P95 回到 4 秒。整个定位过程从“用户觉得卡”到“瓶颈在 prefill 队列”只花了不到半小时。如果没有 TTFT 和 TPOT 拆分看到总延迟 9 秒只会无从下手可能会先去检查网络再去查数据库大错方向。4.3 告警规则预算阈值与延迟突变告警规则要能真正执行。我设置了三条核心告警全部基于日志聚合成本警戒单日总 Token 消耗超过预算的 80% 时告警避免月底账单爆掉延迟突变TTFT P95 相比前一天同时段上涨超过 50% 且持续 5 分钟介入排查错误率突增错误率超过 1% 且超过前一天均值的一倍告警不要设得过于敏感否则全是噪音最后反而没人看。每一条告警都要能直接关联到具体日志查询告警内容里直接带 Kibana 查询链接是最实用的做法。还有一点告警发送后要有处理人和恢复确认机制不然告警只是发进群里该解决的问题没人看。5. 避坑手册几个亲身踩过的坑5.1 字段类型不一致导致聚合失败排查中发现一个历史遗留问题不同服务对同一个字段用了不同名称模型名有的叫 model有的叫 model_name还有的叫 model_id。日志进了 Elasticsearch 后被当成不同类型查询时要么查不到要么类型冲突无法聚合。后来我写了规范文档统一字段命名并在 Logstash 加了一层字段映射处理历史数据。新老服务必须遵守同一套字段规范成本采集和指标聚合才不会乱。5.2 流式响应下的 TTFT 测量误区流式接口和普通接口不一样它不是到最后才返回一次响应而是在生成过程中持续输出。如果按整请求的“响应时间”做统计TTFT 和总耗时会完全丢失。正确做法是在模型 SDK 回调上打点拿到第一个 token 时记录时间戳之后记录流结束的时间戳。我在初期没有处理好这一点看板上的 TTFT 全是零。改成 SDK 回调打点之后延迟数据才真正有意义。5.3 时间戳不同步造成跨服务排序错乱多实例部署时容器默认时间可能不完全同步。日志里记录时间戳时如果用服务器本地时间不同机器上同一事件可能前后相差几十毫秒排序时就乱了。统一用 NTP 同步时间是基础要求更推荐直接在日志里使用带时区信息的标准 UTC 时间并在请求 ID 上做关联而不是依赖时间排序来判断先后顺序。5.4 深入集成时不要忘记上下文传播在 Agent 场景里一次复杂的任务可能调用了多次模型推理还穿插了工具调用。如果每条模型推理日志都记录同一个 request_id就无法区分是哪一步慢。我建议增加 step_id 或 tool_call_id标记请求内部的调用序列。这样从 request_id 里能看到“这单客户整体的体验如何”从 step_id 里能看出“具体哪一步拖了整个流程”。5.5 别把日志当业务数据存日志平台存的是观测数据不是用户的业务资料。不要把对话原文、业务报表这些数据塞进日志索引。虽然这样做排查方便但会带来数据合规成本和存储膨胀。保持日志的“观测性边界”数据要分析时通过接口去业务库拉取不要图方便全部落到日志系统。6. 日志清理与成本控制6.1 索引生命周期管理Elasticsearch 存储的自我膨胀是个大问题。默认保留全部日志半年后磁盘直接告警。我用索引生命周期策略ILM做了分级热节点保存 7 天温节点保存 30 天冷节点保存 90 天超过 90 天自动删除。实测下来这个策略能把存储成本控制在可接受范围同时还保留着回溯排查最近一个季度的能力。如果需要更长期的留存可以只保留聚合指标原始日志不保留。6.2 指标预聚合降低存储成本原始日志全量保存成本高但业务上并不需要随时查每一个原始请求。我加了一套每日预聚合任务每天晚上将当天的 Token 消耗、延迟分位数、错误数量等指标汇总成一张小表长期保存在 Elasticsearch 或数据库里。预聚合数据用于做趋势报告和成本月报原始日志只用于问题排查保留 90 天。这套两层的存储策略大幅节约了资源。6.3 告警频率控制与维护窗口日志系统的告警和运维告警要区分开维护。模型发版时延迟会短暂变化ELK 扩容时可能会有数据写入延迟这些场景都会触发误报。我给告警规则维护了静默窗口发版计划和维护操作前统一静默两小时。告警准确性比起告警数量重要得多保持规则精简一旦触发就是真问题。7. 团队落地建议与心得实施这套体系最难的地方不是技术而是让团队里每个人都认同“日志是产品的一部分”。我从一开始就和后端团队约定每条推理日志的字段不准改出问题先查日志再说。一年下来研发同学的反馈是遇到线上问题直接知道怎么查不需要逐个问链路里其他人。技术选型方面建议根据团队规模做取舍。几十万日请求量Filebeat 加 Elasticsearch 加 Kibana 完全足够上千万日请求量再引入 Prometheus 做指标Grafana 做可视化Jaeger 做追踪。不要一开始就上一套太大的系统先让日志结构对了再慢慢叠加能力。最终我个人的体会是日志和可观测性不承担“让你的模型跑得更好”的任务它只负责“让模型跑得明明白白”。当每一次推理的 Token 消耗和延迟都清清楚楚摆在面前时优化方向就自然清楚了。如果你在搭建大模型日志体系时也遇到过类似问题欢迎交流我很愿意把验证过的方案和踩过的坑再展开讲讲。