This commit is contained in:
2026-09-22 15:42:25 +08:00
commit 8e86665096
60 changed files with 22339 additions and 0 deletions
@@ -0,0 +1,187 @@
# 阶段七 · 综合实战: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 批量回归扩大覆盖";被追问"四分类边界模糊、某类内部还要细分"时,答"可在类内再拆子类,如工具类分超时/报错/参数错误,便于精准修复"。