LangSmith的Trace和Span是什么

Span(跨度)

在LLM可观测 / OpenTelemetry / LangSmith / LangFuse 领域:
Trace:一次完整请求的整条链路(一条大树干)
Span:Trace里面的每一小段操作(树上的每一个节点/树枝)

  • 1条Trace= 1次完整会话/一次Agent执行。
  • 一条Trace由多个Span组成,Span之间有父子嵌套关系,形成树。

举个Agent例子:

Trace: 用户提问:帮我查北京天气 ├─ Span【Agent主chain】(总耗时 2.2s) ├─ Span【调用工具 search天气】(耗时0.8s) └─ Span【LLM生成回答】(耗时1.3s)

每一个方块就是一个Span

Span里面记录什么信息

每个Span一定有:

  1. span_id:自己唯一ID
  2. parent_span_id:父Span的id;顶层Span父id=None
  3. 开始时间、结束时间 → 计算耗时
  4. operation name:操作名字(llm.invoketool.searchgraph_node:call_tool
  5. 输入 inputs,输出 outputs
  6. 状态:成功 / error / cancelled(被取消)
  7. 元数据:token消耗、模型名称、异常堆栈

LangSmith里面叫RunRun = Span,概念等价。
LangSmith术语:Trace是根Run,下面所有子Run就是子Span。

产品整条链路单元片段
LangSmithTrace(根Run)Run(等价Span)
LangFuseTraceObservation(Obs),等价Span
OpenTelemetryTraceSpan

LangSmith内部:Run对象就是Span;_CURRENT_RUNcontextvar存的就是当前正在执行的Span对象。

父子Span是怎么形成的(结合前面contextvars)

  1. 创建父Span(根Run),把它存入ContextVar
  2. 执行子函数,@traceable/ callback 读取context拿到父Span。
  3. 创建子Span,把子Span的parent_span_id = 父span_id
  4. 子Span执行完成,上报。

不需要手动传参,靠上下文隐式拿到父ID,自动嵌套。

三种Span结束状态

  1. ✅正常结束:拿到end回调,记录结束时间,完整上报。
  2. ❌异常报错:触发on_xxx_error回调,标记span状态为error,记录异常栈。
  3. ⚠️被取消(前端停止生成):task.cancel(),协程抛出CancelledError。
    • 不会走到end回调,也不会走到error回调
    • Span只有start,没有end;UI看到一条“截断、未完成”的trace。

区分容易混淆名词

  1. Trace:完整的一次业务请求链路(根)
  2. Span:链路中每一个独立操作片段(子节点)
  3. ContextVar:用来隐式传递当前span对象,方便自动设置parent_span_id
  4. Callback:事件钩子,在业务节点执行的前后,触发创建/结束Span

极简伪代码模拟Span模型

@dataclassclassSpan:span_id:strparent_span_id:str|Nonename:strstart_time:floatend_time:float|Nonestatus:str# ok / error / cancelledinputs:dictoutputs:dict|None# 上下文变量保存当前正在运行的spancurrent_span:ContextVar[Span|None]=ContextVar("current_span")defstart_span(name):parent=current_span.get()span=Span(span_id=str(uuid.uuid4()),parent_span_id=parent.span_idifparentelseNone,name=name,start_time=time.time(),end_time=None,status="ok",inputs={},outputs=None)token=current_span.set(span)returnspan,tokendefend_span(span,token):span.end_time=time.time()report_span(span)# 上报到LangSmith/LangFusecurrent_span.reset(token)

回到Agent业务

用户发消息跑LangGraph:

  1. 新建Trace(根Span)存入contextvar
  2. 执行graph节点,回调触发,新建子Span,parent指向根
  3. 节点内部调用LLM,再新建LLM子Span
  4. 用户点停止,task.cancel,代码直接中断。end_span()不会执行。
    → 所有已经start但是没有end的Span,在平台上展示为残缺链路。

对比LangFuse

LangFuse Observation就是Span,但不依赖contextvars,你必须手动传入父trace/observation_id,或者依赖OTel的上下文。所以不会出现隐式上下文带来的断链,但埋点代码写得更多。

一句话总结:
Trace是一整条请求,Span就是这条请求里面每一步操作的记录单元。LangSmith的Run就是Span。