LangSmith如何根据id关系,在服务端把扁平列表重建为树形结构,用于页面渲染Trace视图

扁平Span列表 → 还原Trace树

场景:LangSmith/LangFuse收到一批扁平的span数组,每一条只有 run_id/span_id、parent_run_id/parent_span_id,没有嵌套结构。

目标:根据id关系,在服务端把扁平列表重建为树形结构,用于页面渲染Trace视图。

原始扁平数据示例

python 复制代码
spans = [
    {"span_id": "A", "parent_span_id": None, "name": "trace_root"},
    {"span_id": "B", "parent_span_id": "A", "name": "agent_node"},
    {"span_id": "C", "parent_span_id": "B", "name": "tool_call"},
    {"span_id": "D", "parent_span_id": "B", "name": "llm_call"},
]

逻辑树:

复制代码
A(root)
└── B
    ├── C
    └── D

算法两步走

  1. 构建id映射字典(索引):key=span_id,value=span对象,同时给每个对象增加一个空children数组。O(n)
  2. 遍历所有span,把自己挂载到父节点的children数组里。O(n)
  3. parent为None的就是根节点,根就是整条Trace。

Python完整可运行还原代码

python 复制代码
from dataclasses import dataclass
from typing import List, Optional

@dataclass
class Span:
    span_id: str
    parent_span_id: Optional[str]
    name: str
    children: List["Span"] # 用于重建树之后存放子节点

# 收到的扁平原始数据
raw_data = [
    {"span_id": "A", "parent_span_id": None, "name": "trace_root"},
    {"span_id": "B", "parent_span_id": "A", "name": "agent_node"},
    {"span_id": "C", "parent_span_id": "B", "name": "tool_call"},
    {"span_id": "D", "parent_span_id": "B", "name": "llm_call"},
]

def build_trace_tree(raw_list) -> Optional[Span]:
    # 1. id -> span 的索引map
    id_map = {}
    for item in raw_list:
        span = Span(
            span_id=item["span_id"],
            parent_span_id=item["parent_span_id"],
            name=item["name"],
            children=[]
        )
        id_map[span.span_id] = span

    root: Optional[Span] = None

    # 2. 遍历,找父节点,挂载到父的children
    for span in id_map.values():
        pid = span.parent_span_id
        if pid is None:
            # 根节点
            root = span
        else:
            # 通过map快速找到父span,把当前span加入父的children
            parent = id_map.get(pid)
            if parent:
                parent.children.append(span)
    return root


def print_tree(node: Span, depth=0):
    """递归打印树,方便看结果"""
    prefix = "  " * depth
    print(f"{prefix}- {node.name}({node.span_id})")
    for child in node.children:
        print_tree(child, depth+1)


if __name__ == "__main__":
    trace_root = build_trace_tree(raw_data)
    print_tree(trace_root)

输出:

复制代码
- trace_root(A)
  - agent_node(B)
    - tool_call(C)
    - llm_call(D)

重点:

原始上报过来的数据根本没有children字段 。children 是服务端内存里临时构造出来,只用于UI渲染;数据库存储依旧存扁平一条条span。数据库不会存树形嵌套JSON。

对应LangSmith真实场景

  1. Python客户端本地:生成一个个独立Run(Span),互相之间只有parent_run_id关联,本地不维护children树。
  2. 通过HTTP批量POST,一条条Run上报LangSmith后端。
  3. LangSmith后端存入数据库,每一行是一个run记录。
  4. 用户打开网页看这条trace:后端查询数据库,把该trace_id下所有run全部查出来得到扁平列表。
  5. 执行上面这个build_trace_tree算法,内存组装树,返回给前端渲染树形链路图。
  6. 请求结束,内存的树直接丢弃;数据库仍然是扁平存储。

那如果链路被取消(前端停止生成)会发生什么?

假设:D(llm_call)被task.cancel,只上报start,没有end事件,甚至D根本没上报上来 。

数据库里只有 A B C。

传入build_trace_tree的列表缺少D。

最后渲染出来的树:

复制代码
‑ trace_root(A)
  ‑ agent_node(B)
    ‑ tool_call(C)
# D直接消失/或者只存在一条残缺、未结束的D记录

页面看到链路断掉,就是因为部分span没有上报到服务端,扁平列表缺少节点,树就残缺。

边界情况

  1. 顺序无关:不管传入的spans顺序是A,B,C,D还是D,C,B,A,id_map哈希表可以正确找到父节点,树构建不受数组顺序影响。
  2. 父节点丢失:某个span的parent_span_id指向一个不存在的id,这个节点就不会挂载到树上,会成为游离孤儿节点。平台会做兼容处理,把孤儿节点挂到根下展示。

和你整套知识串联总结

  1. Python函数是对象 → 回调handler可以传递,各个生命周期触发创建span。
  2. contextvars协程上下文:本地只保存当前栈顶span,拿到父span_id,生成子span。
  3. 本地每个span独立上报,本地没有完整树。
  4. 数据库存储扁平一条条span记录。
  5. 查询时,服务端做id映射 + 挂载children,内存临时还原Trace树给前端展示。
  6. 部分span没上报,还原出来的树就残缺,就是停止生成看到的现象。

LangFuse、OpenTelemetry后端,还原Trace树,用的几乎一模一样的算法。

相关推荐
Ven%几秒前
在魔搭 Notebook 容器里跑 Qwen Code:从零配置到省魔粒的完整实践
人工智能·python·ai·ai编程·modelscope
2401_885885046 分钟前
巴林语音通知接口如何对接?系统告警语音API接入教程
android·前端·网络·python·语音识别
执假以为真29 分钟前
Python内存分析器memray的简单介绍
python
收纳过往38 分钟前
数据库加索引的时机:慢查询日志说话
android·开发语言·数据库·python·低代码·rxjava
小鹿学程序1 小时前
市赛python学习
python
高洁011 小时前
知识图谱:结构化表达实体与关系的知识组织技术
人工智能·python·深度学习·机器学习·迁移学习
骊山道童1 小时前
第 1 周 LLM 原理与高效微调知识精讲手册
python
Metaphor6922 小时前
轻松使用 Python 将 RTF 转换为 Word(含批量转换与格式互转)
python·word·rtf
zh路西法2 小时前
【深入理解 LSTM】从时间反向传播(BPTT)到长时记忆:门控机制与实验验证
pytorch·python·rnn·gru·lstm·bptt
青少儿编程课堂2 小时前
蓝桥杯青少组 + 全国青少年信息素养大赛 + 电子学会图形化考级|校园推箱子大挑战解析
c++·python·算法·bfs·信息学竞赛