OpenTelemetry GenAI可观测性实战:串起模型、工具、Token与错误

一个Agent用了四十多秒才返回"查询失败",传统APM往往只能告诉你入口HTTP请求很慢。真正的问题可能藏在十几层调用里:第一次模型推理选错工具,搜索接口超时后被重试,第二次模型又带入过长历史,最后输出Token触发上限。若模型、工具和重试各写各的日志,排障只能凭时间戳拼图。

另一个常见问题是成本失控。账单显示某模型一天使用了大量Token,却无法回答"哪个Agent、哪条业务路径、哪种工具失败后消耗最多"。团队于是把完整prompt和response全部写入日志,短期能查问题,随后又遇到隐私泄露、索引费用和访问权限失控。可观测性不是记录得越多越好,而是在统一语义下记录恰当的边界。

OpenTelemetry的GenAI语义约定为模型推理、Agent调用、工具执行、Token用量和错误提供共同名称。它不绑定某一家模型,也不要求某一种后端。应用产生trace、metric和可选event,经OTLP进入Collector,再由Collector采样、脱敏和导出到现有观测平台。截至2026年10月,GenAI语义约定仍在快速演进,官方已把它迁移到独立的semantic-conventions-genai仓库,因此生产代码需要固定版本,并把自定义字段与标准字段分开管理。

1. 先定义要回答的问题

在安装SDK之前,先列出观测系统必须回答的工程问题。一次用户请求经历了哪些模型调用和工具调用?哪个步骤占用最长时间?自动重试发生了几次?输入与输出Token分别是多少?错误属于模型限流、网络超时、工具业务失败还是本地解析失败?同一个Agent版本上线后,延迟和失败率是否回归?

这些问题决定信号选择。Trace描述一次请求的因果链与时间边界;Metric观察大量请求的分布和趋势;Log记录离散诊断信息;Event适合带独立时间点的状态变化。把所有信息都塞进span attribute会制造巨大span,把每次模型调用只做成日志又失去父子关系。

一个实用的span树通常以invoke_agent为根,在下面创建每次模型推理span与execute_tool span。模型返回tool call并不意味着工具已经执行;"模型选择工具"和"应用真正执行工具"是两个不同事实。工具失败后再次推理,也应成为新的兄弟span,而不是覆盖第一次调用。
#mermaid-svg-5JdcF2A5JI78odM8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5JdcF2A5JI78odM8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5JdcF2A5JI78odM8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5JdcF2A5JI78odM8 .error-icon{fill:#552222;}#mermaid-svg-5JdcF2A5JI78odM8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5JdcF2A5JI78odM8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5JdcF2A5JI78odM8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5JdcF2A5JI78odM8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5JdcF2A5JI78odM8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5JdcF2A5JI78odM8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5JdcF2A5JI78odM8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5JdcF2A5JI78odM8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5JdcF2A5JI78odM8 .marker.cross{stroke:#333333;}#mermaid-svg-5JdcF2A5JI78odM8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5JdcF2A5JI78odM8 p{margin:0;}#mermaid-svg-5JdcF2A5JI78odM8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5JdcF2A5JI78odM8 .cluster-label text{fill:#333;}#mermaid-svg-5JdcF2A5JI78odM8 .cluster-label span{color:#333;}#mermaid-svg-5JdcF2A5JI78odM8 .cluster-label span p{background-color:transparent;}#mermaid-svg-5JdcF2A5JI78odM8 .label text,#mermaid-svg-5JdcF2A5JI78odM8 span{fill:#333;color:#333;}#mermaid-svg-5JdcF2A5JI78odM8 .node rect,#mermaid-svg-5JdcF2A5JI78odM8 .node circle,#mermaid-svg-5JdcF2A5JI78odM8 .node ellipse,#mermaid-svg-5JdcF2A5JI78odM8 .node polygon,#mermaid-svg-5JdcF2A5JI78odM8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5JdcF2A5JI78odM8 .rough-node .label text,#mermaid-svg-5JdcF2A5JI78odM8 .node .label text,#mermaid-svg-5JdcF2A5JI78odM8 .image-shape .label,#mermaid-svg-5JdcF2A5JI78odM8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-5JdcF2A5JI78odM8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5JdcF2A5JI78odM8 .rough-node .label,#mermaid-svg-5JdcF2A5JI78odM8 .node .label,#mermaid-svg-5JdcF2A5JI78odM8 .image-shape .label,#mermaid-svg-5JdcF2A5JI78odM8 .icon-shape .label{text-align:center;}#mermaid-svg-5JdcF2A5JI78odM8 .node.clickable{cursor:pointer;}#mermaid-svg-5JdcF2A5JI78odM8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5JdcF2A5JI78odM8 .arrowheadPath{fill:#333333;}#mermaid-svg-5JdcF2A5JI78odM8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5JdcF2A5JI78odM8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5JdcF2A5JI78odM8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5JdcF2A5JI78odM8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5JdcF2A5JI78odM8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5JdcF2A5JI78odM8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5JdcF2A5JI78odM8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5JdcF2A5JI78odM8 .cluster text{fill:#333;}#mermaid-svg-5JdcF2A5JI78odM8 .cluster span{color:#333;}#mermaid-svg-5JdcF2A5JI78odM8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5JdcF2A5JI78odM8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5JdcF2A5JI78odM8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-5JdcF2A5JI78odM8 .icon-shape,#mermaid-svg-5JdcF2A5JI78odM8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5JdcF2A5JI78odM8 .icon-shape p,#mermaid-svg-5JdcF2A5JI78odM8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5JdcF2A5JI78odM8 .icon-shape .label rect,#mermaid-svg-5JdcF2A5JI78odM8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5JdcF2A5JI78odM8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5JdcF2A5JI78odM8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5JdcF2A5JI78odM8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 返回tool call
结果
聚合
Token
Token
OTLP
HTTP请求span
invoke_agent support-bot
chat model-A
execute_tool search_orders
订单服务HTTP/DB spans
chat model-A
最终回答
调用时长与工具次数Metrics
OpenTelemetry Collector
Trace/Metric/Log后端

