17 KiB
Executable File
17 KiB
Executable File
阶段七 · 小点 3:监控与运维
所属:阶段七 工程化、部署与运维 定位:上线只是开始,能不能持续观察到健康度、及时告警、闭环迭代,决定了 Agent 能不能长期跑稳。这一讲讲清三大监控支柱、黄金四指标、告警分级与上线迭代闭环。
精简大纲
- 三大监控支柱:Logs / Metrics / Traces
- 黄金四指标(Agent 版)
- 告警阈值与分级
- 观测平台:LangSmith / LangFuse / 自建 / OpenTelemetry
- 上线迭代闭环(Bad-case 收集 → 归类 → 修复 → 回归)
学习内容详情
核心认知:Agent 运维三样都要——日志、指标、追踪,缺一样排障就"盲"。
1. 三大监控支柱(Logs / Metrics / Traces)
graph LR
A[三大支柱] --> B[日志 Logs<br/>单次事件明细]
A --> C[指标 Metrics<br/>可聚合数值序列]
A --> D[链路追踪 Traces<br/>跨节点完整路径]
- 日志:单次事件的明细记录(谁、何时、发生了什么)。
- 指标:可聚合的数值时间序列(QPS、成功率、P95 耗时)。
- 链路追踪:一次请求跨节点 / 跨步骤的完整路径。
- Agent 运维三样都要,缺一样排障就盲。
- 链路追踪:Agent 每一步完整日志;
- 工具调用审计日志——谁调用、入参、返回值(安全与排障关键)。
🔬 深度理解 · 分布式追踪与 trace_id:
- 本质:trace_id 是一次请求在整个分布式系统里"横穿多个节点 / 多次调用"的公共身份证——只要每步埋点都带上同一个 ID,就能把一次 Agent 会话里零散的日志、多个 span、多条工具调用串成一条完整路径。
- 机制:入口(网关 / API)生成 trace_id(常见随机 / ULID),通过 HTTP header 或消息元数据层层透传给下游;每段执行被打包成一个 span(含开始/结束、父子关系、语义属性),所有 span 凭同一 trace_id 归并成一条 trace;落到 OTel 即
trace_id + span_id + parent_span_id的三级结构。- 为什么重要:Agent 一次回答往往跨"LLM 调用 + 多个工具 + 检索",排障时要先回答"用户报的问题对应哪次请求、走到哪一步挂的"——没有 trace_id,日志是散的、跨服务也无法关联;它也是下文 Bad-case 闭环里"带完整 trace_id 进队列"的前提,能一键回到根因。
- 易错点:① 只在 API 层打点、不把 ID 透传给下游/worker,跨进程链就断了——透传是追踪的灵魂;② 异步任务(队列 consumer)里忘了从任务元数据带 trace_id,导致"后台任务对不上会话";③ 日志记了 ID,却在指标/归一化层不关联,三支柱"对不起来";④ 用手工拼接而非标准的上下文传播(Context Propagation)传 ID,下游读不到。
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 才知道真实体感。
- 记住:任何告警规则先从这四个里挑。
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)与长尾:
- 本质:P95 是"把耗时排序后第 95 百分位的值",回答"95% 的请求比它快",是对大多数用户体感的分位数刻画,而非平均——LLM 延迟有显著长尾(偶发 30s+),均值会被少数极端值剧烈拖夸而失真。
- 机制:生产里不存全量明细去精确排序,而是按耗时**分桶直方图(Histogram)**累计计数(Prometheus 用
le桶),查询时由累计分位数估算出 P95;它天然是"可聚合数值序列"(Metrics),无需保留每条请求的原始日志。- 为什么重要:容量判断与告警要用"代表性"而非"平均"——均值 2s 完全掩盖了"偶尔有 15% 的人等了 8s"的糟糕体感;把 P50 / P95 / P99 横着看,能同时定位"多数有多快、典型慢、极端长尾",信息量远大于单一均值。
- 易错点:① 误以为生产 P95 是精确排序——它来自直方图分桶的近似估算,不同后端精度有差异;② 只看 P95 不看 P99,最尾端的极端用户被忽略;③ 把 P95 与"最大值"混谈;④ 用一个全局 P95 阈值套所有业务——客服对话与批处理报表的 SLO 天然不同(呼应下文"分场景基线")。
- 第五个维度——成本指标(Agent 特有,也最易失控):黄金四指标之外必须加盯 token 消耗速率与单次会话成本——前者看「烧得多快」(饱和度),后者看「一次对话花多少钱」(乘以会话量就是日账单)。模型换代、prompt 变长、循环失控都会让成本悄悄翻倍,而延迟与成功率可能纹丝不动——四指标都绿、账单爆了,只有成本指标能第一时间暴露。
3. 告警阈值与分级
- 阈值要「可行动」——告了没人处理的告警等于噪音。
| 级别 | 触发条件 | 处理 |
|---|---|---|
| P0 | 成功率跌破 90% | 立即电话 |
| P1 | 失败率 > 5% | 工单 |
| P2 | P95 变慢 | 次日关注 |
- 分场景基线:阈值来自各自场景的 SLO,不是全局统一值——客服对话首响 < 3s 是硬指标(首响应慢一秒用户就流失),离线批处理报表跑 10 分钟也无可厚非。先给每类场景定 SLO,再由 SLO 推导各自的阈值与分级,别拿一条 P95 阈值套所有业务。
🔬 深度理解 · SLI / SLO 与"可行动"告警:
- 本质:SLI(服务等级指标)回答"你到底在测什么"——如首响应耗时、成功率;SLO(服务等级目标)回答"你承诺它要达标到什么程度"——如"客服首响 P95 < 3s、月度达标 99%";告警阈值 = 从 SLO 反推出的"该反应了"红线。
- 机制:先定 SLI(要可量化、可采集、且与用户体感相关)→ 再定 SLO(目标值 + 观测窗口)→ 由 SLO 推导告警阈值与分级(P0 是破 SLO 红线,P1 / P2 是红线前的预警档)→ 用 SLO 达成情况做迭代反馈。SLO 常配"错误预算(error budget)":一个月内允许若干未达标,预算耗尽前预警而非瞬间刷屏。
- 为什么重要:没有 SLO 的告警是"拍脑袋的阈值",告了也说不清"到底紧不紧急";有了 SLO,告警才有"是否危及契约"的语义,也才能做到"阈值可行动、不刷屏"的分级。不同业务 SLO 不同,这正是"别拿一条 P95 套所有业务"的根源。
- 易错点:① 把 SLI 定义成"好测但与用户体验无关"的指标(测进程存活却不测首响延迟);② 不设观测窗口 /错误预算,把 SLO 当瞬时值,导致告警抖动刷屏;③ 先拍阈值、再回头"圆"SLO,而不是由 SLO 推导阈值;④ 不分业务场景、全站共用一个 SLO。
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 成本,自带看板与评测。
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):
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 闭环)
graph TD
A[线上问题自动进 Bad-case 队列<br/>低分会话/点踩/解析失败] --> B[周一 过队列四分类归档]
B --> C[挑 Top3 高频]
C --> D[周三 修复<br/>改prompt/修工具/调检索]
D --> E[补进评测集]
E --> F[周五 回归测试]
F -->|通过| G[灰度10% 观察]
G -->|指标向好| H[全量]
- Bad-case 上报通道:线上问题自动进队列:低分会话 / 用户点踩 / 解析失败异常 → 带完整
trace_id进 Bad-case 表。 - 失败四分类:prompt / 模型 / 工具 / 检索(对照阶段五评测)。
- 每周迭代 SOP:
- 周一:过 Bad-case 队列按四分类归类 → 挑 Top3 高频问题;
- 周三:修复(改 prompt / 修工具 / 调检索)→ 补进评测集;
- 周五:回归测试通过 → 灰度 10% 流量观察指标 → 全量。
指标趋势向上(成功率 / 成本 / 延迟)= 闭环健康;原地打转 = 回头查根因,别在同一个根因上反复打补丁。
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 而不是只看平均值?
- 面试官想考察:是有统计素养还是只会背"分位数"名词,是否理解长尾与容量决策的关系。
- 专业作答(含深度):
- 本质:均值被少数极端值(长尾)剧烈拖夸,既说不清"大多数用户多快"、也没暴露"究竟有多少人很慢",信息量最差;百分位回答"有 X% 的请求比它快",直面体感。
- 机制:LLM 延迟天然长尾(偶发 30s+、重试放大、排队),均值失真严重;生产用直方图分桶估算百分位,无需保留全量明细。
- 成组看:把 P50 / P95 / P99 放一起,区分"多数有多快、典型慢、极端长尾",比单个均值信息量大得多;P95 明显慢于 P50、P99 又远大于 P95,说明长尾重,应优先优化慢请求。
- 与告警/容量结合:阈值用 P95/P99 按场景定,容量与排障都更有代表性。
- 加分亮点 / 深度追问:可主动说"生产 P95 是直方图近似、不是精确排序第 95 位"体现原理深度;被追问"长尾怎么来的、怎么排掉"时,答"来自偶发慢调用+重试放大+排队,可用超时上限、缓存、小模型、异步化削减"。
面试题 2:trace_id 是如何在分布式 / Agent 链路里贯穿的?为什么说它是排障与 Bad-case 闭环的基石?
- 面试官想考察:是否真正落地过分布式追踪,而不只是知道"有个 ID"。
- 专业作答(含深度):
- 贯穿路径:入口生成 trace_id → 通过 HTTP header / 消息元数据逐层透传给下游 → 每段执行打包成 span → 凭同一 trace_id 归并成一条 trace(OTel 即 trace_id + span_id + parent_span_id 三级)。
- 异步续传:队列 consumer / worker 必须从任务元数据里带出 trace_id,否则"后台任务对不上会话"。
- 基石作用:Agent 一次回答跨 LLM + 工具 + 检索,排障要先回答"对应哪次请求、走到哪步挂的";日志带 trace_id 才能与 span、指标对起来;Bad-case 带完整 trace_id 进队列才能一键回到根因。
- 加分亮点 / 深度追问:可提"透传是追踪的灵魂、context propagation 规范"体现严谨;被追问"三支柱怎么才算对起来"时,答"指标异常→用 trace_id 定位 trace→再拉同一 trace_id 的日志明细"。
面试题 3:P0 / P1 / P2 告警分级应该怎么定?它和 SLI / SLO 是什么关系?
- 面试官想考察:告警是否"可行动、不刷屏",有没有 SLI/SLO 工程素养,而非只会背几个级别。
- 专业作答(含深度):
- 由 SLO 反推:先定 SLI(可量化且与用户体感相关)→ 再定 SLO(目标值 + 观测窗口)→ 由 SLO 推导阈值与分级:P0 破红线(如成功率 < 90%)立即处理,P1/P2 是红线前的预警档。
- 可行动:告警要能被"分级处理"对应(电话 / 工单 / 次日关注),告了没人处理等于噪音。
- 分场景:不同业务 SLO 不同(客服首响 < 3s vs 批处理跑 10 分钟),不能用一条全局阈值套所有场景。
- 加分亮点 / 深度追问:可主动提"错误预算(error budget)与告警抖动抑制,避免瞬时抖动刷屏";被追问"SLO 定了 99% 怎么算 error budget"时,答"月度可容忍未达标时间 = (1−99%) × 当月总时长,预算逼近 0 时预警而非立即报警"。