怎么给 LLM 调用链做链路追踪?

进阶实践原理约 10 分钟读完

一句话回答

一个 Trace 代表一次完整的用户请求,由多个 Span 组成,每个 Span 是其中的一步操作(一次检索、一次模型调用、一次工具调用),记录起止时间、输入输出和属性,Span 之间的父子关系组成一棵树。LLM 应用的一次请求往往包含查询改写、检索、多轮模型调用和工具调用,用嵌套的 Span 记录下来,就能看清每一步花了多少时间和 token、在哪一步出了错。实现上可以用 OpenTelemetry 埋点(它有面向生成式 AI 的语义约定),也可以用 Langfuse、LangSmith 这类专门的 LLM 观测平台。

详细解析

Trace 和 Span

  • Trace:一次请求的完整链路,其中所有 Span 共享同一个 trace ID
  • Span:一个操作,包含名称、开始和结束时间、父 Span 的 ID、属性(键值对)、事件和状态(默认是 Unset,出错时设为 Error)
  • 上下文传播:同一进程内,通过上下文对象把当前 Span 传给下一层;跨服务时通过请求头传递(W3C Trace Context 标准的 traceparent 头),下游服务就能接着这条链路继续记录

一次请求的 Trace 长什么样

文本
POST /api/chat                         总耗时 6.2s
├─ rewrite_query   小模型               0.6s   输入 820 / 输出 45 tokens
├─ retrieve                             0.3s
│  ├─ vector_search                     0.12s  返回 50 条
│  ├─ keyword_search                    0.08s  返回 50 条
│  └─ rerank                            0.2s   保留 5 条
├─ chat 主模型(第 1 轮)                1.8s   首 token 0.7s,发起工具调用 get_order
├─ execute_tool get_order               0.4s   成功
└─ chat 主模型(第 2 轮)                3.1s   首 token 0.9s,输出 410 tokens

数字只是示例。从这棵树能直接看出:最慢的是第 2 轮生成,检索里的两路召回是并行的,模型调用了一次工具后再生成回答。

每个 Span 上记录哪些字段(模型和参数、token 用量、检索结果、工具的参数和结果等),见 可观测性要关注哪些数据。根 Span 上再记录用户 ID(哈希后)、会话 ID、功能入口和 Prompt 版本,之后的用户反馈和评分也关联到这个 trace 上。

OpenTelemetry 的生成式 AI 语义约定

OpenTelemetry 是厂商中立的观测标准,包括 API、SDK 和 OTLP 传输协议,数据可以发到任何兼容的后端。它为生成式 AI 定义了一套语义约定,统一了属性名,例如:

属性 含义
gen_ai.operation.name 操作类型,如 chat、embeddings、execute_tool
gen_ai.provider.name 模型供应商
gen_ai.request.model、gen_ai.response.model 请求的模型、实际响应的模型
gen_ai.usage.input_tokens、gen_ai.usage.output_tokens 输入、输出的 token 数
gen_ai.response.finish_reasons 结束原因

模型调用的 Span 名称建议用"操作名 模型名"的形式(如 chat 加上模型名),工具调用用 execute_tool 加上工具名。输入输出的消息内容可能包含隐私,在约定里属于需要主动开启才采集的属性。这套约定还在演进,属性名改过(比如供应商原来记在 gen_ai.system 里,后来改成 gen_ai.provider.name),不同版本的埋点库输出的名字可能不一样,使用前以官方文档标注的状态为准。

用什么工具

  • 专门的 LLM 观测平台:例如 Langfuse(开源,可以自托管)、LangSmith 等,提供 trace 可视化、按 trace 打分和标注、Prompt 管理、评测集管理
  • 已有的观测体系:已经用 OpenTelemetry 加 Jaeger、Grafana Tempo 这类后端的团队,可以直接在现有链路里加上生成式 AI 的属性
  • 自动埋点:很多框架和 SDK 有现成的集成,能自动记录模型调用;检索、工具这类自己写的业务步骤,仍然要手动埋点

