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

📅 2026/8/6 2:21:14
LangSmith如何根据id关系,在服务端把扁平列表重建为树形结构,用于页面渲染Trace视图
扁平Span列表 → 还原Trace树场景LangSmith/LangFuse收到一批扁平的span数组每一条只有run_id/span_id、parent_run_id/parent_span_id没有嵌套结构。目标根据id关系在服务端把扁平列表重建为树形结构用于页面渲染Trace视图。原始扁平数据示例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算法两步走构建id映射字典索引keyspan_idvaluespan对象同时给每个对象增加一个空children数组。O(n)遍历所有span把自己挂载到父节点的children数组里。O(n)parent为None的就是根节点根就是整条Trace。Python完整可运行还原代码fromdataclassesimportdataclassfromtypingimportList,OptionaldataclassclassSpan:span_id:strparent_span_id:Optional[str]name:strchildren: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},]defbuild_trace_tree(raw_list)-Optional[Span]:# 1. id - span 的索引mapid_map{}foriteminraw_list:spanSpan(span_iditem[span_id],parent_span_iditem[parent_span_id],nameitem[name],children[])id_map[span.span_id]span root:Optional[Span]None# 2. 遍历找父节点挂载到父的childrenforspaninid_map.values():pidspan.parent_span_idifpidisNone:# 根节点rootspanelse:# 通过map快速找到父span把当前span加入父的childrenparentid_map.get(pid)ifparent:parent.children.append(span)returnrootdefprint_tree(node:Span,depth0):递归打印树方便看结果prefix *depthprint(f{prefix}-{node.name}({node.span_id}))forchildinnode.children:print_tree(child,depth1)if__name____main__:trace_rootbuild_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真实场景Python客户端本地生成一个个独立Run(Span)互相之间只有parent_run_id关联本地不维护children树。通过HTTP批量POST一条条Run上报LangSmith后端。LangSmith后端存入数据库每一行是一个run记录。用户打开网页看这条trace后端查询数据库把该trace_id下所有run全部查出来得到扁平列表。执行上面这个build_trace_tree算法内存组装树返回给前端渲染树形链路图。请求结束内存的树直接丢弃数据库仍然是扁平存储。那如果链路被取消前端停止生成会发生什么假设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没有上报到服务端扁平列表缺少节点树就残缺。边界情况顺序无关不管传入的spans顺序是A,B,C,D还是D,C,B,Aid_map哈希表可以正确找到父节点树构建不受数组顺序影响。父节点丢失某个span的parent_span_id指向一个不存在的id这个节点就不会挂载到树上会成为游离孤儿节点。平台会做兼容处理把孤儿节点挂到根下展示。和你整套知识串联总结Python函数是对象 → 回调handler可以传递各个生命周期触发创建span。contextvars协程上下文本地只保存当前栈顶span拿到父span_id生成子span。本地每个span独立上报本地没有完整树。数据库存储扁平一条条span记录。查询时服务端做id映射 挂载children内存临时还原Trace树给前端展示。部分span没上报还原出来的树就残缺就是停止生成看到的现象。LangFuse、OpenTelemetry后端还原Trace树用的几乎一模一样的算法。