跳到主内容
快讯直播
AI智模界
教程

给模型工作负载做登记、追踪与一键急停

过去一年,模型相关的故障大多是“跑飞了”:某个后台智能体在循环里反复调用工具,某个批处理任务误用了生产密钥,某个实验脚本忘了设上限。出问题时,团队往往只能眼看账单上涨,或者花很久才定位到哪个进程在打请求。

这篇教程要搭的是一套“控制室”:先登记每个工作负载,再追踪它们的运行状态,最后装上一个能在分钟级真正生效的急停开关,并用演练验证它确实能停。做完之后,你应该能拿到四个具体成果:

1. 一份工作负载登记表,明确每个调用方的负责人、预算和急停开关名;

2. 一套带标签的运行指标,能在出事的几分钟内看出是哪个负载、哪个环节在消耗额度;

3. 三级熔断策略:限流、降级、停止,按严重程度自动或手动触发;

4. 一次完整的急停演练记录,包含从按下开关到请求归零的实际耗时。

关于平台界面的说明:Anthropic 控制台里工作区、API 密钥、用量与成本、速率限制这些能力的位置和命名可能随版本调整,部分账号里会有专门的控制面板入口。下文用“控制台”统称这些管理界面,涉及具体菜单位置请以官方文档当前版本为准。真正决定急停是否有效的,是下面这套工程做法,而不是某个按钮。

前置条件清单

动手前先确认这些条件,能省掉后面一半的返工。

  • 权限:控制台里能创建工作区、签发与吊销 API 密钥、查看用量与成本的角色。
  • 凭据管理:有地方存密钥(Vault、云厂商 Secrets Manager、K8s Secret 均可),不能散落在代码和聊天记录里。
  • 标签规范:约定好工作负载 ID 的格式,例如 <团队>-<系统>-<用途>,如 growth-agent-daily-report。
  • 一个配置中心:Redis、etcd、配置服务、甚至对象存储上的一个 JSON 文件都能当“开关总线”,关键是所有进程都能读到。
  • 可观测性底座:能打日志、能上报自定义指标的地方(Prometheus、云监控、日志平台任选)。
  • 值班机制:每个工作负载至少有一个负责人在册,出事时知道该找谁。

先别急着上工具。下面第一步是纯人工的,但它的价值比后面所有代码都高。

步骤一:先盘清“谁在跑模型”——建立工作负载登记表

控制室的前提是“有名单”。没有名单,你看到用量暴涨也不知道该关谁。

把登记表当成代码来管,放在仓库里,走 PR 评审。建议的最小字段集:

```yaml

workloads/registry.yaml

version: 1

workloads:

  • id: growth-agent-daily-report

display_name: 每日增长分析智能体

owner: team-growth

oncall: "@zhangsan" # 值班人,必须能被打醒

env: prod # prod / staging / dev / experiment

purpose: 每天拉取渠道数据并生成分析摘要

data_class: internal # public / internal / confidential

workspace: ws-prod-growth # 控制台工作区,决定账单归属

credential_ref: secret/prod/growth-agent-key # 只存引用,不存明文

kill_switch_key: ks:workload:growth-agent-daily-report

budgets:

daily_usd: 40

monthly_usd: 600

limits:

max_steps: 60 # 智能体单次任务最大步数

max_wall_clock_min: 25 # 单次任务最长挂钟时间

max_tokens_per_task: 400000

runbook: docs/runbooks/growth-agent.md

enabled: true

```

几个字段值得单独解释:

  • credential_ref 而不是密钥本身:登记表会进仓库,明文密钥进去就等于泄露。
  • kill_switch_key:提前约定好开关名,急停时不用临时想名字,也不会关错对象。
  • max_steps / max_wall_clock_min:长跑智能体失控最常见的原因就是“没有步数上限”,模型自己停不下来。
  • oncall:没有负责人的工作负载,出事时没人能判断“该不该停”。

登记表要有校验。加一个 CI 检查,字段缺失或 ID 重复就挂掉:

```python

scripts/validate_registry.py

import sys, yaml, re

REQUIRED = ["id", "owner", "oncall", "env", "workspace",

"credential_ref", "kill_switch_key", "budgets", "limits", "runbook"]

def main(path="workloads/registry.yaml"):

data = yaml.safe_load(open(path, encoding="utf-8"))

seen, errors = set(), []

for w in data.get("workloads", []):

for f in REQUIRED:

if not w.get(f):

errors.append(f"{w.get('id','?')}: 缺少字段 {f}")

if w["id"] in seen:

errors.append(f"{w['id']}: ID 重复")

seen.add(w["id"])

if not re.fullmatch(r"[a-z0-9]+(-[a-z0-9]+){2,}", w["id"]):

errors.append(f"{w['id']}: ID 需为 三段小写短横线格式")

if w["env"] == "prod" and w["budgets"].get("daily_usd", 0) <= 0:

errors.append(f"{w['id']}: 生产负载必须设置日预算")

if errors:

print("\n".join(errors)); sys.exit(1)

print(f"OK: {len(seen)} 个工作负载")

if __name__ == "__main__":

main()

```