代码示例

用 OpenTelemetry 的 API 手动埋点。还需要初始化 SDK 和导出器,把数据发到后端;在 Node.js 里,Span 的父子关系能跨 await 传递,靠的是 SDK 注册的异步上下文管理器。这些初始化代码这里省略:

TypeScript
import { trace, SpanStatusCode, type Attributes, type Span } from '@opentelemetry/api'

const tracer = trace.getTracer('chat-service')

// 创建一个 Span,自动记录耗时和异常
function withSpan<T>(name: string, attributes: Attributes, fn: (span: Span) => Promise<T>): Promise<T> {
  return tracer.startActiveSpan(name, { attributes }, async (span) => {
    try {
      return await fn(span)
    } catch (err) {
      span.recordException(err as Error)
      span.setStatus({ code: SpanStatusCode.ERROR, message: String(err) })
      throw err
    } finally {
      span.end() // 无论成功失败都要结束 Span
    }
  })
}

async function answer(question: string, userHash: string) {
  return withSpan('chat_request', { 'app.user': userHash }, async () => {
    // 在父 Span 的回调里创建的 Span,会自动成为它的子节点
    const docs = await withSpan('retrieve', {}, async (span) => {
      const hits = await hybridSearch(question, 5)
      span.setAttribute('app.retrieved_ids', hits.map((h) => h.id))
      return hits
    })

    const attrs = { 'gen_ai.operation.name': 'chat', 'gen_ai.provider.name': PROVIDER, 'gen_ai.request.model': MODEL }
    return withSpan(`chat ${MODEL}`, attrs, async (span) => {
      const res = await llm.chat({ model: MODEL, messages: buildMessages(question, docs) })
      span.setAttribute('gen_ai.usage.input_tokens', res.usage.inputTokens)
      span.setAttribute('gen_ai.usage.output_tokens', res.usage.outputTokens)
      return res.content
    })
  })
}

面试官可能追问

流式输出时,Span 什么时候结束?

在流真正结束时才结束 Span:正常读完、用户中断或者出错。中间可以把收到第一个片段的时间作为事件或属性记下来,用来计算首 token 延迟。用户中断要单独标记,不要和错误混在一起统计。

前端、网关、模型服务分属不同的服务,怎么串成一条链路?

通过请求头传播 trace 上下文(traceparent),每个服务从请求头里取出上一级的 Span,在它下面继续记录。前端也可以生成或拿到 trace ID,用户点踩时一起上报,反馈就能和整条链路关联起来。调用外部的模型 API 时,链路只能记录到出站请求为止,看不到供应商内部的过程。

Agent 跑了几十步,trace 太大怎么看?

记录时控制体积:每一步的 Span 只记录必要的属性,大段内容(完整的工具返回、长文档)截断,或者存到别处只保留引用。在根 Span 上汇总总步数、总 token 和总费用,先看整体。查看细节时用时间线(瀑布图)找出最慢的一步,再按状态筛出出错的 Span。

用专门的 LLM 观测平台,还是自己基于 OpenTelemetry 搭?

专门的平台开箱即用,对 Prompt、token、评分这类 LLM 特有的数据展示得更好,还带标注和评测功能;基于 OpenTelemetry 自建,可以和现有的监控、日志体系统一,数据完全由自己掌控。两者也可以结合:用 OpenTelemetry 埋点,同时导出到 LLM 观测平台和现有的后端,前提是平台支持接收 OpenTelemetry 的数据。

易错点

  • 只在入口打一条日志,看不到内部的每一步
  • 并行或异步的步骤没有正确传递上下文,Span 之间丢了父子关系,trace 变成一堆零散的记录
  • 把完整的用户输入输出直接写进属性,没有考虑大小限制和隐私
  • 出错时没有结束 Span,或者没有标记错误状态

AI 模拟面试官

用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮

登录后就可以和 AI 面试官对练,面试记录也会保存下来。登录

这道题你掌握了吗?

选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。

学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。