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

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 阶段"大模型调用链路追踪"的五篇文章已全部完成,你已具备从原理到落地、从接入到规范的全栈能力。

更多推荐