不要把"Agent思考"虚构成无法验证的内部步骤。可观测性应记录应用真正观察到的操作:调用模型、接收tool call、执行函数、访问数据库、重试与返回。若框架有明确的plan阶段,可以按约定记录;若只是从自然语言中猜测"它大概在规划",不应创建看似精确的span。

2. GenAI语义约定的关键边界

模型推理span表示调用生成式AI模型或服务并获得响应,通常使用CLIENT kind;同进程模型可以使用INTERNAL。通用命名建议是{gen_ai.operation.name} {gen_ai.request.model},例如chat model-a。关键字段包括gen_ai.operation.name、gen_ai.provider.name、请求模型、响应模型、输入/输出Token和finish reasons。具体字段要求会随约定版本演进,应以固定版本文档为准。

本地Agent执行用invoke_agent,span名在名称可用时为invoke_agent {gen_ai.agent.name},kind为INTERNAL。它覆盖一次Agent调用,而不是整个用户会话。多轮对话可以通过会话或对话标识关联,但不要让一个span跨越数小时等待用户输入;长span既难采样,也模糊了真正的活跃耗时。

工具执行使用gen_ai.operation.name=execute_tool,span名为execute_tool {gen_ai.tool.name},kind为INTERNAL。gen_ai.tool.call.id可把模型输出的工具调用与执行关联起来。工具内部若发HTTP或数据库请求,已有自动插桩会在该span下继续产生子span。这样慢点究竟在Agent编排、工具包装器还是下游服务,一眼可以分开。

工具参数和结果属于Opt-In内容,可能包含口令、查询语句、个人信息和大段文档。默认只记录工具名、类型、调用ID、结果状态和大小等元数据。只有在明确的调试环境、经过字段级脱敏并有严格访问控制时,才短期捕获内容。官方2026年文章也强调,GenAI内容默认不捕获,完整prompt、tool schema、arguments与results需要显式开启。

