利用 OpenTelemetry 统一收集与关联 LLM 调用链路在微服务架构全面走向智能化的演进过程中大模型调用已经成为后端业务链路中的核心组成部分。传统微服务间的 RPC 调用通常在几十毫秒到几百毫秒内完成调用链路清晰且符合标准的请求-响应生命周期。然而大模型交互具备高度异构性交互耗时长经常长达数秒乃至数十秒、普遍采用 Server-Sent EventsSSE流式输出、计费与算力消耗以 Token 为单位、并且高度依赖动态多轮提示词上下文。很多架构团队在将大模型服务接入既有的微服务链路追踪体系如 SkyWalking、Jaeger 或 Zipkin时普遍遇到了链路严重断层、首字生成延迟TTFT无法独立度量、Token 成本难以精准归因到业务租户等运维困境。基于 OpenTelemetry 最新制定的生成式 AI 语义约定Semantic Conventions for GenAI构建统一的 LLM 观测与全链路关联体系是解决这一架构断层的关键实践。传统 APM 体系接入大模型时的四大核心断层在分布式系统中W3C Trace Context 规范为跨服务调用提供了标准化的traceparent与tracestate传播机制。但在大模型推理场景下传统的 APM 探针与观测体系会遭遇显著的功能失效流式长连接导致 Span 生命周期失真在使用流式输出时客户端向模型网关发起 HTTP 请求网关建立 TCP 连接并返回200 OK仅需数十毫秒。如果按照传统 HTTP Client 探针的生命周期管理方式Span 会在收到 Response Header 后立即关闭导致整个链路追踪显示的耗时仅有数十毫秒而实际上用户端等待完整生成的长达数十秒的流式传输过程完全脱离了监控视线。生成式 AI 核心元数据完全缺失传统的链路追踪仅记录 HTTP Method、URI、Status Code 以及网络层耗时无法捕获大模型特有的关键特征例如模型供应商、实际生效模型名称、Temperature 与 Top-P 采样参数、Prompt Token 数、Completion Token 数以及结束标识如 stop、length、content_filter 等。反应式非阻塞调用链中的 Context 丢失主流的大模型接入框架如 Spring AI、LangChain4j 以及基于 WebClient/Netty 的底层通信层普遍采用 Project Reactor 进行反应式编程。当数据流在Flux或Mono管道中经历线程池切换、异步缓冲与背压控制时基于 ThreadLocal 存储的 Trace Context 极易丢失导致下游链路出现游离 Span。Prompt 与 Completion 数据合规与脱敏困境APM 探针如果默认采集所有 Payload会将包含用户个人隐私PII、敏感商业数据甚至系统核心 System Prompt 暴露在 APM 控制台中若完全不采集又导致生产环境排查模型幻觉或异常输出时无据可查。观测维度传统微服务 RPC 链路追踪OpenTelemetry GenAI 标准链路追踪核心黄金指标吞吐量QPS、平均响应时间RT、P99 延迟、HTTP 错误率首字耗时TTFT、每秒生成 Token 速率TPS、Token 消耗总量、模型异常结束率Span 属性规范http.status_code,rpc.system,net.peer.namegen_ai.system,gen_ai.request.model,gen_ai.usage.prompt_tokens,gen_ai.usage.completion_tokensSpan 生命周期发起请求 - 接收完整响应体 - Span 立即关闭建立连接 - 接收首个分块标记 TTFT 事件 - 持续接收数据块 - 流完全结束时关闭 Span数据脱敏要求过滤 Header 认证信息与 Cookie必须具备针对 Prompt 与 Completion 的运行时正则脱敏、向量哈希化与长度截断能力上下文传播仅依赖 HTTP Header 注入traceparent除 HTTP Header 外必须贯通 Reactive Reactor Context 与多轮对话 Baggage核心实现基于 OpenTelemetry Java SDK 构建 LLM 观测拦截器为了在无侵入的前提下捕获大模型的调用全貌我们需要在底层 HTTP 通信组件中挂载自定义拦截器。以下展示基于 Spring 反应式 WebClient 与 OpenTelemetry Java SDK 实现的高性能 LLM 链路拦截器。该拦截器能够精确度量首字生成耗时TTFT、流式生命周期以及 Token 用量聚合public class OpenTelemetryLlmExchangeFilterFunction implements ExchangeFilterFunction { private final Tracer tracer; private static final AttributeKeyString GEN_AI_SYSTEM AttributeKey.stringKey(gen_ai.system); private static final AttributeKeyString GEN_AI_REQUEST_MODEL AttributeKey.stringKey(gen_ai.request.model); private static final AttributeKeyLong GEN_AI_USAGE_PROMPT_TOKENS AttributeKey.longKey(gen_ai.usage.prompt_tokens); private static final AttributeKeyLong GEN_AI_USAGE_COMPLETION_TOKENS AttributeKey.longKey(gen_ai.usage.completion_tokens); private static final AttributeKeyLong GEN_AI_RESPONSE_TTFT AttributeKey.longKey(gen_ai.response.ttft_ms); public OpenTelemetryLlmExchangeFilterFunction(OpenTelemetry openTelemetry) { this.tracer openTelemetry.getTracer(com.company.observability.llm, 1.2.0); } Override public MonoClientResponse filter(ClientRequest request, ExchangeFunction next) { String targetModel resolveTargetModel(request); // 开启客户端 Span声明符合 GenAI 语义的 Span Kind Span span tracer.spanBuilder(chat targetModel) .setSpanKind(SpanKind.CLIENT) .setAttribute(GEN_AI_SYSTEM, openai-compatible) .setAttribute(GEN_AI_REQUEST_MODEL, targetModel) .startSpan(); long requestStartTime System.currentTimeMillis(); AtomicBoolean firstTokenMarked new AtomicBoolean(false); AtomicLong completionTokenCounter new AtomicLong(0); // 注入 W3C Trace Context 到出站 HTTP 标头 ClientRequest.Builder requestBuilder ClientRequest.from(request); W3CTraceContextPropagator.getInstance().inject( Context.current().with(span), requestBuilder, (builder, key, value) - builder.header(key, value) ); return next.exchange(requestBuilder.build()) .map(clientResponse - { // 拦截响应数据流捕获流式生命周期事件 return clientResponse.mutate() .body(fluxBody - fluxBody.doOnNext(dataBuffer - { if (firstTokenMarked.compareAndSet(false, true)) { long ttft System.currentTimeMillis() - requestStartTime; span.setAttribute(GEN_AI_RESPONSE_TTFT, ttft); span.addEvent(gen_ai.content.first_token); } completionTokenCounter.incrementAndGet(); })) .build(); }) .doOnError(throwable - { span.recordException(throwable); span.setStatus(StatusCode.ERROR, LLM 调用失败: throwable.getMessage()); span.end(); }) .doOnCancel(() - { span.setAttribute(gen_ai.response.finish_reason, cancelled); span.setStatus(StatusCode.OK, 客户端主动取消流式接收); span.end(); }) .doFinally(signalType - { // 流式传输完毕补充记录统计指标并闭合 Span if (SignalType.ON_COMPLETE.equals(signalType)) { span.setAttribute(GEN_AI_USAGE_COMPLETION_TOKENS, completionTokenCounter.get()); span.setStatus(StatusCode.OK); } span.end(); }) .contextWrite(context - context.put(TraceContextElement.KEY, span)); } private String resolveTargetModel(ClientRequest request) { String modelHeader request.headers().getFirst(X-Target-Model); return modelHeader ! null ? modelHeader : default-inference-model; } }异步反应式流中的上下文传播与 Token 聚合机制在基于 Project Reactor 的反应式链条中上下文的无缝传递是链路追踪不中断的前提。当大模型返回长流式响应时下游往往会并行触发文本过滤、异步持久化或向量库检索这就需要解决反应式上下文与线程绑定的难题1. Reactor 上下文与 MDC 日志强绑定在 Java 21 与 Spring Boot 3 环境下推荐启用 Micrometer Context Propagation 机制。通过全局注册 ThreadLocal 访问器可以确保每当 Netty EventLoop 线程切换到自定义业务调度线程池时Span 信息能够自动填充到 MDC 中使普通的业务日志输出与 TraceID 严格关联。public class ObservabilityConfiguration { PostConstruct public void initContextPropagation() { Hooks.enableAutomaticContextPropagation(); ContextRegistry.getInstance().registerThreadLocalAccessor( trace_context_accessor, MDC::getCopyOfContextMap, MDC::setContextMap, MDC::clear ); } }2. 多轮对话 Baggage 透传与业务租户关联在企业级 AI 应用中单次会话可能包含多轮问答。为了在链路后端精准分析某个终端用户在整个会话维度的累计 Token 消耗可以通过 OpenTelemetry Baggage 将tenant_id、session_id以及user_tier注入上下文。Baggage 会伴随所有子 Span 跨进程传播最终在 Collector 端被提取为时序指标的标签从而支撑多租户成本分摊与配额限流。生产级链路排障与根因分析路径当生产环境收到大模型交互卡顿或服务雪崩的告警时排障工程师可以按照以下标准化步骤逐层下钻定位定位慢链路 Trace 树在分布式追踪控制台中检索gen_ai.system openai-compatible且耗时超过阈值如大于 10 秒的请求。分析首字耗时TTFT与传输耗时占比如果gen_ai.response.ttft_ms占整体耗时的 85% 以上说明瓶颈在模型推理集群的显存排队、调度网关拥堵或供应商侧并发超限此时调优客户端网络没有意义。如果ttft_ms极小如 300ms但整体耗时极长需检查gen_ai.usage.completion_tokens。若生成的 Token 数异常偏高接近最大输出上限通常是因为提示词缺少约束导致模型出现循环幻觉或长文本输出。定位网络中断与客户端异常取消排查 Span 状态为 Cancelled 的链路。如果大量链路集中在连接建立后 1~2 秒内被取消通常是前端网关配置的 Read Timeout 过短或者前端用户由于无响应提示而频繁刷新页面导致的无效重试。核查 Token 激增异常源头按照tenant_id聚合gen_ai.usage.prompt_tokens。如果发现某一租户的输入 Token 突然飙升往往是因为上游业务在拼接历史记录时未设置合理的滑动窗口截断将数十轮历史上下文无损传入。架构选型对比与权衡在构建大模型观测平台时业界主要存在四种主流实现方案方案类别代表工具优势劣势适用场景OpenTelemetry 原生方案OTel Java SDK Prometheus Tempo协议标准化、无缝融入现有 APM 体系、零额外中间件维护成本需要自研部分 LLM 语义拦截器与前端看板定制已有成熟微服务 APM 基础设施的自研团队专用 LLM 观测平台LangSmith / Arize Phoenix开箱即用、提示词版本管理与评估体系完善、可视化能力极强数据外发存在合规风险、私有化部署成本高、与传统 RPC 链路脱节AI 原生应用创新团队、独立智能体系统网关层代理切面APISIX / Kong AI Gateway统一在网关层做 Token 统计与限流、对后端代码完全无侵入无法感知业务层内部的复杂 Agent 循环与工具调用链路多语言异构后端、仅作统一模型反向代理应用层日志切面自研 Logstash / ELK Pipeline实现极简单、业务自定义字段灵活度极高无法生成标准的分布式调用拓扑图无法精细化关联异步流式事件早期业务快速验证阶段生产落地避坑指南严禁在 Span 属性中无截断落盘全量 PromptOpenTelemetry Collector 对于单个 Span 的 Attribute 总字节数通常有默认限制如 8192 字节。若直接将动辄几万字的上下文写入属性会导致 Collector 内存剧烈膨胀甚至丢弃整条 Span。生产环境中Prompt 内容应仅在开启 Debug 级别时异步输出到加密审计日志Span 内部仅保留输入字符数与 Hash 校验和。防范客户端连接断开时的 Span 内存泄漏在流式长连接中如果前端用户关闭浏览器标签页Netty 连接会触发对端关闭。如果拦截器只在onComplete中闭合 Span未处理doOnCancel与doOnError信号会导致未完成的 Span 对象长久滞留在 Tracer 内存队列中最终引发老年代内存耗尽OOM。高频指标打点的基数爆炸Cardinality Explosion预防在将 Token 用量暴露给 Prometheus 时切勿将user_id或包含唯一值的prompt_id作为 Metric 的 Dimension 标签只能使用tenant_id、model_name等有限枚举维度的标签防止指标时序库因索引膨胀而崩溃。