news 2026/10/1 17:41:30

企业级LLM应用监控实战:成本追踪与性能分析从零搭建

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
企业级LLM应用监控实战:成本追踪与性能分析从零搭建

最近我们团队负责的智能客服系统正式接入了多个大模型,日均请求量在几千到上万之间波动。一开始大家只关心效果,直到月底财务把账单甩到群里——光调用模型就花了六位数,而且没有任何明细能说清楚是哪条业务线、哪个用户、哪个场景烧了这么多钱。同时客服那边开始抱怨系统时不时卡顿,有的用户等十几秒才收到首个字。我觉得不能再拍脑袋优化了,于是用两周时间从零搭了一套LLM监控系统,核心覆盖成本追踪与性能分析。这套系统上线后,成本立刻降了约三成,线上告警也能精确到某一个请求ID。这篇文章就把我踩过的坑和具体搭建步骤写出来,适合所有已经在用或准备用LLM做业务、又担心费用失控和响应变慢的团队参考。

1. 为什么企业级LLM应用必须做监控

很多小团队刚开始接大模型接口时,只关心“这个模型回得好不好”,Token消耗和调用延迟都被当成后置问题。可一旦请求量上来,它们两就会反过来咬你一口。先不谈安全合规,单说成本和性能这两件事,就足以逼你搭建一套正经的LLM监控系统。

1.1 成本失控:一次对话可能比你想的贵十倍

大模型是按Token计费的,Token可以粗略理解成“模型读和写的字符片段”,一个汉字通常等于1到2个Token,一个英文单词约等于1个Token。表面上每个Token单价并不高,但实际跑起来后,你很快会发现几个隐藏的“吞Token黑洞”:

第一是上下文续接。你做客服机器人时,为了记住对话历史,往往会把前面所有轮次的内容重新发给模型。假设用户聊了20轮,每轮平均500字,再算上系统提示词、格式化结构、工具定义,这些内容每次请求都要整体重发。这意味着一个真实业务请求可能消耗的Token是用户输入文本的5倍甚至10倍。第二是重试导致的双倍计费。一个请求如果因超时或网络抖动被触发自动重试,大概率会重复计费,而且失败的那一次也照样扣钱。第三是不同模型之间的距离。GPT-4级别模型的单位Token价格可能是轻量模型的几十倍,稍微用错模型,成本差距会非常夸张。

我在刚开始统计成本时发现,某个内部运营工具每天只有几百次调用,但费用占总账单的比例超过四分之一。后来查出来是代码里写死了“必须用最强模型生成摘要”,完全没有考虑这个任务其实用轻量模型就能搞定。这个案例很典型:没有成本追踪之前,这些问题就像藏在棉花里的针,外表感觉不到,账单一到就戳得人心疼。所以成本追踪的首要目标是“摸清每一分钱去哪了”,然后才能谈省钱。

1.2 性能风险:延迟和错误会直接毁灭产品口碑

用户对大模型应用的耐心,比传统网页还要低。你加载一个网页花了3秒,用户还能忍;如果和AI对话时首字迟迟不出来,用户立刻会感觉“系统卡住了”。偏客服、问答这类场景尤其明显,响应慢一点,满意度曲线掉得飞快。

LLM请求的延迟有个特点:它不是一条固定直线。同样一个模型,请求内容短时可能几百毫秒就返回,内容长或并发高时就要等到十几秒甚至几十秒。而且大模型供应商的服务端压力你是完全不可见的,上一秒还稳定,下一秒可能整体变慢。如果没有监控,你根本分不清到底是模型供应商出了问题、你的网络链路出了问题,还是你的Prompt太长导致生成时间过久。

不只延迟,错误率同样重要。模型接口返回的错误五花八门,有鉴权问题、限流问题、Schema校验失败、上下文长度超限,还有各种网关错误。多数错误不会让进程崩溃,但会让用户看到一条“AI服务开小差”的提示。长期不监控,你的产品就变成了薛定谔的AI——看起来能用,但你不知道它什么时候会对特定用户群体发火。

1.3 监控也是优化手段,不只是记录工具

