Agent 也要可观测:不然你只是在看一段神秘录像
原创 · 约 40 分钟阅读 · 阅读 --
Last updated on

Agent 也要可观测:不然你只是在看一段神秘录像

作者: Alex Xiang


古董级程序员,大厂出来后一直在创业公司,现在仍在一线做 AI 相关的工程。更完整的技术记录写在微信公众号「字与码」:工作经历、对新工具的看法,以及这些年踩过的坑,会不定期发在那里。若这篇对你有用,欢迎顺手关注。

有一次,一个“供应商健康诊断 Agent”在周报里漏掉了两个低频供应商。

它的最终回答看起来很完整:列了接口成功率、错误分类、主要供应商、环比变化,还给了“建议继续观察”的结论。业务同学看完觉得不对,因为那两个低频供应商虽然调用量小,但一个连续三天超时,另一个最近几次都是鉴权失败。按人工排查标准,它们都应该进风险列表。

如果只看 Agent 的最终回答,你几乎没法判断它错在哪里。它没有报错,HTTP 是 200,模型也没有胡说八道。它只是悄悄漏算了。

这类问题让我越来越确信:Agent 上线以后,可观测性不是锦上添花。没有 trace,你看到的只是剪完的录像;有 trace,才知道它当时看到了什么、跳过了什么、工具怎么返回、错误怎么被分类、为什么最后给了那个结论。

Agent 可观测 trace 结构图

一次漏算的现场记录

这个 Agent 的任务并不花哨。每天早上,它会根据用户选择的时间窗口,调用内部监控查询工具,拉取供应商维度的调用量、成功率、错误类型、延迟分位数和最近异常样本,然后生成一段诊断。目标是帮运营和技术支持在早会上快速看到哪些供应商值得跟进。

产品说明里写的是:

输入:时间范围、业务线、供应商集合或默认全部供应商。
输出:供应商健康分层、异常原因、建议动作、需要人工复核的样本。
成功标准:覆盖时间窗口内所有有调用或有错误记录的供应商;高风险供应商不得漏报;错误分类要区分客户参数错误、供应商故障、平台内部故障和权限问题。

出事那天,用户输入的是:“帮我看一下过去 7 天供应商健康情况,重点标出需要跟进的。”Agent 最终输出了 12 个供应商的概览,其中 3 个黄色预警,没有红色高风险。人工复核时发现,真实数据里有 14 个供应商在窗口内有记录。漏掉的两个分别是:

supplier_id7 天调用量异常人工判断
supplier_delta189 次超时,集中在近 3 天低频但高风险,应列入红色
supplier_kappa75 次鉴权失败需要区分是客户配置还是供应商凭证问题

这不是普通服务那种“某个接口 500 了”的故障。Agent 的每一步都可能有问题:它可能没把低频供应商查出来,可能查出来后被摘要时丢了,可能工具返回了错误但被当成空结果,可能模型认为调用量太低不值得写,可能错误分类规则把鉴权失败归到了“客户参数问题”,所以没有进入供应商风险。

没有 trace,排障会变成猜谜。

Trace 先回答四个问题

我不喜欢一上来谈“Agent 可观测平台应该长什么样”。排障时,trace 先要回答四个朴素问题:

  1. 它以为什么是任务?
  2. 它看到了哪些上下文和数据?
  3. 它调用了哪些工具,工具真实返回了什么状态?
  4. 它为什么把某些结果写进结论,把另一些结果丢掉?

那次漏算的 trace 摘要里,最关键的几段是这样的:

{
  "trace_id": "tr_health_20260408_091522",
  "task": {
    "task_type": "supplier_health_diagnosis",
    "tenant_hash": "tn_redacted_42",
    "requested_window": {
      "from": "2026-04-01T00:00:00+08:00",
      "to": "2026-04-08T00:00:00+08:00"
    },
    "business_line": "default",
    "success_criteria": [
      "cover_all_suppliers_with_calls_or_errors",
      "flag_high_risk_even_when_volume_is_low",
      "separate_auth_error_from_provider_5xx"
    ]
  },
  "outcome": {
    "agent_reported": "completed",
    "reviewed": "partial",
    "review_reason": "missed_low_frequency_suppliers"
  }
}

如果 task 里没有 success_criteria,后面就很难判断“漏掉低频供应商”到底是不是故障。很多 Agent 系统的第一个可观测缺口就在这里:它只记录用户说了什么,不记录系统把任务解释成什么。

