Files
python-agent/docs/07-阶段七-工程化部署与运维/04-阶段七综合实战-监控与运维Agent.md
T
2026-09-22 15:42:25 +08:00

187 lines
14 KiB
Markdown
Executable File
Raw 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.
# 阶段七 · 综合实战:Agent 观测台 + 告警 + Bad-case 闭环(离线可运行)
> 所属:阶段七 工程化、部署与运维
> 定位:把阶段七第 3 小点「监控与运维」的三大支柱、黄金四指标、告警分级、Bad-case 闭环,落成一份**离线可运行**的最小观测体系——模拟一批 Agent 请求,观察健康度、触发告警、把低分会话归类修复。
## 一句话看懂本 Demo
`monitoring_agent.py` 用纯 Python 标准库模拟一个 Agent 服务的"观测台":
```mermaid
graph TD
A["阶段七综合实战 monitoring_agent.py"] --> B[Part1 三大支柱<br/>日志/指标/追踪]
A --> C[Part2 黄金四指标 + 告警<br/>成功率/QPS/P95/token]
A --> D[Part3 Bad-case 闭环<br/>周一归类→周三修复→回归关闭]
```
> 核心一句话:**上线只是开始,能不能持续看到健康度、及时告警、闭环迭代,决定 Agent 能不能长期跑稳**。这份 Demo 就是最小可用的观测闭环。
## 运行方式
```
python3 code/阶段七/monitoring_agent.py
```
不需要 API Key、不需要外网、不需要装任何第三方库(只依赖标准库)。
## 完整代码位置
[code/阶段七/monitoring_agent.py](../../code/阶段七/monitoring_agent.py)
---
## 每个 Part 在讲什么
### Part 1 · 三大支柱(Logs / Metrics / Traces)
三个收集器分别对应监控三支柱:
| 支柱 | 类 | 记录什么 |
|-|-|-|
| 日志 Logs | `LogCollector` | 谁、何时、发生了什么(`agent start / done / failed`) |
| 指标 Metrics | `MetricsCollector` | 可聚合数值(耗时、成败、token) |
| 追踪 Traces | `TraceCollector` | 一次请求的跨步骤链路(`agent start` → `llm` span) |
```python
def simulate_traffic(mat, logs, traces, n=60):
"""模拟 60 次请求, 产生日志/指标/追踪 + 一些失败样本"""
# 用函数属性自增计数生成全局唯一 trace_id(等价真实环境的 UUID)
simulate_traffic.i = getattr(simulate_traffic, "i", 0) + 1
tid = f"t{simulate_traffic.i:03d}"
logs.info(tid, "agent start", step="receive")
traces.start(tid, "llm")
ok = random.random() > 0.28 # 约两成失败
lat = random.uniform(0.4, 3.4) if ok else random.uniform(2.0, 8.0)
mat.record_call(lat, ok, prompt_tk=..., completion_tk=...)
```
> ⏳ **短期可不深究**:里面 `simulate_traffic.i = getattr(...)` 用"函数属性自增"生成全局唯一 `trace_id` 只是离线 Demo 省依赖的**教学技巧**——真实生产是在网关/中间件用 UUID/ULID 生成并经 header 透传,第一遍不必在这点抠细节。
> ⏳ **短期可不深究**:`random.random()>0.28`(成败比)和 `random.uniform` 的延迟区间只是**随机模拟参数**,为了让例子出现告警而设——第一遍理解"它造出失败/长尾样本供演示告警与闭环"即可,不用纠结这些概率/区间取值是否贴合真实分布。
> 运维铁律:**一次请求可以从日志看到明细、从追踪看到路径、从指标看到聚合**,三样对起来才能排障,缺一样就"盲"。
> 🔬 **深度理解 · 三支柱为什么要"对起来"排障**:
> 1. **本质**:日志、指标、追踪是同一个问题的三种"视图"——日志给"这一次发生了什么"的明细,指标给"整体长什么样"的可聚合数字,追踪给"请求跨了哪些步骤"的路径;单独看任何一个都是"盲人摸象",只有用同一个 `trace_id` 把三者串起来,才能完成"现象 → 定位 → 根因"的闭环。
> 2. **机制**:真实生产里 Prometheus 扛指标(每秒 QPS / 成功率 / 延迟分位)、Jaeger / LangFuse 扛 span(一次 Agent 会话的调用树)、日志库每行带 `trace_id`;排障动作为"看指标发现异常 → 用 trace_id 追到那一次 trace 看走到哪一步 → 再拉同一 trace_id 的日志明细",三者在同一请求 / 同一时间窗口上对齐。
> 3. **为什么重要**:Demo 里三个 Collectors 都在内存、天然"对得起来";到生产一分家就断——若不强制带 `trace_id`、不打通三套系统,你会看到"QPS 掉了(指标)"却不知是模型超时还是工具报错,因为追踪和日志无从翻起。这正是"缺一样就盲"的真正含义。
> 4. **易错点**:① 三套系统各自埋点却不用同一个 `trace_id`,跨系统对不上号;② 只存指标不存日志明细,异常窗口无法复盘;③ 把 span 当日志、把日志当 span,单一视图信息残缺;④ 忘了给 Bad-case 补 `trace_id`,导致后续闭环无从定位。
### Part 2 · 黄金四指标 + 告警(Google SRE · Agent 版)
`evaluate_golden()` 从指标收集器里取数,逐条做可行动告警:
```python
def evaluate_golden(mat) -> dict:
r = mat.report()
out = {"QPS": r["QPS"], "token累积": r["token累积"]}
sr = r["成功率"]; fr = 1 - sr; p95 = r["P95延迟(s)"]
alerts = []
if sr < 0.90:
alerts.append(("P0", "成功率跌破90%", "立即电话"))
elif fr > 0.05:
alerts.append(("P1", "失败率>5%", "工单"))
if p95 > 2.0:
alerts.append(("P2", "P95变慢", "次日"))
return out # 阈值"可行动", 告了不处理的等于噪音
```
Demo 里 `p95` vs `mean` 同时打印,能直观看到 LLM 长尾——均值(2.77s)反映大多数请求较快,而 P95(6.76s)提示确有相当比例的慢请求;只看均值会低估极端延迟,这正是要盯 P95/P99 的原因。
> 🔬 **深度理解 · 为什么"只看均值会低估极端延迟"**:
> 1. **本质**:均值把所有请求耗时代数平均,一个极端长尾就足以把整体均值抬高,既不能告诉"大多数用户多快"、也没暴露"究竟有多少人很慢",是信息量最差的一类汇总;分位数(P95 / P99)回答"有 X% 的请求比它快",直击体感。
> 2. **机制**:LLM 服务延迟几乎必然长尾(偶发慢调用、重试放大、排队使后面请求等更久),均值被右尾剧烈拖动;只有 `min/P50/P95/P99/max` 成组看,才能同时看清轮廓(P50)、普遍体感(P95)、极端长尾(P99 / max)。
> 3. **为什么重要**:它直接决定扩容 / 加超时 / 做缓存这类决策的质量——均值 2.9s 看着还行、P95 6.7s 说明确有相当比例的慢请求,若只看均值会误判"还不错"、漏掉该被优化的长尾用户;这也是容量告警要按分位数定的原因。
> 4. **易错点**:① 把生产 P95 当成精确排序第 95 位——它来自直方图分桶的**近似估算**;② 观察窗口混入非典型批次(上线一批重型任务)让 P95 失真;③ 单看均值或单看 P95 都片面,要**成组看趋势**。
### Part 3 · Bad-case 闭环(周一归类 → 周三修复 → 回归关闭)
`BadCaseLoop` 实现每周迭代 SOP:
```python
def weekly_sop(self):
"""周一: 过队列四分类归档 → 挑 Top3 高频"""
cats = {}
for item in self.queue:
c = classify(item["case"]) # prompt / 模型 / 工具 / 检索
cats[c] = cats.get(c, 0) + 1
return sorted(cats.items(), key=lambda x: x[1], reverse=True)[:3]
```
闭环逻辑:问题进队列 → 四分类归档 → 挑 Top3 → 修复 → 补评测集 → 回归通过才关闭;回归不通过不关闭。
> 🔬 **深度理解 · 失败四分类与 Bad-case 闭环**:
> 1. **本质**:把"线上用户觉得不对"的坏例,先归到 **prompt / 模型 / 工具 / 检索** 四类根因之一,再按频率挑 Top 修复——本质上是一套"用错误样本驱动迭代"的 RAG / Agent 质量方法论,让改进有方向、而非凭感觉乱调。
> 2. **机制**:低分会话 / 点踩 / 解析失败自动进队列(带 `trace_id`)→ 按四分类归类(prompt 措辞歧义 / 模型能力不足 / 工具报错、参数错、超时 / 检索召回或排序不对)→ 统计频次挑 Top3 → 针对性修复(改 prompt / 修工具 / 调检索)→ 把修复样本**补进评测集** → 回归通过才 close,不过进复查。
> 3. **为什么重要**:没有分类维度,"迭代"就退化成"反复改、无方向";四分类恰好对照阶段五评测的三个环节,让"线上坏例"与"离线评测"用同一套语言,坏例还能反哺评测集,形成"评测越来越准、线上越来越好"的正循环。
> 4. **易错点**:① 用"症状"当"根因"分类(模型答错其实可能是检索没喂对上下文,却归成模型类);② 只看数量不按影响力排,高频但影响小 vs 低频但致命要分开加权;③ 修复了不补评测集,回归只能靠感觉、无法防回归;④ 回归不通过也 close,导致闭环"假关闭"。
```
[周一归类] 失败四分类: {'tool': 17} → Top3: [('tool', 17)]
[修复] tool 类 17 条, fix=给工具加超时+重试, 回归通过, 已关闭
```
---
## 运行结果预览
```
>>> Part 1 三大支柱
[追踪] 最新一次请求 spans: ['llm']
[日志] 该请求日志: [('info','agent start'), ('info','agent done')]
>>> Part 2 黄金四指标 + 告警
QPS: 1.0 token累积: 43967 成功率: 0.717
告警: [('P0','成功率跌破90%','立即电话'), ('P2','P95变慢','次日')]
P95 vs 均值: 约 7.0 vs 约 2.9 (示例值, 数值随随机延迟分布而变)
>>> Part 3 Bad-case 闭环
[周一归类] 失败四分类: {'tool': 17} → Top3: [('tool', 17)]
[修复] tool 类 17 条, fix=给工具加超时+重试, 回归通过, 已关闭
```
## 换真实生产只需对号入座
| 教学实现 | 生产替换 |
|-|-|
| `MetricsCollector` 内存计数器 + 直方图 | Prometheus(counter / histogram) |
| `evaluate_golden` 逐条告警判定 | Prometheus 告警规则 / Alertmanager 分级通知 |
| `TraceCollector` 内存 span | LangFuse / LangSmith / Jaeger |
| `BadCaseLoop` 内存队列 | 线上低分会话自动入库(带 trace_id) |
| 每周 SOP 手写 | 结合阶段五的 LLM-as-judge 批量回归 |
## 本节自检
- [ ] 能说清日志 / 指标 / 追踪三支柱,知道排障为什么三者要对起来
- [ ] 能默写黄金四指标,并解释为什么看 P95 而不是只看均值
- [ ] 已实现至少一项可行动告警(成功率 / LLM 失败率)
- [ ] 已跑通一次「Bad-case 收集 → 归类 → 修复 → 评测回归」闭环
---
## 本节常见面试题(深度解析)
> 针对本节核心知识的面试高频点,配合"本质+机制"式理解,能让你答得既有深度又有广度。
### 面试题 1:这份"内存版"观测 Demo 换到生产,要替换成什么?为什么能"一条线对应替换"?
- **面试官想考察**:能否把教学抽象正确映射到真实技术栈,是否理解"抽象 vs 实现"的关系,而不只是会跑 Demo。
- **专业作答(含深度)**:
1. **逐项替换**:`MetricsCollector`(内存计数器+直方图)→ Prometheus(counter / histogram);`evaluate_golden`(逐条告警判定)→ Prometheus 告警规则 / Alertmanager 分级通知;`TraceCollector`(内存 span)→ OpenTelemetry + Jaeger / LangFuse / LangSmith;`BadCaseLoop`(内存队列)→ 线上低分会话自动入库(带 trace_id);每周 SOP 手写 → 用 LLM-as-judge 批量回归。
2. **为何能对应**:因为教学里的"类"本就是真实组件的共性抽象——Counter / Histogram、告警规则、span、闭环流程是同一套概念,只是从"内存实现"换成"生产实现"。
3. **生产要点**:三支柱要用同一个 `trace_id` 打通、指标与追踪走 OpenTelemetry 标准协议(一次埋点多后端),否则"对起来排障"在生产里会失效。
- **加分亮点 / 深度追问**:可主动点明"用 OTLP 标准协议是为了避免锁死某个厂商观测后端";被追问"Prometheus histogram 的 bucket 该怎么设计"时,答"按 LLM 延迟的典型区间(0.5s/1s/2s/5s/10s...)设计,桶太粗精度差、太细成本高"。
### 面试题 2:为什么说"只看均值会低估极端延迟"?你在实际观测里看什么?
- **面试官想考察**:对 LLM 长尾与分位数指标是否有实践层面理解,能否落地到决策。
- **专业作答(含深度)**:
1. **本质**:均值被长尾剧烈拖夸、又掩盖了"有多少人很慢",是失真最严重的汇总;回到本 Demo 示例——均值 2.9s 但 P95 6.7s,正是长尾的诊断信号。
2. **看什么**:成组看 `P50 / P95 / P99 / max`,P99 远超 P95 说明长尾重,就该去优化慢请求(缓存、小模型、超时上限、异步化),而不是盲目加机器。
3. **生产注意**:P95 来自直方图分桶的近似估算;观察窗口要避开刚上线的非典型批次,否则 P95 会失真。
- **加分亮点 / 深度追问**:可补"长尾常来自偶发慢调用 + 重试放大 + 排队效应,P99 更适合做容量规划";被追问"长尾怎么量化判断是否改善"时,答"比较同口径同窗口的 P99 趋势,而不是均值"。
### 面试题 3:Bad-case 闭环里的"失败四分类"为什么重要?如何防止闭环"假关闭"?
- **面试官想考察**:是否有质量数据闭环意识,是否理解"用坏例驱动迭代、且用评测防回归"。
- **专业作答(含深度)**:
1. **四分类为何重要**:prompt / 模型 / 工具 / 检索提供了改进的"根因定向",避免无方向乱改;它对照阶段五评测环节,让线上坏例与离线评测用同一套语言。
2. **闭环流程**:坏例自动进队列(带 trace_id)→ 四分类归档 → 按频次×影响挑 Top → 修复 → **补进评测集** → 回归通过才 close。
3. **防"假关闭"**:回归不通过绝不 close、用症状当根因要纠正、先看影响再看频次——"补评测集 + 回归门禁"是防止假关闭的关键。
- **加分亮点 / 深度追问**:可主动讲"坏例反哺评测集形成正循环、用 LLM-as-judge 批量回归扩大覆盖";被追问"四分类边界模糊、某类内部还要细分"时,答"可在类内再拆子类,如工具类分超时/报错/参数错误,便于精准修复"。