生产级Agent平台可观测性建设:从SLO到链路追踪实战
2026/9/13 6:43:59 网站建设 项目流程

接手生产 Agent 平台的可观测性建设时,我最深的感受是:模型跑得好不好,团队讨论得很热闹,但平台本身是否健康、用户请求是否真的稳定,反而没人能第一时间说清。所谓生产级 Agent 平台,并不是把模型 API 封装一下就行,它由模型网关、工具调用、知识库检索、多轮记忆、任务调度等多个环节组成,任何一个环节抖动,都可能让一次看似正常的请求变成一句冷冰冰的 “Agent couldn't generate a response”。要让这种系统有兜底感知,核心就是围绕 SLO 搭起指标、日志、链路追踪三位一体的可观测体系。这篇文章会从 SLO 怎么定、指标怎么埋、日志怎么关联、追踪怎么串起来,到排障时真正用得上的技巧,完整走一遍。

1. 生产 Agent 平台的可观测性为什么这么难

1.1 Agent 系统与传统服务的本质差异

传统后端服务的可观测性很好做。一个 HTTP 请求进来,中间调几个数据库或缓存,最后返回 200 或 500,请求 ID 一路透传,日志一查就能定位。但 Agent 平台不是这种模型。一次用户请求进来之后,Agent 可能进入一个几十秒甚至几分钟的长任务循环:先调用一次模型做意图理解,再去检索知识库,然后决定调用哪个工具,拿到工具结果之后还得再交给模型做下一步判断。这个过程中,任意一步都可能重试、超时、失败,也可能在某个分支上直接结束。

更麻烦的是,传统请求的“成功”很好判断:状态码是 2xx 就算成功。但 Agent 任务经常会出现“语义失败”。比如任务最终返回了 completed 状态,但用户拿到的回答是“我不知道”,或者工具调用根本没有产生有效结果。这种情况从系统层面看是成功,从用户角度看是失败。如果平台只用 HTTP 状态码和请求完成率来衡量可用性,一定会出现监控全绿、用户疯狂投诉的诡异局面。

还有一个维度是并发和资源。传统服务可以通过副本数扩容来消化流量,但 Agent 平台的瓶颈往往不在自身 CPU,而在模型供应商的限流、工具服务的响应速度、向量库的召回耗时。这些外部依赖一旦抖动,Agent 自身的资源占用可能很低,任务却集体变慢。做过一次线上事故复盘就会发现,根因往往不在 Agent 服务本身,而在它编排的那个“外部世界”。

1.2 SLO 不是指标越多越好,而是用户可感知

很多团队一开始做 SLO,会把所有能采到的指标都放进去:GPU 利用率、Token 消耗量、模型调用延迟、工具平均耗时、队列深度、内存水位……列了二三十项,看起来非常全面,但真到故障时看板根本看不过来。SLO 的本质不是运维指标,而是“用户可感知的服务水平目标”。如果用户觉得“任务能在 30 秒内给我一个有价值的回答”是核心体验,那你的 SLO 就该围绕这个体验去定,而不是围绕某个模型上游的 P99 延迟去定。

所以在设计 SLO 之前,必须先把用户视角翻译成技术指标。对于 Agent 平台,最常见的用户可感知维度有四个:任务能不能启动、任务能不能在预期时间内完成、任务完成时有没有实际产出、任务过程中的工具调用是不是靠谱。这四个维度可以进一步映射成可用性、延迟、有效性和工具成功率。真正进入 SLO 清单的指标,应该控制在五到七个以内,每个都要能回答一个具体的用户问题,而不是“看起来很有价值”的系统指标。

我见过一个反面案例:团队把“LLM API 调用成功率”设为核心 SLO,结果模型供应商限流导致调用成功率掉了,告警触发,大家开始疯狂排查,但用户侧的任务其实通过重试和降级策略完成了,体验完全没有受损。这种把内部依赖指标当作用户 SLO 的做法,只会带来告警噪音和无效的应急响应。SLO 的出发点,永远是站在用户位置问一句:这次请求,你觉得行不行。

2. 从需求到落地:SLO 的设计与目标拆解

2.1 先对齐 SLI:延迟、成功率和质量怎么取舍