跑一次:

```bash

python scripts/validate_registry.py

```

清单建立之后,写一条硬规则:新负载不登记,不给密钥。这条规则执行三周,登记表就会自然成为可信来源。

步骤二:一个负载一份凭据,全部打上标签

急停之所以经常失效,是因为十个负载共用一个 API 密钥:吊销这把密钥,全公司一起挂;不吊销,失控的负载继续跑。

做法是每个工作负载独立签发密钥,按环境分工作区。命名带上工作负载 ID,方便在控制台里反查:

```bash

密钥只写进密钥管理系统,不落到终端历史

export ANTHROPIC_API_KEY="$(vault kv get -field=key secret/prod/growth-agent-key)"

固定请求头,方便网关层做归因

curl https://api.anthropic.com/v1/messages \

-H "x-api-key: $ANTHROPIC_API_KEY" \

-H "anthropic-version: 2023-06-01" \

-H "content-type: application/json" \

-H "x-workload-id: growth-agent-daily-report" \

-d '{

"model": "'"$MODEL"'",

"max_tokens": 1024,

"messages": [{"role": "user", "content": "ping"}]

}'

```

x-workload-id 是自定义头。如果请求走自建网关,网关把它写进指标标签;如果不走网关,就在 SDK 客户端里统一注入。模型名用环境变量传入,具体可用型号以官方文档当前版本为准,不要硬编码在某处。

同时给每个工作负载设两个“软闸”:

  • 速率限制:在控制台的用量/限额页面按工作区设定,先设一个明显够用但不离谱的值,运行一周后按 P95 上调。
  • 预算告警:日预算 70% 提醒负责人,100% 触发自动降级(下一步实现)。

步骤三:在调用路径上装一个能读开关的包装器

这一步是整个方案的核心。所有模型调用必须经过同一个函数,函数在做任何事之前先做三件事:查开关、查预算、查步数。绕过它直接调 SDK 的代码,等于给自己留了后门。

```python

llm_gate.py

import os, time, json, redis, requests

REDIS = redis.Redis.from_url(os.environ["SWITCH_REDIS_URL"])

API_KEY = os.environ["ANTHROPIC_API_KEY"]

WORKLOAD_ID = os.environ["WORKLOAD_ID"] # 从登记表注入

BASE = "https://api.anthropic.com/v1/messages"

class Stopped(Exception):

"""急停触发,调用方必须向上冒泡,禁止重试"""

def read_switch() -> dict:

"""返回 {'level': 0|1|2|3, 'reason': str},读不到时按保守值处理"""

raw = REDIS.get(f"ks:workload:{WORKLOAD_ID}")

if raw is None:

读不到开关:生产环境按 L1 限流,避免误放行失控负载

return {"level": 1, "reason": "switch-unreachable"}

return json.loads(raw)

def call_model(prompt: str, *, task: dict, max_tokens: int = 1024) -> str:

sw = read_switch()

if sw["level"] >= 3:

raise Stopped(f"L3 急停生效: {sw['reason']}")

步数与挂钟时间护栏

if task["steps"] >= int(os.environ["MAX_STEPS"]):

raise Stopped("超出最大步数")

if time.time() - task["started_at"] > int(os.environ["MAX_WALL_CLOCK_SEC"]):

raise Stopped("超出最大挂钟时间")

L2:降级——缩短输出、关闭工具、换小模型

payload = {"model": os.environ["MODEL"], "max_tokens": max_tokens,

"messages": [{"role": "user", "content": prompt}]}

if sw["level"] == 2:

payload["max_tokens"] = min(max_tokens, 256)

payload["model"] = os.environ["MODEL_FALLBACK"]

for attempt in range(2):

r = requests.post(BASE, timeout=60, json=payload, headers={

"x-api-key": API_KEY, "anthropic-version": "2023-06-01",

"content-type": "application/json", "x-workload-id": WORKLOAD_ID,

})

if r.status_code == 429 and sw["level"] == 1 and attempt == 0:

time.sleep(2 ** attempt) # L1 只做有限重试,且不无限退避

continue

r.raise_for_status()

task["steps"] += 1

return r.json()["content"][0]["text"]

raise RuntimeError("调用失败")

```

配套的环境变量在启动时从登记表注入,容器启动脚本里做一次即可:

