Agent 工程 · 第 10 章|可观测性:三信号、trace 树、GenAI 语义约定、SLO

第 10 章 · 可观测性

生产事故的常态不是"系统挂了",而是"用户说 Agent 答非所问,但你连它当时看到什么都不知道"。可观测性解决的是:任何一次 Agent 行为都可以事后完整还原。三件工具:日志(离散事件)、指标(聚合数值)、追踪(请求级因果树)。

10.1 日志:结构化与脱敏

Loguru/logging 的常规实践之外,Agent 系统的特殊纪律:

结构化优先。日志字段比自由文本可查一万倍:

logger.bind(
    trace_id=trace_id, chat_id=chat_id, agent_id=agent_id,
    tool="query_db", latency_ms=183, ok=True,
).info("tool executed")

敏感信息分级。日志会进聚合系统、会被很多人查到:

  • 绝不入日志:API Key、用户 token、密码、完整身份证/手机号;
  • 脱敏后入日志:用户消息(保留排查能力,遮蔽 PII);
  • LLM 请求/响应用独立的日志通道(它们既敏感又是排查核心,单独存储与权限)。

trace-id 贯穿。一次用户请求经手:路由 → Agent 循环 → 模型调用 → 工具执行 → 子系统。每条日志带同一 trace_id,排查时一把捞全。中间件设置、contextvars 传播(跨异步任务)是标准实现。

10.2 追踪:Agent 的 trace 是一棵树

传统 Web 的 trace 是一条链;Agent 的 trace 是树——一次会话里嵌套着模型调用、工具执行、子智能体,每个都是带输入输出的 span:

trace: 会话 chat_abc (48.2s, $0.31)
├── span: llm call #1 (gpt-4o, 1.2s, in 2.1k tok / out 350 tok)   → tool_call×2
├── span: tool: read_file (0.05s, ok, 4.2KB)
├── span: tool: web_search (2.1s, ok, 12 hits)
├── span: llm call #2 (2.8s, …)                                   → tool_call×1
│   └── span: subagent: deep-research (31s, $0.22)
│       ├── span: llm call …×12
│       └── span: tool: web_search …×6
└── span: llm call #3 (3.1s) → final answer

从这棵树能直接读出的排查结论:花费大头在子智能体;第 2 次模型调用后的工具选择了 web_search 而不是内部工具(工具选择问题的线索);token 曲线逐轮上涨(上下文管理问题的线索)。

trace 上下文的传播是工程难点:Agent 是异步的(asyncio create_task 会复制 contextvars),子任务的 span 要挂回父 span。实现模式:Langfuse/OTel SDK + contextvar 保存当前 span,异步任务创建时自动继承——这就是为什么第 1 章强调 trace_id 用 contextvars 而不是函数参数层层传递。

10.3 指标:Agent 的金牌指标

Prometheus 指标四类型(Counter 只增、Gauge 瞬时值、Histogram 分布、Summary 分位)。Agent 系统在红金四信号(延迟/流量/错误/饱和度)之上,加三个金牌指标:

from prometheus_client import Counter, Histogram

llm_calls_total = Counter(
    "agent_llm_calls_total", "LLM 调用总次数", ["model", "status"])
llm_latency_ms = Histogram(
    "agent_llm_latency_ms", "LLM 调用耗时(ms)", ["model"],
    buckets=(100, 250, 500, 1000, 2500, 5000, 10000, 30000, 60000))
llm_tokens_total = Counter(
    "agent_llm_tokens_total", "LLM token 消耗", ["model", "direction"])  # in/out
task_success_total = Counter(
    "agent_task_total", "Agent 任务数", ["agent", "status"])             # success/fail
first_token_latency = Histogram(
    "agent_first_token_ms", "首 token 延迟", ["model"],
    buckets=(200, 500, 1000, 2000, 4000, 8000))
task_cost = Histogram(
    "agent_task_cost", "单任务估算成本", ["agent"],
    buckets=(0.01, 0.05, 0.1, 0.5, 1, 5, 20))

三个金牌指标为什么是它们:

  • 首 token 延迟:用户感知的"快慢"主要是它(等 30 秒出完整答案 vs 0.5 秒开始流式,体验天差地别);
  • 任务成功率:Agent 的"可用性"不是 HTTP 200(模型胡说八道也是 200),是任务级成败;
  • 单任务成本分布:均值没意义(长尾任务吃掉预算),要看直方图与 P95。