SLO 必须建立在可量化的 SLI 之上。Agent 平台一般建议从四个候选 SLI 里选:

  • 可用性 SLI:所有进入系统的 Agent 任务中,最终到达终止状态(completed 或 failed)且没有在调度层被丢弃的比例。推荐分子分母都排除“用户主动取消”这类人为终止任务,否则 SLO 会被非技术行为稀释。
  • 延迟 SLI:从任务进入平台到最终完成(end-to-end latency),以及从任务进入平台到用户看到首个 Token 的时间(time-to-first-token)。前者反映整体效率,后者反映用户“开始等待”的感受。流式场景下后者更贴近体验。
  • 有效性 SLI:任务完成后,平台从 Agent 输出中解析出的最终结果是否非空、是否通过答案校验器(比如 JSON 格式正确、回答包含关键引用)。这一步很多团队会忽略,但它是识别“语义失败”最重要的手段。
  • 工具调用成功率 SLI:Agent 在任务过程中发起的工具调用中,最终返回成功状态的比例。这个指标能快速暴露外部依赖问题,但不能直接当作总体验指标,因为 Agent 可能通过兜底逻辑绕开失败的工具。

一句话总结选型思路:可用性和延迟是必选,有效性和工具成功率按业务场景选。如果是客服 Agent,有效性 SLI 比延迟重要;如果是自动化编码 Agent,工具调用成功率几乎等同于服务质量。千万不能只盯着延迟,否则你优化出来的可能是一个回复很快但经常胡说八道的 Agent。

2.2 黄金信号在 Agent 场景下的变化

传统 RED 方法关注 Rate、Errors、Duration,在 Agent 平台里需要变通。Rate 不能只看每秒请求数,还要看每秒任务启动数、每秒进入工具调用阶段的次数、每秒模型请求并发数。Errors 也不能只看最终失败,要按阶段拆:意图识别失败、工具调用超时、模型输出解析失败、上下文超长截断失败。Duration 同样需要分阶段:单次 LLM 调用的耗时、工具执行的耗时、Agent 步骤之间的思考耗时、流式输出的网络耗时。

另一个容易被忽视的是队列等待时间。Agent 平台通常有调度器,任务先入队再执行。当模型上游被限流时,队列会快速积压,但任务本身的执行耗时可能没有变化。如果只监控任务执行耗时,你会看到一个假象:一切正常,但用户等待时间翻倍。所以建议给 Agent 平台单独加一个 USE 模型的变体:Utilization(模型并发连接数占上限的比例)、Saturation(任务队列深度、等待中任务数)、Errors(超时、取消、限流错误)。这套指标和 RED 体系结合,才能覆盖 Agent 平台“调度 + 执行 + 外部依赖”的完整链路。

2.3 制定 SLO 的具体步骤与误差预算

明确了 SLI 之后,SLO 就是给每个 SLI 设定目标和时间窗口。

具体步骤可以这样走:

  1. 先拉一个月的历史数据,计算当前各项 SLI 的真实基线。不要凭感觉写“99.9%”,如果当前基线只有 98%,那 99.9% 会让团队永远处于告警状态。
  2. 根据业务优先级设定目标。客服场景可能要求“任务在 20 秒内完成”达到 95%;编码场景可能要求“工具调用成功率”达到 99%。建议初期设置 2 到 3 个目标,不要所有 SLI 都做成 SLO。
  3. 为每个 SLO 推导错误预算。比如一个月约 100 万任务,SLO 是 99% 的完成率,那么一个月最多允许 1 万次失败任务。按 30 天折算,每天大约允许 333 次失败,把这个数值告诉研发团队,他们就能量化地评估一次发布是否安全。
  4. 设置错误预算燃烧告警。推荐使用多窗口法:1 小时内错误预算消耗超过 5% 触发警告,超过 14.4% 触发紧急告警;6 小时内消耗超过 3% 触发警告,超过 7.5% 触发紧急告警。这种算法比简单的“错误率高于 P99”更准确,能把偶发抖动和持续恶化区分开。

需要注意,SLO 不是一成不变的。模型升级、工具调整、业务节奏变化都会影响基线。我习惯每个季度重新评估一次目标,把“总是达不到的目标”调低,把“太容易达到的目标”拉高,让 SLO 真正成为驱动改进的牵引力,而不是墙上的一张纸。