我见过不少团队把监控理解为“事后看报表”,实际上,监控系统最大的价值是能引导你实时做优化。比如通过性能数据发现,某个场景90%的时间都花在“生成第一个Token之前”,也就是模型预填充阶段。那很可能是因为你的Prompt带有巨大的工具定义和示例,每次都要让模型先“读”一遍。这时候如果把工具定义精简、把上下文裁剪一部分,TTFT就能明显下降,成本也能同步降低。

再比如通过成本追踪发现,某个高频调用可以用“语义缓存”命中,完全不需要重复请求模型。我在优化之后加了一层缓存,命中率大概在30%左右,那部分调用成本直接归零。这些都是监控数据反哺业务的好例子。

2. 监控系统的整体架构与数据链路

拿传统后端监控的思路来搭LLM监控,最容易犯的错就是“只做两层”:应用埋点上报,然后看个曲线。但LLM调用涉及的数据维度比普通API多得多,比如模型名、Token用量、Prompt大小、流式状态等,而且这些数据横跨了成本与性能两个维度。所以需要一条清晰的数据链路,从采集到存储到展示告警都提前设计好。

2.1 采集层:从一个请求的完整生命周期讲起

一个典型的LLM调用过程大概是:用户输入进入你的后端服务,服务拼装Prompt后调用模型API,模型流式返回文本,你再把结果转发给前端。在这个过程里,每一个环节都应该被埋点。

我建议先从中间件开始,也就是拦截所有调用模型API的入口和出口。开源的SDK通常自带Usage信息,但不要只记usage字段,还要记录一个“请求快照”。一个可用的采集结构大致包含这些字段:

  • 请求ID、时间戳、业务线标识、用户标识、调用来源(哪个应用模块)。
  • 请求的模型名称、供应商名称、接入点名称。
  • 请求内容的Token数(如果没有现成数值,用分词的tokenizer估算),以及系统提示词、上下文、用户输入分别占多少。
  • 响应内容里的usage(prompt_tokens、completion_tokens)、FinishReason。
  • 性能数据:接入前等待时间、网络往返时间、首个Token生成时间、总耗时、是否流式。

采集方式可以用两种并行。第一种是同步日志,每条请求结束后写一条JSON日志,保证完整关联;第二种是指标打点,把延迟、Token数等聚合成Metrics推到Prometheus,用来做实时告警。

# 一个轻量的LLM调用中间件伪代码(FastAPI风格) async def llm_call_with_metrics(request_ctx: RequestContext): start = time.perf_counter() raw_request = build_request(request_ctx) response = await provider.chat(raw_request) elapsed = time.perf_counter() - start record_log( request_id=request_ctx.request_id, model=raw_request.model, prompt_tokens=raw_request.estimated_tokens, completion_tokens=response.usage.get("completion_tokens", 0), latency_ms=elapsed, first_token_latency_ms=response.first_token_latency_ms, status="success", ) observe_metrics( name="llm_request_latency_milliseconds", value=elapsed, labels={"model": raw_request.model, "scene": request_ctx.scene} ) return response

埋点层不要做得太重,把明细日志和聚合指标分开处理就好。这样即使指标数据被Prometheus拉走了,明细日志仍然能支撑后续查账单、查单个请求的问题。

2.2 存储层:用明细表支撑查账和分析

我把存储分成了两大部分:明细日志和分析指标。明细日志负责回答“某个请求发生了什么”这类问题,分析指标负责回答“现在整体延迟和成本是什么趋势”。

明细日志我推荐用ClickHouse或高配PostgreSQL,核心是一张不丢细节的调用账单表。如果你嫌重,先用PostgreSQL也能跑,但要注意数据膨胀。日志量到每天几十万条之后,建议还是用列式存储,压缩率高,查询聚合也快。

一张基本的表结构大概长这样:

CREATE TABLE llm_call_records ( request_id String, ts DateTime64(3), scene String, user_id String, trace_id String, provider String, model String, prompt_tokens UInt64, completion_tokens UInt64, total_tokens UInt64, unit_cost_usd Decimal(18, 8), total_cost_usd Decimal(18, 8), latency_ms UInt32, ttft_ms UInt32, finish_reason String, status String, error_message String ) ENGINE = MergeTree() PARTITION BY toYYYYMMDD(ts) ORDER BY (ts, request_id)

