Java 程序员第 46 阶段05:大模型调用链路追踪,SkyWalking 排查线上性能,Trace与Span建模规范

- Trace 与 Span 基本概念
- Span 类型:Entry / Exit / Local
- 大模型链路中的 Span 建模
- Tag 标签规范设计
- Log 与 Tag 最佳实践
- 跨进程与跨线程上下文传播
- 标签规范落地建议
1. Trace 与 Span 基本概念

链路追踪的基本数据模型只有两个核心对象:**Trace** 与 **Span**。把一次用户请求想象成一棵"调用树",树的根是 Trace,树的每个节点是 Span。
1.1 Trace
`Trace` 代表一次完整请求的端到端路径,由全局唯一的 `TraceId` 标识。一个 Trace 包含若干按层级嵌套的 Span。
1.2 Span
`Span` 代表调用树中的一个"节点",即某段工作(一次方法调用、一次 RPC、一次 DB 查询)。关键属性:
|
属性 |
含义 |
|
--- |
--- |
|
spanId |
当前 Span 唯一 ID |
|
parentSpanId |
父 Span ID(根 Span 为 -1) |
|
operationName |
操作名,如 `HTTP/POST`、`LLM/gpt-4o` |
|
startTime / endTime |
起止时间(计算耗时) |
|
tags |
键值对标签 |
|
logs |
带时间戳的事件 |
1.3 一段最小 Trace
TraceId = abc123
└─ Span#1 (Entry, /chat/ask) parent=-1
├─ Span#2 (Local, buildPrompt) parent=1
├─ Span#3 (Exit, vectorSearch) parent=1
└─ Span#4 (Exit, LLM/gpt-4o) parent=1
2. Span 类型:Entry / Exit / Local

SkyWalking 把 Span 分为三种语义类型,理解它们对正确建模至关重要。
2.1 EntrySpan(入口 Span)
服务端**接收**请求时创建,是链路的"入口"。例如 HTTP 服务端收到请求、MQ 消费者拉到消息。一个线程同一时刻只允许有一个活跃的 EntrySpan。
// 由 Spring MVC 插件自动创建 EntrySpan
@PostMapping("/chat/ask")
public Response ask(...) { ... }
2.2 ExitSpan(出口 Span)
客户端**发起**下游调用时创建,是链路的"出口"。例如调用 HTTP 下游、访问数据库、调用远端大模型。ExitSpan 负责把上下文注入到请求中,实现跨进程续接。
AbstractSpan span = ContextManager.createExitSpan(
"LLM/gpt-4o", carrier, "llm-gateway:443");
2.3 LocalSpan(本地 Span)
进程内的一段重要工作,既不是入口也不是出口,如"组装 Prompt""重排序"。用 `@Trace` 或 `createLocalSpan` 创建,用于细分业务耗时。
AbstractSpan span = ContextManager.createLocalSpan("rerank");
三种类型的关系见图 figure_05_2。
3. 大模型链路中的 Span 建模

