跳到主要内容

OpenTelemetry GenAI Semantic Conventions — 架构与原理

30 秒导读: 当你想知道「我这次 LLM 调用花了多少 token、用了哪个 provider、调了哪个工具、耗时多久」,这些字段该叫什么名、什么类型、什么时候必填——OpenTelemetry 的 GenAI 语义约定就是回答这个问题的官方字典。它不是一段运行的代码,而是一堆 YAML 定义,经 Weaver 工具编译成人读的 Markdown 表和各语言的常量代码,让 Python、JS、Java 的 instrumentation 和 Grafana、Jaeger 等后端对同一字段名达成一致

1. 这是什么(零基础也能懂)

一句话定义: GenAI 语义约定是一份给 LLM/agent 遥测数据用的命名规范——它规定「输入 token 数」这个量必须叫 gen_ai.usage.input_tokens、是整数、在 client span 上推荐记录,而不是让每家随便起名。

它解决谁的什么痛: 假设你接了三家 LLM(OpenAI、Anthropic、Bedrock),想在一个面板里看「跨所有 provider 的 token 花费」。如果 OpenAI 的 SDK 把它记成 prompt_tokens、Anthropic 记成 input_token_count、Bedrock 记成 inputTokens,你的面板就拼不起来。语义约定强制大家都用 gen_ai.usage.input_tokens,跨厂商的聚合才成立。

关键直觉——它管「名字与含义」,不管「怎么采集」:

  • 不是 SDK,不会帮你拦截 OpenAI 调用;
  • 不是后端,不会帮你存储或画图;
  • 它是中间那张契约:instrumentation 作者照它发数据,后端照它解读数据。

类比:它之于可观测,像 HTTP 状态码表之于 Web——404 是什么含义大家事先约定好,客户端和服务器才不用逐个对接。

用起来什么样: 一次 LLM 客户端调用产生的 span,按约定大致长这样(示意,字段名取自真实定义):

span name: "chat gpt-4" # {gen_ai.operation.name} {gen_ai.request.model}
span kind: CLIENT
attributes:
gen_ai.provider.name = "openai" # 必填
gen_ai.operation.name = "chat" # 必填
gen_ai.request.model = "gpt-4"
gen_ai.response.model = "gpt-4-0613"
gen_ai.usage.input_tokens = 100
gen_ai.usage.output_tokens= 180
server.address = "api.openai.com"

字段名、类型、必填级别全部来自下面要讲的 YAML 定义,例如 gen_ai.provider.namechat span 上是 required(model/gen-ai/deprecated/spans-deprecated.yaml:160-162,span.gen_ai.inference.client)。

[!IMPORTANT] 本仓在这个 commit 的真实状态:GenAI 约定已经「搬家」了。 docs/gen-ai/*.md 全部是 11 行的「已移动」占位页(docs/gen-ai/README.md:1-11),正文指向新仓 open-telemetry/semantic-conventions-genai。本仓里仍能读到完整定义的地方,是 model/gen-ai/deprecated/model/mcp/deprecated/model/openai/deprecated/ 这些「弃用墓碑」YAML——它们保留了全部字段,只是统一标了 deprecated

所以本文档双线讲:既讲这些 YAML 里沉淀下来的 GenAI 领域模型(它就是约定本身,只是被标了弃用),也讲承载它的 semconv 机器(YAML 语法、Weaver 编译、Rego 策略)。详见第 4 章。

2. 顶层全景(它大概怎么转)

这套东西的「价值流」是:人写 YAML → 机器编译 → 出文档和代码 → instrumentation 和后端各取所需

你/贡献者 构建期(Weaver 容器)
┌───────────────────────┐ ┌──────────────────────────────────┐
│ model/**/*.yaml │ │ ① Rego 策略闸门 (check-policies) │
│ 「数据字典」源 │──────▶ │ 命名/兼容/弃用 规则不过就拒 │
│ registry: 定义字段 │ │ │
│ span/metric/event: │ │ ② Weaver registry generate │
│ 组合字段成信号 │ │ YAML ──▶ docs/*.md 表格 │
└───────────────────────┘ │ YAML ──▶ 各语言常量代码(外部) │
└──────────────────────────────────┘

┌───────────────────────────────────┴───────────────┐
▼ ▼
instrumentation 作者 遥测后端 / 用户
(照字段名发 span/metric) (照字段名解读、聚合、画图)
│ ▲
└────────────── 运行期产生的遥测 ──────────────────┘

怎么读这张图: 左边是人维护的唯一真相源(YAML);中间 Weaver 容器在构建期做两件事——先过策略闸门,再编译产物;右边是两类下游消费者,它们只认编译出来的字段名,从不直接读 YAML。

主要部件一句话职责:

部件干什么在哪
registry.*定义单个字段(名字、类型、枚举值、说明)model/**/registry-deprecated.yaml
span / metric / event把字段组合成一个具体信号,并定必填级别model/**/spans-deprecated.yaml
Weaver(外部工具)解析 YAML、跑策略、生成 Markdown 与代码容器 otel/weaver:v0.24.2(dependencies.Dockerfile:6)
Rego 策略构建期校验命名/向后兼容/弃用规则policies/*.rego
schema 文件记录跨版本的字段改名映射,供数据迁移schemas/<version>
docs/**/*.mdWeaver 生成的人读文档(GenAI 这块现为占位页)docs/gen-ai/

注意「字段」和「信号」住在两个文件里: registry 字段原子在 model/**/registry-deprecated.yaml,span/metric/event 信号分子在 model/**/spans-deprecated.yaml(及 metrics-/events-)。后面引用字段定义时一律指向 registry-deprecated.yaml,引用 span/attribute-group 时才指向 spans-deprecated.yaml——别混。

主线走一遍(高层):

  1. 贡献者在 registry.gen_ai 里新增/改一个字段(如 gen_ai.usage.input_tokens,model/gen-ai/deprecated/registry-deprecated.yaml:577)。
  2. make check-policies 把模型和上一个发布版比对,Rego 规则确保没破坏兼容(Makefile:287-300)。
  3. make table-generation 让 Weaver 把 YAML 渲染进 docs/ 的 Markdown 表(Makefile:164-178)。
  4. 发版时,若字段改了名,就在 schemas/<新版本> 写一条 rename_attributes 映射(见第 3 章)。
  5. 下游各语言 SDK 用 Weaver 把同一份 YAML 生成本语言常量,instrumentation 引用这些常量发遥测。

3. 阅读地图(建议顺序)

这套内容有两面:「字典语法」「字典内容」。按下面顺序读最顺:

  1. 先读 01-yaml-model.md —— 看懂一条约定在 YAML 里怎么写:registry 定义字段、group 用 extends/ref 拼装、span/metric/event 三种信号的差异。这是后面一切的语法基础。
  2. 再读 02-genai-domain-model.md —— 看字典内容本身:GenAI 到底约定了哪些字段(provider、operation、token、tool、agent、retrieval、evaluation),逐个讲清含义和坑。这是你做 LLM 可观测最常查的一章。
  3. 然后读 03-codegen-and-policy.md —— YAML 如何经 Weaver 变成文档/代码,Rego 策略如何在构建期把关,schema 版本文件如何让改名不破坏老数据。
  4. 最后读 04-the-move-and-deprecation.md —— 关键背景:为什么本仓的 GenAI 现在只剩「墓碑」,弃用/改名机制怎么编码进 YAML 与策略,以及这对读者意味着什么。

只想查「某个字段叫什么、什么类型」→ 直接跳第 2 章。 想给约定提 PR 或理解 CI 为什么报错 → 看第 1、3 章。