3. 指标体系建设:从采集到告警

3.1 Agent 平台需要哪些基础指标

指标设计的原则是“跟着请求生命周期走”。一个 Agent 任务从进入到结束,至少需要覆盖以下关键节点:

指标名类型说明关键标签
agent_task_totalCounter任务总数,按状态分类status, workflow, version
agent_task_duration_secondsHistogram任务端到端耗时status, workflow, model
agent_task_error_totalCounter任务错误数error_type, phase
agent_llm_call_totalCounterLLM 调用次数model, provider
agent_llm_duration_secondsHistogramLLM 调用耗时,注意区分布鲁克model, mode
agent_llm_first_token_secondsHistogram首 Token 延迟model, provider
agent_tool_call_totalCounter工具调用次数tool, status
agent_queue_depthGauge任务队列深度queue, priority
agent_token_usage_totalCounterToken 消耗,便于成本核算model, type(prompt/completion)

这里要特别提醒:不要给这些指标挂过多的业务标签。比如把“用户 ID”“Session ID”做成标签,一到晚上大流量进来,Prometheus 的时间序列数量会瞬间爆炸。用户粒度的数据应该放在日志或追踪里,指标只保留聚合维度层面的标签,最多挂到 workflow、model、tool、phase 级别。

3.2 指标采集与存储选型

Agent 平台的主流方案仍然是 Prometheus + Grafana。业务服务通过 Prometheus SDK 暴露 /metrics,再由 Prometheus 定时抓取。对于需要跨服务汇聚的指标,建议先用 OpenTelemetry SDK 生成 metric,再通过 OTel Collector 统一转发到 Prometheus,避免每个语言栈自己实现一套采集逻辑。

采集层有个容易被忽略的问题:Agent 任务动辄几十秒到几分钟,如果用传统的 counter 做增量统计,有时任务还没结束就被 Prometheus 抓走了,导致指标“半截”。所以长任务建议多埋 Histogram 和 UpDownCounter,在任务真正结束时才提交最终结果,而不是在任务开始时把“进行中”当作“已完成”。

存储选型上,小规模直接用 Prometheus 内置 TSDB 就够;到了多集群、多租户阶段,可以接入 Thanos 或 VictoriaMetrics。Grafana 做看板时,多建几个和 SLO 直接对应的视图:概览页放 SLI 趋势和错误预算消耗,排障页放队列深度和外部依赖错误率。不要做一张塞满几十个 panel 的“大屏”,那是给管理者看的仪式感,不是给工程师用的排障工具。

3.3 告警规则与降噪

指标采集只是地基,告警才是真正影响日常工作的东西。告警规则设计不好,会出现两个极端:要么噪音太多,群里一天响几十次;要么规则太松,真正出问题时告警反而没触发。

第一条经验:不要对原始错误率直接设置阈值告警。比如“错误率 > 5% 告警”,在流量低谷时一次假告警就会触发;在流量高峰时错误率 4% 但影响了几千用户,可能又不会触发。建议统一用错误预算燃烧率计算。上面已经给了多窗口的推荐值,实际落地时可以先用 1 小时窗口做“紧急告警”,6 小时窗口做“警告”,不要一上来就搞 24 小时窗口的静态阈值。

第二条经验:每条告警必须携带跳转链路。Prometheus 的 Alertmanager 只负责发出告警,但告警文本里至少要有 trace_id、task_id 和日志查询链接。否则告警发出去了,人还要手动打开两三个系统去查这台机器、这个服务、这个时间段的日志,等查完黄花菜都凉了。建议在 Agent 服务的异常日志里统一打点 task_id,告警模板里用模板语法拼出日志平台的搜索 URL,一键跳转。

第三条经验:用静默机制处理已知故障。模型供应商大范围故障时,所有依赖该上游的任务都会失败,告警会像瀑布一样涌进来。在维护窗口内,建议按 provider 维度设置静默规则,同时保留“上游故障识别”的独立看板。等故障恢复后再复盘,不要让工程师把精力消耗在主告警之外的二三十条衍生告警上。

4. 日志体系建设:结构化、关联与闭环

4.1 Agent 日志的独特挑战

