OTel GenAI semconv 精读
OTel GenAI semconv 精读
日期: 2026-07-06 阶段: Phase 1 - 产品定义×评测×可观测底座 标签: #opentelemetry #genai-semconv #observability #span-attributes
核心问题
要给 AML Copilot 算 $/案件、追每次 LLM 调用的 token 与 finish_reason、把 trace 与 eval 用同一执行 ID 串起来,前提是埋点字段有统一语义。各家厂商(OpenAI SDK、Anthropic SDK、LangChain)各埋各的字段,换厂商就要重写仪表盘——这正是 OpenTelemetry GenAI semantic conventions(semconv)要解决的:用一套标准属性名(gen_ai.request.model、gen_ai.usage.input_tokens…)描述「一次模型调用」,让后端(Datadog / Langfuse)零改代码就能解析。
但有个硬约束:截至 2026-03 这套规范仍是 experimental(Development 状态),属性名随时可能改。今天回答三问:(A) span/属性/事件三层约定到底定义了什么;(B) 为什么 prompt/completion 内容默认不采集(opt-in 的隐私设计);(C) experimental 状态意味着什么变更风险,以及怎么在埋点时就把这个风险隔离掉。
关键内容
A. 三层约定:span 名 + 元数据属性 + 内容事件
semconv 把「一次 GenAI 操作」拆成三层信息,分层是为了让元数据(永远安全)与内容(可能含 PII)物理分离。
第一层 — span 名约定。官方规范(gen-ai-spans,2026-03 口径)规定 span 名 SHOULD 为 {gen_ai.operation.name} {gen_ai.request.model},例如 chat gpt-4o-mini。注意它不带租户/用户信息——span 名进了链路检索索引,若把内容塞进去会全量泄露,所以约定刻意只放「操作类型 + 模型」这两个低敏维度。
第二层 — span 属性(元数据)。这是默认采集的核心,描述「这次调用是什么、花了多少」。下表是规范定义的关键属性(requirement level 直接引自 gen-ai-spans 规范):
| 属性名 | 类型 | 是否默认采集 | 语义 |
|---|---|---|---|
gen_ai.operation.name | string | Required | 操作类型(chat / embeddings / generate_content) |
gen_ai.provider.name | string | Required | 提供方标识(openai / anthropic / azure.ai.inference) |
gen_ai.request.model | string | Conditionally Required | 请求的模型名(gpt-4o-mini) |
gen_ai.response.model | string | Recommended | 实际响应的模型名(gpt-4o-mini-2024-07-18,常含日期戳) |
gen_ai.request.temperature | double | Recommended | 采样温度 |
gen_ai.request.max_tokens | int | Recommended | 请求的最大输出 token |
gen_ai.usage.input_tokens | int | Recommended | 输入/prompt token 数 |
gen_ai.usage.output_tokens | int | Recommended | 输出/completion token 数 |
gen_ai.response.finish_reasons | string[] | Recommended | 停止原因数组(["stop"] / ["length"] / ["content_filter"]) |
gen_ai.response.id | string | Recommended | 响应唯一 ID(厂商侧) |
error.type | string | Conditionally Required | 失败时的错误类(如超时、限流) |
finish_reasons 设计成数组而非标量,不是随手为之:批量/多候选生成(n>1)一次调用会有多个结束原因,标量会丢信息;而对成本与 SLA 监控,length(被 max_tokens 截断)与 stop(正常收尾)必须可区分——前者意味着回答可能不完整,是质量告警信号。
第三层 — 内容事件/属性(opt-in)。prompt 与 completion 正文走单独的 opt-in 通道,见 B 节。
反直觉洞察(口径陷阱):
gen_ai.request.model与gen_ai.response.model经常不相等。你请求gpt-4o-mini,响应回gpt-4o-mini-2024-07-18。算成本/对账要用 response.model(真实计费模型),但做产品分组要用 request.model(用户视角的模型选择)。混用这两个字段是 LLM 可观测最常见的统计错误——它会让你的「模型 A vs B」对比表把同一模型的不同快照拆成两行。
B. 内容捕获 opt-in:默认零 prompt 的隐私设计
规范明确规定承载内容的三个属性默认不采集:
gen_ai.system_instructions(Opt-In)— 与对话历史分离的系统提示gen_ai.input.messages(Opt-In)— 作为模型输入的对话历史gen_ai.output.messages(Opt-In)— 模型返回的消息
规范原文:「OpenTelemetry instrumentations SHOULD NOT capture them by default, but SHOULD provide an option for users to opt in.」开启方式在多数仪表化库里是环境变量 OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true(Greptime 2026-05 实测口径)。
这个「默认关」的设计是隐私优先(privacy-by-default),对金融场景是刚需而非可选项:AML 案件的 prompt 里塞满了客户姓名、账号、交易对手——这些是 PII/受监管数据,一旦默认进了链路存储,就等于把受监管数据复制进了一个本不该存它的系统,直接触发数据驻留与最小化合规问题。把内容捕获做成 opt-in,意味着安全默认不需要任何配置;要采内容时是一次显式的、可审计的决定(谁开的、在哪个环境开的)。
设计上还有第二重价值:元数据(token 数、模型、耗时)与内容解耦后,绝大多数可观测需求(成本、延迟、错误率、finish_reason 分布)根本不需要内容。也就是说默认配置已经能支撑 $/案件、P95 延迟、截断率等全部运营指标——内容捕获只在调试单条失败 trace 时才临时按需开启。
决策流:是否对某次调用开启内容捕获?
if 环境 == production 且 数据含 PII:
capture_content = false # 默认,合规基线
elif 环境 == staging 且 正在调试某 failure_class:
capture_content = true (限时) # 显式 opt-in,留审计记录
并对 input/output 跑 PII 脱敏管道再落库
else:
capture_content = false
反直觉洞察(隐性成本):很多团队以为「先把内容全采下来,以后要用再说」是安全的——恰恰相反。内容是链路数据里体积最大、最敏感、留存合规风险最高的部分。默认采内容会让你的可观测后端在不知不觉中变成一个影子 PII 数据库,而它的访问控制、留存策略、加密等级通常远低于你的核心业务库。opt-in 不是限制,是把这个高危决定从「默认发生」改成「必须有人负责」。
C. experimental(2026-03)的变更风险与版本迁移
semconv 当前 Status 徽章是 Development(即 experimental),且没有公开的稳定化时间表——规范原文:「Attribute names and structures may still change」(Greptime 2026-05 转述官方迁移说明)。这不是理论风险,已经发生过:v1.36 之前的旧属性格式与之后的新格式不兼容,OTel 用环境变量做迁移闸门。
关键的版本迁移机制有两个互相独立的开关,初学者极易混淆:
| 环境变量 | 控制什么 | 取值示例 | 误用后果 |
|---|---|---|---|
OTEL_SEMCONV_STABILITY_OPT_IN | 用哪个版本的属性约定 | gen_ai_latest_experimental(发最新实验版,不再发 ≤v1.36 旧版) | 不设→仍发旧属性名,后端解析不到新字段 |
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT | 是否采集内容(B 节) | true / false(默认) | 误设 true→PII 入库 |
v1.36 是「过渡基线」:现存仪表化默认仍发旧格式,必须显式 OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental 才切到新版(Greptime 2026-05)。Datadog LLM Observability 于 2025-12-01 宣布原生支持 v1.37+ 的 GenAI semconv,映射 gen_ai.request.model / gen_ai.usage.input_tokens / gen_ai.usage.output_tokens / gen_ai.usage.total_tokens / gen_ai.provider.name / gen_ai.operation.name,无需改代码即可在 OTLP 管道里解析。
变更风险的真正落点是**「转 stable 时属性改名 → 埋点全返工」**:
反直觉洞察(埋点的真正成本不在写,在改名):一次性把属性名硬编码进 50 处仪表化调用很快,但当 semconv 从 experimental 转 stable 时若
gen_ai.usage.input_tokens改成别的名字(实验阶段完全允许),你要改的不只是 50 处埋点——还有所有仪表盘查询、所有 eval 里按属性名做的成本聚合、所有告警规则。改名的爆炸半径远大于埋点本身。这就是为什么 Day 23 要做一个独立属性映射层:自有稳定字段 → semconv 单向映射,semconv 改名时只改映射层一处,业务代码与仪表盘查询全程用自有字段,零返工。
一个完整 chat span 的属性 JSON(默认配置,无内容)长这样:
{
"name": "chat gpt-4o-mini",
"attributes": {
"gen_ai.operation.name": "chat",
"gen_ai.provider.name": "openai",
"gen_ai.request.model": "gpt-4o-mini",
"gen_ai.response.model": "gpt-4o-mini-2024-07-18",
"gen_ai.request.temperature": 0.2,
"gen_ai.request.max_tokens": 1024,
"gen_ai.usage.input_tokens": 142,
"gen_ai.usage.output_tokens": 87,
"gen_ai.response.finish_reasons": ["stop"],
"gen_ai.response.id": "chatcmpl-..."
}
}
注意整段 JSON 里没有任何 prompt/completion 正文——这就是 B 节「默认零内容」的具体形态。
设计要点/决策表
| 要点 | 说明 | 与朴素做法差异 |
|---|---|---|
| span 名只放 operation+model | 检索索引层不含敏感信息 | 朴素做法把用户 query 塞进 span 名→全量泄露 |
| request.model ≠ response.model | 成本用 response、分组用 request | 混用导致同模型被拆成多行 |
| finish_reasons 用数组 | 区分 stop/length/content_filter,支持 n>1 | 标量丢截断信号 |
| 内容默认 opt-in | privacy-by-default,元数据已覆盖运营指标 | 默认采内容→影子 PII 库 |
| 两个 OPT_IN 开关独立 | 版本开关 vs 内容开关分离 | 混淆导致要么解析不到新字段、要么误采 PII |
| 锁定 experimental 版本 | 显式 gen_ai_latest_experimental | 不设→发旧格式,后端对不上 |
对本项目的落地
- 新建 src/aml/observability/attributeMap.ts(Day 23 详设):定义自有稳定字段(如
aiCallModel/aiCallInputTokens/aiCallFinishReasons)→ semconv 属性的单向映射常量表,全仓库埋点只引用自有字段,semconv 改名时仅改此文件。这是对本节 C「改名爆炸半径」的直接防御。 - $/案件 成本聚合的字段来源:成本计算(W7 gateway 实测)用
gen_ai.response.model× token 数算单价,不能用 request.model(C 节 A 小节口径陷阱)。聚合逻辑将与 src/aml/evalBaseline.ts 的产出按同一执行 ID 关联,使「这条 eval 失败的案件花了多少钱」可查。 - 内容捕获策略:生产环境对 AML 案件 prompt 一律
CAPTURE_MESSAGE_CONTENT=false(B 节合规基线);仅在 staging 调试某个 failure taxonomy 类别时限时开启,且开启时输入须先过 src/aml/generator.ts 已有的合成数据通道,避免真实 PII 进链路。 - 截断率作为质量信号:把
finish_reasons中length占比做成一条监控指标——SAR 草稿被 max_tokens 截断意味着证据链不完整,这是 Day 3 PRD「不产生幻觉/不丢证据」指标的可观测对应项。 - 静态导出约束的诚实标注(Day 24 展开):本项目
output:export无后端 collector,semconv span 当前在前端本地可视化(对接 src/agent/trace/useTraceStore.ts),真实 OTLP collector 是计划态,不谎称已接生产 Datadog。
参考资料
- OpenTelemetry — Semantic conventions for generative client AI spans(span 名约定、全套属性 requirement level、内容属性 opt-in 原文)官方规范,Development 状态 (2026-03 口径)
- OpenTelemetry — Gen AI 属性注册表 registry/attributes/gen-ai/(属性类型权威来源)(2026-03)
- Greptime — How OpenTelemetry Traces LLM Calls, Agent Reasoning, and MCP Tools(
OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental、v1.36 过渡基线、CAPTURE_MESSAGE_CONTENT、示例 span JSON)(2026-05-09) - Datadog — Datadog LLM Observability natively supports OpenTelemetry GenAI Semantic Conventions(v1.37+ 原生支持、映射的 gen_ai.* 属性、OTLP 管道)(2025-12-01)
- OpenTelemetry Blog — Inside the LLM Call: GenAI Observability with OpenTelemetry(traces/metrics/events 整体框架)(2026)
SOTA 检查 (2026-06-11)
- OTel GenAI semconv 当前状态:仍是
Development(experimental),无公开稳定化时间表,官方明示「属性名与结构可能继续变」——这是本笔记最重要的时效结论,直接驱动 Day 23 的属性映射层设计。 - 是否仍是 SOTA:✅ 是。GenAI 可观测的事实标准就是 OTel semconv,2025-2026 厂商(Datadog 2025-12 原生、Langfuse、Arize Phoenix、Traceloop OpenLLMetry)全部向其对齐,无竞争性替代约定。
- 2026 进展:约定已从「单次 LLM 调用」扩展到 agent 编排 span(gen-ai-agent-spans)、MCP 工具调用、内容捕获、质量评估(
gen_ai.evaluation.result事件)多层;v1.37 是 Datadog 接入基准。 - 变更风险监控:W4 埋点开工前须复查 semconv 是否已转 stable;若已转,核对
gen_ai.usage.*/gen_ai.response.finish_reasons是否改名,并据此回填本笔记与 attributeMap.ts。 - 过时认知警示:2024 年那套各 SDK 自定义
llm.*/ai.*私有属性的埋点法已被 semconv 取代——引用旧博客(如 2024-otel-generative-ai)的属性名时须核对是否仍有效,旧属性在gen_ai_latest_experimental下已不发出。 - WebSearch 验证关键词: "OpenTelemetry gen_ai semantic conventions stable 2026", "OTEL_SEMCONV_STABILITY_OPT_IN gen_ai"