返回 AIPA 笔记
AIPA Day 23

独立属性映射层

独立属性映射层

2026-07-07
anti-corruption-layerdddtelemetrysemconv-mapping

日期: 2026-07-07 阶段: Phase 1 - 产品定义×评测×可观测底座 标签: #anti-corruption-layer #ddd #telemetry #semconv-mapping

核心问题

Day 22 留下一个硬约束:OTel GenAI semconv 仍是 experimental(2026-03),属性名随时可能改;HTTP semconv 转 stable 时一次性改了 17 个属性名(http.methodhttp.request.method 等,Honeycomb/Better Stack 2026 文档实证),GenAI 几乎必然走同样的路。如果业务代码、仪表盘查询、eval 成本聚合全都直接硬编码 gen_ai.usage.input_tokens 这种实验期属性名,改名时爆炸半径覆盖全栈。

今天回答:怎么用 DDD 的**防腐层(Anti-Corruption Layer, ACL)**思想,在自有稳定字段与 semconv 属性之间插一层单向映射,让「semconv 改名 → 只改一个文件」成立。落地到 src/aml/observability/attributeMap.ts

关键内容

A. 防腐层(ACL)的本质:保护核心模型不被外部模型污染

ACL 是 Eric Evans 在《Domain-Driven Design》提出的经典模式(Azure Architecture Center 2026-05-28 仍在维护此条目)。它的意图一句话:在两个语义不同的子系统之间放一个翻译层,让一方的模型保持不变,而不牺牲另一方的设计

原文关键句:「Isolate the different subsystems by placing an anti-corruption layer between them. This layer translates communication between the two systems... you can keep one system unchanged without compromising the design and technological approach of the other.」

ACL 解决的「污染」是什么?当你的现代系统必须对接一个 schema 混乱、API 过时、且你无法控制其演进的外部系统时,若直接让外部模型渗入核心代码,核心代码就被迫迁就外部的坏设计与变更。OTel GenAI semconv 对本项目就是这样一个外部系统——它由 OTel SIG 控制、处于实验期、会改名、且本项目无权决定它何时稳定。把 semconv 属性名直接写进业务逻辑,就是让一个「你不控制、还会变」的模型污染你的核心。

ACL 把这层污染挡在边界外:核心侧永远用自有稳定词汇说话,ACL 负责把自有词汇翻译成外部词汇

反直觉洞察(ACL 不是为了「现在」省事,是为了「将来」隔离变更):刚写时直接用 semconv 属性名反而更快——少一层映射、少一个文件。ACL 的全部价值在变更发生那一刻才兑现:semconv 改名时,没有 ACL 的项目要改 N 处(埋点 + 仪表盘 + eval + 告警),有 ACL 的项目只改 1 处。这是一笔「现在多花 5 分钟、将来省 5 小时」的保险——而 experimental 状态恰恰意味着「将来」一定会来。

B. 单向映射的接口设计:自有字段 → semconv

ACL 的一个核心纪律(Azure 文档明示):翻译是单向的、有方向性的。「Communication between subsystem A and the anti-corruption layer always uses the data model of subsystem A. Calls from the anti-corruption layer to subsystem B conform to that subsystem's data model.」

映射到本项目:核心侧(subsystem A)是 AML Copilot 的业务/可观测代码,永远用自有稳定字段;外部侧(subsystem B)是 OTel semconv。方向是自有字段 → semconv,单向,不反向。原因是 emit telemetry 时数据只从核心流向 collector;不需要把 semconv 翻回自有字段(除非读取第三方 trace,那是另一个 ACL 实例)。

接口设计(TS 伪代码,将落 src/aml/observability/attributeMap.ts):

// 自有稳定字段:项目内永远用这套词汇,不随 semconv 变
export interface AiCallTelemetry {
  callModelRequested: string   // 用户视角的模型选择
  callModelResponded: string   // 真实计费模型(算成本用,见 Day22)
  inputTokens: number
  outputTokens: number
  finishReasons: string[]      // ['stop' | 'length' | 'content_filter']
  provider: string             // 'openai' | 'anthropic'
  operation: 'chat' | 'embeddings'
}

// 单向映射常量:自有字段名 → 当前 semconv 属性名
// semconv 改名时,只改这张表的右侧值,业务代码零改动
const SEMCONV_MAP = {
  callModelRequested: 'gen_ai.request.model',
  callModelResponded: 'gen_ai.response.model',
  inputTokens:        'gen_ai.usage.input_tokens',
  outputTokens:       'gen_ai.usage.output_tokens',
  finishReasons:      'gen_ai.response.finish_reasons',
  provider:           'gen_ai.provider.name',
  operation:          'gen_ai.operation.name',
} as const

