AI 服务链路追踪:一次推理请求要能从网关看到模型 AI 服务链路追踪一次推理请求要能从网关看到模型一、推理超时了但不知道卡在哪没有端到端追踪的 AI 服务是黑盒在一个典型的 AI 推理链路上请求经过 API 网关、认证服务、请求队列、推理调度器最终到达模型推理引擎vLLM/TGI。中间可能还穿插了 RAG 检索、向量搜索、Prompt 模板渲染等环节。当 P99 延迟从 3 秒飙升到 15 秒时排查路径通常是这样问网关团队、问推理团队、问模型团队——每个团队都拿出一段自己的日志告诉你我们这里没问题。这就是链路追踪缺失的典型症状。传统微服务追踪可以告诉你请求经过了哪些服务和每个服务的耗时但在 AI 场景中推理引擎这一步骤是最关键的黑盒——你只知道请求进入了 vLLM不知道它花了多少时间在 Prompt 处理上、多少时间在模型前向传播上、TTFT首 Token 延迟是多少。基础设施不需要漂亮话。一次推理请求的链路追踪如果不能在 Span 里显式标记Prompt 处理耗时 120ms调度排队 350msModel Forward 1800ms首 Token 生成 120ms那监控再漂亮也等于没有。二、AI 推理链路的 Span 结构设计OpenTelemetry 的 Span 模型天然支持层次化的时间记录。在 AI 链路中需要额外定义反映推理内部阶段的 Span 属性。sequenceDiagram participant GW as API 网关 participant Auth as 认证服务 participant Scheduler as 推理调度器 participant vLLM as vLLM 推理引擎 participant GPU as GPU Note over GW,GPU: Trace ID: abc123 GW-GW: Span: gateway.proxy GW-Auth: Span: auth.validate_token Auth--GW: 验证通过 GW-Scheduler: Span: scheduler.dispatch Scheduler-Scheduler: Span: queue.wait (350ms) Scheduler-vLLM: Span: inference.generate vLLM-vLLM: Span: prefill.processbr/(120ms, 2456 tokens) vLLM-GPU: Span: model.forwardbr/(1800ms) vLLM-vLLM: Span: decode.first_tokenbr/(TTFT: 120ms) loop Token 生成 vLLM-GPU: Span: decode.next_token Note over vLLM,GPU: 每 token 一个 eventbr/而非独立 Span end vLLM--Scheduler: Span: inference.donebr/(total: 3200ms, tokens: 512) Scheduler--GW: 响应返回 GW--GW: Span: gateway.proxy end几个关键的 Span 设计决策prefill 和 decode 必须分开。在 Transformer 推理中prefill 阶段处理输入 Prompt和 decode 阶段逐 token 生成的性质完全不同。Prefill 是计算密集型并行处理所有输入 tokendecode 是内存带宽密集型每次自回归生成一个 token。两者的延迟特征不同排查时需要分开观察。TTFT 必须作为独立 metric 记录。首 Token 延迟是用户体验的核心指标它由 prefill 时间 调度排队时间 第一个 decode 步骤的时间组成。在 Span 中记录 TTFT 可以区分模型在处理 Prompt和模型在等待 GPU 资源。Token 级别的生成不做独立 Span。一次推理可能生成数千个 token每个 token 创建一个 Span 会导致 Trace 数据爆炸。每个 token 生成记录为 Span Event零成本的轻量级时间戳而不是独立的 Span。三、生产级链路追踪实现3.1 vLLM 推理引擎中的 Span 埋点vLLM 的异步调度器AsyncLLMEngine是链路追踪的关键埋点位置。以下代码展示如何在 vLLM 的请求处理流程中注入 OpenTelemetry Span# vllm_instrumentation.py import time from contextlib import contextmanager from opentelemetry import trace from opentelemetry.trace import Status, StatusCode tracer trace.get_tracer(vllm.inference) # 定义 Span 属性常量约定命名便于后端查询。 ATTR_MODEL_NAME llm.model.name ATTR_PROMPT_TOKENS llm.prompt.tokens ATTR_COMPLETION_TOKENS llm.completion.tokens ATTR_TTFT_MS llm.ttft_ms ATTR_QUEUE_WAIT_MS llm.queue_wait_ms contextmanager def trace_scheduling(request_id: str, arrival_time: float): 追踪请求在 vLLM 调度队列中的等待时间。 with tracer.start_as_current_span(scheduler.wait) as span: span.set_attribute(request.id, request_id) try: yield span except Exception as e: span.set_status(Status(StatusCode.ERROR, str(e))) raise finally: wait_ms (time.time() - arrival_time) * 1000 span.set_attribute(ATTR_QUEUE_WAIT_MS, wait_ms) contextmanager def trace_prefill(request_id: str, prompt_len: int): 追踪 Prefill 阶段。 with tracer.start_as_current_span(inference.prefill) as span: span.set_attribute(request.id, request_id) span.set_attribute(ATTR_PROMPT_TOKENS, prompt_len) prefill_start time.time() try: yield span except Exception as e: span.set_status(Status(StatusCode.ERROR, str(e))) raise finally: prefill_ms (time.time() - prefill_start) * 1000 span.set_attribute(prefill.duration_ms, prefill_ms) contextmanager def trace_decode(request_id: str): 追踪 Decode 阶段包含首 Token 生成和后续 token 生成。 with tracer.start_as_current_span(inference.decode) as span: span.set_attribute(request.id, request_id) decode_start time.time() first_token_recorded False token_count 0 try: # 通过 span 对象传递计数器让调用者在每生成一个 token 时更新。 yield {span: span, first_token_recorded: False, token_count: 0} except Exception as e: span.set_status(Status(StatusCode.ERROR, str(e))) raise finally: decode_ms (time.time() - decode_start) * 1000 span.set_attribute(decode.duration_ms, decode_ms) span.set_attribute(ATTR_COMPLETION_TOKENS, token_count)在 vLLM 的AsyncLLMEngine.step()方法中集成async def instrumented_step(self): for seq_group in self.scheduler.get_scheduled_seq_groups(): with trace_prefill(seq_group.request_id, seq_group.get_prompt_len()): # 原始 prefill 逻辑 await self._run_prefill(seq_group) with trace_decode(seq_group.request_id) as decode_ctx: async for output in self._run_decode(seq_group): # 记录首 token 事件 if not decode_ctx[first_token_recorded]: decode_ctx[first_token_recorded] True ttft (time.time() - seq_group.arrival_time) * 1000 decode_ctx[span].add_event(first_token, attributes{ ATTR_TTFT_MS: ttft, }) decode_ctx[token_count] 1 yield output3.2 gRPC 网关中的 Trace 传播推理服务的 API 网关需要在 gRPC metadata 中传播 Trace Context确保网关 Span 和推理 Span 串联package gateway import ( context go.opentelemetry.io/otel go.opentelemetry.io/otel/propagation google.golang.org/grpc/metadata ) // InjectTraceContext 将 OpenTelemetry Trace Context 注入 gRPC metadata。 func InjectTraceContext(ctx context.Context) context.Context { md, ok : metadata.FromOutgoingContext(ctx) if !ok { md metadata.New(nil) } // 使用 W3C TraceContext 传播格式。 otel.GetTextMapPropagator().Inject(ctx, metadataReaderWriter{md}) return metadata.NewOutgoingContext(ctx, md) }3.3 采样策略不要让 Trace 吃掉推理延迟全量 Trace 成本太高——在 1000 QPS 的推理集群中全量 Tracing 每天产生 TB 级数据。需要根据请求延迟选择性采样// 智能采样策略对所有请求记录 Span但只上报慢请求的完整 Trace。 func SamplingProcessor(next trace.SpanProcessor) trace.SpanProcessor { return latencyAwareSampler{ next: next, slowThreshold: 3 * time.Second, } } type latencyAwareSampler struct { next trace.SpanProcessor slowThreshold time.Duration } func (s *latencyAwareSampler) OnEnd(data trace.ReadOnlySpan) { latency : data.EndTime().Sub(data.StartTime()) if latency s.slowThreshold { // 快请求仅记录聚合指标不上报完整 Trace。 // 可以通过 Span 的 IsRecordingfalse 实现。 return } s.next.OnEnd(data) }四、AI 链路追踪的特殊挑战异步推理的 Span 关联。批量推理Batch Inference场景中一个推理引擎同时处理多个请求它们的 GPU 计算时间重叠。这意味着 prefill Span 和 decode Span 不是串行的——多个请求的 Span 在时间轴上交叉。OpenTelemetry 的 Span 模型支持这种交叉但在 Jaeger/Tempo 的可视化界面中交叉的 Span 可能被误解为耗时异常。需要确保推理引擎的 Span 使用正确的 parent context。模型版本追踪。不同版本的模型可能有完全不同的延迟特征。vLLM 支持的--model参数直接决定了使用哪个模型。在 Span 中记录模型名称和版本如Qwen2-7B-Instructv1.2是定位延迟劣化的前提。如果两次部署之间模型版本变了但 Trace 中没有记录排查时会把模型升级导致的延迟增加误判为基础设施问题。大 Span 的存储成本。一次推理可能长达 60 秒decode Span 覆盖整个 token 生成过程。如果在这 60 秒内decode Span 产生了 2000 个 Event每个 Event 包含 3-5 个 Attribute单条 Trace 的体积可能超过 50KB。每秒钟 100 个这样的请求就是 5MB/s 的 Trace 数据写入。采样策略必须和推理延迟绑定而不是和请求量绑定。五、总结AI 服务的链路追踪不只是能看到请求经过哪些微服务而是要能精确到推理引擎内部的时间分解。核心落地步骤在推理引擎中埋点 prefill、decode 和 TTFT 三个关键 Span。prefill 和 decode 的延迟是推理优化的两个不同方向必须分开观测。Trace Context 从网关传播到推理引擎。使用 W3C TraceContext 标准确保在 gRPC metadata 中传递 traceparent header。Token 级别的事件用 Span Event 而非独立 Span。减少 Trace 体积避免存储成本爆炸。采样策略与推理延迟绑定。慢请求 100% 上报完整 Trace快请求仅上报聚合指标。一次推理请求的完整链路追踪——从网关到模型从 prefill 到最后一个 token——决定了你在线上出了问题后用 5 分钟定位还是 50 分钟排查。