工具调用没有失败,但参数已经歪了

继续往下看,问题先出在查询阶段。Agent 第一次调用供应商列表工具:

{
  "span_id": "sp_002",
  "span_type": "tool_call",
  "tool_name": "supplier_metrics.query",
  "step_index": 2,
  "input_summary": {
    "window_days": 7,
    "business_line": "default",
    "min_calls": 20,
    "include_error_only": false,
    "group_by": ["supplier_id"]
  },
  "output_summary": {
    "status": "ok",
    "row_count": 12,
    "latency_ms": 842,
    "truncated": false
  },
  "error": null
}

接口是成功的。问题在 min_calls: 20include_error_only: false。这两个参数来自工具 schema 的默认值,原本是为了日常看大盘时过滤噪声。可这次用户要的是健康诊断,成功标准里明明写了“低频高风险也要标出”。Agent 没有显式覆盖默认值,于是工具帮它过滤掉了两个供应商。

如果日志只记录“调用了 supplier_metrics.query,返回 12 行”,这个问题很难发现。trace 必须记录参数摘要,尤其是会影响数据覆盖范围的参数。完整原始参数不一定都能进日志,但像 min_callsinclude_error_onlywindow_daysgroup_by 这种诊断字段必须可见。

后来我们给工具调用 span 加了一个 coverage_policy

{
  "coverage_policy": {
    "intended": "all_suppliers_with_calls_or_errors",
    "actual_filters": {
      "min_calls": 20,
      "include_error_only": false
    },
    "coverage_risk": "may_drop_low_frequency_suppliers"
  }
}

这个字段不是模型自己随便写的,而是工具 wrapper 根据参数规则生成。这样看板上可以直接统计“存在覆盖风险的任务”,不用等业务同学人工发现漏算。

上下文摘要把关键约束挤掉了

第二个问题在上下文。这个 Agent 每次任务前会注入几段上下文:系统规则、业务口径、错误分类说明、上一次周报摘要、工具 schema 摘要。为了节省 token,它有一个 context compactor,会把长规则压缩成短摘要。

出事那天的 context span 是:

{
  "span_id": "sp_001",
  "span_type": "context",
  "step_index": 1,
  "context_refs": [
    {
      "ref_id": "rule_supplier_health_v3",
      "title": "供应商健康诊断规则",
      "version": "2026-03-20",
      "included_tokens": 412,
      "compression": "summary"
    },
    {
      "ref_id": "taxonomy_error_v5",
      "title": "错误分类口径",
      "version": "2026-03-28",
      "included_tokens": 368,
      "compression": "summary"
    },
    {
      "ref_id": "weekly_report_2026_04_01",
      "title": "上周供应商周报摘要",
      "included_tokens": 530,
      "compression": "full_summary"
    }
  ],
  "dropped_context_refs": [
    {
      "ref_id": "rule_supplier_low_volume_exception",
      "reason": "token_budget",
      "priority": "medium"
    }
  ]
}

这里已经能看到第二个坑:rule_supplier_low_volume_exception 被丢了。那条规则写的是“当供应商调用量低于 20 但错误率超过 40%,或连续 3 天出现同类错误时,必须进入人工复核列表”。它被标成了 medium priority,压缩器在 token 紧张时优先保留上周报告,反而丢了当日诊断更需要的例外规则。

这类问题很容易被误判成“模型没遵守规则”。实际是模型根本没看到那条规则。Agent 可观测性里,上下文不是一坨 prompt,而是一组可审计的引用:哪些规则被注入,哪些被压缩,哪些被丢弃,丢弃原因是什么。

我们后来改了上下文优先级:任务成功标准和安全/覆盖规则永远高于历史摘要;历史摘要只能作为背景,不能挤掉约束。trace 里也增加了 required_context_missing

{
  "required_context_missing": [
    {
      "ref_id": "rule_supplier_low_volume_exception",
      "impact": "coverage",
      "should_block": true
    }
  ]
}

这个字段一旦出现,Agent 不应该继续生成完整报告,而应该降级成“数据覆盖不完整,需要重新查询或人工复核”。

错误分类比异常堆栈更重要

漏掉的另一个供应商 supplier_kappa 不是被 min_calls 单独害的。它的 5 次鉴权失败来自另一个查询工具 supplier_errors.search。工具实际返回过,但错误分类在 Agent 内部被改写了。

