ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java 程序员第 46 阶段11:大模型调用链路追踪,SkyWalking 排查线上性能,大模型 Token 耗时拆解请求推理回调各阶段 Span 标记实战

Java 程序员第 46 阶段11:大模型调用链路追踪,SkyWalking 排查线上性能,大模型 Token 耗时拆解请求推理回调各阶段 Span 标记实战 当大模型LLM接口接入业务系统后最常被问到的一个问题是这条请求为什么这么慢 很多同学第一反应是去看模型厂商的 RT响应时间或者去翻网关日志。但大模型调用的耗时并不是一个简单的发出到收到区间它内部至少由三段构成请求构造与网络传输、模型推理含首 Token 等待 TTFT、以及流式回调与结果解析。如果只用一条整体 Span 把整个调用包起来线上一旦变慢你根本分不清是网络抖了、模型排队了还是你的后处理逻辑卡住了。本篇要解决的问题就是借助 SkyWalking 的 Span 模型把一次大模型调用自上而下拆成请求 / 推理 / 回调三层 Span并且用实战代码把每一层的耗时、Token 量、模型名等关键属性打上标签。读完后你将能用 SkyWalking UI 直接看到某次慢请求到底慢在哪一段从而精准定位瓶颈。为什么需要拆解大模型调用的 Token 耗时SkyWalking 中 Span 的层级模型三阶段耗时拆解实战请求、推理、回调代码实战基于 SkyWalking Java Agent 自定义 SpanSpring Boot 中集成与配置在 UI 中分析各阶段耗时与定位瓶颈最佳实践与常见坑1. 为什么需要拆解大模型调用的 Token 耗时在传统的 HTTP 微服务调用里一次 RPC 的耗时基本等同于建连 发送 服务端处理 接收。但大模型调用有它非常独特的结构集中体现在以下三点第一首 Token 延迟TTFTTime To First Token与总耗时TTLTTime To Last Token是两个完全不同的指标。流式SSE接口下用户往往在几秒内就看到第一个字但整段文本可能十几秒才吐完。如果只记录整体耗时你无法区分模型开始生成慢还是模型生成量大。第二请求阶段本身也有成本。构造 Prompt、做 RAG 检索、把对话历史序列化、做敏感词过滤这些都可能消耗数百毫秒却发生在调用模型之前。把它们和模型推理混在一起会让你误判模型性能。第三回调阶段容易被忽视。流式响应到达后业务侧要做 JSON 解析、内容拼接、向量落库、消息推送。这些后处理在大并发下会成为隐性瓶颈。因此合理的可观测性设计是把一次大模型调用拆成三段独立 SpanRequest Span从 Prompt 构造完成到请求字节发出含建连、TLS 握手、上行传输。Inference Span从请求发出到收到首个 Token 的响应头再到响应体流结束记录 TTFT 与 TTLT。Callback Span从响应体解析完成到业务消费结束落库、推送、回调上游。2. SkyWalking 中 Span 的层级模型SkyWalking 的链路模型核心是 Trace 与 Span。一个 Trace 由一次完整请求触发内部包含一棵 Span 树。每个 Span 有类型Entry Span链路入口比如被外部 HTTP 调用的 Controller 方法。Exit Span调用下游服务时产生比如用 HTTP Client 调用模型网关。Local Span进程内本地方法耗时我们用它来标记推理回调这类不跨网络的子过程。父 Span 通过 parentSpanId 串联子 Span 的起止时间必然落在父 Span 区间内。SkyWalking 在 UI 的追踪列表里会把一个 Trace 渲染成瀑布图Waterfall每一层缩进代表 Span 层级这正是我们分析三段耗时的利器。对于大模型调用我们建议的 Span 树结构是Trace (用户请求)└── EntrySpan: POST /api/chat├── LocalSpan: prompt.build (构造 Prompt RAG 序列化)├── ExitSpan: http - llm-gateway (请求发出)│ └── LocalSpan: llm.inference (TTFT 到 TTLT含流式解析)└── LocalSpan: llm.callback (落库 推送 上游回调)注意 inference 作为 Exit Span 的子 Span 更合理因为 Exit Span 已经代表了网络往返而推理等待发生在这一网络往返内部把它挂到 Exit 下面可以在 UI 上直观看到网络段里推理段占了多少。3. 三阶段耗时拆解实战请求、推理、回调下面给出一段端到端的时序帮助建立心智模型。假设用户调用业务接口业务先拼 Prompt再请求模型网关网关转发到厂商厂商流式返回业务逐块回调处理。用户 业务服务(Instrumented) 模型网关 模型厂商| | | ||--POST /chat---| | || |--prompt.build(本地Span)--| || |--http exit(请求Span)----| || | |--转发----------|| | | | 排队推理| |--首Token(响应头)---------|---------------|| |llm.inference(本地Span)| || |--流式Token块(响应体)------|---------------|| |--llm.callback(本地Span)---| ||--SSE 流------| | |关键埋点位置请求段在进入 HTTP 客户端发送前 createExitSpan记录 model、endpoint、requestTokens预估。推理段捕获首字节到达时刻算 TTFT捕获流结束时刻算 TTLT记录 completionTokens、ttft、ttlt。回调段每个 Token 块消费完成后整体业务处理结束关闭 callback Span记录 callbackCost。4. 代码实战基于 SkyWalking Java Agent 自定义 SpanSkyWalking 的 Java Agent 提供了 org.apache.skywalking.apm.toolkit.trace 包需引入 apm-toolkit-trace 依赖以及 TraceContext / ActiveSpan API方便在业务代码里手动创建 Local Span 和打标签。首先在 pom.xml 引入 toolkitprovided 即可运行期由 agent 注入dependencygroupIdorg.apache.skywalking/groupIdartifactIdapm-toolkit-trace/artifactIdversion9.7.0/versionscopeprovided/scope/dependency核心工具类封装三层埋点。下面示范如何用 ActiveSpan 与 Span 对象手动记录三段耗时package com.demo.llm.trace;import org.apache.skywalking.apm.toolkit.trace.ActiveSpan;import org.apache.skywalking.apm.toolkit.trace.TraceContext;import java.util.function.Supplier;public final class LlmSpanUtil {/** 请求段标记模型与请求方向 */public static void tagRequest(String model, String endpoint, int requestTokens) {ActiveSpan.tag(llm.phase, request);ActiveSpan.tag(llm.model, model);ActiveSpan.tag(llm.endpoint, endpoint);ActiveSpan.tag(llm.requestTokens, String.valueOf(requestTokens));}/** 推理段在首Token到达时调用记录TTFT */public static void markFirstToken(long startNanos, int completionTokens) {long ttftMs (System.nanoTime() - startNanos) / 1_000_000;ActiveSpan.tag(llm.phase, inference);ActiveSpan.tag(llm.ttft, ttftMs ms);ActiveSpan.tag(llm.completionTokens, String.valueOf(completionTokens));}/** 回调段记录回调处理耗时 */public static void tagCallback(long callbackCostMs) {ActiveSpan.tag(llm.phase, callback);ActiveSpan.tag(llm.callbackCost, callbackCostMs ms);}/** 统一包裹一次本地Span */public static T T aroundLocal(String operation, SupplierT body) {// 由 agent 在调用处自动创建 LocalSpan配合 Trace 注解return body.get();}}在调用大模型的核心方法上用 Trace 注解开启一个本地 Span并在内部用 ActiveSpan 打标package com.demo.llm.service;import org.apache.skywalking.apm.toolkit.trace.Trace;import org.apache.skywalking.apm.toolkit.trace.ActiveSpan;import org.springframework.stereotype.Service;Servicepublic class ChatService {Trace(operationName llm.inference)public String chat(String model, String prompt) {long start System.nanoTime();// 1) 请求段LlmSpanUtil.tagRequest(model, https://gw.internal/v1/chat, estimateTokens(prompt));// 2) 推理段含TTFTStringBuilder sb new StringBuilder();boolean first true;for (String token : LlmClient.stream(model, prompt)) {if (first) {LlmSpanUtil.markFirstToken(start, token.length());first false;}sb.append(token);}// 3) 回调段long cbStart System.nanoTime();persist(sb.toString());LlmSpanUtil.tagCallback((System.nanoTime() - cbStart) / 1_000_000);return sb.toString();}private int estimateTokens(String s) { return s.length() / 2; }private void persist(String s) { /* 落库/推送 */ }}5. Spring Boot 中集成与配置要让上面的埋点生效需要在启动参数里挂载 SkyWalking Agent并配置好服务名与后端地址。agent.config 关键项config/agent.configagent.service_name: ${SW_AGENT_NAME:llm-business}collector.backend_service: ${SW_AGENT_COLLECTOR_BACKEND_SERVICES:oap:11800}plugin.toolkit.log: ${SW_PLUGIN_TOOLKIT_LOG:INFO}启动脚本中挂载 agentjava -javaagent:/opt/skywalking/agent/skywalking-agent.jar \-DSW_AGENT_NAMEllm-business \-DSW_AGENT_COLLECTOR_BACKEND_SERVICESoap:11800 \-jar llm-business.jar对于 OpenAI 风格或 OKHttp 客户端SkyWalking 自带的 HTTP 插件会自动生成 Exit Span无需手写。但大模型推理等待发生在 Exit Span 内部因此我们额外用 Trace 的 Local Span 把推理单独剥出来才能在 UI 上把网络往返里的模型推理单独高亮。如果你使用的是 Spring AI 的 ChatClient可以在 ChatClient 外层包裹一个 Trace 方法再配合 Advisor 机制在 before / after 回调里打 requestTokens / completionTokens 标签做到零侵入埋点。6. 在 UI 中分析各阶段耗时与定位瓶颈部署并触发几次调用后打开 SkyWalking UI → 追踪按服务 llm-business 过滤点开一条 Trace你会看到瀑布图里清晰地排列着 prompt.build、http - llm-gateway、llm.inference、llm.callback 四段。定位瓶颈的标准动作若 prompt.build 占比异常高超过 30%说明 RAG 检索或历史拼装是问题应优化向量检索或裁剪上下文。若 llm.inference 中 ttft 高但总量小通常是模型冷启动或网关排队联系模型侧排查并发配额。若 llm.callback 高多半是逐 Token 落库或同步推送阻塞应改为异步批量。若 http - llm-gateway 的 Exit Span 总时长远大于内部 llm.inference说明网络或网关转发有损耗。下表给出常见现象与归因的快速对照现象高占比 Span可能原因排查方向------------用户等很久才出第一个字llm.inference / ttft模型排队、冷启动网关并发配额、模型预热出字后迟迟不结束llm.inference / ttlt生成 token 过多控制 max_tokens、精简 Prompt接口整体慢但推理快prompt.buildRAG/序列化重向量库索引、缓存上下文推理结束但响应慢llm.callback同步落库/推送改异步、批量写入网络抖动http - gateway (exit)跨机房、TLS就近接入、连接池7. 最佳实践与常见坑实践中踩过的坑总结如下第一不要滥用 Local Span。每多一层 Span 都会增加 OAP 的存储与聚合压力。只拆你真正会去分析的层建议稳定为 三到四层 即可避免给每个 Token 块都建 Span。第二Tag 值要可枚举、低基数。把 userId、完整 Prompt 这类高基数内容写进 Tag会让 OAP 的 tag 索引爆炸。应只记录 model、phase、ttft 这类有限取值完整内容走日志关联见下一篇。第三TTFT 与 TTLT 务必成对上报。单独看 TTLT 容易误判例如 2 秒出首字、总共 12 秒和 8 秒出首字、总共 9 秒瓶颈完全不同。第四流式场景注意 Span 生命周期。流式响应可能跨多个线程Netty EventLoop 读、业务线程消费务必保证 ActiveSpan 的打标发生在与创建 Span 相同的上下文中或用 TraceContext 传递 traceId 跨线程续接。第五采样率要权衡。全量采集大模型链路在高峰会非常吃存储建议对正常请求抽样如 1/10对慢请求ttft 超阈值或异常请求 100% 采集可借助 SkyWalking 的慢 Span 采样配置。最后给出一段三段耗时的推荐 Tag 清单作为团队规范沉淀Tag 名含义类型---------llm.phase阶段request/inference/callback枚举llm.model模型标识枚举llm.requestTokens请求 token 估算数值llm.completionTokens输出 token 数数值llm.ttft首 token 延迟(ms)数值llm.ttlt末 token 延迟(ms)数值llm.callbackCost回调处理耗时(ms)数值把这套 Span 结构落地后你再面对大模型接口慢的工单就不必靠猜而是直接打开 SkyWalking 看哪一段最长的瀑布条对症下药。
返回列表