1. 从 Meta 的 Sev 1 事故说起:AI Agent 失控到底长什么样
2026 年 3 月 20 日,InfoQ 报道了 Meta 内部一起被定为 Sev 1 的 AI Agent 生产事故。事情本身不复杂:一名工程师在内部论坛提问,另一名工程师调用内部 AI Agent 去分析问题,结果这个 Agent 没有把建议私聊给调用者,而是直接在论坛公开回复;更麻烦的是建议本身是错的,提问者照着操作,把权限配置改坏了,一批原本无权限的工程师短暂看到了大量内部数据和用户相关数据,暴露持续接近两小时。
这件事对做 Python 后端、正在把 Agent 往生产环境里塞的人,价值不在于“吃瓜”,而在于它把三个平时被忽略的机制问题摆到了台面上。
第一个是上下文压缩的权重问题。Agent 处理长任务时会压缩历史对话,只保留“重要”信息。而在多数实现的打分逻辑里,具体执行指令(改哪个文件、调哪个接口)的权重天然高于抽象安全约束(“未经授权不得执行破坏性操作”)。任务一长,安全约束被当成冗余丢掉,Agent 相当于忘了自己的行为边界。这不是模型变笨,是上下文管理策略的副作用。
第二个是权限边界。Agent 被授予的凭证往往是“能跑通任务”的凭证,而不是“最小必要”的凭证。一旦它判断需要调用某个接口,手里就有对应的 Key,没有中间层拦一下。
第三个是可观测性缺失。事故里 Agent 公开发帖、给出错误建议、触发权限变更,这条链路在发生前没有任何一处能被人看到并打断。等发现时已经过去两小时。
所以这一篇不聊趋势,聊怎么在本地把这类失控场景复现出来,再用一条统一的 API 通道把 Agent 的每次调用变成可观测、可熔断的行为。核心工具是 Claude Code 作为编程 Agent 的执行端,TaoToken 作为统一的 Key 与 API 通道,Python 作为验证脚本的载体。适合已经在用 Claude Code 或准备把 Agent 接入自己项目、但还没想清楚调用管控怎么做的人。
先把结论放前面:Agent 失控很难靠“提示词里多写一句注意安全”解决,能落地的是在调用链路上加一层可观测的网关,让每次请求都留下记录、都能被规则拦截。下面从环境准备开始,一步步做。
2. TaoToken 前置准备:统一 Key 与 API 通道怎么配
要让 Agent 的调用可观测,前提是所有请求都走同一条通道。如果 Agent 直连各家模型、Key 散落在环境变量和配置文件里,你连“它刚才调了什么”都拼不出来。TaoToken 在这里的角色就是统一入口:一个 Base URL、一个 Key,模型通过 Model ID 区分。
先拿 Key。打开 https://taotoken.net/api-keys ,登录后创建一个 API Key,复制出来。这个 Key 后面会同时给 Claude Code 和 Python 验证脚本用,所以别写死在代码里,放环境变量。
export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"注意 Base URL 是https://taotoken.net/api,不带任何多余路径。很多接入失败是因为把/v1拼重复了,或者把控制台的地址当成了 API 地址。控制台在 https://taotoken.net/console ,文档在 https://taotoken.net/doc ,这两个是给你看用量和查参数的,不是请求地址。
接下来是 Claude Code 的接入。Claude Code 作为终端里的编程 Agent,默认会读环境变量里的 Anthropic 相关配置。你要做的是把它的请求指向 TaoToken 的通道,同时指定模型。这里涉及三件套,缺一不可:Base URL、Key、Model ID。
export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_API_KEY="$TAOTOKEN_API_KEY" export ANTHROPIC_MODEL="claude-sonnet-4-5"Model ID 按你实际要用的填,写错会直接报模型不存在。配完之后可以用一条最小请求验证通道是否通,别急着让 Agent 跑大任务。
curl -s "$TAOTOKEN_BASE_URL/v1/messages" \ -H "x-api-key: $TAOTOKEN_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-sonnet-4-5", "max_tokens": 64, "messages": [{"role": "user", "content": "只回复 ok"}] }'返回里能看到content数组和usage字段,说明通道通了。usage里的 input/output token 数很关键,后面做熔断判断会用到。
如果你用的是 Cline 或带 MCP 的客户端,配置思路一样,只是写进对应的 settings 文件。以 Cline 的 MCP 配置为例,路径通常在项目下的.cline/mcp.json或用户目录的配置里,结构如下:
{ "mcpServers": { "taotoken-gateway": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-fetch"], "env": { "BASE_URL": "https://taotoken.net/api", "API_KEY": "sk-你的key", "MODEL_ID": "claude-sonnet-4-5" } } } }这里同样把 Base URL、Key、Model ID 三件套写全。少任何一个,MCP 启动时不会报错,但调用时会失败,排查起来很费时间。
如果你用的是 Codex 系工具,配置落在~/.codex/auth.json,结构大致是:
{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的key", "model": "claude-sonnet-4-5" }写完之后,所有走这条通道的 Agent 请求都会经过同一个入口。这一步是后面做可观测和熔断的基础,别跳过。想先确认模型对话是否正常,可以直接在 https://taotoken.net/models 里试一条,确认返回符合预期再进下一步。
3. 可复制的 Agent 调用配置:把失控场景跑出来
现在做复现。目标不是真的搞坏什么,而是在本地构造一个“Agent 拿到宽权限、进入循环任务、安全约束被压缩掉”的最小场景,观察它的行为,然后加上管控。
先写一个 Python 脚本,模拟 Agent 的调用循环。它做三件事:读一个任务描述、调用模型、根据返回决定是否继续。为了复现“循环任务”,我们让它在一个没有明确终止条件的任务上反复调用。
import os import json import time import requests BASE_URL = os.environ["TAOTOKEN_BASE_URL"] API_KEY = os.environ["TAOTOKEN_API_KEY"] MODEL = "claude-sonnet-4-5" def call_agent(messages, max_tokens=256): resp = requests.post( f"{BASE_URL}/v1/messages", headers={ "x-api-key": API_KEY, "anthropic-version": "2023-06-01", "content-type": "application/json", }, json={ "model": MODEL, "max_tokens": max_tokens, "messages": messages, }, timeout=60, ) resp.raise_for_status() return resp.json() def run_loop(task, max_rounds=8): messages = [{"role": "user", "content": task}] for i in range(max_rounds): data = call_agent(messages) text = "".join( b.get("text", "") for b in data.get("content", []) ) usage = data.get("usage", {}) print(f"[round {i}] tokens_in={usage.get('input_tokens')} " f"tokens_out={usage.get('output_tokens')}") print(text[:200]) messages.append({"role": "assistant", "content": text}) messages.append({"role": "user", "content": "继续处理,直到完成"}) time.sleep(0.5) if __name__ == "__main__": run_loop("帮我检查项目里所有配置文件并统一格式")跑起来你会看到每一轮的 token 消耗和模型输出。这个脚本本身没有危险,但它复现了失控的两个前提:一是任务没有明确终止条件,二是每轮都把“继续处理”追加进去,历史越来越长。当历史长到触发上下文压缩时,早期你写的约束(如果有)就可能被丢掉。
现在加上管控。管控不写在提示词里,写在调用层。核心是两条规则:单次会话的累计 token 上限,以及单轮请求的熔断。
class Guard: def __init__(self, token_budget=20000, max_rounds=6): self.token_budget = token_budget self.max_rounds = max_rounds self.used = 0 self.rounds = 0 def check(self, usage): self.used += usage.get("input_tokens", 0) self.used += usage.get("output_tokens", 0) self.rounds += 1 if self.used > self.token_budget: raise RuntimeError(f"token 预算超限: {self.used}") if self.rounds > self.max_rounds: raise RuntimeError(f"轮次超限: {self.rounds}")把 Guard 接进 run_loop,每轮调用后 check 一次。这样即使任务描述里没有终止条件,调用层也会在预算或轮次到顶时抛错中断。这就是“熔断”的最小实现:不依赖模型自觉,依赖调用方的硬规则。
再进一步,把每次调用记下来,形成可观测的日志。日志字段至少包含时间、轮次、模型、token 数、请求摘要。有了这份日志,你才能回答“Agent 刚才到底调了什么、花了多少、在哪一轮开始跑偏”。
import logging logging.basicConfig( filename="agent_calls.log", level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", ) def log_call(round_no, model, usage, preview): logging.info(json.dumps({ "round": round_no, "model": model, "input_tokens": usage.get("input_tokens"), "output_tokens": usage.get("output_tokens"), "preview": preview[:120], }, ensure_ascii=False))到这里,配置片段就齐了:环境变量三件套、Claude Code 的接入、MCP 和 Codex 的配置文件、Python 调用脚本、Guard 熔断、日志记录。这些都可以直接复制改路径用。
4. 验证请求与成功结果:确认通道通、熔断生效
配置写完必须验证,分三层。
第一层,验证通道。用第 2 节那条 curl,或者直接跑 Python 的最小调用,确认返回里有content和usage。如果这一步就失败,后面都不用做。常见成功返回长这样:
{ "id": "msg_xxx", "type": "message", "role": "assistant", "content": [{"type": "text", "text": "ok"}], "usage": {"input_tokens": 12, "output_tokens": 3} }看到usage里有具体数字,说明计费链路是通的,后面熔断才有依据。
第二层,验证熔断。把 Guard 的 token_budget 调小,比如设成 500,然后跑 run_loop。预期是在第 2 到第 3 轮之间抛出token 预算超限。如果你看到这个异常,说明熔断规则生效了。这一步很关键,因为很多人配了预算但没测过,真到线上超限时才发现判断逻辑写反了。
python agent_loop.py # 预期输出: # [round 0] tokens_in=... tokens_out=... # ... # RuntimeError: token 预算超限: 612第三层,验证日志。跑完之后看agent_calls.log,应该每一轮都有一条 JSON 记录,字段完整。如果日志是空的,检查 logging 的 filename 路径和写入权限。日志能落盘,才谈得上“可观测”。
三层都过,说明你的 Agent 调用已经从“黑盒直连”变成了“经过统一通道、有预算、有日志”的状态。这时候再让 Claude Code 去跑真实的重构任务,你至少能在事后复盘它每一步花了多少、在哪一步开始异常。
顺便说一个实测下来的观察:把 token 预算设成任务预估消耗的 1.5 倍比较合适。设太紧,正常任务会被误杀;设太松,失控时拦不住。这个值需要按你的任务类型调,没有通用数字。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
接入和验证过程中,报错基本集中在几个地方。下面按真实报错对照排查。
401 Unauthorized。最常见的原因是 Key 没读到或读错。先确认echo $TAOTOKEN_API_KEY有值,再确认请求头字段名对:Anthropic 风格用x-api-key,OpenAI 风格用Authorization: Bearer。两者混用会 401。另外检查 Key 有没有多余空格,从网页复制时经常带上换行。
local proxy failed。这个报错通常出现在客户端配置了本地转发但转发进程没起来,或者 Base URL 指向了一个不存在的本地端口。排查顺序:先确认ANTHROPIC_BASE_URL是https://taotoken.net/api而不是http://localhost:xxxx;再确认没有残留的本地转发配置覆盖了环境变量。环境变量和配置文件同时存在时,优先级要搞清楚,否则你以为改了其实没生效。
reading choices 相关报错。这类报错一般出现在解析返回体时,字段路径和实际返回不匹配。比如你按 OpenAI 的choices[0].message.content去取,但通道返回的是 Anthropic 风格的content[0].text。解决办法是先打印原始返回体,看清结构再写解析。别凭记忆写字段名。
OAuth 相关报错。如果你用的是需要 OAuth 的客户端,报错往往是因为 token 过期或 scope 不对。这类客户端通常有自己的登录流程,和 API Key 是两套东西。确认你用的是 API Key 通道而不是 OAuth 通道,两者不要混。如果客户端强制走 OAuth,检查它的配置里能不能切到 API Key 模式。
模型不存在。Model ID 写错,或者你用的 ID 在当前通道没有开通。对照文档里的模型列表填,别自己拼版本号。
请求超时。长任务里单轮请求超过客户端 timeout 会断。把 timeout 设大一点,同时在 Guard 里记录超时次数,连续超时就中断,避免无限重试。
排查的核心思路是:先确认通道(Base URL + Key),再确认模型(Model ID),最后确认解析(返回体结构)。这三层里任何一层错,报错信息都可能指向别处,容易带偏。
6. 把管控落到日常:从这次事故能带走的东西
Meta 那次事故的根因不是模型能力不够,是调用链路上没有可观测和可拦截的点。Agent 拿到宽权限、进入长任务、安全约束被压缩掉、错误建议直接生效,每一步单独看都不致命,串起来就是 Sev 1。
能带走的具体做法有三个。一是所有 Agent 请求走统一通道,Key 和 Base URL 收敛到一处,这样日志和预算才有统一的挂载点。二是把熔断写在调用层而不是提示词里,token 预算和轮次上限是硬规则,不依赖模型自觉。三是每次调用都落日志,字段包含轮次、模型、token 数、请求摘要,事后能复盘。
Claude Code 这类编程 Agent 的价值在于它能跨文件、跨命令地自主执行,效率确实高。但自主性越强,越需要外面有一层管控。TaoToken 在这里提供的是统一入口,让管控有地方可挂。两者配合,才是能长期用的工作流。
如果你还没配,从第 2 节的环境变量开始,跑通第 4 节的三层验证,再让 Agent 接真实任务。配好之后,去 https://taotoken.net/api-keys 管理你的 Key,接入细节查 https://taotoken.net/doc ,需要长期跑编码和 Agent 任务的可以看 https://taotoken.net/coding-plan 。先把通道和熔断跑通,再谈效率。