日志是排查 Agent 问题时最直接的依据,但 Agent 平台的日志比传统服务更难处理。一个任务内部可能有多次模型的 Prompt 和 Response,每次可能几百上千 Token,如果全部打日志,一天下来存储成本会非常惊人。更麻烦的是这些模型交互内容往往包含用户隐私和业务敏感数据,比如用户上传的简历、代码仓库里的源码,直接格式化输出到日志系统会带来严重的安全风险。

所以日志设计的第一原则不是“尽可能全”,而是“哪些必须记、哪些坚决不记”。事件类日志(何时调用模型、耗时多少、工具结果是否成功)必须记;Payload 类日志(Prompt 全文、模型原始回复、工具入参)必须限制采样和脱敏。简单说,日志负责回答“这个任务经历了什么”,Payload 负责回答“这个环节具体说了什么”,两者解耦。

我还遇到过一种比较隐蔽的问题:Agent 任务在子线程里异步执行,开发人员在主线程打日志时,日志系统已经拿不到当前上下文里的 trace_id 和 task_id。结果就是日志散落在各个服务里,完全串不起来。要解决这个问题,必须在框架层统一封装日志上下文管理器,让子线程、协程、异步回调都自动继承父链路 ID,而不是靠每个工程师自己记得在日志里加一个 task_id 字段。

4.2 日志规范与关联 ID 设计

无论底层用 Elasticsearch 还是 Loki,统一推荐结构化 JSON 日志,这样日志平台可以按字段索引和聚合。一个 Agent 平台的标准日志字段至少包含:

{ "timestamp": "2025-03-01T12:00:01.123Z", "level": "INFO", "service": "agent-orchestrator", "trace_id": "d6f0a8c1e2b34f5a", "task_id": "task_8f91ab", "session_id": "sess_9932cd", "event_type": "llm_request", "model": "claude-sonnet-4", "duration_ms": 1543, "status": "ok", "message": "LLM call completed" }

字段里的 event_type 是整个日志体系的核心。建议按 Agent 生命周期统一枚举:task_start、task_end、llm_request、llm_response、tool_call、tool_result、memory_read、message_delivered、error、timeout。每个枚举值都有固定的校验字段,这样做有两个好处:一是日志平台可以直接按 event_type 统计事件分布;二是后端拿到日志后可以直接渲染出一个任务的“时间线”,而不需要逐条读 message。

关联 ID 的传递要从最外层开始。用户请求经过 API 网关时,网关生成一个全局唯一的 task_id,同时透传给 Agent 编排服务、模型网关、工具服务。建议用 W3C traceparent 格式做链路传递,再由 OpenTelemetry 自动把 trace_id 和 correlation id 打点到日志里。这里有一个实操细节:不要用随机字符串做 task_id,建议带时间前缀,比如 task_20250301120001_8f91ab,日志排障时能快速判断任务是哪一分钟进入的,不用再多加一个时间字段。

4.3 日志采集、清洗与存储

采集方案推荐统一采用 Filebeat 或 Fluent Bit,把服务本地日志转发到 Kafka,再由日志处理管道写入 Elasticsearch 或 Loki。Agent 平台有较强的突发流量特性,直接串行写入存储容易丢日志,中间加一层 Kafka 做缓冲非常有必要。

清洗管道主要做三件事。第一是字段规整:把不同服务打点不规范的时间格式统一转成 RFC3339,把缺失的 service 字段补上默认值。第二是隐私脱敏:用正则或词表识别 JSON 里的 token、password、apiKey 字段,命中直接替换成[REDACTED]。第三是日志裁剪:一条日志超过预设大小(比如 4KB)时,截断 message 字段,但保留关键字段。这几个环节建议在 OTel Collector 或 Logstash 里做,尽量不要在业务代码里反复改格式。

存储层需要区分冷热。近 7 天日志放到热存储,支持全文检索;超过 7 天、小于 30 天的日志放到冷存储,只保留字段索引,不上全文;超过 30 天的原始日志直接删除,或按天汇总成指标后保存到 Prometheus。这样做不是为了省这几台机器,而是为了控制排障时的查询范围——没有谁会花半小时去翻三个月前的单条日志,真正需要的是“某个模型版本发布后错误率变化了多少”这样的聚合信息。

