业界标准组织发布 LLM 可观测性规范,打破各工具各自为政,让数据互通可比成为可能。
每个 LLM 工具都发明了自己的 tracing 格式。Langfuse 有一套,Helicone 有一套,Arize 也有一套。如果你自己构建过一套——恭喜,你也拥有一种格式。
OpenTelemetry 刚刚为它们发布了一套统一标准。
它规定了 span 应该如何命名、工具调用应该包含哪些属性、如何在不泄露 PII 的情况下记录 prompt,以及 Agent 应该使用哪种 span kind。这套标准名为 GenAI Semantic Conventions,目前仍处于实验阶段。而且,几乎没人写过它真正实现到代码里是什么样子。
我知道,因为我搜过。“OTel GenAI semantic conventions”搜出来的都是规范页面,一篇实战文章都没有。“How to trace LLM agent with OpenTelemetry”搜出来的则是一些无人回答的 StackOverflow 问题。
我们实现了这套规范。四个 PR,一次差距分析,还有真实的改造前后代码。我们甚至在实现过程中发现,我们的 trace 从来没有被导出过——不过那是另一个故事了。
下面就来看看,这份规范究竟说了什么、我们之前哪里做错了,以及你今天应该怎么做。
现在,如果你在追踪 LLM 调用,很可能会写出类似这样的代码:
span.setAttribute("llm.provider", "openai");
span.setAttribute("llm.model", "gpt-4o");
span.setAttribute("llm.tokens.input", 150);
span.setAttribute("llm.cost", 0.003);
我们在 toad-eye v1 中就是这么做的。对我们来说很合理,在自己的 dashboard 里也运行良好。
问题在于:其他人的 dashboard 根本不认识这些属性。从 Jaeger 切换到 Arize Phoenix,你需要重新配置所有东西。把 trace 导出到 Datadog,它看到的只是不带任何 LLM 上下文的原始 span。你的 tracing 成了一座围墙花园。你亲手在自己的代码里埋下了 vendor lock-in。
这正是 OpenTelemetry 当初要解决的问题。现在,它也有了专门针对 GenAI 的规范。
规范定义了三种 operation。每个与 LLM 相关的 span 都要使用其中一种:
chat gpt-4o ← model call
invoke_agent orchestrator ← agent invocation
execute_tool web_search ← tool execution
span 的命名格式是 {operation} {name}。不是你自定义的格式,也不是 gen_ai.openai.gpt-4o(我们以前就是这么命名的——没有任何 backend 能识别)。
下面是我们的改动:
span 命名迁移:旧格式对于所有支持 GenAI 的 backend 来说都是不可见的。
如果你在构建 Agent(ReAct、tool-use、多步骤 Agent),规范定义了身份属性和工具属性:
// What OTel says:
span.setAttribute("gen_ai.agent.name", "weather-bot");
span.setAttribute("gen_ai.agent.id", "agent-001");
span.setAttribute("gen_ai.tool.name", "search");
span.setAttribute("gen_ai.tool.type", "function");
// What we had:
span.setAttribute("gen_ai.agent.tool.name", "search"); // wrong path
// gen_ai.agent.name — didn't exist at all
gen_ai.agent.tool.name 这个路径看起来很合理,读起来甚至也很顺。但规范把工具属性放在 gen_ai.tool.* 下——它是扁平的,并不嵌套在 agent 下面。于是,我们的格式再一次对所有遵循标准的 backend 隐身了。
这是我们从第一天起就做对的一件事,值得专门讲讲,因为大多数团队都会在这里犯错。
规范明确表示:默认不要记录 prompt 和 completion。除非显式启用,否则 instrumentation SHOULD NOT 捕获内容。
官方提供了三种模式:
默认:不记录。span 中既不保存 prompt,也不保存 completion。隐私优先。
通过 span 属性选择启用。使用 gen_ai.input.messages 和 gen_ai.output.messages,以 JSON 字符串的形式记录。
外部存储。把内容存储在其他地方,只在 span 中放置引用。
从 v1 开始,我们就默认设置了 recordContent: false。当规范确认了这种做法时,那是一种难得的时刻:你的直觉得到了一群极其聪明的人的集体认可。
如果你默认就把 prompt 记录到 span 中——或许应该在安全团队替你重新考虑之前,自己先重新考虑一下。
下面是完整情况。不粉饰,也不挑对自己有利的部分。
关键洞察在于:OTel 规范覆盖的是发生了什么。我们覆盖的是为什么发生,以及花了多少。二者并不竞争,而是互补。你的自定义 metrics 应该放在你自己的 namespace 下;规范定义的属性,则应该放在 backend 预期的位置。
我们没有直接彻底切换。v2.4 会同时输出新旧两套属性名:
// New (OTel spec-compliant)
span.setAttribute("gen_ai.tool.name", toolName);
// Old (deprecated, still emitted for backward compat)
span.setAttribute("gen_ai.agent.tool.name", toolName);
双重输出方案:旧属性被标记为 @deprecated,新属性遵循规范。在 v3 发布之前,两套属性都会继续输出。
你可以通过一个环境变量控制何时停止输出 deprecated 属性:
OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
整个改造涉及四个 PR(#170、#171、#172、#173)。v3 将彻底移除这些 deprecated alias。
在实现所有这些改动的过程中,我们进行了一轮手动测试。
结果发现,我们的 trace 从来没有被导出过。一次都没有,从来没有。向 OTel NodeSDK 传入 spanProcessors: [] 时,它会悄无声息地禁用 trace export。我们有 252 个通过的测试,但所有测试都 mock 了 SDK。
所以,我们完美地标准化了所有属性——只不过那些 trace 根本没人能看到。
这两个问题我们都修复了,并在一天之内发布了六个 patch 版本。完整故事会在文章 #2 中讲述。
这正是你应该关心它的原因。今天输出正确的属性 → 明天就能在六种 backend 中可视化你的 trace:
没有 vendor lock-in。只需一套属性,就能在六个地方进行可视化。
如果你正在追踪 LLM 调用——哪怕用的是自定义代码——现在就与规范对齐,可以为以后省去很多麻烦。这套 conventions 虽然仍处于实验阶段,但方向已经确定。
在每个 LLM span 上设置 gen_ai.operation.name:chat、invoke_agent 或 execute_tool
将 span 名称格式化为 {operation} {model_or_agent_name}
使用官方属性:gen_ai.agent.name、gen_ai.tool.name、gen_ai.tool.type
把你自己的自定义属性放在你自己的 namespace 下,而不是 gen_ai.* 下
默认不要记录 prompt/completion——应当由用户选择启用
至少在两个 backend 中测试你的 trace(Jaeger,再加一个 Phoenix 之类专用于 GenAI 的 backend)
完整规范:OpenTelemetry GenAI Semantic Conventions
Agent span:GenAI Agent Spans
#1:我的 AI bot 一夜之间烧光了 API 预算
#2:我审计了自己的工具、修复了 44 个 bug——但它还是不能工作
toad-eye——开源、OTel-native 的 LLM observability 工具:GitHub · npm
部分评论可能只有登录后的访客才能看到。请登录以查看所有评论。
如果需要采取进一步行动,你可以考虑屏蔽此人和/或举报滥用行为。