当时的工具 span:

{
  "span_id": "sp_005",
  "span_type": "tool_call",
  "tool_name": "supplier_errors.search",
  "input_summary": {
    "window_days": 7,
    "supplier_ids": ["supplier_alpha", "supplier_beta", "supplier_kappa"],
    "error_types": ["timeout", "auth", "5xx", "schema"]
  },
  "output_summary": {
    "status": "ok",
    "row_count": 31,
    "error_class_counts": {
      "provider_timeout": 12,
      "provider_5xx": 4,
      "auth_failed": 5,
      "client_schema_error": 10
    }
  }
}

后面的 decision span 却写成:

{
  "span_id": "sp_007",
  "span_type": "decision",
  "decision_type": "risk_classification",
  "input_refs": ["sp_005"],
  "model_rationale_summary": "auth_failed usually indicates client-side credential configuration; do not classify as provider health risk unless provider-wide pattern exists.",
  "classified_counts": {
    "provider_risk": 16,
    "client_issue": 15
  },
  "dropped_suppliers": [
    {
      "supplier_id": "supplier_kappa",
      "reason": "classified_as_client_issue"
    }
  ]
}

这段 rationale 不能说完全错。鉴权失败确实可能是客户配置问题。但业务口径要求的是“供应商健康诊断”,不是“只统计供应商自身 5xx”。如果一个供应商的鉴权失败集中爆发,可能是供应商换了凭证规则、证书过期、回调配置变更,也可能是平台侧同步问题。正确做法不是直接从风险列表删除,而是放进“需区分责任方”的人工复核列表。

所以工具错误不能只记录 exception。更应该有稳定的错误分类和责任口径:

字段例子为什么要记
raw_error_classauth_failed工具或日志原始分类
normalized_error_classauthentication跨工具统一口径
attribution_candidateclient_or_provider_config责任还不确定
confidence0.62低置信度要复核
action_policyinclude_in_review决定是否进入报告

修改后,auth_failed 不再被简单归到客户问题。只有当 trace 中出现明确证据,比如同一客户对多个供应商都鉴权失败,或错误样本里的 credential_id 最近变更,才会降级为客户配置问题。否则进入复核。

仪表盘不是给老板看平均值的

这次事故后,我们把 Agent 看板改成了排障入口,而不是汇报页。第一屏只放几类能引导调查的指标:

指标解释
task_outcomecompleted、partial、needs_review、failed、refused
supplier_coverage_rate报告覆盖的供应商 / 窗口内有调用或错误的供应商
low_volume_high_risk_missed低频高风险漏报数
tool_filter_risk_count可能影响覆盖的工具过滤次数
required_context_missing_count必需上下文缺失次数
error_attribution_unknown_rate错误责任不确定但被强行归类比例
manual_review_queue_size需要人工复核的供应商或样本
cost_per_completed_task完成一次任务的模型和工具成本

这些字段都能点到 trace。比如 low_volume_high_risk_missed 上升,点进去不是一张空洞折线,而是具体任务列表:每个任务显示时间窗口、供应商数量、工具过滤参数、丢弃的上下文、最终报告是否包含复核列表。值班的人可以直接打开某次任务,看到 min_calls 为什么是 20。

普通服务看 P95 延迟和错误率很自然;Agent 还要看过程质量。步骤数突然升高,可能是规划绕路;工具参数修复次数升高,可能是 schema 描述坏了;上下文缺失升高,可能是 token 预算被历史摘要吃掉;单位任务成本升高,可能是重试循环或过长报告。Agent 的健康不是一个 HTTP 200 能说明的。

发布门禁拦的是“悄悄变坏”

这类 Agent 很容易出现一种危险变化:演示问题更漂亮,生产边界更差。比如把模型换强一点,报告文字更自然;把 prompt 改得更积极,建议更多;把默认过滤调高,报告更短更清爽。但低频异常被漏掉,责任不确定的错误被写死,高风险任务没有进入复核。

所以发布门禁要围绕任务事实,而不是围绕文采。供应商健康诊断 Agent 后来有一组固定回归样本:

{
  "case_id": "supplier_health_low_volume_003",
  "input": {
    "window_days": 7,
    "business_line": "default",
    "request": "帮我看过去 7 天供应商健康情况,重点标出需要跟进的"
  },
  "fixture": {
    "suppliers": [
      {"supplier_id": "supplier_alpha", "calls": 1280, "errors": 13, "dominant_error": "provider_5xx"},
      {"supplier_id": "supplier_delta", "calls": 18, "errors": 9, "dominant_error": "provider_timeout"},
      {"supplier_id": "supplier_kappa", "calls": 7, "errors": 5, "dominant_error": "auth_failed"}
    ]
  },
  "expected": {
    "must_include_suppliers": ["supplier_delta", "supplier_kappa"],
    "must_include_review_reasons": {
      "supplier_delta": "low_volume_high_error_rate",
      "supplier_kappa": "auth_error_attribution_uncertain"
    },
    "must_not_claim": [
      "no high risk supplier",
      "auth failures are client-only"
    ],
    "required_trace_assertions": [
      "supplier_metrics.query.input_summary.include_error_only == true",
      "supplier_metrics.query.input_summary.min_calls == 0",
      "context.required_context_missing is empty",
      "decision.dropped_suppliers does not include supplier_delta"
    ]
  }
}

这个样本的重点不是最终文字,而是 trace 断言。只要工具参数又回到 min_calls: 20,即使模型最后碰巧写出了 supplier_delta,门禁也会报警。因为生产里不能靠碰巧。

发布表也不只看通过率:

门禁项阈值失败处理
高风险供应商漏报0阻止发布
必需上下文缺失0阻止发布
工具覆盖风险未降级0阻止发布
错误责任强行归类< 1%灰度加人工抽检
任务完成率回退不低于线上版本 1%灰度
P95 步骤数增长不超过 20%性能复核
单任务成本增长不超过 15%成本复核

这里有个细节:task_outcome 允许是 needs_review。Agent 不必所有事情都自动下结论。对低频但高风险、责任不确定、数据覆盖不足的情况,正确行为就是把样本放进人工复核队列,并说明原因。把这种接管算成失败,会逼系统乱答。

成本问题也会藏在 trace 里

修漏算时还顺手发现了另一个问题:有些任务特别贵。平均每次诊断只要 3 次模型调用,但 P95 到了 11 次。打开 trace 发现,Agent 在错误分类不确定时会反复调用模型,让模型自己辩论“这是客户问题还是供应商问题”,却不补充新数据。

一段成本摘要是这样的:

{
  "trace_id": "tr_health_20260410_083011",
  "cost_summary": {
    "model_calls": 9,
    "tool_calls": 4,
    "input_tokens": 28430,
    "output_tokens": 6120,
    "estimated_cost_usd": 0.74
  },
  "loop_summary": {
    "repeated_decision_type": "risk_classification",
    "iterations": 5,
    "new_evidence_after_first_iteration": false,
    "stop_reason": "max_iteration"
  }
}

这个 trace 说明,成本不是模型单价问题,而是流程问题。没有新证据的重复推理,应该直接停止,转入 needs_review。后来加了规则:同一分类决策如果没有新增工具结果,最多重试一次;低置信度不要继续让模型自问自答,而是输出复核原因。

Agent 成本优化不能只看账单。要能按任务、步骤、工具、模型版本、重试原因拆开。最贵的 1% 任务通常不是“真的复杂”,而是流程没有退出条件。

采样策略不能漏掉你最需要看的东西

有些团队担心 trace 太贵,于是只采样 1%。这在普通高频接口上可能能接受,在 Agent 上很容易踩坑。因为你最想看的往往是低频、高风险、用户投诉、人工接管、策略拦截、成本异常这些样本,随机采样很可能刚好漏掉。

供应商健康诊断 Agent 后来的保留策略是:

任务类型Trace 保留
正常完成且低风险结构化摘要全量,详细 span 抽样
partialneeds_review详细 span 全量
高风险供应商相关详细 span 全量
用户反馈为错误原始上下文短期保留,脱敏后长期保留
工具异常或覆盖风险详细工具 span 全量
成本 Top 1%详细 span 全量

这套策略的原则很简单:正常样本可以省,失败和高风险不能省。隐私也要一起设计。日志里默认保存结构化摘要、ID、hash、计数和分类;原始工具结果只保留短期,并受权限控制;导出给复盘或评估集的 trace 必须脱敏。

可观测性不是“把所有 prompt 和返回都存下来”。那样既贵又危险。真正有用的是保留能复盘决策的结构化事实。

OpenTelemetry 只是骨架,业务字段才是肉