错误遵循OpenTelemetry通用错误约定。操作因错误结束时设置错误状态并记录error.type;若模型正常返回"工具业务拒绝",则不一定是span异常。异常、业务失败和模型finish reason是三个维度,不能用一个success=false抹平。

3. 最小可运行的Python插桩

安装API、SDK和OTLP exporter即可手工插桩。库作者通常只依赖API,由宿主应用决定SDK与导出;业务应用则初始化TracerProvider和Exporter。开发环境可先使用console exporter看清span结构,生产环境通过OTLP发给Collector,不要让每个服务直接绑定特定厂商SDK。

text 复制代码
opentelemetry-api
opentelemetry-sdk
opentelemetry-exporter-otlp-proto-http

下面的例子构造一个最小Agent:先调用模型决定是否查天气,执行工具后再次调用模型。模型函数通过依赖注入模拟,代码本身不依赖某家API。关键点是span边界围绕真实操作,Token只在模型响应可用时记录,工具异常由上下文自动关联到当前span。

python 复制代码
from __future__ import annotations

from collections.abc import Callable
from typing import Any

from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter


def configure_tracing() -> trace.Tracer:
    provider = TracerProvider(resource=Resource.create({
        "service.name": "weather-agent",
        "service.version": "1.0.0",
        "deployment.environment.name": "development",
    }))
    provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
    trace.set_tracer_provider(provider)
    return trace.get_tracer("example.weather-agent", "1.0.0")


