将 PromQL 确定性计算与 LLM 判断力结合,自动化完成烧钱溯源、路由归因和政策建议(以 PR 形式人工审核),替代深夜 dashboard 肉搏。
An error budget agent 回答的是每个 burn-rate 告警在凌晨两点迫使 SRE 回答的三个问题:消耗速度有多快、到底是什么在消耗、是否值得为此放缓发布节奏。burn-rate 的计算逻辑存在于 PromQL recording rules 中——确定性、可测试、不涉及模型。LLM 只介入两个真正需要判断的环节:将消耗归因到具体路由、状态码或发布版本,以及起草带有证据支撑的策略决定(发通知、建工单,还是建议冻结部署)。而冻结操作本身以 pull request 的形式发出,由人合并, agent 永不会直接执行操作。
如果你的团队已经在跑 SLO,你已经有了所有原材料。大多数团队缺少的是连接各环节的粘合剂:burn-rate 告警触发后,有人打开四个 dashboard、目测 sum by (route) 的分类、交叉对照发布日志,然后在 Slack 里争论"第 11 天已消耗 62% 预算"究竟意味着什么。这二十分钟的仪式就是 agent 的工作。SLO 基础概念本身在 error budgets 指南和 SLI/SLO 实现教程中有讲解——本文假设那些已存在,并在其上构建分诊层。
永远不要让语言模型计算 burn rate。burn rate 是对计数器的算术运算,算术正是 recording rules 的用武之地。标准的 30 天 99.9% 可用性 SLO 多窗口配置如下:
# slo-rules.yaml — recording rules the agent reads, never writes
groups:
- name: slo-checkout-api
rules:
- record: slo:error_ratio:rate5m
expr: |
sum(rate(http_requests_total{app="checkout-api",status=~"5.."}[5m]))
/ sum(rate(http_requests_total{app="checkout-api"}[5m]))
- record: slo:error_ratio:rate1h
expr: |
sum(rate(http_requests_total{app="checkout-api",status=~"5.."}[1h]))
/ sum(rate(http_requests_total{app="checkout-api"}[1h]))
- record: slo:error_ratio:rate6h
expr: |
sum(rate(http_requests_total{app="checkout-api",status=~"5.."}[6h]))
/ sum(rate(http_requests_total{app="checkout-api"}[6h]))
- record: slo:error_ratio:rate30d
expr: |
sum(rate(http_requests_total{app="checkout-api",status=~"5.."}[30d]))
/ sum(rate(http_requests_total{app="checkout-api"}[30d]))
Burn rate 就是错误比率除以预算比率(99.9% 目标对应 0.001)。burn rate 为 1 表示在这个窗口期内刚好花完预算;14.4 则意味着整月预算在两天内耗尽。Google SRE workbook 中的经典分页阈值——1h 和 5m 窗口同时超过 14.4,或者 6h 和 30m 窗口同时超过 6——保留在 Alertmanager 中,这是它们该待的地方。agent 不决定告警是否触发,它在告警触发后才介入,与 Alertmanager MCP server 的分工一致:确定性系统检测,agent 解释并建议。
agent 有三个工具,全部是固定模板后的只读 PromQL——与 Prometheus MCP server 相同的纪律,但进一步收窄,因为这个 agent 只有一个任务:
# budget_tools.py — the agent's entire read surface
import os, httpx
PROM = os.environ["PROM_URL"]
BUDGET_RATIO = 0.001 # 99.9% SLO
WINDOW_DAYS = 30
def _prom(query: str) -> float | None:
r = httpx.get(f"{PROM}/api/v1/query", params={"query": query}, timeout=15)
r.raise_for_status()
res = r.json()["data"]["result"]
return float(res[0]["value"][1]) if res else None
@mcp.tool()
def get_burn_state(app: str) -> dict:
"""Current burn rates across windows, budget consumed this period,
and projected days to exhaustion at the current 6h burn rate."""
windows = {w: _prom(f'slo:error_ratio:{w}{{app="{app}"}}') or 0.0
for w in ("rate5m", "rate1h", "rate6h", "rate30d")}
burn = {w: round(v / BUDGET_RATIO, 2) for w, v in windows.items()}
consumed = round(windows["rate30d"] / BUDGET_RATIO, 4) # fraction of budget
burn_6h = burn["rate6h"]
days_left = (round((1 - consumed) * WINDOW_DAYS / burn_6h, 1)
if burn_6h > 0 and consumed < 1 else None)
return {"burn_rates": burn, "budget_consumed": consumed,
"days_to_exhaustion_at_6h_rate": days_left}
@mcp.tool()
def burn_breakdown(app: str) -> dict:
"""Top error contributors over the last hour, by route and status."""
q = ('topk(8, sum by (route, status) '
f'(rate(http_requests_total{{app="{app}",status=~"5.."}}[1h])))')
r = httpx.get(f"{PROM}/api/v1/query", params={"query": q}, timeout=15)
r.raise_for_status()
return {"contributors": [
{"route": s["metric"].get("route", "?"),
"status": s["metric"].get("status", "?"),
"errors_per_s": round(float(s["value"][1]), 3)}
for s in r.json()["data"]["result"]]}
@mcp.tool()
def recent_deploys(app: str, hours: int = 24) -> list[dict]:
"""Deploys for this app in the window, from the CD system's API.
Returns [{version, deployed_at, author}] — read-only."""
...
有两个设计要点值得它们的代价。首先,get_burn_state 返回距离耗尽的天数,而不只是百分比——"按当前速率预算还剩 2.1 天"才是推动发布决策的那个数字,在代码中计算意味着模型不会在预测上搞砸。其次,recent_deploys 出现在工具集中是因为没有发布关联的消耗归因就是占星术:回答"是什么在消耗预算"最常见的答案就是"14:20 的那次发布"。
一次调用,输入证据,输出结构化判决:
VERDICT_TOOL = {
"name": "report_budget_verdict",
"description": "Triage an error-budget burn event.",
"input_schema": {
"type": "object",
"properties": {
"severity": {"enum": ["page", "ticket", "note"]},
"attribution": {"type": "string",
"description": "What is burning the budget: route, status, "
"and correlated deploy if any. Cite numbers."},
"freeze_recommended": {"type": "boolean"},
"reasoning": {"type": "string",
"description": "3-5 sentences. Reference budget consumed, "
"days to exhaustion, and burn-rate windows."},
},
"required": ["severity", "attribution",
"freeze_recommended", "reasoning"],
},
}
SYSTEM = (
"You triage SLO error-budget burn for an SRE team.\n"
"Severity: 'page' only when fast-burn windows (5m AND 1h) exceed 14.4, "
"or exhaustion is projected under 3 days. 'ticket' for slow burns that "
"will consume the budget before the window resets. 'note' otherwise.\n"
"Recommend a freeze ONLY when budget consumed exceeds 90%, or a "
"specific recent deploy is the dominant contributor and exhaustion is "
"projected inside the window. A freeze recommendation must name what "
"should be frozen (one service, not the org).\n"
"You have read-only tools. You cannot page anyone or freeze anything; "
"you produce a recommendation with evidence."
)
严重度阶梯镜像了多窗口告警已经编码的内容,这种冗余是有意为之:模型被要求从数字中重新推导严重度,外层包装器则交叉检验它。如果 Alertmanager 触发了 fast-burn 分页但 agent 说 note,包装器仍会升级并在审查中标记分歧。agent 可以下调你对一次消耗的响应噪音级别,但永远不允许下调安全网。
这个设计中最锐利的护栏:freeze_recommended: true 不会停止任何人的部署。它向 CI 已经读取的仓库发起一个 pull request:
# .deploy-policy/freeze.yaml — proposed by the agent, merged by a human
frozen:
- app: checkout-api
reason: "Error budget 94% consumed on day 12 of 30. Fast burn (9.8x/6h)
attributed to /api/v1/payment route 500s beginning with deploy
v2.41.0 at 14:20 UTC. Projected exhaustion: 1.8 days."
until: "2026-08-20"
proposed_by: "error-budget-agent"
每个服务的部署 workflow 中加一个十行 GitHub Actions step,grep 这个文件并在匹配的 app 上失败运行。这就是全部的执行机制,它有三个 API 驱动冻结不具备的特性:建议带着可审查的证据到达 diff 中,人的决定是一次有审计轨迹的合并,而解冻就是 git revert。这与 AI agent 的 GitOps 论证相同——agent 在为审查而构建的媒介中提议——再加上触碰生产节奏的任何操作都需要人工确认的原则。部署冻结是组织级别的写操作。它应该花人类一次点击,但不应该更少。
until 日期比看起来更重要。无期限的冻结会腐烂成永久流程;有期限的冻结则迫使 error budget 存在的那个对话——要么可靠性工作发生了、预算恢复了,要么团队主动延长它。
在 verdicts 到达任何人的 pager 或仓库之前,以观察模式运行 agent 两到三周——标准的影子模式流程。每个 burn-rate 告警触发一次完整分诊运行; verdicts 落入日志频道。然后对照人类实际做的事打分:每个人类升级的消耗应该是 page 或 ticket,团队手动执行的每个冻结应该对应 freeze_recommended: true,而——更嘈杂的失败——团队正确忽略为毛刺的每次消耗应该是 note。毛刺是这个 agent 赢得或失去信任的地方。一个在每次瞬时 6x 消耗上都喊冻结的 error-budget agent 会在一个月内被静音,而一个被静音的 agent 比它取代的电子表格更糟,因为每个人都相信有东西在看着。
将影子模式中记录的证据束保留为回归测试fixture,并在每次 prompt 或模型变更时重放它们。归因准确率很容易离线打分:fixture 中的主导路由和关联发布是已知的;断言 verdict 命名了它们。
agent 继承了 SLI 的所有弱点。如果你的可用性 SLI 统计 HTTP 5xx 而最糟糕的故障模式是返回 stale-but-200 响应,预算永远不会燃烧、agent 永远沉默——SLI 设计在这一切的上游。按路由和发布关联进行归因是间接的:在事故期间落地的发布会被归咎于事故,所以 verdict schema 强制模型引用时间证据而不是断言因果。多服务请求路径会模糊所有权——由下游慢响应导致的 checkout-api 消耗仍然显示为 checkout 的预算,这个 agent 不会理清那个;分布式追踪才会的。而通过 PR 冻结的模式假设部署流经读取策略文件的 CI——从笔记本上 kubectl apply 会绕过它,这是关闭那条路径的论据,而不是给这个 agent 更多权力的理由。先从影子模式开始,再接 PR 路径,让 verdict 质量为 pager 集成代言。第一次 agent 的 PR 带着燃烧的路由、违规发布、和 1.8 天的倒计时出现——在任何仪表板打开之前——电子表格就结束了。