OpenTelemetry 的 trace/span 结构很适合承载 Agent 过程,GenAI 相关语义约定也让模型调用的字段更统一:模型名、token、延迟、stop reason、请求类型等。但只靠通用字段不够。供应商健康诊断这个案例里,真正帮我们定位问题的是业务字段:

Span通用字段业务字段
tasktrace_id、duration、statustask_type、success_criteria、business_line
contextinput token、context sizerequired_context_missing、context_refs、dropped_context_refs
tool_calltool_name、latency、statusmin_calls、include_error_only、coverage_policy
model_callmodel、tokens、stop_reasondecision_type、confidence、new_evidence_refs
decisionspan links、attributesdropped_suppliers、attribution_candidate、action_policy
outcomestatus、errortask_outcome、manual_review_items、missed_high_risk_count

这也是我对 Agent 可观测性的基本看法:工具层面的统一标准很重要,但每个业务 Agent 都要定义自己的成功标准和关键字段。没有业务字段,trace 只能告诉你“模型调用了 9 次”;有业务字段,trace 才能告诉你“它因为默认 min_calls 漏掉了低频高风险供应商”。

失败样本要变成评估集

那次漏算最后进入了固定评估集,名字就叫 supplier_health_low_volume_003。后续每次改 prompt、改工具 schema、改上下文压缩、换模型,都会跑它。评估不只检查最终报告,还检查 trace。

一条评估记录大概长这样:

字段内容
case_idsupplier_health_low_volume_003
input_request过去 7 天供应商健康诊断
fixture_version2026-04-low-volume-v2
expected_supplierssupplier_delta, supplier_kappa
expected_outcomecompleted_with_review_items
trace_assertions工具不过滤低频、必需上下文不缺失、错误责任不强行归类
answer_assertions不得声称无高风险;必须说明复核原因
risk_levelhigh
owneragent_platform

这让事故不再是一次性教训。只要同类问题回来,门禁会挡住。

我更喜欢这种“从 trace 到评估”的闭环,而不是单独维护一套理想化测试题。真实失败会带着噪声、边界和奇怪表达,正好能保护生产。

产品边界也被 trace 逼清楚了

做完这次复盘,产品文档也改了。以前“供应商健康诊断”听起来像自动给结论,后来明确拆成三类输出:

  • confirmed_risk:证据足够,能明确标红或标黄。
  • needs_review:数据异常但责任不确定,需要人工看样本。
  • insufficient_data:工具失败、上下文缺失或覆盖不足,不能给完整结论。

这三个 outcome 都是正常结果,不是只有 confirmed_risk 才算成功。Agent 的可观测性会反过来约束产品语言:什么叫完成,什么叫部分完成,什么时候必须停下来,什么时候可以建议人工复核。边界不清,trace 里就会充满“completed”,但业务永远觉得它没完成。

这也是 Agent 和传统接口很不一样的地方。传统接口可以用 HTTP 状态表达很多事;Agent 的 HTTP 200 只表示服务返回了,不能表示任务真的完成。任务状态必须单独定义。

我现在会先问这些问题

如果一个 Agent 准备上生产,我会先问可观测性,而不是先问模型多强。

它有没有稳定的 task_idtrace_id?任务成功标准有没有进入 trace?上下文注入是否能看到引用、版本、压缩和丢弃原因?工具参数里影响覆盖和权限的字段是否可见?工具错误有没有结构化分类?模型的关键决策有没有输入引用和置信度?人工接管是不是一等 outcome?成本能不能按步骤拆?失败样本能不能进入评估集?发布门禁会不会检查 trace,而不是只看最终回答?

这些问题听起来繁琐,但都来自生产里的真实麻烦。Agent 出错不一定会崩,它可能只是漏算、误归因、绕路、过度自信、吞掉工具失败、把不确定写成确定。最终回答越漂亮,越容易掩盖这些过程问题。

供应商健康诊断那个案例最后的修复并不神秘:工具默认参数改成由任务类型显式决定;低频高风险规则提升为必需上下文;鉴权失败改为责任不确定时进复核;trace 增加覆盖风险、上下文缺失和错误归因字段;仪表盘能点到样本;发布门禁加入 trace 断言。没有哪一步需要玄学,前提是你能看见过程。

Agent 可观测性真正要解决的不是“日志够不够多”,而是“系统做出结论时,事实链是否能被还原”。能还原,问题就能归类、修复、回归;不能还原,每次事故都只能重新猜一遍。

参考资料