# 阶段七 · 小点 3:监控与运维 > 所属:阶段七 工程化、部署与运维 > 定位:上线只是开始,能不能持续观察到健康度、及时告警、闭环迭代,决定了 Agent 能不能长期跑稳。这一讲讲清三大监控支柱、黄金四指标、告警分级与上线迭代闭环。 ## 精简大纲 1. 三大监控支柱:Logs / Metrics / Traces 2. 黄金四指标(Agent 版) 3. 告警阈值与分级 4. 观测平台:LangSmith / LangFuse / 自建 / OpenTelemetry 5. 上线迭代闭环(Bad-case 收集 → 归类 → 修复 → 回归) ## 学习内容详情 > 核心认知:Agent 运维三样都要——日志、指标、追踪,缺一样排障就"盲"。 ### 1. 三大监控支柱(Logs / Metrics / Traces) ```mermaid graph LR A[三大支柱] --> B[日志 Logs
单次事件明细] A --> C[指标 Metrics
可聚合数值序列] A --> D[链路追踪 Traces
跨节点完整路径] ``` - **日志**:单次事件的明细记录(谁、何时、发生了什么)。 - **指标**:可聚合的数值时间序列(QPS、成功率、P95 耗时)。 - **链路追踪**:一次请求跨节点 / 跨步骤的完整路径。 - **Agent 运维三样都要**,缺一样排障就盲。 - 链路追踪:Agent 每一步完整日志; - **工具调用审计日志**——谁调用、入参、返回值(安全与排障关键)。 > 🔬 **深度理解 · 分布式追踪与 trace_id**: > 1. **本质**:trace_id 是一次请求在整个分布式系统里"横穿多个节点 / 多次调用"的公共身份证——只要每步埋点都带上同一个 ID,就能把一次 Agent 会话里零散的日志、多个 span、多条工具调用串成一条完整路径。 > 2. **机制**:入口(网关 / API)生成 trace_id(常见随机 / ULID),通过 HTTP header 或消息元数据层层透传给下游;每段执行被打包成一个 span(含开始/结束、父子关系、语义属性),所有 span 凭同一 trace_id 归并成一条 trace;落到 OTel 即 `trace_id + span_id + parent_span_id` 的三级结构。 > 3. **为什么重要**:Agent 一次回答往往跨"LLM 调用 + 多个工具 + 检索",排障时要先回答"用户报的问题对应哪次请求、走到哪一步挂的"——没有 trace_id,日志是散的、跨服务也无法关联;它也是下文 Bad-case 闭环里"带完整 trace_id 进队列"的前提,能一键回到根因。 > 4. **易错点**:① 只在 API 层打点、不把 ID 透传给下游/worker,跨进程链就断了——**透传是追踪的灵魂**;② 异步任务(队列 consumer)里忘了从任务元数据带 trace_id,导致"后台任务对不上会话";③ 日志记了 ID,却在指标/归一化层不关联,三支柱"对不起来";④ 用手工拼接而非标准的上下文传播(Context Propagation)传 ID,下游读不到。 ```python def audit_tool_call(user, tool, params, result): """工具调用审计日志: 谁调用 / 入参 / 返回值——排障与安全审计都需要""" print(f"[AUDIT] user={user} tool={tool} " f"params={str(params)[:100]} -> {str(result)[:100]}") # 生产写日志系统 ``` ### 2. 黄金四指标(Google SRE,Agent 版) | 黄金四指标 | 含义 | |-|-| | **P95 延迟** | 接口延迟第 95 百分位 | | **QPS** | 流量 | | **LLM 调用失败率** | 错误率 | | **token 消耗速率** | 饱和度 | - **P95 意义**:95% 的请求快于该值,比平均值更真实——LLM 延迟长尾严重(偶发 30s+ 拖高均值),看 P95/P99 才知道真实体感。 - **记住**:任何告警规则先从这四个里挑。 ```python def p95(values: list) -> float: """P95: 排序后取第95百分位的值, 比平均更能反映 LLM 长尾的真实体感""" s = sorted(values) idx = int(len(s) * 0.95) return 0.0 if not s else s[min(idx, len(s) - 1)] lats = [1.2, 1.5, 2.1, 3.0, 20.5, 1.8, 1.4] # 有一个 20s 的长尾偶发 print(f"均值≈{round(sum(lats)/len(lats),1)}s P95≈{p95(lats)}s") # 均值被长尾拉高但 P95 更能反映大多数用户体验 ``` > 🔬 **深度理解 · 百分位指标(P95 / P99)与长尾**: > 1. **本质**:P95 是"把耗时排序后第 95 百分位的值",回答"95% 的请求比它快",是**对大多数用户体感**的分位数刻画,而非平均——LLM 延迟有显著长尾(偶发 30s+),均值会被少数极端值剧烈拖夸而失真。 > 2. **机制**:生产里不存全量明细去精确排序,而是按耗时**分桶直方图(Histogram)**累计计数(Prometheus 用 `le` 桶),查询时由累计分位数估算出 P95;它天然是"可聚合数值序列"(Metrics),无需保留每条请求的原始日志。 > 3. **为什么重要**:容量判断与告警要用"代表性"而非"平均"——均值 2s 完全掩盖了"偶尔有 15% 的人等了 8s"的糟糕体感;把 P50 / P95 / P99 横着看,能同时定位"多数有多快、典型慢、极端长尾",信息量远大于单一均值。 > 4. **易错点**:① 误以为生产 P95 是精确排序——它来自直方图分桶的**近似估算**,不同后端精度有差异;② 只看 P95 不看 P99,最尾端的极端用户被忽略;③ 把 P95 与"最大值"混谈;④ 用一个全局 P95 阈值套所有业务——客服对话与批处理报表的 SLO 天然不同(呼应下文"分场景基线")。 - **第五个维度——成本指标(Agent 特有,也最易失控)**:黄金四指标之外必须加盯 **token 消耗速率与单次会话成本**——前者看「烧得多快」(饱和度),后者看「一次对话花多少钱」(乘以会话量就是日账单)。模型换代、prompt 变长、循环失控都会让成本悄悄翻倍,而延迟与成功率可能纹丝不动——**四指标都绿、账单爆了,只有成本指标能第一时间暴露**。 ### 3. 告警阈值与分级 - 阈值要「**可行动**」——告了没人处理的告警等于噪音。 | 级别 | 触发条件 | 处理 | |-|-|-| | **P0** | 成功率跌破 90% | 立即电话 | | **P1** | 失败率 > 5% | 工单 | | **P2** | P95 变慢 | 次日关注 | - **分场景基线**:阈值来自**各自场景的 SLO**,不是全局统一值——客服对话**首响 < 3s 是硬指标**(首响应慢一秒用户就流失),离线批处理报表跑 10 分钟也无可厚非。先给每类场景定 SLO,再由 SLO 推导各自的阈值与分级,别拿一条 P95 阈值套所有业务。 > 🔬 **深度理解 · SLI / SLO 与"可行动"告警**: > 1. **本质**:SLI(服务等级指标)回答"你到底在测什么"——如首响应耗时、成功率;SLO(服务等级目标)回答"你承诺它要达标到什么程度"——如"客服首响 P95 < 3s、月度达标 99%";告警阈值 = 从 SLO **反推**出的"该反应了"红线。 > 2. **机制**:先定 SLI(要可量化、可采集、且与用户体感相关)→ 再定 SLO(目标值 + 观测窗口)→ 由 SLO 推导告警阈值与分级(P0 是破 SLO 红线,P1 / P2 是红线前的预警档)→ 用 SLO 达成情况做迭代反馈。SLO 常配"错误预算(error budget)":一个月内允许若干未达标,预算耗尽前预警而非瞬间刷屏。 > 3. **为什么重要**:没有 SLO 的告警是"拍脑袋的阈值",告了也说不清"到底紧不紧急";有了 SLO,告警才有"是否危及契约"的语义,也才能做到"阈值可行动、不刷屏"的分级。不同业务 SLO 不同,这正是"别拿一条 P95 套所有业务"的根源。 > 4. **易错点**:① 把 SLI 定义成"好测但与用户体验无关"的指标(测进程存活却不测首响延迟);② 不设观测窗口 /错误预算,把 SLO 当瞬时值,导致告警抖动刷屏;③ 先拍阈值、再回头"圆"SLO,而不是由 SLO 推导阈值;④ 不分业务场景、全站共用一个 SLO。 ```python def alert(metric: str, value: float): """告警分级: 阈值可行动, 别刷屏""" rules = { "success_rate": ((0.90, 0.05), "P0", "立即电话"), "failure_rate": ((0.05, 0.01), "P1", "工单"), "p95_latency": ((2.0, 1.0), "P2", "次日关注"), } for name, ((high, low), level, action) in rules.items(): pass # 示意: 按 metric 匹配阈值并分级告警 ``` ### 4. 观测平台 - **LangSmith**:官方 SaaS;**LangFuse**:开源自托管(数据不出内网时首选):记录每次 run 的 LLM 输入输出、工具调用、token 成本,自带看板与评测。 ```mermaid graph LR A[LangSmith] -->|官方 SaaS 省事| B[数据可能出境] C[LangFuse] -->|开源自托管| D[数据不出内网 ★内网首选] ``` - **自建最小观测体系**:metrics(计数器 + 直方图)→ 内存时序 → 告警判定 → Bad-case 队列;生产等价物 Prometheus + Grafana。 #### OpenTelemetry:可观测性标准协议 - **OTel 是 2025-2026 可观测性事实标准**:CNCF 开源项目,一套 API / SDK + OTLP 传输协议统一 Logs / Metrics / Traces 三支柱的采集与上报——不必再为每个观测后端各写一套埋点。 - **GenAI 语义约定(Semantic Conventions)**:OTel 的 GenAI SIG 已为 LLM / Agent 定义标准 span 属性——`gen_ai.operation.name`(`chat` / `embeddings` / `invoke_agent` / `execute_tool`)、`gen_ai.model.name`、token 用量属性(`gen_ai.usage.input_tokens` / `gen_ai.usage.output_tokens`)。按约定命名属性,任何后端都能「读懂」你的 Agent 链路,成本与耗时可直接聚合。 - **Agent / 框架层 span 约定已发布**:GenAI SIG 已含《GenAI agent/framework spans》规范(`invoke_agent` / `execute_tool` / `invoke_workflow` 等操作名),落地时按此约定命名即可,自定义属性留扩展位。 > ⏳ **短期可不深究**:OTel GenAI 的 agent/framework spans 语义约定还在随社区快速迭代,具体操作名可能调整——第一遍只要知道"有标准约定、落地时按规范命名字段"即可,那几个 `invoke_agent / execute_tool` 的具体名在你真要埋点时再看最新规范,不必现在记。 - **价值:一次埋点、多后端**——Langfuse / LangSmith 均支持 OTLP 接入,同一份埋点也可发 Jaeger(traces)/ Prometheus(metrics)。换观测平台不重写一行代码,只改 exporter 的 endpoint。 最小接入(`pip install opentelemetry-sdk opentelemetry-exporter-otlp`): ```python from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter # 一次埋点 → OTLP 发给任意支持的后端(Langfuse / LangSmith / Jaeger ...) provider = TracerProvider() provider.add_span_processor( # BatchSpanProcessor: 批量异步导出 BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4317")) ) trace.set_tracer_provider(provider) tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("agent_run") as span: # Agent 一步 = 一个 span span.set_attribute("gen_ai.operation.name", "invoke_agent") # GenAI 语义约定 span.set_attribute("gen_ai.model.name", "deepseek-chat") span.set_attribute("gen_ai.usage.input_tokens", 1200) # token 用量进 span ... # Agent 主流程 ``` - **框架自动埋点**:不想手写 span,可用 Traceloop 的 **openllmetry**——OTel 生态的 LLM 框架 instrumentation,支持 LangGraph / CrewAI / LlamaIndex 等主流框架,装包 + 配 OTLP endpoint,框架内部调用自动成 trace。 > ⏳ **短期可不深究**:openllmetry 这类"框架自动埋点"工具迭代很快、对各框架支持度参差,第一遍不必深挖——上面手写 span 的接入原理已经够用,等真要为项目上链路追踪时再评估选型、查其当时支持哪些框架。 ### 5. 上线迭代闭环(Bad-case 闭环) ```mermaid graph TD A[线上问题自动进 Bad-case 队列
低分会话/点踩/解析失败] --> B[周一 过队列四分类归档] B --> C[挑 Top3 高频] C --> D[周三 修复
改prompt/修工具/调检索] D --> E[补进评测集] E --> F[周五 回归测试] F -->|通过| G[灰度10% 观察] G -->|指标向好| H[全量] ``` - **Bad-case 上报通道**:线上问题自动进队列:低分会话 / 用户点踩 / 解析失败异常 → **带完整 `trace_id`** 进 Bad-case 表。 - **失败四分类**:prompt / 模型 / 工具 / 检索(对照阶段五评测)。 - **每周迭代 SOP**: - **周一**:过 Bad-case 队列按四分类归类 → 挑 Top3 高频问题; - **周三**:修复(改 prompt / 修工具 / 调检索)→ 补进评测集; - **周五**:回归测试通过 → 灰度 10% 流量观察指标 → 全量。 > **指标趋势向上(成功率 / 成本 / 延迟)= 闭环健康;原地打转 = 回头查根因**,别在同一个根因上反复打补丁。 ```python def badcase_close(opened: list, who: str, fix: str, eval_ok: bool) -> str: """一条 Bad-case 的完整闭环: 归类→修复→回归通过才关闭""" if not eval_ok: return f"[{who}] 修复后回归未通过, 不关闭, 进入复查" return f"[{who}] 已修复({fix})并通过评测回归, 关闭" ``` ## 本节自检 - [ ] 能说清日志 / 指标 / 追踪三支柱与黄金四指标(+ 成本第五维) - [ ] 能说清 OpenTelemetry 的价值(一次埋点多后端)与 GenAI 语义约定 - [ ] 已实现至少一项业务监控告警(任务成功率 / LLM 失败率) - [ ] 已跑通一次 Bad-case 收集 → 归类 → 修复 → 评测回归的闭环 - [ ] 能画出并执行「周一归类 → 周三修复 → 周五回归」的每周 SOP --- ## 本节常见面试题(深度解析) > 针对本节核心知识的面试高频点,配合"本质+机制"式理解,能让你答得既有深度又有广度。 ### 面试题 1:为什么监控要看 P95 / P99 而不是只看平均值? - **面试官想考察**:是有统计素养还是只会背"分位数"名词,是否理解长尾与容量决策的关系。 - **专业作答(含深度)**: 1. **本质**:均值被少数极端值(长尾)剧烈拖夸,既说不清"大多数用户多快"、也没暴露"究竟有多少人很慢",信息量最差;百分位回答"有 X% 的请求比它快",直面体感。 2. **机制**:LLM 延迟天然长尾(偶发 30s+、重试放大、排队),均值失真严重;生产用**直方图分桶**估算百分位,无需保留全量明细。 3. **成组看**:把 P50 / P95 / P99 放一起,区分"多数有多快、典型慢、极端长尾",比单个均值信息量大得多;P95 明显慢于 P50、P99 又远大于 P95,说明长尾重,应优先优化慢请求。 4. **与告警/容量结合**:阈值用 P95/P99 按场景定,容量与排障都更有代表性。 - **加分亮点 / 深度追问**:可主动说"生产 P95 是直方图近似、不是精确排序第 95 位"体现原理深度;被追问"长尾怎么来的、怎么排掉"时,答"来自偶发慢调用+重试放大+排队,可用超时上限、缓存、小模型、异步化削减"。 ### 面试题 2:trace_id 是如何在分布式 / Agent 链路里贯穿的?为什么说它是排障与 Bad-case 闭环的基石? - **面试官想考察**:是否真正落地过分布式追踪,而不只是知道"有个 ID"。 - **专业作答(含深度)**: 1. **贯穿路径**:入口生成 trace_id → 通过 HTTP header / 消息元数据**逐层透传**给下游 → 每段执行打包成 span → 凭同一 trace_id 归并成一条 trace(OTel 即 trace_id + span_id + parent_span_id 三级)。 2. **异步续传**:队列 consumer / worker 必须从任务元数据里带出 trace_id,否则"后台任务对不上会话"。 3. **基石作用**:Agent 一次回答跨 LLM + 工具 + 检索,排障要先回答"对应哪次请求、走到哪步挂的";日志带 trace_id 才能与 span、指标对起来;Bad-case 带完整 trace_id 进队列才能一键回到根因。 - **加分亮点 / 深度追问**:可提"透传是追踪的灵魂、context propagation 规范"体现严谨;被追问"三支柱怎么才算对起来"时,答"指标异常→用 trace_id 定位 trace→再拉同一 trace_id 的日志明细"。 ### 面试题 3:P0 / P1 / P2 告警分级应该怎么定?它和 SLI / SLO 是什么关系? - **面试官想考察**:告警是否"可行动、不刷屏",有没有 SLI/SLO 工程素养,而非只会背几个级别。 - **专业作答(含深度)**: 1. **由 SLO 反推**:先定 SLI(可量化且与用户体感相关)→ 再定 SLO(目标值 + 观测窗口)→ 由 SLO 推导阈值与分级:P0 破红线(如成功率 < 90%)立即处理,P1/P2 是红线前的预警档。 2. **可行动**:告警要能被"分级处理"对应(电话 / 工单 / 次日关注),告了没人处理等于噪音。 3. **分场景**:不同业务 SLO 不同(客服首响 < 3s vs 批处理跑 10 分钟),不能用一条全局阈值套所有场景。 - **加分亮点 / 深度追问**:可主动提"错误预算(error budget)与告警抖动抑制,避免瞬时抖动刷屏";被追问"SLO 定了 99% 怎么算 error budget"时,答"月度可容忍未达标时间 = (1−99%) × 当月总时长,预算逼近 0 时预警而非立即报警"。