```bash

export WORKLOAD_ID=growth-agent-daily-report

export MAX_STEPS=60

export MAX_WALL_CLOCK_SEC=1500

export MODEL="$(cat /etc/llm/model.txt)" # 具体型号以官方文档为准

export MODEL_FALLBACK="$(cat /etc/llm/model_fallback.txt)"

```

写开关的辅助函数,让“按开关”变成一个命令:

```python

ks.py

import json, sys, redis, os

r = redis.Redis.from_url(os.environ["SWITCH_REDIS_URL"])

wid, level, reason = sys.argv[1], int(sys.argv[2]), sys.argv[3]

r.set(f"ks:workload:{wid}", json.dumps({"level": level, "reason": reason}))

print(f"{wid} -> L{level} ({reason})")

```

```bash

python ks.py growth-agent-daily-report 3 "发票数据异常,人工介入"

```

注意:Stopped 异常不允许被重试逻辑吞掉。很多事故的真相是“急停生效了,但队列的重试把任务又跑了一遍”。在任务队列层也要加判断,收到 Stopped 就把任务标记为 aborted 而不是 failed,避免自动重投。

步骤四:让运行态看得见——四条必看指标

登记和开关解决“能不能停”,可见性解决“该不该停”。每条调用产生一条结构化日志:

```json

{

"ts": "2025-01-01T10:00:00Z",

"workload_id": "growth-agent-daily-report",

"task_id": "t-8f21",

"step": 17,

"model": "…",

"input_tokens": 3120,

"output_tokens": 480,

"latency_ms": 2380,

"tool_calls": ["fetch_channel_data", "fetch_channel_data"],

"switch_level": 0,

"outcome": "ok"

}

```

从日志里聚出四类指标,做成一块看板,出问题时只看这一块:

1. 花费速率:每分钟消耗金额,按 workload_id 拆线。看的是斜率,不是总额。

2. 步数分布:每个任务的实际步数直方图。某个负载的 P95 步数突然翻倍,就是循环的前兆。

3. 工具调用重复度:同名工具在单任务内被调用超过 N 次,几乎可以判定为原地打转。

4. 开关状态:所有负载的当前等级,一眼看到谁被限流、谁被停。

一个能直接跑的重复度检测,接在任务循环里:

```python

from collections import Counter

def check_loop(task: dict, tool_name: str, threshold: int = 5):

task.setdefault("tool_seq", []).append(tool_name)

c = Counter(task["tool_seq"][-20:])

if c[tool_name] >= threshold:

raise Stopped(f"工具 {tool_name} 在最近 20 步内重复 {c[tool_name]} 次")

```

这类基于“形状”的检测,比单纯看花费更早发现问题——账单涨起来的时候,通常已经晚了二十分钟。

步骤五:分级熔断——先限流,再降级,最后才停

一上来就全停,业务方第二天就会要求拆掉这套东西。三级策略更容易长期存活:

等级触发场景动作谁按
L1 限流花费速率超日预算的 1.5 倍、错误率升高并发降到 1、退避重试、排队自动
L2 降级日预算用尽、连续 3 次步数超限max_tokens 压到 256、关工具、换备用模型自动
L3 急停数据泄露迹象、明确跑飞、人工判断拒绝一切新请求,任务标记 aborted人(或强规则自动)

自动升级规则用一个定时任务实现,每 60 秒评估一次:

```python

autobrake.py —— 每分钟跑一次

import os, json, redis, datetime

r = redis.Redis.from_url(os.environ["SWITCH_REDIS_URL"])

for wid in os.environ["MANAGED_WORKLOADS"].split(","):

spent = float(r.get(f"cost:today:{wid}") or 0)

daily = float(r.get(f"budget:daily:{wid}") or 0)

if daily and spent > daily * 1.5:

r.set(f"ks:workload:{wid}", json.dumps({"level": 3, "reason": f"日预算 {daily} 已用 {spent:.0f},超 1.5 倍"}))

elif daily and spent > daily:

r.set(f"ks:workload:{wid}", json.dumps({"level": 2, "reason": "日预算用尽,降级运行"}))

```

用 SET 覆盖而不是追加,保证任意时刻只有一个明确的等级;评估函数是幂等的,重复执行不会出错。

步骤六:一键急停要“一键”,还要真的“停”

急停按钮有三个层次,按见效速度从快到慢排:

第一层:翻开关(秒级,可逆)

```bash

python ks.py growth-agent-daily-report 3 "跑飞,人工介入"

```

包装器下一次调用就会抛 Stopped。前提是包装器每次都读开关,而不是启动时读一次缓存十分钟。

第二层:吊销密钥(分钟级,影响面精确到该工作负载)

在控制台里禁用该工作负载对应的 API 密钥。这一步拦住的是所有新请求,包括绕过包装器的野代码。注意它拦不住已经发出的在途请求,也拦不住“重启后重新读到新密钥”的容器——所以先翻开关,再吊销。