很多团队一开始会用ES来存日志,也能用,但成本与性能分析需要大量按模型、按场景的GROUP BY聚合,ES的聚合性能不如ClickHouse。如果你还在起步阶段,我的建议是别在存储上投入太多精力,先保证完整记录,后续再迁移都来得及。

2.3 可视化与告警链路:Prometheus + Grafana 的组合

相对来说,指标侧我推荐比较常见的Prometheus生态。你的应用通过PrometheusSDK暴露/metrics端点,Prometheus定期抓取,Grafana负责展示,Alertmanager负责发告警。这套组合足够稳定,插件丰富,而且大家熟悉,排障效率高。

如果你已经有微服务监控体系,直接把指标挂进去就行,没必要单独搭一套。指标命名建议统一前缀:

  • llm_total_tokens_consumed_total总Token消耗累计;
  • llm_cost_usd_total总成本累计;
  • llm_request_latency_seconds请求延迟直方图;
  • llm_first_token_latency_seconds首Token延迟直方图;
  • llm_error_requests_total错误请求计数;
  • llm_success_requests_total成功请求计数。

有了这些指标,Grafana上可以很快画出趋势图。一个比较实用的Dashboard组合是:左上角显示按小时成本,右上角显示请求量和Token吞吐,中间放P50/P95/P99延迟曲线,底部放各模型错误率。这个布局能让你一屏看清系统和供应商的健康度。

3. 成本追踪从零落地

有了前面的数据链路,成本追踪就不再是“看完账单拍大腿”,而是可以一个请求一个请求地精确核算。

3.1 建立“价格表”和全局Token计量

做成本追踪第一步,是把所有可能用到的模型价格配置成一张“价格表”。价格信息要包含模型名称、供应商、输入单价(per 1M tokens)、输出单价(per 1M tokens)、币种。不要相信记忆,大模型价格变动频繁,最好每次发版前更新一次。

在计算成本时,需要注意:总成本 = prompt_tokens * 输入单价 + completion_tokens * 输出单价。但很多模型对长上下文还有特殊的“缓存读取价格”,比如命中缓存后输入Token价格会更便宜。如果你的系统用了这类能力,价格表里要加一个cached_prompt_unit_cost字段,单独计算。

我踩过的坑是,系统早期没有区分输入和输出价格,统一按某个“平均价”估算,结果和账单对不上,差十万块。后来改成精确价格表,每个请求在写入明细表时就算好成本,月底对账几乎零误差。

3.2 多维成本聚合:谁花的钱多一眼看出

成本追踪的目的不是做一堆报表,而是快速定位问题。所以聚合维度非常重要。我用的几个核心维度是:业务线(scene)、用户(user_id)、模型(model)、供应商(provider)、时间粒度(小时或天)。通过这几个维度,几乎可以回答所有常见问题。

举一个真实场景:业务方问“这几天成本为什么涨了三倍”,我不会急着让他们自查,而是先跑一条查询:

SELECT scene, model, sum(total_tokens) AS tokens, sum(total_cost_usd) AS cost_usd FROM llm_call_records WHERE ts >= now() - INTERVAL 7 DAY GROUP BY scene, model ORDER BY cost_usd DESC LIMIT 20;

结果往往是某个新上线的场景用了贵模型,或某个老场景的请求量突然大涨。锁定后再点进去看明细,很快就能找到具体原因。

为了减少查询压力,我还会做一个预聚合表,按小时和场景粒度把Token、成本、请求数、错误数提前算好。这样做报表和看板时响应快,毕竟明细表只用来做深挖。

3.3 预算控制与动态配额:硬限制和软告警

成本追踪不只是“事后看账”,更要在预算边界上提前拦截。实现方式分两级:软告警和硬限制。软告警是指当某条业务线的日成本超过预设阈值时,通过告警渠道通知负责人;硬限制则是指当超出预算时,在应用层直接拒绝或降级调用,避免继续烧钱。

硬限制不建议直接用数据库查询来判断,因为频繁调用会打爆数据库。我倾向于用一个轻量的Redis计数器方案:每天凌晨把预算值写入Redis,每个请求结束后在Redis里累加当天已用成本,检查累计值是否超过阈值。由于成本计算已经完成,这里只是加法和比较,性能开销很小。一旦超限,服务端可以返回一个自定义错误,或自动切换到更便宜的备用模型,效果立竿见影。

