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.name 在 chat 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/**/*.md | Weaver 生成的人读文档(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——别混。
主线走一遍(高层):
- 贡献者在
registry.gen_ai里新增/改一个字段(如gen_ai.usage.input_tokens,model/gen-ai/deprecated/registry-deprecated.yaml:577)。 make check-policies把模型和上一个发布版比对,Rego 规则确保没破坏兼容(Makefile:287-300)。make table-generation让 Weaver 把 YAML 渲染进docs/的 Markdown 表(Makefile:164-178)。- 发版时,若字段改了名,就在
schemas/<新版本>写一条rename_attributes映射(见第 3 章)。 - 下游各语言 SDK 用 Weaver 把同一份 YAML 生成本语言常量,instrumentation 引用这些常量发遥测。
3. 阅读地图(建议顺序)
这套内容有两面:「字典语法」和「字典内容」。按下面顺序读最顺:
- 先读
01-yaml-model.md—— 看懂一条约定在 YAML 里怎么写:registry 定义字段、group 用extends/ref拼装、span/metric/event 三种信号的差异。这是后面一切的语法基础。 - 再读
02-genai-domain-model.md—— 看字典内容本身:GenAI 到底约定了哪些字段(provider、operation、token、tool、agent、retrieval、evaluation),逐个讲清含义和坑。这是你做 LLM 可观测最常查的一章。 - 然后读
03-codegen-and-policy.md—— YAML 如何经 Weaver 变成文档/代码,Rego 策略如何在构建期把关,schema 版本文件如何让改名不破坏老数据。 - 最后读
04-the-move-and-deprecation.md—— 关键背景:为什么本仓的 GenAI 现在只剩「墓碑」,弃用/改名机制怎么编码进 YAML 与策略,以及这对读者意味着什么。
只想查「某个字段叫什么、什么类型」→ 直接跳第 2 章。 想给约定提 PR 或理解 CI 为什么报错 → 看第 1、3 章。