大模型后端上线后,怎样把慢调用拆开看
大模型后端上线后,怎样把慢调用拆开看
下面以一次模拟排障为例:网关的尾延迟持续升高,需要区分检索、模型调用和工具执行分别占用了多少时间。
常规链路追踪往往只把一次请求记成一个很长的 HTTP Span。此时无法区分时间耗在检索、模型调用还是工具执行,也无法看出是否发生了不必要的重试。下面讨论的是用于设计观测点的排查模型,具体阈值应由服务的容量和用户等待预期决定。
这就是大模型服务集成的典型痛点。当系统从传统的确定性 RPC 调用演变为多轮交互的 Agent 链条,基于传统 HTTP/RPC 协议栈的链路追踪体系瞬间失灵。要彻底排查这类问题,必须在工程层将 LLM 语义拆解为粒度极细的 Trace 节点。
1. 传统 APM 探针失灵的根因拆解
在传统的微服务架构中,一个 RPC 请求的生命周期是确定且线性的。调用方发出 Request,提供方返回 Response,中间的数据库与缓存操作都有明确的客户端 Client 拦截器进行 Span 记录。
大模型 Agent 的运行逻辑截然不同。
一个用户的 Prompt 提交给 Agent 后,内部可能会经历以下过程:
- 向量数据库(Vector DB)进行 3 次相似度检索,拼接 Context;
- 首次调用 LLM API,模型返回
tool_calls指令; - 网关执行本地工具(如查询 MySQL 订单表);
- 将工具返回结果二次打包给 LLM API;
- LLM API 以 Server-Sent Events (SSE) 流式返回最终结果。
如果只记录网关入口和出口,这 5 个内部步骤的时间花在哪儿完全不可见。更危险的是,若 LLM 陷入 Tool Calling 死循环,网关内存和连接数会在极短时间内被撑爆。
我们需要重新定义大模型语义下的 OpenTelemetry 属性字段(Semantic Conventions),将 Prompt Token 数量、Completion Token 数量、模型名称以及 Tool 调用的上下文强制注入到 Trace Span 中。
2. Agent 链条全链路 Trace 模型设计
为了厘清大模型服务集成的物理调用关系,我们将 Trace 模型划分为三层结构:
- Gateway Main Span:记录整个大模型请求的生命周期与总耗时。
- Child Spans:分别记录
rag.retrieval(知识库检索)、llm.chat_completion(大模型交互)与agent.tool_execution(本地工具执行)。 - Stream Metrics:记录 SSE 首字延迟(TTFT, Time To First Token)与吐字速率(Tokens/sec)。
请求进入网关后,应在服务调用、异步任务和响应返回的每个节点持续透传 Trace 上下文。
3. 基于 OpenTelemetry Java SDK 的生产级埋点实现
在 Spring Boot 3.x 架构下,我们通过实现自定义的 OpenTelemetry Tracer 与 ClientHttpRequestInterceptor,拦截所有的 HTTP 请求与 SSE 流,准确采集 LLM 元数据。
以下为生产环境落地的核心拦截器代码:
package com.example.llm.gateway.observability;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Component
public class LlmTracingInterceptor implements ClientHttpRequestInterceptor {
private final Tracer tracer;
public LlmTracingInterceptor(Tracer tracer) {
this.tracer = tracer;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
// 只有针对 LLM API 的请求才注入专门的 LLM 语义 Span
if (!request.getURI().getPath().contains("/chat/completions")) {
return execution.execute(request, body);
}
Span span = tracer.spanBuilder("llm.chat_completion")
.setAttribute("gen_ai.system", "DeepSeek")
.setAttribute("gen_ai.request.model", "deepseek-chat")
.setAttribute("gen_ai.request.temperature", 0.7)
.startSpan();
try (Scope scope = span.makeCurrent()) {
// 解析请求体中的 Prompt 字节数,避免全量解析 JSON 损耗性能
span.setAttribute("gen_ai.request.prompt_bytes", body.length);
long startTime = System.currentTimeMillis();
ClientHttpResponse response = execution.execute(request, body);
long latency = System.currentTimeMillis() - startTime;
span.setAttribute("gen_ai.response.latency_ms", latency);
span.setAttribute("http.status_code", response.getStatusCode().value());
// 提取响应 Header 中的真实 Token 统计(如 Provider 支持)
String promptTokens = response.getHeaders().getFirst("x-llm-prompt-tokens");
String completionTokens = response.getHeaders().getFirst("x-llm-completion-tokens");
if (promptTokens != null) {
span.setAttribute("gen_ai.usage.prompt_tokens", Long.parseLong(promptTokens));
}
if (completionTokens != null) {
span.setAttribute("gen_ai.usage.completion_tokens", Long.parseLong(completionTokens));
}
span.setStatus(StatusCode.OK);
return response;
} catch (Exception ex) {
span.recordException(ex);
span.setStatus(StatusCode.ERROR, "LLM API Call Failed: " + ex.getMessage());
throw ex;
} finally {
span.end();
}
}
}
配合 Micrometer 自定义 Metrics,我们将 Token 消耗和首字延迟推送到 Prometheus:
package com.example.llm.gateway.observability;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class LlmMetricsCollector {
private final Counter promptTokenCounter;
private final Counter completionTokenCounter;
private final Timer ttftTimer;
public LlmMetricsCollector(MeterRegistry registry) {
this.promptTokenCounter = Counter.builder("llm.tokens.prompt.total")
.description("Total prompt tokens consumed")
.register(registry);
this.completionTokenCounter = Counter.builder("llm.tokens.completion.total")
.description("Total completion tokens generated")
.register(registry);
this.ttftTimer = Timer.builder("llm.response.ttft.latency")
.description("Time To First Token latency")
.publishPercentiles(0.5, 0.95, 0.99)
.register(registry);
}
public void recordTokens(long prompt, long completion) {
promptTokenCounter.increment(prompt);
completionTokenCounter.increment(completion);
}
public void recordTTFT(long durationMs) {
ttftTimer.record(durationMs, TimeUnit.MILLISECONDS);
}
}
4. 现场排障过程与生产终端调试命令
探针上线后,当报警再次响应时,工程师可以通过跳板机直接使用 Shell 命令和 OpenTelemetry 导出日志进行准确定位。
首先,检查 Prometheus 输出的 LLM 网关可观测指标口径:
# 查询当前网关节点实时暴露的 LLM 指标,过滤 Token 计数与延迟
curl -s "$SERVICE_BASE_URL/actuator/prometheus" | grep llm_
# 输出结果示例:
# llm_tokens_prompt_total 452810.0
# llm_tokens_completion_total 128490.0
# llm_response_ttft_latency_seconds_max 6.42
如果发现 ttft_latency 最大值达到 6.4 秒,说明大模型服务提供商本身响应缓慢。但如果 ttft 正常,而总耗时极长,则需要进一步抓取 Jaeger/Tempo 中的 TraceId。
在 Linux 终端通过 Grep 在网关日志中追查特定的异常 Trace:
# 过滤包含超时异常且带有 TraceId 的请求日志
grep "llm.chat_completion" /var/log/llm-gateway/app.log | grep "ERROR" -B 2 -A 5
# 使用 bpftrace 检查 JVM 网关进程与远端 LLM API (443 端口) 的 TCP 建立连接与 RTT 耗时
sudo bpftrace -e 'kprobe:tcp_v4_connect { $sk = (struct sock *)arg0; @start[tid] = nsecs; } kretprobe:tcp_v4_connect /@start[tid]/ { $lat = nsecs - @start[tid]; printf("Connect latency: %d ms\n", $lat / 1000000); delete(@start[tid]); }'
如果追踪显示模型首字延迟正常而工具 Span 反复出现,就应检查格式校验失败后的重试策略。不要先假定问题在模型侧;先用同一请求的 Span 顺序、错误类别和重试记录复现,再调整重试上限或回退路径。
5. 生产环境告警收口与观测防线
有了细粒度的可观测数据后,告警规则不能再粗暴地只对网关 HTTP P99 延迟进行报警。必须建立分层防线:
-
第一道防线:LLM Provider 状态告警
当llm.response.ttft.latency的 P99 超过 4000ms 时,自动触发 API 提供商降级熔断,切换到备用模型节点。 -
第二道防线:Agent 死循环闸门
在 Trace 拦截器中计数,单次 Request 内agent.tool_execution的 Span 数量超过 5 个时,强制终止 Trace 并抛出ToolLoopException,不再浪费 Token 预算。 -
第三道防线:Token 异常飙高拦截
监控单次请求gen_ai.usage.prompt_tokens> 16384 的异常 Request,直接熔断并记录快照日志,防范恶意 Prompt 攻击拖垮网关内存。
通过这三层观测防线的建立,大模型服务不再是一个黑盒。排障时间从过去的上百分钟缩短到了 5 分钟以内。
更多推荐


所有评论(0)