大模型链路比传统 RPC 复杂,建模不当会导致"慢段被吞掉"。推荐的分段策略:
3.1 必建模的四段
|
段 |
类型 |
operationName |
关注点 |
|
--- |
--- |
--- |
--- |
|
接口入口 |
Entry |
`/chat/ask` |
总耗时 |
|
检索(RAG) |
Exit |
`VectorSearch` |
向量时延 |
|
提示词组装 |
Local |
`buildPrompt` |
渲染耗时 |
|
大模型推理 |
Exit |
`LLM/{model}` |
TTFT / Token / 成本 |
3.2 流式响应的双阶段建模
大模型流式输出要把"首字时延"和"完整生成"拆开,否则一个超长 Span 掩盖问题:
// 阶段一:等待首 token(TTFT)
AbstractSpan waitSpan = ContextManager.createLocalSpan("LLM.waitTTFT");
long ttft = ...; ActiveSpan.tag("llm.ttft.ms", String.valueOf(ttft));
ContextManager.stopSpan();
// 阶段二:流式输出(TPOT 由总时长 - TTFT 推算)
AbstractSpan genSpan = ContextManager.createLocalSpan("LLM.streamOutput");
while (hasNextToken()) { consume(token); }
ContextManager.stopSpan();
4. Tag 标签规范设计
Tag 是让链路"可分析"的关键。没有规范的 Tag,追踪数据就是一堆无意义的字符串。
4.1 命名规范
- **统一前缀**:大模型相关一律用 `llm.` 前缀,便于过滤与聚合。
- **小写点分**:`llm.model`、`llm.token.prompt`,禁止空格与中文。
- **值类型稳定**:同一 key 的值类型保持一致(数字就是数字,不要混字符串)。
- **避免全链路高频写大对象**:Tag 会随 Span 上报存储,禁止把整段对话内容塞进 Tag。
4.2 大模型必带 Tag 清单
|
Tag Key |
含义 |
示例 |
|
--- |
--- |
--- |
|
`llm.model` |
模型名 |
`gpt-4o` |
|
`llm.token.prompt` |
输入 Token 数 |
`1280` |
|
`llm.token.completion` |
输出 Token 数 |
`540` |
|
`llm.token.total` |
总 Token 数 |
`1820` |
|
`llm.ttft.ms` |
首字时延 |
`860` |
|
`llm.cost.usd` |
调用成本 |
`0.021` |
|
`llm.provider` |
供应商 |
`openai` |
|
`llm.is_stream` |
是否流式 |
`true` |
4.3 错误与降级 Tag
ActiveSpan.tag("llm.error.code", "429");
ActiveSpan.tag("llm.retry.count", "2");
ActiveSpan.tag("llm.fallback", "true"); // 是否降级到小模型
ActiveSpan.error(e); // 标记 Span 错误状态
5. Log 与 Tag 最佳实践
Tag 适合"结构化、定长的维度",Log 适合"带时间戳的事件明细"。
5.1 何时用 Tag,何时用 Log
|
场景 |
选用 |
原因 |
|
--- |
--- |
--- |
|
过滤/聚合维度(模型、耗时) |
Tag |
可索引、可分组 |
|
异常堆栈 |
Log |
文本明细,非聚合 |
|
单次调用的 Token 数 |
Tag |
数值,便于求和 |
|
调试用的中间变量 |
Log |
偶发,不常查 |
5.2 代码中的正确姿势
try {
CompletionResult r = llmClient.complete(model, prompt);
ActiveSpan.tag("llm.token.total", String.valueOf(r.totalTokens()));
} catch (RateLimitException e) {
ActiveSpan.tag("llm.error.code", "429");
ActiveSpan.log("rate limited, will retry"); // 事件日志
ActiveSpan.error(e);
throw e;
}
> 最佳实践:Tag 控制在 10 个以内、值尽量短;把长文本、堆栈放 Log;两者都通过 TraceId 与 Logging 系统打通。
6. 跨进程与跨线程上下文传播
链路要"连续不断",靠的是上下文传播。这是建模规范里最容易被忽视、却最重要的一环。
6.1 跨进程传播(ContextCarrier)
上游 ExitSpan 把 TraceId/SpanId 注入请求头(SkyWalking 用 `sw8` 头),下游 EntrySpan 解析并续接:
// 上游
ContextCarrier carrier = new ContextCarrier();
ContextManager.inject(carrier, httpRequest::setHeader);
// 下游
ContextManager.extract(carrier, httpRequest::getHeader);
6.2 跨线程传播(RunnableWrapper / CallableWrapper)
线程池、异步编排会丢失上下文,必须用包装器:
executor.submit(RunnableWrapper.of(() -> callLLM(model, prompt)));
executor.submit(CallableWrapper.of(() -> generate(model, prompt)));
图 figure_05_3 展示了上下文如何在进程与线程间"接力"。
7. 标签规范落地建议
规范只有落地才有价值。给出三步走落地法:
7.1 封装 SDK,统一埋点
不要在每个业务里手写 `ActiveSpan.tag`,而是封装一个 `LlmTracer` 工具类,强制带上全部必填 Tag:
public final class LlmTracer {
public static <T> T trace(String model, Supplier<T> call) {
AbstractSpan span = ContextManager.createExitSpan("LLM/" + model,
new ContextCarrier(), "llm-gateway:443");
try {
ActiveSpan.tag("llm.model", model);
ActiveSpan.tag("llm.provider", "openai");
T result = call.get();
// 自动补充 token / ttft / cost
return result;
} finally {
ContextManager.stopSpan();
}
}
}
7.2 用 OAL 把 Tag 变指标
在 OAP 的 `oal` 中基于 Tag 定义指标,让"最贵调用""最慢模型"可看板化:
llm_cost_sum = from(ExitSpan.tag("llm.cost.usd")).sum();
llm_ttft_avg = from(ExitSpan.tag("llm.ttft.ms")).longAvg();
llm_token_sum = from(ExitSpan.tag("llm.token.total")).longSum();
7.3 建立评审清单
- [ ] 每个大模型调用都有 `llm.model` 与 `llm.token.total`
- [ ] 流式调用拆分 TTFT 与输出两段
- [ ] 异步调用全部用 Wrapper 包装
- [ ] 错误分支打 `llm.error.code` 并 `ActiveSpan.error`
- [ ] Tag 使用 `llm.` 前缀,小写点分命名
小结
本篇系统给出了大模型链路的建模规范:Trace 是端到端路径、Span 分 Entry/Exit/Local 三类;大模型链路必须显式建模"检索—组装—推理"各段,流式响应拆分为 TTFT 与输出两段;Tag 用 `llm.` 前缀、小写点分、类型稳定的命名,覆盖模型、Token、时延、成本与错误;并通过 ContextCarrier / Wrapper 保证跨进程跨线程传播。至此,第 46 阶段"大模型调用链路追踪"的五篇文章已全部完成,你已具备从原理到落地、从接入到规范的全栈能力。
更多推荐
所有评论(0)