# Redis预算控制示例 cost = compute_cost(usage, model_price) today = datetime.utcnow().strftime("%Y%m%d") key = f"cost_budget:{scene}:{today}" new_total = redis.incrbyfloat(key, cost) if new_total > scene_budget_limit[scene]: redis.decrbyfloat(key, cost) # 回滚,避免超额 raise BudgetExceededError(scene=scene)

注意不要把成本判断放在真正调用模型之前,因为你不调用就不知道准确的Token用量,预算控制会有较大误差。模型调用之后做控制,最多只会因为并发导致略微超出一点,配合告警完全可以接受。

4. 性能分析从零落地

性能分析的目标是回答两个问题:模型服务“现在到底快不快”,以及“如果慢了,瓶颈出在哪个环节”。这两问题并不是同一个Dashboard能解决的,背后需要不同的指标体系和链路追踪方式。

4.1 核心性能指标:别只看“平均延迟”

很多团队刚开始只看“平均响应时间”,这是最容易被迷惑的数字。一次超长的慢请求就能把平均值拉到天上,导致你以为全局都慢了。我建议至少围绕几个指标做分析:

指标含义推荐监控方式
TTFT(Time To First Token)从发出请求到收到第一个Token的耗时分位数 P50/P95/P99
TBT(Token间延迟)流式响应中相邻Token返回的时间间隔关注高百分位
总延迟从发出请求到完整响应结束的耗时分位数 + 按模型对比
请求成功率成功响应占总请求比例按场景/模型分组
Token生成速率completion_tokens / 总耗时用于衡量模型“输出速度”

其中最容易被忽视的是TTFT。流式接口下,用户感知的“第一反应速度”就是TTFT,它直接决定用户是否认为系统卡顿。总延迟虽然也很重要,但流式场景里只要TTFT够快,总延迟稍微长一点用户也能接受。

用Grafana做监控时,Prometheus的Histogram类型可以比较好地支持分位数统计。比如定义llm_first_token_latency_seconds为Histogram,配置Bucket从0.01到30秒,然后在Grafana里用histogram_quantile(0.99, sum(rate(...)))查询P99。这样你看到的是真实分布,不是平均值骗人的幻觉。

4.2 在真实请求中埋点与链路追踪

如果你已经有OpenTelemetry或SkyWalking,建议直接用现成的链路追踪,把LLM调用当作一个Span接入。比如在OpenTelemetry里添加一个llm类型的Span,记录模型名、Token用量、TTFT、错误信息等。这样当调用链出错时,你能同时看到数据库查询耗时、外部API耗时、LLM调用耗时,整个请求流程一目了然。

没有链路系统也不要紧,我早期就只在自己的代码里加了一个简单装饰器,把所有关键时间点打印到结构化日志里。重点要记录四个时间点:请求开始、进入模型服务商网关前、收到首个Token、收到最终响应。用这四个点相减,就能算出“本地排队耗时”“网络/服务端耗时”“生成耗时”。

@dataclass class LlmTiming: request_start: float gateway_start: float first_token_at: float end_at: float @property def queue_ms(self): return (self.gateway_start - self.request_start) * 1000 @property def ttft_ms(self): return (self.first_token_at - self.gateway_start) * 1000 @property def generation_ms(self): return (self.end_at - self.first_token_at) * 1000

注意,gateway_start必须在发送HTTP请求之前打点,这样能捕捉到连接池等待、DNS解析等本地耗时。很多工具只统计请求结束后拿到的耗时,往往丢失了最前端的几毫秒,对慢请求排查不够用。

4.3 性能瓶颈定位案例

这里分享一个线上真实排障案例。某天客服系统反馈“AI回复特别慢”,我看Grafana上的总延迟曲线确实从2秒涨到了8秒,但TTFT曲线基本没动,说明问题出在生成阶段,而不是连接或预处理阶段。