// 翻译函数:核心模型 → semconv 属性 bag(emit 时唯一出口)
export function toSemconvAttributes(t: AiCallTelemetry): Record<string, unknown> {
  return {
    [SEMCONV_MAP.callModelRequested]: t.callModelRequested,
    [SEMCONV_MAP.callModelResponded]: t.callModelResponded,
    [SEMCONV_MAP.inputTokens]:        t.inputTokens,
    [SEMCONV_MAP.outputTokens]:       t.outputTokens,
    [SEMCONV_MAP.finishReasons]:      t.finishReasons,
    [SEMCONV_MAP.provider]:           t.provider,
    [SEMCONV_MAP.operation]:          t.operation,
  }
}

自有字段 ↔ semconv 映射表(成本聚合、eval、告警全部只引用左列):

自有稳定字段当前 semconv 属性(2026-03 实验版)默认采集改名风险
callModelRequestedgen_ai.request.model高(HTTP 先例改过 model 类)
callModelRespondedgen_ai.response.model
inputTokensgen_ai.usage.input_tokens
outputTokensgen_ai.usage.output_tokens
finishReasonsgen_ai.response.finish_reasons
providergen_ai.provider.name高(曾叫 gen_ai.system
operationgen_ai.operation.name

注意 provider 那行的改名风险已经发生过:semconv 早期用 gen_ai.system,后改为 gen_ai.provider.name——这正是 ACL 要挡的那类变更的活样本。

反直觉洞察(ACL 必须只做翻译,不能塞业务逻辑):Azure 文档明确警告——「focus the anti-corruption layer on translation logic. Avoid placing business rules or orchestration in the layer.」很容易顺手在映射层里加「如果 provider 是 openai 就乘以单价算成本」之类的逻辑,这会让映射层变成一个隐藏的业务模块,下次改成本逻辑还得动它,失去「纯映射、极稳定」的价值。成本计算属于 evalBaseline/成本聚合模块,映射层只做名字翻译,一行业务都不能有。

C. 一处改名全局生效的工程价值

把 semconv 转 stable 当作必然事件来设计。HTTP semconv 的迁移给了完整剧本(OpenTelemetry db-migration 文档 + Honeycomb 2026):转 stable 时不仅改名,还提供 OTEL_SEMCONV_STABILITY_OPT_INhttp/dup 模式——同时发新旧两套属性,让下游平滑迁移。

ACL 让本项目可以复刻这套「双发→切换→下线旧名」流程,而且控制点收敛到一个文件:

semconv GenAI 转 stable,input_tokens 改名场景下的迁移步骤:
  1. 改 attributeMap.ts:inputTokens 映射值由旧名改为新名
     (若需双发期,临时同时 emit 新旧两 key,对应 http/dup 思路)
  2. 业务代码、成本聚合、eval、告警 —— 零改动(都只用 inputTokens)
  3. 仪表盘查询 —— 若仪表盘也只读自有维度则零改动;
     若直读 semconv 名,则这是唯一需要同步的外部消费点,单独迁移
  4. 双发期结束后,从 map 删除旧 key

量化对比「有 ACL vs 直连 semconv」在一次改名时的改动面:

维度直接硬编码 semconv 名经 ACL(attributeMap.ts)
改名时需改的源文件数N(≈所有埋点 + 聚合 + 告警,本项目预计 8-12 处)1(仅 map)
仪表盘查询返工全部(按 semconv 名查)0(按自有维度查)
双发期支持成本手工在每处加旧+新两份map 中临时双 key,一处搞定
引入新后端(如换 Datadog→Langfuse)可能再次全栈适配map 输出层适配,核心不动
误改 PII 采集开关的耦合风险内容开关与字段名混在埋点处字段映射与内容 opt-in 物理分离
一次性额外成本0一个文件 + 一次翻译调用

「引入新后端」这行是 ACL 的额外红利:Datadog(2025-12 原生支持 semconv)与 Langfuse 都吃 semconv 属性,所以核心侧只要产出 ACL 翻译后的标准属性,换后端基本零改核心——这把 Day 25 的 Langfuse 自托管选型也解耦了。

设计要点/决策表

要点说明与朴素做法差异
自有字段为唯一词汇业务/eval/告警只认 inputTokens朴素做法全栈直写 gen_ai.usage.input_tokens
映射单向自有 → semconv,emit 唯一出口不做双向,避免无谓复杂度
map 为纯常量表改名只改右侧值业务逻辑混入会破坏「极稳定」
复刻 http/dup 双发转 stable 期临时双 key无 ACL 时双发要逐处手改
隔离后端选型输出层适配 Datadog/Langfuse核心不绑定具体后端
翻译失败可观测映射缺字段时记日志(Azure 建议)静默丢字段难排查

对本项目的落地

  • 新建 src/aml/observability/attributeMap.ts:实现 B 节的 AiCallTelemetry 接口、SEMCONV_MAP 常量、toSemconvAttributes() 翻译函数。这是 Day 22「改名爆炸半径」防御的物理落点,也是整个 P1 可观测底座的边界文件。
  • 全仓库埋点纪律:Day 24 的全链路埋点(orchestrator/工具/检索/生成 span)一律构造 AiCallTelemetry 自有对象,只在 emit 那一刻调 toSemconvAttributes()——semconv 属性名不得出现在 attributeMap.ts 以外任何文件。这条纪律可用一条 lint/grep 规则在 CI 强制(搜 gen_ai\. 命中非 map 文件即失败)。
  • 成本聚合解耦:$/案件 聚合(与 src/aml/evalBaseline.ts 产出按执行 ID 关联)读 callModelResponded + outputTokens 自有字段算单价,不碰 semconv 名——遵守 C 节「ACL 只翻译、成本逻辑归成本模块」的纪律。
  • 后端无关性:Day 25 选 Langfuse 自托管 vs 维持本地可视化时,因核心只产自有对象、ACL 产标准属性,切换后端只动 ACL 输出适配,不动 src/agent/trace/useTraceStore.ts 的核心数据结构。
  • 诚实标注:当前 output:export 无 collector,toSemconvAttributes() 的输出先喂前端本地可视化;真实 OTLP 导出是计划态。映射层先行落地不依赖后端就位——这正是 ACL「边界先定、实现后补」的价值。

参考资料

  1. Microsoft Azure Architecture Center — Anti-Corruption Layer Pattern(ACL 意图、单向翻译原文、「只放翻译逻辑勿放业务规则」警告、observability 建议)(更新 2026-05-28)
  2. Eric Evans — Domain-Driven Design: Tackling Complexity in the Heart of Software(ACL 模式原始出处,经典著作 2003)
  3. Honeycomb Docs — Migrate to Stabilized OpenTelemetry HTTP Semantic Conventions(HTTP 转 stable 改名 17 个属性、OTEL_SEMCONV_STABILITY_OPT_IN http/dup 双发模式)(2026)
  4. OpenTelemetry — Database semantic convention stability migration guide(db.systemdb.system.name 改名先例、迁移过渡期双发机制)(2026)
  5. Better Stack — The Missing Guide to OpenTelemetry Semantic Conventions(http.methodhttp.request.method 等改名清单、稳定化破坏性变更)(2026)
  6. AWS Prescriptive Guidance — Anti-corruption layer pattern(ACL 作为翻译/中介层的实现指引)(2025-2026)

SOTA 检查 (2026-06-11)

  • ACL 模式仍是 SOTA 且与本场景高度契合:Azure 条目 2026-05-28 刚更新,AWS、DeviQ、OneUptime(2026-01)均在重申 ACL 用于隔离「不可控的外部模型变更」——semconv experimental 正是教科书式的适用场景。
  • 改名是已证实的真实风险而非假设:HTTP/DB semconv 已实际经历转 stable 改名(17 个 HTTP 属性、db.systemdb.system.name),GenAI semconv 仍 Development 且无稳定时间表(Day 22 结论),按先例几乎必然改名——ACL 设计的前提成立。
  • 是否仍是 SOTA:✅ ACL(隔离层)+ 单向映射是处理「依赖实验期外部约定」的事实最佳实践;OTel 官方迁移指南本身也推荐用 Collector 层做属性转换(OTTL processor 双向升降级),与本项目应用层 ACL 思路同源,二者可叠加(应用层 ACL 管自有字段,Collector 层管跨版本兼容)。
  • 过时认知警示:把 ACL 误解为「重量级独立服务」是旧观念——本项目的 ACL 是一个轻量映射模块(一个 ts 文件 + 一个翻译函数),无需独立部署;Azure 文档亦明示 ACL 可实现为「a component within the application」。
  • 待跟踪:semconv 转 stable 的实际改名清单一旦发布,立即据此更新 attributeMap.ts 的 SEMCONV_MAP 右侧值并回填本笔记;同时评估是否进入 http/dup 式双发过渡期。
  • WebSearch 验证关键词: "anti-corruption layer telemetry semconv 2026", "OpenTelemetry semantic convention migration attribute rename"