5. 链路追踪:一次 Agent 任务如何被串起来

5.1 追踪层级设计

链路追踪是 Agent 平台排障的关键,也是大多数人刚开始最容易做错的部分。如果你只给 Agent 框架打一个大的 span,那只能看到“这个任务耗时多少”,根本定位不到到底是模型调用慢,还是工具调用慢。反过来说,如果给每一个小循环都打 span,一个任务可能会生成上百个 span,存储和查询成本都会失控。

推荐按四层建模:

  • 第一层是入口层:用户请求触发的整个 Agent 执行过程,对应一个根 span,名称建议为agent.run,属性里记录 task_id、user_id、workflow 名称。
  • 第二层是编排层:Agent 框架的每次循环、每个阶段(规划、工具调用、反思、记忆更新)单独建 span,名称类似agent.stepagent.tool_decisionagent.memory_update
  • 第三层是模型调用层:每次 LLM 请求都建立一个 child span,名称统一为llm.call,属性里记录 model、provider、prompt_tokens、completion_tokens、temperature。
  • 第四层是外部依赖层:工具调用、知识库检索、缓存访问各自建 span,名称建议直接用工具名,比如tools.search_docstools.execute_sql

层级确定之后,还要约定 span 的命名规范。不要用span-1span-2这种难以理解的名字,建议用小写点分格式,同一个模块保持稳定。稳定命名看起来是小问题,但等你要做跨请求聚合分析时,如果每个开发都随手起名,Trace 页面会变成一千个不同的 span 名称,完全没法统计耗时分布。

5.2 采样策略与 OpenTelemetry 实践

Agent 任务因为 span 数量大,必须认真考虑采样策略。全量采样理论上排障最方便,但在生产环境基本撑不住,尤其当每个任务包含几十次 LLM 调用和工具调用时,存储成本会成倍增加。

推荐的做法是“任务级采样 + 失败全采”。具体来说:

  1. 在 Agent 编排服务创建根 span 时,根据 task_id 的 hash 决定本任务是否采样。正常任务按 10% 比例采样;带有 error 状态的任务强制保留,不进入采样过滤。
  2. 通过 OpenTelemetry 的 Tail Sampling Processor 实现失败任务全采。Collector 在收到 span 后,根据 status 属性判断是否属于失败链路,如果是则保留整条 trace,否则再按比例采样。
  3. 对慢任务做 100% 保留。定义“慢任务”为端到端耗时超过 P95 的任务,Tail Sampling Processor 里增加一个基于agent.run.duration_ms属性的过滤规则。这样才能保留那些最值得优化的长尾问题。

使用 OpenTelemetry SDK 时,建议把上下文传播设置为 W3C Trace Context。有些 Agent 框架自带的 tracing 默认只追踪 LLM 调用,不追踪自己的编排逻辑,需要额外手动创建 span。另外要注意,异步任务里必须显式传入父 context,否则会生成割裂的 trace,两个子 span 不在同一条链路上。

还有一个小技巧:在 root span 的 attribute 里保存完整任务信息,比如task_idsession_idworkflow_versionmodel_name。这样即使下游 span 采样丢失,根 span 还在,通过 task_id 仍能从日志平台把整个任务的日志捞出来。

5.3 视图与排障:哪些 Span 必须打标

链路追踪系统建好之后,还要定义排障视图。我最常用的几个查询和视图:

  • 任务耗时分布:按agent.run根 span 聚合duration_ms,再按 workflow 分组,看哪个业务线的任务最慢。
  • Span 耗时 Top N:列出单个 Agent 任务里耗时最长的 10 个 span。这是最快定位瓶颈的方式。有一次线上任务平均耗时从 12 秒涨到 20 秒,查 Trace 后发现tools.search_docs的耗时从 3 秒涨到了 11 秒,根因是知识库索引异常。
  • 首 Token 延迟:单独看llm.call下的time_to_first_token,如果这个指标很高,说明模型供应商侧存在问题;如果很低但整体耗时长,问题大概率出在 Agent 编排工具调用等待上。
  • 错误跨服务分布:统计 status = ERROR 的 span 按 service / span kind 分组,能快速看出错误到底集中在模型网关、工具服务还是编排器。

