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一定有:
span_id:自己唯一IDparent_span_id:父Span的id;顶层Span父id=None- 开始时间、结束时间 → 计算耗时
- operation name:操作名字(
llm.invoke、tool.search、graph_node:call_tool) - 输入 inputs,输出 outputs
- 状态:成功 / error / cancelled(被取消)
- 元数据:token消耗、模型名称、异常堆栈
LangSmith里面叫Run,Run = Span,概念等价。
LangSmith术语:Trace是根Run,下面所有子Run就是子Span。
| 产品 | 整条链路 | 单元片段 |
|---|---|---|
| LangSmith | Trace(根Run) | Run(等价Span) |
| LangFuse | Trace | Observation(Obs),等价Span |
| OpenTelemetry | Trace | Span |
LangSmith内部:
Run对象就是Span;_CURRENT_RUNcontextvar存的就是当前正在执行的Span对象。
父子Span是怎么形成的(结合前面contextvars)
- 创建父Span(根Run),把它存入
ContextVar。 - 执行子函数,
@traceable/ callback 读取context拿到父Span。 - 创建子Span,把子Span的
parent_span_id = 父span_id。 - 子Span执行完成,上报。
不需要手动传参,靠上下文隐式拿到父ID,自动嵌套。
三种Span结束状态
- ✅正常结束:拿到end回调,记录结束时间,完整上报。
- ❌异常报错:触发
on_xxx_error回调,标记span状态为error,记录异常栈。 - ⚠️被取消(前端停止生成):
task.cancel(),协程抛出CancelledError。- 不会走到end回调,也不会走到error回调
- Span只有start,没有end;UI看到一条“截断、未完成”的trace。
区分容易混淆名词
- Trace:完整的一次业务请求链路(根)
- Span:链路中每一个独立操作片段(子节点)
- ContextVar:用来隐式传递当前span对象,方便自动设置parent_span_id
- 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:
- 新建Trace(根Span)存入contextvar
- 执行graph节点,回调触发,新建子Span,parent指向根
- 节点内部调用LLM,再新建LLM子Span
- 用户点停止,task.cancel,代码直接中断。
end_span()不会执行。
→ 所有已经start但是没有end的Span,在平台上展示为残缺链路。
对比LangFuse
LangFuse Observation就是Span,但不依赖contextvars,你必须手动传入父trace/observation_id,或者依赖OTel的上下文。所以不会出现隐式上下文带来的断链,但埋点代码写得更多。
一句话总结:
Trace是一整条请求,Span就是这条请求里面每一步操作的记录单元。LangSmith的Run就是Span。