继续看Token生成速率,发现从50 Token/秒降到了10 Token/秒。我第一时间怀疑是不是对应用户的历史消息太长,导致模型要读的上下文变大了。查了明细表,发现慢请求里记忆历史字符串长度为5000多字,是普通请求的3倍。后来我们把历史消息压缩成摘要,只保留最近5轮对话,TTFT和生成速率都恢复到了正常水平。

另一个案例是在某个并发高峰期,TTFT突然飙到了15秒。查链路发现本地到模型服务商的网络往返只有200毫秒,但服务商网关排队时间超过10秒,明显是供应商侧限流或者服务端过载。这时候不是做应用优化能解决的,我选择在代码里加了一个“退避重试+自动降级到备选供应商”的逻辑,大幅缓解了用户体验。

所以性能分析一定要把耗时拆开,否则你只是在看一个整体风险,根本不知道要优化谁。

5. 常见问题与排查技巧实录

搭建监控系统不是写几个脚本就结束,真正磨人是上线后的各种异常。下面这些坑都是我自己踩过或者团队同事踩过的,列出来给后面的人当避雷针。

5.1 Token统计对不上,谁偷了你的Token?

最典型的问题是本地估算的Token数和供应商账单里的Usage对不上。主要原因包括:

  • 模型API本身会额外附加一些隐藏指令或JSON格式要求,本地没有统计;
  • 函数调用(Function Calling)的工具参数也被模型当成输入Token消耗;
  • 供应商对某些标点、特殊字符的Token化方式和开源Tokenizer细节不同。

我的处理方式是“以供应商账单为准”。在每次响应里读response.usage字段,而不是本地估算。本地估算只能用来做预判和限额,最终成本记录必须以服务端返回为准。如果供应商没返回usage,就按模型官方Tokenizer跑一遍估算,但要在系统标注为“估算值”。

5.2 日志和监控数据疯狂增长,存储成本反超调用成本

这是监控系统上线后很容易出现的“第二波账单”。一条LLM请求日志如果带上完整Prompt和响应内容,动辄几KB,按日均十万请求算,一天就是几个GB,一个月下来存储费用非常可观。

我建议做三层处理:第一,默认不存储完整Prompt,只存截断到200字符的样本;需要排查时再用TraceID去临时抓取,或者只对特定业务线保留完整内容。第二,明细表设置TTL,例如成本报表保留90天,性能指标保留30天,超过自动删除。第三,对非常高基数的用户分析,使用预聚合表而不是明细表。通过这三招,我的存储成本控制在总成本的5%以内。

5.3 误报告警轰炸,如何调参更合理

监控告警刚上线时,所有人都会被一秒一次的告警弄得神经衰弱。原因很简单:你给延迟设置了一个固定的阈值,比如P99大于5秒,但LLM供应商的延迟天然是波动的,白天高峰和凌晨差很多。

更合理的告警策略是采用分位数或相对基线:计算过去15分钟的延迟相比过去7天同一时间段延迟是否上涨超过50%,再触发告警;或者只对P99大于某个绝对且明确异常的值(比如超过10秒)告警。同时要设置告警抑制和分组,避免同一个接口故障连续发几百条消息。我们后来还给告警加了“自动静默”逻辑:如果同一Trace ID触发了多条告警,只保留一条中心告警。

5.4 Request failed / Schema invalid 这类供应商异常怎么定位

有一类棘手错误是llm request failed: provider rejected the request schema or tool payload。很多团队第一次碰到会以为是自己代码报错,但细看会发现是发给模型的工具定义(Tool Payload)不符合供应商的Schema要求,比如工具参数里出现了未声明的类型、重复字段、或者函数名保留字。这种错误和监控系统的关系在于:它经常会在某次发布后成批出现,非常容易被误判成网络故障。

排查方法并不复杂:在监控系统里筛出该错误码的请求,对比出错请求的工具定义JSON和最近发布记录。通常问题出在新增了一个工具,但schema没有同步到所有环境。我会在采集层把Tool Payload的哈希值也记录下来,一旦出错,可以直接按哈希值聚合,快速定位到底哪个工具定义有问题。

5.5 定时任务和批处理场景该怎么监控

前面讲的都是在线交互请求,但很多企业还会用LLM跑离线任务,比如批量生成摘要、编辑文案、知识库切分。这些任务的特点是单次请求时间长、批量并发高,如果还按在线请求的监控方式,容易在高峰期把并发打满,导致在线业务受影响。

