可观测性:OTel GenAI span 与 Prometheus 指标 本节摘要:无可观测性的外壳是一个花钱的黑盒。本节手搓一个 span 构建器:发符合 OpenTelemetry GenAI 语义约定的记录,每 span 一行写进 JSON-Lines 文件,并以 Prometheus 文本格式暴露计数器与直方图。整版纯标准库 Python,离线运行。三类失败:缺失 trace(周二出事只有 500 行聊天日志)、不可解析 trace(外壳用了自家临时字段名,Grafana/Jaeger 读不了)、未聚合指标(能看到一次慢调用却答不出「readfile 的 p95 是多少」)。GenAI 语义约定正是为此而生——发标准属性,每个 OTel 兼容后端都能读。
本节摘要:无可观测性的外壳是一个花钱的黑盒。本节手搓一个 span 构建器:发符合 OpenTelemetry GenAI 语义约定的记录,每 span 一行写进 JSON-Lines 文件,并以 Prometheus 文本格式暴露计数器与直方图。整版纯标准库 Python,离线运行。三类失败:缺失 trace(周二出事只有 500 行聊天日志)、不可解析 trace(外壳用了自家临时字段名,Grafana/Jaeger 读不了)、未聚合指标(能看到一次慢调用却答不出「read_file 的 p95 是多少」)。GenAI 语义约定正是为此而生——发标准属性,每个 OTel 兼容后端都能读。
对应原课程:Phase 19 · Lesson 28 ·
observability-otel-traces(原英文phases/19-capstone-projects/28-observability-otel-traces/docs/en.md)。本节属「Agent Harness 深度构建赛道」第九节。
阅读完本节,你应当能够:
json.loads 往返并匹配规范形状。生产编程 Agent 每轮产三类工件:模型调用、工具执行、验证门决策。无结构化遥测,这些都没用。三类失败:缺失 trace(出事只有聊天日志,没记录哪个工具跑了、多久、多少 token 进提示、门拒了啥)、不可解析 trace(外壳写了 span 但用自家临时字段名,Grafana/Honeycomb/Jaeger/本地 CLI 都读不了)、未聚合指标(能看到一次慢调用却答不出 p95)。
OpenTelemetry GenAI 语义约定正是为此而生。它们定义一组跨 LLM 框架共享的标准属性。你的外壳发这些属性,每个 OTel 兼容后端都能读。
外壳每个操作产一个 span。span 有 trace id(整次 Agent 调用)、span id(这一操作)、name(如 gen_ai.chat、gen_ai.tool.execution)、遵循 GenAI 约定的属性、起止时间、状态。
GenAI 约定标准化这些属性键:gen_ai.system(哪个 provider,如 anthropic/openai)、gen_ai.request.model、gen_ai.request.max_tokens、gen_ai.usage.input_tokens、gen_ai.usage.output_tokens、gen_ai.response.model、gen_ai.response.id、gen_ai.operation.name,加工具专属键 gen_ai.tool.name 与 gen_ai.tool.call.id。
导出器写 JSONL,一行一个 JSON 对象——这是下游能流式读、grep、导入的最简格式。真 OTel 导出器会说 OTLP gRPC;本节的 JSONL 导出器是离线等价物,每台工作站都退出零。
span 构建器(上下文管理器):
@contextmanager def span(self, name, attrs, parent=None): s = GenAISpan(trace_id=self.trace_id, span_id=os.urandom(16).hex(), parent_span_id=parent, name=name, attributes=dict(attrs), start_unix_nano=time.time_ns()) try: yield s except Exception as e: s.status, s.status_message = "ERROR", repr(e); raise finally: s.end_unix_nano = time.time_ns(); self.exporter.export(s)
指标住在 trace 旁。计数器每次工具调用 +1:tools_called_total{tool="read_file"}。直方图记观测延迟:tool_latency_ms{tool="read_file"}。两者序列化成 Prometheus 文本呈现格式——拉取式指标的事实标准。
main.py 发:GenAISpan(trace_id/span_id/parent_span_id/name/attributes/start_unix_nano/end_unix_nano/status/status_message/events)、SpanBuilder(带 span(name, attrs, parent=None) 上下文管理器)、JSONLExporter(export(span) 追加一行)、Counter/Histogram 加 MetricsRegistry、prometheus_exposition(registry) 产文本格式、wrap_tool_call(name) 装饰器发 span 更新指标。demo 合成一次完整 Agent 调用(gen_ai.chat span 包工具 span),写 traces.jsonl,打 Prometheus 呈现,退出零。
span id 与 trace id 是 16 字节十六进制串,从 os.urandom 生成,匹配 OTel 的 W3C trace context。导出器从不抛——IO 错被呈现但外壳继续跑。直方图用固定桶集(OTel 毫秒延迟默认:5/10/25/50/100/250/500/1000/2500/5000/10000/+Inf),样本存列表,呈现时按需算每桶计数。
OTel Python SDK 是真依赖,也是几千行代码、OTLP 导出器的多进程、淹没一节课预算的运行时成本。手搓版教线格式。生产里你把同样属性接进真 SDK,白拿 OTLP 导出器、批处理、资源检测。约定是稳定的——本节发的线格式到 2030 年仍能解析,因为 OTel 从不破坏 GenAI 属性名,只加新的。
第 25 节门链、第 26 节沙箱、第 27 节评估套件,本节让这三都可观测。第 29 节把端到端 demo 的每步包进 span,末尾打 Prometheus 文本。
gen_ai.tool.execution span 含 gen_ai.tool.name 与 gen_ai.tool.call.id。json.loads 读回,确认所有属性在往返中保真。gen_ai.system/request.model/usage.input_tokens/tool.name 等,跨框架共享。下一节,我们把整条赛道缝起来——端到端编码 Agent demo,门链、沙箱、评估、可观测性合一个能修真 bug 的系统。