def call_model(
    tracer: trace.Tracer,
    messages: list[dict[str, str]],
    request: Callable[[list[dict[str, str]]], dict[str, Any]],
) -> dict[str, Any]:
    configured_model = "demo-model"
    with tracer.start_as_current_span("chat demo-model", kind=trace.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", configured_model)
        try:
            response = request(messages)
        except TimeoutError:
            span.set_attribute("error.type", "timeout")
            span.set_status(trace.Status(trace.StatusCode.ERROR, "model timeout"))
            raise
        usage = response.get("usage", {})
        if isinstance(usage.get("input_tokens"), int):
            span.set_attribute("gen_ai.usage.input_tokens", usage["input_tokens"])
        if isinstance(usage.get("output_tokens"), int):
            span.set_attribute("gen_ai.usage.output_tokens", usage["output_tokens"])
        if isinstance(response.get("model"), str):
            span.set_attribute("gen_ai.response.model", response["model"])
        return response


def execute_tool(
    tracer: trace.Tracer,
    name: str,
    call_id: str,
    invoke: Callable[[], str],
) -> str:
    with tracer.start_as_current_span(f"execute_tool {name}") as span:
        span.set_attribute("gen_ai.operation.name", "execute_tool")
        span.set_attribute("gen_ai.tool.name", name)
        span.set_attribute("gen_ai.tool.type", "function")
        span.set_attribute("gen_ai.tool.call.id", call_id)
        try:
            return invoke()
        except Exception as exc:
            span.set_attribute("error.type", type(exc).__name__)
            span.set_status(trace.Status(trace.StatusCode.ERROR, str(exc)))
            raise


def run_agent(
    tracer: trace.Tracer,
    question: str,
    model: Callable[[list[dict[str, str]]], dict[str, Any]],
    weather: Callable[[str], str],
) -> str:
    with tracer.start_as_current_span("invoke_agent weather-assistant") as span:
        span.set_attribute("gen_ai.operation.name", "invoke_agent")
        span.set_attribute("gen_ai.agent.name", "weather-assistant")
        span.set_attribute("gen_ai.agent.version", "1.0.0")
        messages = [{"role": "user", "content": question}]
        first = call_model(tracer, messages, model)
        tool_call = first.get("tool_call")
        if not isinstance(tool_call, dict):
            return str(first["text"])
        result = execute_tool(
            tracer, "get_weather", str(tool_call["id"]),
            lambda: weather(str(tool_call["city"])),
        )
        messages.append({"role": "tool", "content": result})
        return str(call_model(tracer, messages, model)["text"])


if __name__ == "__main__":
    tracer = configure_tracing()
    calls = iter([
        {"model": "demo-model-1", "usage": {"input_tokens": 9, "output_tokens": 4},
         "tool_call": {"id": "call-1", "city": "上海"}},
        {"model": "demo-model-1", "usage": {"input_tokens": 18, "output_tokens": 8},
         "text": "上海今天多云。"},
    ])
    answer = run_agent(tracer, "上海天气如何?", lambda _: next(calls), lambda _: "多云")
    assert answer == "上海今天多云。"

这段示例没有记录prompt与tool arguments,只记录低敏感元数据。Token字段来自provider响应,而不是用字符串长度估算。不同模型的Tokenizer不同,客户端估算可以用于预算预警,却不应冒充账单事实。若provider未返回usage,应让字段缺失,并用"usage缺失率"监控插桩质量。

还要避免重复插桩。若模型SDK已经自动产生符合语义约定的span,业务层不要再包一个同含义的chat span,否则每次调用会出现两份时长和Token。业务层保留invoke_agent,让SDK负责模型CLIENT span;工具若已有MCP插桩,也应明确哪一层拥有execute_tool语义。

4. Trace Context如何跨过工具与队列

同步HTTP工具通常由OpenTelemetry HTTP自动插桩注入traceparent和tracestate,下游提取后生成子span。MCP 2026版也明确记录了W3C Trace Context在_meta中的键名。无论走哪种传输,传播的是上下文,不是把trace ID手工拼进日志字符串。

异步队列更容易断链。生产者发送任务时注入上下文,消费者提取并创建CONSUMER或内部span;任务可能排队很久,排队时间与执行时间要区分。对于数小时后台任务,可以用span link关联创建请求,而不是让入口HTTP span一直保持开启。

不要接受外部任意baggage并直接复制到metric label。baggage会跨服务传播,若放入用户输入、邮箱或会话ID,不仅泄露信息,还会制造高基数。只允许少量经过审核的键,并在信任边界过滤。

一次Agent调用可能并行执行多个工具,trace天然能表示这些分支。不要依赖span展示顺序判断因果,父子关系和links才是可靠结构。工具结果回到模型时,可以通过tool call ID关联,但这个ID也不宜作为metric维度。

5. Metrics:从单次证据走向总体趋势

Trace适合回答"这一单为什么慢",Metric适合回答"过去一小时是否普遍变慢"。官方GenAI约定包含模型客户端操作时长、Token使用、Agent调用时长、Agent工具调用次数以及工具执行时长等指标。具体名称与属性仍可能演进,必须与采用的语义版本一致。

直方图比平均值更有用。模型延迟常呈长尾,平均数会掩盖少量极慢请求;Token也需要观察分布,而不是只看总和。按gen_ai.request.model、provider、operation和低基数Agent名称切分通常有意义,按用户ID、prompt hash、tool call ID或错误消息切分会造成时序爆炸。

工具调用次数是Agent循环失控的直接信号。一次调用从通常一两个工具突然变为数十个,可能是模型重复选择、工具结果格式不稳定或终止条件失效。只看最终成功率会漏掉这种"成功但昂贵"的退化。相应告警应同时考虑样本量,低流量窗口中的单次异常不宜触发大规模事故响应。

成本可以由Token乘以价格表估算,但价格不是OpenTelemetry语义的一部分,且会随供应商、缓存Token和套餐变化。更稳妥的是保留标准Token metric,在报表层按带生效日期的价格维表计算,明确区分"观测用估算"和供应商最终账单。

下面建立三类低基数指标,并用一个上下文管理器记录工具时长。它展示的是手工插桩方式;若SDK或框架已经发出同名标准指标,应复用而不是重复计数。

python 复制代码
from __future__ import annotations

import time
from contextlib import contextmanager
from collections.abc import Iterator

from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import ConsoleMetricExporter, PeriodicExportingMetricReader


reader = PeriodicExportingMetricReader(ConsoleMetricExporter(), export_interval_millis=5000)
metrics.set_meter_provider(MeterProvider(metric_readers=[reader]))
meter = metrics.get_meter("example.genai-metrics", "1.0.0")

model_duration = meter.create_histogram(
    "gen_ai.client.operation.duration", unit="s",
    description="GenAI client operation duration",
)
token_usage = meter.create_histogram(
    "gen_ai.client.token.usage", unit="{token}",
    description="Number of input or output tokens used",
)
tool_duration = meter.create_histogram(
    "gen_ai.execute_tool.duration", unit="s",
    description="Tool execution duration",
)


def record_model_call(model: str, elapsed: float, input_tokens: int, output_tokens: int) -> None:
    common = {"gen_ai.operation.name": "chat", "gen_ai.request.model": model,
              "gen_ai.provider.name": "example"}
    model_duration.record(elapsed, common)
    token_usage.record(input_tokens, common | {"gen_ai.token.type": "input"})
    token_usage.record(output_tokens, common | {"gen_ai.token.type": "output"})


@contextmanager
def measure_tool(name: str) -> Iterator[None]:
    started = time.perf_counter()
    error_type = ""
    try:
        yield
    except Exception as exc:
        error_type = type(exc).__name__
        raise
    finally:
        attrs = {"gen_ai.tool.name": name}
        if error_type:
            attrs["error.type"] = error_type
        tool_duration.record(time.perf_counter() - started, attrs)


if __name__ == "__main__":
    record_model_call("demo-model", 0.25, 12, 5)
    with measure_tool("get_weather"):
        result = "多云"
    assert result == "多云"

生产指标通常由Collector或后端聚合。客户端不要把完整URL、原始异常文本和动态模型响应ID当属性。若确实需要按租户排查,优先在trace里用受控、可访问隔离的租户标识,而不是给每条metric加tenant ID。

6. 内容捕获、脱敏与采样

GenAI可观测性的最大诱惑是记录内容,因为内容最能解释模型行为;最大风险也来自内容,因为prompt可能包含个人信息、商业秘密、访问令牌、检索文档和工具结果。默认关闭内容捕获是一条合理基线。元数据已经能解决大量延迟、错误和成本问题。

需要内容调试时,可以采用四层控制。第一,按环境关闭生产默认捕获;第二,只对特定trace或故障类型采样;第三,依据结构化字段脱敏,而不是用一条正则处理所有文本;第四,把内容与普通trace分开存储,使用更严格权限和更短保留期。访问内容本身也要审计。

"先全量采集,再在Collector删除"并不总是安全,因为敏感内容已经进入进程内队列、网络和Collector。能在插桩点不生成,就不要生成。若框架自动捕获内容,要明确关闭开关,并用测试检查导出的OTLP,而不是只看配置文件。

采样也影响因果链。头部采样在请求开始时不知道后面会不会失败,尾部采样可以在Collector看到完整trace后保留错误、极慢和高Token调用。尾部采样需要更多缓冲资源,因此应设置等待窗口、内存上限和降级策略。无论哪种采样,都不能把metric建立在"只保留错误trace"的样本上,否则成功率和Token总量会严重偏差。

7. 错误、重试与取消应该怎样记录

模型SDK的自动重试通常属于一次逻辑操作,语义约定建议span覆盖包含所有自动重试的完整时长。若能观察每次尝试,可以添加受控event或子span,但必须避免把一次调用的Token重复计入。业务层显式重试则往往是多个独立模型调用,应分别建span,并用父Agent span关联。

限流错误要记录稳定的error.type,不要把包含request ID和动态文本的异常消息用作metric维度。HTTP 429、provider限流类型和本地预算拒绝应可区分。工具返回isError或领域拒绝时,根据其是否属于操作异常决定span状态,同时在业务结果字段中保留可查询类别。

用户取消流式响应时,span应在取消实际发生后结束,不能继续等后台生成而显示"成功"。如果provider仍计费,usage可能晚于取消返回;观测系统要允许Token缺失或延迟补充,不能编造零值。零表示确实使用了零个Token,缺失表示未知,两者业务含义不同。

超时要分层:HTTP入口截止时间、Agent总预算、模型单次超时、工具超时各自独立。子操作不能拥有比父预算更长的无限超时。trace中记录真实结束点后,开发者才能判断是模型慢,还是上层过早取消。

8. Collector是治理中心,不只是转发器

所有服务直连厂商后端会让采样、脱敏、重试和凭据散落。Collector提供统一OTLP入口,可以批处理、内存限流、资源属性补全、敏感字段删除、尾部采样和多后端导出。应用只负责产生正确语义,环境负责决定数据去向。

部署Collector时要防止观测系统反过来拖垮业务。SDK导出使用批处理和有界队列,Collector设置memory limiter与batch processor,出口故障时允许丢弃低价值遥测而不是阻塞用户请求。可观测性失败不应改变工具执行结果,但丢弃率本身要有内部遥测。

资源属性记录稳定的服务身份,例如service.name、版本与部署环境。不要把agent名称只放在service.name里,因为一个服务可能运行多个Agent版本;也不要把每个会话当成服务实例。资源描述"谁在运行",span描述"做了什么"。

语义版本升级应经过转换和回归。字段重命名时,Collector可在过渡期把旧厂商字段规范化为gen_ai.*,但要标明来源,避免同一调用同时存在两套互相矛盾的Token。2026年的OpenTelemetry Demo就加入了GenAI normalizer processor,用来把生态遥测转换到官方语义约定,这说明规范统一往往需要现实的适配层。

9. 从Dashboard到SLO

第一张Dashboard不要追求华丽,先包含四块:Agent端到端时长与错误率、模型调用时长与Token分布、工具时长与失败类型、每次Agent调用的工具次数。再按环境、服务版本、Agent名称、模型和工具等低基数维度过滤。

告警围绕用户可感知结果建立。入口请求错误率升高但模型和工具正常,可能是编排或输出解析问题;模型延迟正常而Agent总时长上升,可能是工具循环;输出Token突然下降同时finish reason变化,可能是上限配置错误。单看某一个vendor指标很难得出这些结论,trace树和标准metric需要配合。

SLO应把"成功"定义清楚。HTTP 200不代表Agent完成任务,模型自然语言道歉也不等于工具成功。可以在业务层记录最终结果类别,并通过trace关联到失败步骤。质量正确性还需要离线评测与在线反馈,可观测性只能说明系统如何运行,不能仅凭延迟和无异常证明答案正确。

发布验证可以对比Agent版本的p50/p95时长、每调用工具次数、Token分布、错误类别和usage缺失率。任何数字都应来自自己的流量基线,不能照抄别人的阈值。低流量服务应使用更长窗口或事件审查,避免百分位不稳定。

10. 常见故障与排查路径

如果所有span都是孤岛,先检查入口是否提取trace context、异步任务是否注入与提取、反向代理是否保留标准头。不要手工覆盖trace ID。跨信任域接收外部上下文时,还要验证格式并控制baggage。

如果一个模型调用出现两条几乎相同的span,检查是否同时启用了自动插桩和业务包装。确定语义所有者:SDK负责模型CLIENT span,应用负责Agent和自定义工具,HTTP库负责网络子span。少一层比重复一层更容易理解。

如果Token总量与账单相差很大,检查usage是否覆盖流式完成、缓存Token、重试与失败调用,字段单位是否一致,采样是否误用于聚合。账单仍是财务事实,OpenTelemetry数据用于工程归因,两者应定期对账而不是强行视为完全相等。

如果观测平台费用暴涨,先查看span体积和属性基数。完整prompt、工具结果、向量、动态错误消息和用户ID都是高风险项。删除无用内容通常比调高后端配额更有效。再检查Agent循环是否真的增加了调用数量,费用上涨也可能是业务故障的症状。

如果错误span很多但用户无感,检查业务拒绝是否被错误标记为异常;如果用户失败但trace全绿,检查代码是否吞掉异常、只在返回文本中表达失败。错误语义要与实际操作边界一致,不能为了Dashboard好看把失败标成成功。

11. 把在线观测与离线评测连接起来

延迟低、没有异常、Token少,只能证明系统运行得顺,不证明答案正确。Agent可能快速调用了错误工具,也可能用很少Token自信地给出错误结论。因此线上trace与离线评测需要共享可关联但低敏感的标识,例如Agent版本、Prompt模板版本、工具集合版本和评测数据集版本。

评测任务调用真实Agent时,同样创建invoke_agent根span,但资源属性标记为evaluation环境,避免与生产SLO混合。每条样本的期望结果留在评测存储,不必放进span;trace只记录不可逆样本ID、执行路径和结果类别。失败样本可通过trace定位究竟是检索、工具、模型还是解析器造成,再把根因转成回归用例。

发布前可以比较候选版本与基线版本:任务通过率属于评测系统,端到端时长、工具次数和Token分布来自OpenTelemetry。两组数据联合才能识别"质量略升但成本翻倍"或"延迟下降但工具选择退化"。不要把一个综合分数掩盖权衡,应分别设定质量门槛与资源预算。

线上用户反馈也可形成受控event。点赞、人工纠错或升级客服是一个时间点事件,携带结果类别和当前trace关联信息即可,不应复制整段对话。反馈可能晚于Agent调用,使用link或业务关联ID连接,不要让原span一直开着等待几天后的评价。

发现生产失败后,回放必须先脱敏并隔离副作用。不能为了复现把原始工具调用直接重放到支付、邮件或删除接口。为工具提供只读模拟器或录制的响应,再比较Agent路径。trace是诊断证据,不是获得用户授权重新执行操作的凭证。

12. Schema治理与插桩契约测试

GenAI语义约定独立演进,字段稳定性并不完全一致。团队应建立一份采用清单:语义约定提交或发布版本、SDK版本、自动插桩包、Collector转换规则以及Dashboard查询。升级任意一层时运行契约测试,检查实际导出的OTLP,而不是只靠类型检查。

契约测试可以启动内存或测试Exporter,执行一次确定性Agent:一次模型调用、一次工具调用、再一次模型调用。断言存在一个invoke_agent、两个推理span和一个execute_tool,父子关系正确;Token字段只出现在模型span;工具名和调用ID存在;prompt、令牌和工具结果未被默认导出。这个小场景比大量mock测试更能发现重复插桩与字段漂移。

自定义字段使用组织命名空间,并记录何时可以删除。若标准后来提供相同含义字段,先双写一段短期窗口并验证查询,再迁移Dashboard,最后删除自定义字段。不要把自定义属性直接命名为尚未标准化的gen_ai.*,否则未来官方字段同名但语义不同,数据将无法区分。

Collector中的转换也要版本化。处理器把旧字段映射到新字段时,遇到目标字段已存在应定义优先级并计数冲突,不能静默覆盖。转换前后的样本OTLP作为golden文件保存,升级Collector后对比。观测管线本身需要发布流程,因为一条错误转换规则能让所有服务的成本报表同时失真。

属性基数可以设置自动守卫:Collector统计每个键的估计唯一值,超过预算时删除、哈希或降级到日志,并告警给插桩所有者。守卫是最后防线,不应替代代码评审。模型响应ID、会话ID和用户ID几乎总是不适合作为metric attribute,即使某次压测中基数看起来不高。

13. 安全事件中的可观测性

Agent安全事故往往表现为正常API调用:模型因提示注入选择了高风险工具,工具也用合法凭据执行。仅靠异常率无法发现。观测模型应记录经过归类的工具风险级别、授权决策结果、是否经过人工确认和策略版本,但不要记录密钥或原始敏感参数。

例如邮件发送工具的span可以显示工具名、风险类别、策略允许或拒绝以及审批模式。若同一Agent版本突然大量调用高风险工具,即使全部成功,也值得调查。策略拒绝不是系统异常,却是安全指标;它可以作为event或独立计数器,不能为了降低"错误率"而完全丢弃。

Prompt Injection文本本身不宜全量进入trace。可以记录检测器版本、规则类别和决策,并把需要人工分析的少量内容写入隔离证据库。证据库使用独立密钥、短保留期和严格访问审批。普通研发看得到"因为外部文档指令被拒绝",不一定需要看到文档中的个人信息。

trace context也可能被滥用。外部调用方可以伪造traceparent尝试与内部trace关联,或通过baggage传播恶意高基数字段。入口应使用标准解析器,按信任策略决定继承、重建或link外部上下文,并过滤baggage白名单。trace ID不是认证令牌,知道它不能获得对应数据的访问权。

审计日志和调试trace用途不同。涉及谁在何时批准了什么操作的记录,应进入防篡改、保留期明确的审计系统;trace可以引用审计事件ID,却不应被当成唯一审计账本。采样、过期和后端故障都可能让trace不完整,而合规审计通常要求更强保证。

14. 上线边界与演进策略

OpenTelemetry不会自动修复慢模型、错误工具或Prompt Injection。它让这些问题可定位,并为容量、预算和安全审计提供证据。内容捕获更不是安全防护;若工具权限过大,trace只能记录越权已经发生。授权、沙箱、输入验证和人工确认仍必须在执行路径上。

GenAI语义约定处于Development的部分可能发生不兼容变化。固定依赖和约定版本,维护一小层集中映射,契约测试检查导出的span名、kind和关键属性。不要把属性名散落在数百个业务函数里,也不要为了"未来所有provider"设计庞大抽象;先覆盖实际使用的Agent、模型和工具边界。

最小上线顺序是:先贯通入口、Agent、模型与工具trace;再加入低基数延迟和Token metric;然后配置Collector批处理、脱敏与采样;最后才在受控场景启用内容。每一步都验证OTLP实际输出和后端查询,而不是只验证代码没有报错。

总结

GenAI系统难以排障,不是因为没有日志,而是因为模型、工具、重试和Token缺少共同因果结构。OpenTelemetry用invoke_agent、模型推理和execute_tool等语义把真实操作连成span树,再用标准metric观察延迟、Token和工具次数的总体分布。

可靠方案要坚持四条边界:span围绕真实操作而非想象中的思考;内容默认不采集,必要时按字段脱敏和短期采样;metric只使用低基数维度;语义约定固定版本并通过Collector集中治理。这样当Agent再次"慢、贵、错"时,团队看到的不再是一句失败文本,而是一条可以定位、量化和回归验证的执行链。

参考资料

  1. OpenTelemetry:Inside the LLM Call,GenAI可观测性实践
  2. OpenTelemetry GenAI语义约定仓库
  3. GenAI Client Spans语义约定
  4. GenAI Agent与Framework Spans语义约定
  5. GenAI Metrics语义约定
  6. OpenTelemetry Python手工插桩
  7. OpenTelemetry Python文档
  8. OpenTelemetry Collector配置文档
  9. OpenTelemetry通用错误记录约定
  10. W3C Trace Context
  11. OpenTelemetry:AI Agent Observability
  12. OpenTelemetry Demo 3.0与GenAI normalizer
  13. OpenTelemetry语义约定总览
相关推荐
嫂子开门我是_我哥1 小时前
青少年抑郁、焦虑、失眠与自杀相关表现的症状联系:一篇网络分析论文所使用的算法深度解读
论文阅读·算法·论文笔记
yichengerp1 小时前
国内中小电子工厂用哪个erp系统好?
大数据·运维·人工智能·云计算·制造
q27551300421 小时前
微纳代理 WN8034F 国产降噪音频方案
人工智能·语音识别
喜欢打篮球的普通人1 小时前
MiniMind 学习笔记(二十二):数学 Cookbook——把概率空间、期望、似然一次讲明白
人工智能·笔记·学习
渊鱼L1 小时前
CAD随机颗粒插件2D V2.0 更新说明
算法
程序猿阿森1 小时前
混合精度详解:FP16 vs BF16,Loss Scaling 与 PyTorch 实战
人工智能·pytorch·python
Lazionr1 小时前
unordered_map和unordered_set的使用
开发语言·c++·算法
ADark1 小时前
FDE 入门 · 08|没人爱做的交付尾巴
人工智能·agent
alonglong1 小时前
macOS 定时任务排错:退出码、错误消息、旧结论,三个都不承载事实
人工智能