第三层:下游断供(准实时)

如果工作负载会调用自建工具网关,在网关层加同样的开关判断,直接返回 503。这一层往往比模型侧更快生效,因为工具调用通常是失控循环里最贵、最有副作用的部分。

三件事按顺序做,写成 runbook,放在登记表链接的路径里:

```markdown

急停 Runbook:growth-agent-daily-report

1. 翻开关:python ks.py <id> 3 "<原因>" # 目标 < 60s

2. 确认指标:花费速率曲线在 2 分钟内归零

3. 若未归零:控制台禁用该工作负载密钥,排查是否有人绕过包装器

4. 若仍有请求:网关层封禁该 workload_id

5. 在 #incident 频道 @oncall,附上开关命令与时间戳

6. 恢复前必须记录根因,且把 MAX_STEPS 或预算调整一并提交 PR

```

步骤七:演练,否则你不知道它到底能不能停

没演练过的急停,和没有急停差不多。每月一次,选一个非关键负载,做一次完整的“假事故”:

```bash

1. 起一个持续跑的任务(在测试环境)

python run_agent.py --workload growth-agent-daily-report-test --loop 200 &

2. 记录时间,翻开关

date +%s > /tmp/t0

python ks.py growth-agent-daily-report-test 3 "月度演练"

3. 观察日志里最后一次成功调用与第一次 Stopped

grep -E "switch_level|Stopped" /var/log/agent.jsonl | tail -20

4. 计算 MTTS(mean time to stop)

echo $(( $(date +%s) - $(cat /tmp/t0) ))

```

演练要记录三个数字:按下开关到最后一个请求完成的秒数(MTTS)、是否有任务被队列重投、恢复后是否反复触发。MTTS 超过 5 分钟,就说明某处缓存了开关或者存在绕过路径,回去改包装器。

演练产出一份 5 行记录,归档即可:

```yaml

  • date: 2025-01-15

workload: growth-agent-daily-report-test

mtts_seconds: 12

side_effects: 无

issues: 库存同步任务因重试被重投 1 次,已在队列层加 aborted 处理

```

常见坑与排错

急停按了,请求还在跑。 九成是开关被进程内缓存,TTL 又设得很长。把开关读取放在每次调用前,缓存 TTL 不超过 5 秒;或者用 Redis pub/sub 主动推送失效通知。

按了开关,但队列把任务重跑了。 任务框架把 Stopped 当普通异常重试。在消费者里显式区分:aborted 不重投,failed 才重投。

吊销密钥后,容器重启又活了。 密钥来自环境变量或挂载文件,新容器读到了另一份未吊销的凭据。急停时要同时检查密钥来源,把所有同源凭据一起处理。

日预算设了,但没拦住。 预算只统计到天,而失控负载可能在 20 分钟内烧完全天额度。必须同时有“花费速率”告警(每分钟金额),速率比总量更早报警。

看板上一堆流量不知道是谁的。 有负载没打 x-workload-id。用网关做强制校验:没有该头的请求直接 400,逼迫团队接入。

告警发到没人看的频道。 把告警直接发给登记表里的 oncall,写到人,不写到群。

演练时一切正常,真实故障时失效。 演练用的是测试负载,而生产负载有额外的缓存层或批量合并逻辑。至少每季度拿一个生产负载做“只能停 30 秒”的轻量演练。

日志里带上了敏感数据。 结构日志只记元数据(token 数、耗时、工具名),不记 prompt 正文;需要排查时按 task_id 去受控存储里取。

下一步建议

控制室建起来之后,按这个顺序继续加固,投入产出比更高:

1. 把登记表接进 CI/CD:部署流水线启动时校验 WORKLOAD_ID 是否已登记、预算是否已设,未登记直接拒绝部署。

2. 把开关做成有审批的操作:L3 可以一键按,但恢复要两个人确认,避免“按了又马上恢复”的来回抖动。

3. 按任务类型分预算:交互式请求和批量分析的成本结构完全不同,混在一个预算里会互相挤占。

4. 给工具调用加副作用隔离:写操作类工具(发邮件、改库存、调支付)走一个带幂等键和二次确认的网关,即使模型跑飞也不会造成不可逆后果。

5. 季度复盘一次“差一点出事”:把花费速率异常但没触发熔断的案例拿出来看,调整阈值。

最后提醒一句:控制台的具体页面路径、可用模型、限额规则都在持续变化,配置时以官方文档当前版本为准;但这套“登记 → 可见 → 分级 → 演练”的结构不依赖任何具体版本,换平台也能照搬。真正的急停按钮不是某个页面上的开关,而是你的调用路径上那个每次都会执行的检查。

AI 生成本文由 AI 基于公开信息自动生成,仅供参考。