otel trace仔细讲解

回复 joye: 先给结论:OTel 里的 trace 是“一条请求从头到尾的经历记录”,span 是其中每一段。trace_id 把同一趟旅程的所有片段串起来,span_id 标识单个片段,通过 parent_span_id 构成父子关系,也就是一棵树。agent 的可观测性之所以困难,几乎全部来自这棵树的形状和普通后端请求的树不一样。 先讲机制。 一个 trace 就是一次完整执行,比如一个 bot 收到一条消息、跑完一整轮。它下面挂根 span,再往下挂子 span。每个 span 记录这几样:span_id、parent_span_id、trace_id、名称、起止时间戳、status、attributes(键值对)、events(带时间戳的瞬时记录)、links(跨 trace 的软引用)。span 是原子单位,它可以嵌套,也可以并行成为兄弟节点。 数据结构是树,但存储是平的。导出到后端时全都是一条条扁平的 span 记录,树由 parent_span_id 重建。这点很关键:你要做聚合分析,扫的是平表,一次 trace 就是若干行,跟前面聊的 Parquet/DuckDB 那套能接上。 传输用 OTLP,可以走 gRPC 也可以走 HTTP,格式是 protobuf 或 JSON。SDK 里导出是异步批量的,中间夹一个 BatchSpanProcessor,攒够一批再发。所以 trace 天生不是实时的,会有几秒到几十秒的延迟,别指望它当监控的即时信号用。 上下文传播是整套东西的地基。W3C 那个 traceparent header 携带 trace_id、parent_span_id 和采样标志,跨服务、跨进程靠它续接。谁丢了 header,谁那一段就成了孤儿 span,树就断了。 现在说 agent 场景下和普通后端的四个本质差异。 第一,树的宽度和深度都失控。一个后端请求,span 深度大概五六层,宽度也有限。agent 一轮里,模型调用下面挂工具调用,工具可能是浏览器操作,浏览器下面一串页面动作;如果是多 agent,还有 spawn 子 agent、消息传递、任务交接。深度上十几层、宽度几十个兄弟节点很常见。后果是:全量采样存不起,可视化也糊成一团。所以 agent 的 trace UI 必须支持折叠和按类型过滤,否则根本没法看。 第二,时间轴不是线性的,是“等待”占大头。一次 LLM 调用可能三十秒,期间什么都没发生,然后两百毫秒内跑完十个工具。业界常规做法是把 span 打上 gen_ai 那套语义约定:gen_ai.system、gen_ai.request.model、gen_ai.usage.input_tokens、output_tokens,以及请求和响应的内容(这个一般要显式开,默认不记,因为含隐私)。这样你才能从 trace 里直接算出「这一轮花了多少钱、慢在哪一步」。 第三,token 和成本是特殊量纲。trace 系统原本处理的是延迟和错误率,agent 需要的是累计计数(counter)。所以这些挂在 span attribute 上,再由后端做聚合。你不能靠数 span 数量来估成本,一次调用换了模型、走了缓存,价格就完全不一样,必须记在 attribute 里。 第四,有状态、有记忆。普通后端无状态,一次请求一次 trace 就完了。agent 里 session 是跨 trace 的,用户第 5 轮的对话依赖第 1 轮的记忆。OTel 里没有 session 这个原生概念,只能用 resource 或 span attribute 自己挂一个 session_id、conversation_id,查询时自己拼。这是目前生态里最不规范的一块,各家 Langfuse、Phoenix、LangSmith 各写各的。 再说你在做的自进化那件事,这层跟 trace 关系最紧。 span 里的 events 是给这个用的:你可以在一次 span 里打一个 event,标记“这里策略切换了”“这次工具调用失败并触发重试”。要收集 badcase,最直接的路径就是 span 上加一个评分 attribute(人工评或自动评都行),然后按评分过滤。这比事后另建一套数据管道省事得多,因为 trace 已经在那儿了,你只是在上面加了标签。 但要注意两件事。一是 trace 内容是含敏感数据的,openinference 那套规范里 message 内容是 opt-in 的,自托管随便,一旦要上云或者给别人看,脱敏管道必须先有。二是 trace 的采样策略会直接毁掉你的 badcase 收集:按 10% 采样,坏的那批大概率被采掉。正确做法是尾部采样,出错或成本超阈值的强制保留,正常路径才降采样。这个决定影响你后面所有的分析。 还有一个容易被忽略的:span 的 links。多 agent 场景里,一个子 agent 可能被两个上游同时触发,或者一个任务从 A 派到 B 再回到 A,这时候严格树结构表达不了,要用 link 而不是 parent。很多人一开始都硬塞成父子,结果树里出现同一件事被记两次,统计全歪。 要不要我顺手把这套落到你具体的技术栈上,比如自托管的话选哪个后端(Phoenix 还是 Langfuse 还是直接 ClickHouse 自己搭),以及在你那套多 agent 里 trace 边界该切在哪一层?这个才是真正影响后面做自进化的地方。