Files
2026-09-22 15:42:25 +08:00

224 lines
17 KiB
Markdown
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 阶段七 · 小点 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<br/>单次事件明细]
A --> C[指标 Metrics<br/>可聚合数值序列]
A --> D[链路追踪 Traces<br/>跨节点完整路径]
```
- **日志**:单次事件的明细记录(谁、何时、发生了什么)。
- **指标**:可聚合的数值时间序列(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 队列<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% 流量观察指标 → 全量。
> **指标趋势向上(成功率 / 成本 / 延迟)= 闭环健康;原地打转 = 回头查根因**,别在同一个根因上反复打补丁。
```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 时预警而非立即报警"。