排障时还要注意区分“等待”和“执行”。在 Trace 页面里,一个agent.stepspan 耗时长,可能是模型 API 响应慢,也可能是 Agent 框架在做无用的循环思考。建议在 span 属性里加一个agent_step_type,区分 thinking、waiting_for_tool、tool_executing、final_answer。这样就能一眼看出时间是花在“思考”还是“等待外部结果”,避免盲目去优化模型 Prompt。

6. 常见问题与排查技巧实录

6.1 指标有但看不出问题:归因断裂

我遇到过最典型的场景是:任务 P95 延迟突然走高,但服务的 CPU、内存都正常,Prometheus 面板上一片绿色。后来细查才发现,模型供应商那边在流式输出过程中出现了持续几秒的网络抖动,但我们的指标只统计了“模型调用总耗时”,没有统计“首个 Token 到达后的分片间隔”。排障时根本无法判断延迟是模型生成慢还是网络传输慢。

这个案例给我的教训是:Agent 平台一定要增加流式体验相关的指标,比如llm_first_token_secondsllm_stream_interval。指标不仅能告诉你“慢”,还要告诉你“慢在哪个阶段”。如果只埋一个总耗时指标,所有问题都会被黑板擦成一团。

6.2 日志太多没钱存怎么办

日志成本是 Agent 平台不可回避的问题。按每天 10 万任务、每个任务 100 条日志、每条 1KB 算,一天会产生 10GB 日志,一个月就是 300GB,这还不算模型交互的完整 Prompt。应对办法有两个方向:一是只保留关键字段,原始 JSON 里的 message 可以截断,嵌套对象拍平,日志体积能减少 40% 到 60%;二是事件采样,普通的成功任务日志按 10% 保存,失败和慢任务日志 100% 保存。

我更推荐的做法是“日志只留过程,Payload 另存”。模型请求和响应的完整内容放到对象存储或专门的 Trace 存储里,设置过期时间;日志系统里只保留摘要字段,比如 prompt 的字符数、模型回复的首 Token、工具返回的状态。排障时如果真要拿到原结果,再通过 trace_id 去对象存储里拉取。这样既保住了排查能力,又不会让日志系统变得又臃肿又昂贵。

6.3 链路采样丢了关键 Span

有一次我们线上爆发工具调用失败,但 Trace 库里只能找到一半的链路,另一半被吞吐量采样器丢掉了。后来我们对失败任务做了 Tail Sampling,又加了一条规则:任务状态是 failed 的一定保留整条 Trace,工具调用失败的 span 也强制保留。因为失败任务数量相对少,这个策略对存储成本的影响很小,但给排障带来的收益巨大。

这里要特别提醒:如果使用门控采样(Head Sampling),一定要保证根 span 决定采样结果后,子 span 能继承这个决定,否则下游服务可能各自为政,导致同一任务的部分 span 保留、部分丢失,最终拼不出完整链路。

6.4 告警风暴与 SLO 误报排查

告警风暴通常不是因为指标太多,而是因为告警规则没有经过“故障场景演练”。比如你把“任务完成率告警”设成 99%,结果某个时间点上游数据库主从切换,任务完成了但部分工具调用失败,任务状态仍然是 completed,导致完成率没掉、工具成功率却崩了。如果你的告警里没有覆盖工具成功率,这个问题就可能被忽略,直到用户投诉。

所以我建议把 SLO 告警拆成两层:一层是面向用户的核心 SLO 告警,包括任务完成率和延迟;另一层是针对 Agent 平台自身能力的子 SLO 告警,包括工具调用成功率、模型调用成功率、队列积压。核心告警响应用户问题,子 SLO 告警暴露内部隐患。两条线并行,彼此之间不要互相淹没。


最后再分享一个我个人反复验证过的经验:可观测性体系不是设计出来的,是被真实故障逼出来的。与其一开始铺开几十个指标、上百条日志规范,不如先挑一次线上最严重的 Agent 事故,从“那次事故我们漏了什么信息”出发,反推指标体系、日志字段和 Trace Span 怎么补。把第一轮补齐之后,再考虑 SLO 目标怎么定、告警规则怎么降噪。这样搭建起来的三件套,每一块都能回答一个真实问题,而不是躺在监控面板里的装饰品。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询