我建议给离线任务单独加一层“并发控制”和“队列指标”。监控时要格外关注批次成功率和平均单条成本,如果某批次失败率超过阈值,应该立刻暂停,避免白白烧钱。成本追踪上,离线任务往往是一次性大额消耗,如果在聚合维度里不区分任务类型,很容易让你误以为业务异常增长。

6. 写在最后:一点个人工具箱与心得

这套系统我们从无到有用了大约两周时间,第一周做埋点、存储和看板,第二周补告警和预算管控。现在每天各类LLM调用数据都能实时看到,成本从“月底抽盲盒”变成了“小时级透明”,性能问题也能在用户大面积感知之前被定位到具体请求,整体收获非常大。

如果你也想做,我的建议是不要一开始就追求大而全的工具链。先用一个小服务把结构化日志落好,再加上Grafana和Prometheus,就已经能覆盖90%的需求。后面根据痛点再逐步加预算控制、链路追踪、语义缓存。我自己的体会是,成本追踪和性能分析是同一套数据的一体两面,记录好请求级明细,两个问题都能解开;反而是那些“看起来高大上”的模型路由、动态网关,等你数据足够丰富了再考虑也不迟。

最后分享一个小技巧:不要只盯着模型供应商提供的用量接口,最好的监控时机是在你代码调用模型的前后各埋一个点,然后把这些信息串成一条完整的请求链。只有这样,当账单和体验一起出问题时,你才能从容地指着一行日志说:“看,就是它。”

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 17:40:37

TCP网络编程实战:从协议设计到聊天室并发实现

简介:电子科技大学通信与信息工程学院网络软件设计项目是一套面向计算机相关专业学生的课程设计与毕业设计参考资源,覆盖需求分析、系统设计、编码实现与测试等完整流程,能够帮助学习者将理论知识与实际开发相结合。压缩包共50个文件&#xf…

作者头像 李华
网站建设 2026/10/1 17:39:56

OpenCV+HOG+SVM人体识别完整工程:海康摄像头取流、YV12转RGB与检测实践

简介:面向毕业设计、课程设计与计算机视觉入门人群的完整工程包,基于海康威视网络摄像头实时采集画面,结合OpenCV与HOGSVM算法实现人体识别与检测。程序采用C编写,集成Qt图形界面,并划分主窗口、摄像头采集、YV12图像格…

作者头像 李华
网站建设 2026/10/1 17:38:49

番茄叶片病害目标检测:数据集构建与YOLOv8训练避坑指南

简介:这是一套面向番茄叶片病害识别的目标检测数据集,适合深度学习初学者和农业视觉研究人员用于模型训练、算法对比与复现实验。数据覆盖blight-disease、mosaic-virus、redspider-infection三类典型叶片病害,同时提供YOLO与VOC两种标注格式…

作者头像 李华
网站建设 2026/10/1 17:38:42

Unity场景加载优化实战:时长分布、瓶颈拆解与优化路径

做Unity开发这些年,被问得最多的问题之一就是:场景加载多久算正常?尤其对于商业项目,加载时长的意义远超技术指标本身——它直接影响玩家的耐心、留存、评分,甚至是付费意愿。这篇文章不打算甩一句“看情况”就完事&am…

作者头像 李华
网站建设 2026/10/1 17:38:11

基于GAN的复杂背景文字图像修复:原理、训练与工程实践

简介:基于GAN实现复杂背景的文字图像修复是一套完整的Python源码项目,面向计算机视觉和图像处理开发者,用于解决复杂背景下文字图像的生成式修复问题。项目包含训练脚本trainwork.py和测试脚本testwork.py,以及大量图像样本、中文…

作者头像 李华
网站建设 2026/10/1 17:36:14

基于SpringBoot+RabbitMQ+Redis的私信系统架构设计与实战

私信系统这东西,看着功能简单,不就是"你发一条、我收一条"嘛。可真要在一个日活几十万的社交平台上落地,从消息不丢、不乱序,到已读回执实时同步,再到历史消息秒开不卡顿,每一环都是坑。我去年完…

作者头像 李华