用 OpenTelemetry 串联 AI 决策与人工审批
文章介绍如何将请求、上下文检索、模型调用和工具执行纳入分布式追踪,并保存追踪上下文。对于隔日发生的人工审批,则通过新追踪关联原始请求,同时讨论追踪能力的边界。
AI 深度解读
退款回复由 AI 起草,隔天由同事批准,一周后客户提出异议。模型、输入和审批人的信息可能都在,却分散在不同系统里,无法可靠串起来。原文给出的办法是:用 OpenTelemetry 连接生成与审批过程,再用业务存储保留决策证据。
trace_id 适合串联一次决策的执行过程,但不能单独承担长期留证。 对正在做客服助手、工具调用和人工审核流程的团队,这个区分比多记几条模型日志更有用。
跨了一天的审批,应该怎样接回生成过程
W3C 于 2021 年 11 月 23 日发布的 Trace Context Recommendation,定义了通过 traceparent 请求头传播追踪上下文的方法。其中 trace-id 是一个 16 字节的值,同一次请求处理中的各个 span 共享它。span 可以理解为追踪链路里的一段操作。
在 AI 功能里,请求、检索上下文、调用 chat model-a、执行工具,可以属于同一条同步链路。原文示意中的 trace_id 4bf9... 就负责把这些步骤连起来。
人工审批另算。
草稿可能第二天才被打开,此时应该创建新的 trace,再通过 span link 指向生成草稿的原始上下文。这样既保留因果关系,也不会把一天的等待伪装成一个持续运行的操作。
下面这段代码解决两个问题:记录模型调用身份,并让保存后的草稿携带可供审批端恢复的追踪上下文。
from opentelemetry import trace
from opentelemetry.trace import Link, SpanKind
from opentelemetry.propagate import inject, extract
tracer = trace.get_tracer("support-assistant")
MODEL = "model-a-2026-01-15" # 固定模型 ID,不使用别名
def draft_reply(ticket: dict, call_model) -> dict:
with tracer.start_as_current_span("draft_reply"):
with tracer.start_as_current_span(
f"chat {MODEL}", kind=SpanKind.CLIENT) as span:
span.set_attribute("gen_ai.operation.name", "chat")
span.set_attribute("gen_ai.provider.name", "example")
span.set_attribute("gen_ai.request.model", MODEL)
resp = call_model(MODEL, ticket["text"])
span.set_attribute("gen_ai.response.model", resp["model"])
span.set_attribute("gen_ai.response.id", resp["id"])
carrier = {}
inject(carrier) # 写入 W3C traceparent
return {"ticket_id": ticket["id"], "text": resp["text"],
"trace": carrier} # 随草稿保存
def approve(draft: dict, reviewer_id: str) -> None:
origin = trace.get_current_span(extract(draft["trace"]))
with tracer.start_as_current_span(
"review.approve",
links=[Link(origin.get_span_context())]) as span:
span.set_attribute("app.reviewer.id", reviewer_id)
span.set_attribute("app.ticket.id", draft["ticket_id"])
关键是 inject(carrier) 所处的位置:模型调用的 span 已经结束,当前仍处于 draft_reply 内,因此保存的是草稿生成操作的上下文,审批通过它关联回原 trace,并非直接链接到模型调用的那个 span。
模型日志要回答「请求了谁,实际由谁处理」
原文采用 OpenTelemetry 的 GenAI semantic conventions,为不同供应商的调用提供统一字段。其客户端推理 span 名称采用 {gen_ai.operation.name} {gen_ai.request.model},例如 chat model-a。
字段要求也有区别:
gen_ai.operation.name和gen_ai.provider.name为必需字段。gen_ai.request.model在可获得时要求记录。gen_ai.response.model和gen_ai.response.id为推荐字段。
同时保存请求模型和返回模型,是为了应对别名解析到新快照的情况。代码中的 model-a-2026-01-15 展示了固定模型 ID 的写法,不代表原文验证过某个真实模型的能力。
另一个容易影响排障的细节是重试:同一个逻辑操作的自动重试,应由一个 span 覆盖,避免让重试看起来像几次独立决策。
不过,这套约定仍标记为 Status: Development,且已迁到独立仓库。文章没有给出具体约定版本,接入时需要确认所用埋点库支持哪些字段,不能把文中的名称当成永远不变的接口。
追踪连得起来,不代表证据留得下来
原文建议默认不把指令、输入和输出写进 span。这些内容可能敏感,体积也可能很大。示例虽然把工单文本传给模型,但追踪属性只记录模型名称和标识,没有记录正文。
需要保留内容时,可以存到外部存储,再在 span 上记录引用,让正文拥有独立的访问控制。
更大的问题是采样和保留期限。Trace Context 的 sampled 标志只表示调用方可能记录了追踪数据,并不保证某次决策一定有完整 trace;追踪存储的保留窗口通常服务于调试,也可能早于争议发生时间结束。
因此,原文要求把草稿、模型、审批人和结果写进自行控制、明确保留期限的业务存储,以工单 ID 为查询入口,再附上 trace_id。审核人员通常拿着工单来查,不会先知道追踪 ID。
文中的测试使用 SDK 的内存导出器验证关联关系:
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
InMemorySpanExporter)
def test_review_links_to_model_call():
exporter = InMemorySpanExporter()
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(exporter))
trace.set_tracer_provider(provider)
fake = lambda m, t: {"model": m, "id": "resp_1", "text": "ok"}
draft = draft_reply({"id": "T-1", "text": "refund?"}, fake)
approve(draft, "u_17")
spans = {s.name: s for s in exporter.get_finished_spans()}
chat, review = spans[f"chat {MODEL}"], spans["review.approve"]
assert review.context.trace_id != chat.context.trace_id
assert review.links[0].context.trace_id == chat.context.trace_id
最后两行验证的是审批与模型调用属于不同 trace,且审批 link 指向原 trace。它没有断言链接的具体 span_id,也没有验证生产环境中的持久化、采样或证据完整性。
我的判断
我认同把运行追踪与决策记录分开。相比只在日志里打印工单 ID,这套做法增加了操作间的结构化关系;相比把全部内容塞进追踪平台,它又能分别设置内容权限和保留期限。
但示例离完整方案还有距离。approve 没有显式清空当前上下文,独立运行时可以产生新 trace,接到已有 Web 请求链路中时则可能继承该请求。实际接入要明确审批应加入当前请求,还是另建根 trace。
原文也没有交代如何防止记录被修改、如何处理写入失败,或如何证明保存的是审批当时看到的草稿。它提供的是关联方案,不能据此推出完整审计能力。
此外,文章由 Claude 为 Obligra Verify 团队生成,声明核对过引用来源,并介绍了处于 public preview 的 Obligra Verify™。文中没有生产规模实测、性能对照或产品比较数据,架构建议可以参考,产品效果仍缺乏证据。
能拿来做什么
- 给已有 AI 草稿流程增加持久化追踪上下文,让后续人工操作能够恢复来源;如果后端用 Node,需要按对应 SDK 实现传播与链接,原文只提供 Python 示例。
- 在模型调用层统一记录请求模型、返回模型和响应 ID,并检查自动重试是否被错误拆成多次逻辑操作。
- 以工单 ID 保存草稿、模型、审批人和结果,同时记录
trace_id,让业务查询能接到仍在保留期内的运行细节。 - 为跨时审批添加关联测试,并按实际运行环境检查现有请求上下文,确保 trace 的归属符合设计。