埋点点位的选择决定可维护性:在 LLM 调用层注册 hook(post-hook 拿到 usage/延迟),模型调用与工具执行自动记账——业务代码零侵入。标签纪律:标签基数必须有限(model/agent/status),绝不放 user_id/chat_id——基数爆炸拖垮时序库,且用户级指标撞隐私红线(用户级分析去 trace/日志里做)。

10.4 OpenTelemetry GenAI 语义约定

自创埋点模型是 2024 年的行业病,2026 年已经收敛:OTel GenAI 语义约定(gen_ai.*)定义了 LLM 调用、Agent、工具的 span 与属性标准,主流 APM(Datadog 等)已原生支持。

核心约定(节选):

span / 属性 含义
span 名 chat {model} 一次 LLM 调用
gen_ai.system 供应商(openai / anthropic / …)
gen_ai.request.model / response.model 请求/实际服务模型(路由降级后会不同)
gen_ai.usage.input_tokens / output_tokens token 用量
gen_ai.agent.name / gen_ai.agent.id Agent 身份
gen_ai.tool.name / gen_ai.tool.call.id 工具执行 span
gen_ai.prompt.{n}.content(敏感,按需关闭) 消息内容

采用标准的价值:换 APM 供应商零迁移;与社区仪表盘/告警模板兼容;跨团队指标口径统一。自创的价值为零——除非你的场景确实超出约定覆盖。

10.5 SLO:把指标变成承诺

指标堆满看板不等于可观测,**SLO(服务等级目标)**把指标变成可执行的承诺:

SLO-1 可用性:   Agent 任务成功率        ≥ 98%(30 天滚动)
SLO-2 响应性:   P95 首 token 延迟       ≤ 2s
SLO-3 成本:     单任务 P95 成本         ≤ $0.5,月总成本 ≤ 预算

工程闭环:每条 SLO 对应错误预算(1 - SLO 的量)——错误预算烧穿(如成功率 97.5%)就冻结功能发布、全力修复。这让"要不要为发新功能冒险"从一个政治问题变成一个数字问题。

告警规则示例(PromQL 意识形态):

- alert: TaskSuccessRateLow
  expr: sum(rate(agent_task_total{status="success"}[30m]))
      / sum(rate(agent_task_total[30m])) < 0.95
  for: 10m        # 持续 10 分钟才告警——过滤抖动
- alert: FirstTokenP95High
  expr: histogram_quantile(0.95,
        sum(rate(agent_first_token_ms_bucket[15m])) by (le)) > 3000

10.6 Agent 特有的排障剧本

常规排查(看日志/指标)之外,Agent 的四类特有故障与排查顺序:

  1. "答非所问":找 trace → 看该轮的输入上下文(是不是召回污染/历史串味)→ 看工具结果(是不是工具返回了错数据)→ 再看模型输出。顺序很重要:八成问题在上游数据不在模型;
  2. 死循环:指标上 llm_calls_total 涨但任务完成数不动 → 抓一个 trace 看重复模式 → 是工具报错固执重试还是目标漂移 → 分别对应注入重复检测提示 / 修预算收尾;
  3. 成本突增:token 曲线按天对比 → 定位是某类会话变长(上下文泄漏)还是某工具返回暴涨(数据面变化)——CostPage/成本账本按维度下钻;
  4. 间歇性失败:pass^k 暴露 → trace 里对比成败两次的同位差异 → 常见根因是工具偶发超时被模型吞掉或检索命中不稳定。

实现作业

  1. 给第 2-9 章的 Agent 加完整可观测:结构化日志(trace_id 用 contextvars 贯穿)、Prometheus 指标(10.3 的六件套)、trace 树(用 Langfuse 自托管或 OTel + Jaeger all-in-one 容器);
  2. 跑一个含子智能体的任务,从 trace 树读出:最贵的 span、工具调用次数、token 随轮数的曲线;
  3. 写两条 SLO 告警规则(成功率、P95 首 token),人为制造故障(把模型端点改错),验证告警在预期时间内触发;
  4. 用第 9 章的 pass^k 数据当"任务成功"信号源,接进成功率指标——把离线评测与在线监控打通。

深入材料

← 返回资讯列表

读者留言

COMMENTS 暂无
仅本站原创文章开放留言 · 请勿留下手机号、邮箱等个人信息

还没有留言,来说第一句?