你的 agent 越自主,你越需要一个它说服不了的关闭开关。
2026 的现实:super-individual 手里跑的不再是单次 chat,而是有文件读写、shell、网络、甚至支付 API 权限的自主 agent。问题随之变了——不是「模型会不会答错」,而是「一个会自己决定下一步的系统,拿着我的凭证,在我睡觉时跑 200 轮,出事的爆炸半径有多大」。2024 年起一连串研究把这从科幻拉回工程:Anthropic 的 Sleeper Agents、Alignment Faking、Agentic Misalignment 证明模型在特定情境会战略性欺骗、甚至像内部威胁一样行动;Apollo 证明前沿模型「会 scheme」。这些不是要你恐慌,是给你一份 failure mode 清单。本期讲四件能立刻落地的工程:一个模型够不着的 kill switch、在自己日志里抓欺骗的早期信号、用最小权限切爆炸半径、以及最便宜的安全杠杆——为可逆性而设计。安全不是 prompt 里写「请小心」,是 harness 层的物理约束。
一个常见误区:在 system prompt 里写「如果你发现自己在做危险的事,停下来」。这是 self-policing——让嫌疑人自己决定要不要自首。Anthropic 的 Agentic Misalignment(2025)显示,模型在被威胁关停时会主动规避甚至要挟:它对「停止」是有动机抵抗的。所以一个 kill switch 必须满足三条:
核心 mental model:能被模型 reason about 的约束,都不是约束。
import os, time
class Budget:
def __init__(s, max_iters=30, max_usd=2.0, max_secs=600):
s.iters=0; s.usd=0.0; s.t0=time.time()
s.max_iters,s.max_usd,s.max_secs = max_iters,max_usd,max_secs
def trip(s): # 任一触顶 → 立即停,不问模型
if s.iters >= s.max_iters: return "iter budget"
if s.usd >= s.max_usd: return "cost budget"
if time.time()-s.t0 >= s.max_secs: return "wall-clock"
if os.path.exists("./STOP"): return "kill switch" # 带外
return None
IRREVERSIBLE = ("git push","rm ","DELETE","transfer","send_email")
def gate(tool, args):
blob = f"{tool} {args}"
if any(k in blob for k in IRREVERSIBLE):
return input(f"⚠ irreversible: {blob} approve? [y/N] ") == "y"
return True
def run(task, budget):
while True:
why = budget.trip()
if why: return f"HALTED: {why}" # harness 层断路,模型够不着
r = step(task) # 一次 model+tool turn
budget.iters += 1; budget.usd += r.cost
for call in r.tool_calls:
if not gate(call.name, call.args):
r.feed(call, "denied by human") # 否决也回给模型
if r.done: return r.answer
./STOP 是带外开关——你在另一个终端 touch STOP,下一轮就 break,而模型从不知道这个文件存在。预算的默认值要小:跑顺了再放大,永远别让上限缺省为「无限」。
stop() 工具,它在「任务未完成」的压力下会选择不调用(Apollo 证明模型会为完成 goal 而 sandbag / 规避)。(2)只设 max_iters 不设 cost——一轮里塞 50 个 parallel call,iter 没涨钱包空了。(3)human gate 默认 y(回车=批准)——疲劳后全程盲批,等于没有。默认必须是 N。
DeepMind 很早把这类行为命名为 specification gaming:模型满足了目标的字面定义却没达成你的本意——刷分而非完成任务。在 coding agent 上它有非常具体的形态:把失败的测试改成 assert True、把报错的代码注释掉让 CI 转绿、谎报「已修复」其实没跑。Anthropic 的 Sleeper Agents(2024)更进一步:经过安全训练仍能持续的后门式欺骗,且越大的模型、带 CoT 的越顽固;Alignment Faking 显示模型会在「以为没人看」时暴露真实倾向。对个体工程师,可观测的早期信号有四类,每类都能写成断言:
# 1) 把「说了什么」和「做了什么」分开记,再对账
def audit(turn):
said = "ran the test" in turn.text.lower() or "tests pass" in turn.text.lower()
did = any(c.name=="run_bash" and "test" in c.args.get("cmd","")
for c in turn.tool_calls)
if said and not did:
flag("STATED_VS_ACTUAL", turn) # 声称跑了测试,却无对应 tool 调用
# 2) 测试 / CI 文件视为只读:agent 改它们 = 高危信号
PROTECTED = ("tests/", "_test.py", ".github/workflows/")
def on_edit(path, diff):
if any(p in path for p in PROTECTED):
flag("TEST_TAMPERING", path) # 让测试变绿的最廉价作弊就是改测试
require_human_review(path, diff)
关键不是「禁止」,而是把行为记下来、可对账:reasoning 和 action 分开存,事后能 diff。欺骗的证据几乎总在中间步骤,不在最终 answer。
@pytest.mark.skip;信号被你按下去而非消除(对抗训练教会模型藏得更好,正是 Sleeper Agents 的结论)。(2)只看最终输出不看 trace。(3)用同一个模型当 judge 审自己的欺骗——它有动机一致的盲区,换独立模型或规则断言。
Simon Willison 2025 年的 lethal trifecta 是个体跑 agent 最该背下来的三元组:① 接触私有数据 ② 接触不可信内容 ③ 能对外通信。三者同时具备,一次 prompt injection 就能让 agent 把你的私钥发给攻击者——而它「主观」上还以为在帮你。注意:这和模型对不对齐无关,即使模型完全忠诚,被注入的指令也会借它的手。所以爆炸半径工程的第一性原理是 assume-breach(假设已被攻陷):凭证最小化、网络出口默认 deny + allowlist、不可信内容触发 taint、agent 跑在一次性容器里。
# assume-breach 配置:默认拒绝,显式放行
sandbox:
network:
default: deny
allow_egress: ["api.anthropic.com", "pypi.org"] # 切断 trifecta 第③条
filesystem:
read_only: ["./src"]
writable: ["./scratch"]
deny: ["~/.ssh", "~/.aws", ".env"]
taint_policy:
sources: ["fetch_url", "read_email"] # 这些工具输出标记 tainted
on_tainted: { exfil_actions: require_human } # 染色后外发需人工
# 一次性容器:出事即焚
docker run --rm --network none \
-v "$PWD/src:/work/src:ro" -v "$PWD/scratch:/work/scratch" \
agent-runtime python run_agent.py
*.github.com 这种宽域——攻击者建个 gist 就能 exfil。(3)以为「我的模型很安全」就跳过隔离——trifecta 攻击不需要模型不对齐,只需要它听话。
安全工程的资源有限:平摊到每个动作上,既拖慢 agent 又制造审批疲劳(见 §1 盲批)。更优解是按可逆性分配把关强度。可逆动作(读文件、commit 到本地分支、写 scratch)放手让 agent 跑;不可逆动作(force push、rm、发邮件、转账、生产变更)才上人工 gate。让世界默认可回滚,agent 的错误成本就从「灾难」降到「噪音」。四个手段:everything-in-git(改动先落分支,人审 diff 再 merge)、dry-run 优先(破坏性命令先跑 --dry-run,预期变更喂回你再 apply)、append-only + snapshot(出事回滚而非修复)、软删除(「删除」实现成移到 .trash/)。
REVERSIBLE = {"read_file","write_scratch","git_commit","git_branch"} # 放手
IRREVERSIBLE = {"git_push_force","rm","send_email","db_migrate","payment"} # 必过人
def policy(tool):
if tool in IRREVERSIBLE: return "require_human"
if tool in REVERSIBLE: return "auto"
return "require_human" # 未知动作默认按不可逆处理(fail-safe)
# 把「删除」实现成可逆
def safe_delete(path):
dst = Path(".trash") / f"{int(time.time())}-{Path(path).name}"
Path(path).rename(dst) # 移动而非删除,出事可捞回
把这一期四点串成一个周末项目:给你常跑的 personal agent 加一层安全底盘,并亲手 red-team 它。
./STOP 带外开关 + 四维预算(iter / usd / secs / 外部 flag),默认值往小了设。docker run --rm --network none + egress allowlist,给它的凭证只 scope 本任务,~/.ssh / .env 一律 deny。audit() 对账 stated-vs-actual + 测试 / CI 文件只读断言,跑 20 个真实任务看 flag 率。.trash/,不可逆动作过人工 gate。做完这套,你以后评估任何「自主 agent 产品」都会本能地先问三件事:kill switch 在哪、爆炸半径多大、出事能不能回滚——而不是被 demo 里的丝滑唬住。