大模型 Tool 调用的分布式链路追踪:基于 Micrometer Observation 打通 TraceId 贯穿
在将大模型与企业微服务架构深度整合后,很多运维和架构同学在排查线上慢请求时,经常会发出这种无力的咆哮:
“用户发起的 HTTP 请求进到智能客服网关,TraceId 记录得好好的;大模型流式返回给用户,TraceId 也在。但是!大模型在中间自主发起的那个查库存、核销优惠券的 Tool 接口,在 SkyWalking 和 Zipkin 链路追踪大盘上为什么彻底断掉了?在微服务集群里凭空多出了几千条没有上游 ParentId 的孤儿 Span,根本搞不清楚哪次 RPC 调用是由哪个大模型会话触发的!”
这种“链路失联”是大模型 Agent 落地中最令人抓狂的工程黑洞。
在传统的微服务调用中,Spring Cloud Sleuth 或 SkyWalking 探针会自动通过 HTTP Header 或者 RPC 隐式传参(RRC Attachment)把traceparent一路透传下去。
然而,在 Spring AI 的 Function Calling(Tool Calling)机制中,大模型决定调用 Tool 的过程往往跨越了异步线程池、响应式 Flux 管道以及反射调用上下文。原本保存在ThreadLocal里的分布式追踪上下文,在这一连串的异步穿梭中被全部清空!
下游微服务收到 Tool 的请求时,以为这是一个全新发起的孤立请求,全链路追踪体系当场瘫痪。
今天我们就基于 Spring Boot 3 最新的Micrometer Observation API,手把手实现大模型 Tool 调用的上下文自动染色,彻底打通 TraceId 的全链路贯穿。
一、Tool 链路断层的根本病因分析
要修好链路,先看它是在哪里断掉的:
[用户 HTTP 请求] (带 TraceId: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01) │ ▼ [Spring Boot 3 业务 Controller] (Trace 上下文存活于 ThreadLocal) │ ▼ (发起 Spring AI ChatClient 异步流式推理) [Reactor 工作线程池 (Schedulers.boundedElastic)] <--- 🚨 第一次上下文丢失 (Reactive Boundary) │ ▼ (大模型返回 ToolCall 指令: execute tool 'queryInventory') [Tool 执行器线程池 (ExecutorService / 虚拟线程)] <--- 🚨 第二次上下文丢失 (Async Thread Switch) │ ▼ [Tool 内部发起 Feign / Dubbo 远程调用] │ ▼ [下游微服务] (丢失 Parent Span! 产生孤儿 TraceId,链路彻底断裂)要在这一连串异步与响应式断层中坚守 TraceId,核心思路就是在每一个调度边界,通过Observation Scope(观测作用域)进行手动的捕获与恢复(Capture & Restore)。
二、基于 Micrometer Observation 构建链路染色传递器
Spring Boot 3 废弃了传统的 Sleuth,全面拥抱Micrometer Tracing与Micrometer Observation。
我们首先构建一个专门负责在异步 Tool 执行前恢复观测上下文的工具拦截器:
package com.yali.ai.tracing; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import io.micrometer.tracing.Tracer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.util.concurrent.Callable; @Component public class TraceableToolExecutor { private static final Logger log = LoggerFactory.getLogger(TraceableToolExecutor.class); private final ObservationRegistry observationRegistry; private final Tracer tracer; public TraceableToolExecutor(ObservationRegistry observationRegistry, Tracer tracer) { this.observationRegistry = observationRegistry; this.tracer = tracer; } /** * 在带有分布式链路上下文的受保护环境下执行 Tool 逻辑 */ public <T> T executeWithObservation(String toolName, Callable<T> task) { // 创建一个专属于当前 Tool 调用的 Observation 子 Span Observation toolObservation = Observation.createNotStarted("spring.ai.tool.execution", observationRegistry) .lowCardinalityKeyValue("ai.tool.name", toolName) .contextualName("AI-Tool: " + toolName); // 记录当前 TraceId 用于排查日志 String currentTraceId = tracer.currentSpan() != null ? tracer.currentSpan().context().traceId() : "NONE"; log.info("【Tool 链路染色】开启 Tool 执行观测 Span: tool=[{}], traceId=[{}]", toolName, currentTraceId); return toolObservation.observe(() -> { try { // 此时 Observation 会自动把当前 Span 绑定到当前线程的 ThreadLocal // 内部发起的 OpenFeign / WebClient / RestTemplate 能够原生提取并注入 W3C TraceContext return task.call(); } catch (Exception e) { toolObservation.error(e); log.error("Tool 执行链路异常: tool=[{}], err=[{}]", toolName, e.getMessage()); throw new RuntimeException(e); } }); } }三、与 Spring AI @Tool 注解的无缝切面整合
为了不侵入具体的业务 Tool 代码,我们使用 Spring AOP 针对所有标记了@Tool注解的方法进行统一的切面编织与链路染色:
package com.yali.ai.tracing.aspect; import com.yali.ai.tracing.TraceableToolExecutor; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.ai.tool.annotation.Tool; import org.springframework.stereotype.Component; @Aspect @Component public class ToolTracingAspect { private final TraceableToolExecutor toolExecutor; public ToolTracingAspect(TraceableToolExecutor toolExecutor) { this.toolExecutor = toolExecutor; } /** * 拦截所有标注了 @Tool 注解的方法执行 */ @Around("@annotation(toolAnnotation)") public Object traceToolExecution(ProceedingJoinPoint joinPoint, Tool toolAnnotation) throws Throwable { String toolName = toolAnnotation.name().isEmpty() ? joinPoint.getSignature().getName() : toolAnnotation.name(); // 借由 TraceableToolExecutor 包装执行,打通链路上下文 return toolExecutor.executeWithObservation(toolName, () -> { try { return joinPoint.proceed(); } catch (Throwable e) { if (e instanceof Exception ex) { throw ex; } throw new RuntimeException(e); } }); } }四、打通 HTTP/Feign 下游调用的 Header 透传
有了上面的 Observation 包装,当下游通过 OpenFeign 发起真实 RPC 时,Spring Cloud OpenFeign 会自动配置FeignObservationAnnotationBPP,向 HTTP 请求头中注入标准的 W3C TraceContext:
POST /api/inventory/deduct HTTP/1.1 Host: inventory-service.internal traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-5c3a4f89d34211ef-01 tracestate: Content-Type: application/json在 SkyWalking / Zipkin 监控拓扑大屏上,原本断开的链路瞬间串联成一条优美的树状调用链:
[GET /api/ai/chat] (Web Entry, traceId: 4bf92f3577b34da6a3ce929d0e0e4736) │ ├── [Spring AI Model Inference: GLM 5.3] (耗时 850ms) │ └── [AI-Tool: deductInventory] (SpanId: 5c3a4f89d34211ef) │ └── [POST inventory-service:/api/inventory/deduct] (下游微服务, 耗时 32ms) │ └── [MySQL: UPDATE t_inventory ...] (耗时 2ms)整个会话期间:大模型哪一毫秒决定调 Tool、Tool 在本地花了多长时间准备参数、下游微服务执行了哪条 SQL、耗时多久,全部在同一个 Trace 界面下一目了然!
五、老鸭梨的运维实操心法
把全链路追踪接入大模型,有三个关键点必须落地:
- 为大模型流式推理打上自定义 Tag:
在 Observation 中除了记录默认指标,务必打上业务专有标签,如ai.model.name=glm-5.3、ai.prompt.tokens=1250。当下游链路慢的时候,可以瞬间过滤出是某款特定模型导致的慢调用,还是公共网络问题; - 虚拟线程模式下的上下文传递:
如果你启用了 Java 24 虚拟线程,务必在配置中开启 Micrometer 的虚拟线程上下文传播器(ContextRegistry.getInstance().registerThreadLocalAccessor(...)),否则虚拟线程在卸载挂载时依然可能丢失 MDC 日志打点; - 注意日志脱敏与 Token 保护:
TraceSpan 的 Tag 中严禁把大模型的完整用户隐私 Prompt(如包含用户身份证、手机号)作为 Tag 裸传给可观测系统。这些系统通常是明文存储且全公司可查的,做好合规脱敏才能安稳度过大促; - 自适应动态链路采样率(Adaptive Sampling Rate):
在大促巅峰期,全站 QPS 暴涨数十倍,如果对所有 AI 交互与 Tool Calling 进行 100% 全量 Span 采集上报,可观测平台(如 Elasticsearch / OTel Collector)的存储集群会在半小时内被直接写爆。生产标准实践是:平时配置 10% 的基础采样率,同时配置尾部自适应采样(Tail-based Sampling)。只要单次 Tool 调用耗时超过 500ms,或者发生任意未捕获异常(HTTP 5xx、RPC 失败),该条链路强制 100% 提升为高优先级全量上报,实现兼顾系统低开销与故障 100% 可回溯的完美平衡。
把黑盒的 AI 对话拆解为可度量、可追踪、可诊断的标准化分布式链路,是大模型应用真正走向工业级成熟的成人礼。遵循这套工程范式,当线上再次出现慢调用或故障时,你将永远握有第一时间的根因掌控权。