Chapter 08 · 治理
治理:守正之美,给自主性装上闸
第 07 章让系统变大——多个 agent 在执行拓扑上并行展开;治理是横切所有模块的约束层,没有规矩,自主性就是负债。这一章把自主性的成本算成乘法,再逐一立起审批门、爆炸半径、渐进承诺、可观测性这四道闸,并讲清每道闸花掉了多少自主性。
本章你将建立的 schema
- 复合误差为什么是乘法:99%/步 × 10 步 ≈ 90%,自主长度是首要风险变量,不是单步质量。
- HITL 审批门 = 一次可持久暂停(interrupt/resume);它本质是两阶段提交,且 resume 会重跑整个 node。
- 爆炸半径的两把正交剪刀:sandbox 收环境、least privilege + least agency 收能力。
- 渐进承诺把信任做成单调阶梯(read-only → recommend → autonomous),可观测性用结构化 trace 闭环回检。
08.1治理导论:自主性的成本是乘法
端到端成功率 = 每步可靠性的连乘,不是相加;治理就是把 checkpoint 重新插回这条乘法链,给误差封顶。
面对"agent 单步准确率有 99%,够可靠了吧",最自然的反应是把自主链放长、让它一口气多跑几步。这个反应抓错了风险变量。单步质量高不代表长链可靠——可靠性是连乘的,链越长,沉默累积的误差越多,而第 01 章那个概率节点每一步都在往链里注入一点不确定。治理不是给 agent 加负担,而是给这条乘法链装上封顶阀门。
底层机制(比"模型会犯错"深一层):Anthropic 明说"agentic 系统常用延迟和成本换更好的任务表现""agent 的自主性意味着更高成本,以及复合误差的风险"。把它算成数字:99% 准确率的步骤连跑 10 步,端到端是 0.99^10 ≈ 90%;20 步 ≈ 0.99^20 ≈ 82%;一条 95%/步的 5 步链只剩 0.95^5 ≈ 77%。两股力让它随时间更糟。其一,误差在抵达下一个 checkpoint 前是沉默的。其二,Anthropic 实测无人打断的单轮时长在一个季度内几乎翻倍(约 25→45 分钟,2025-10 至 2026-01),未设 checkpoint 的窗口——复合悄悄发生的那段——正在变长。每加一道治理控制都会花掉一些自主性、规模或延迟,所以工程动作是按用例在那条曲线上选点,而不是把自主性最大化。
| 方案 | 解决什么 | 为什么没选 / 选中 |
|---|---|---|
| 每步都让人审批(最大监督) | 看上去监督最严 | 人会习惯化、橡皮图章——Anthropic 实测约 93% 的提示被无脑批准,监督退化成表演,延迟爆炸 |
| 完全自主、无任何闸(最大自主) | 吞吐最高、延迟最低 | 复合误差无封顶,长链里沉默累积,一次不可逆动作出错就是灾难 |
| 按用例在曲线上选点:仅在不可逆/高爆炸半径处插闸 | 把稀缺的人类注意力集中在不可逆处 | 选中——可逆动作放行、不可逆动作设闸,残余风险是把可逆性误判了 |
# 复合误差:端到端成功率是每步可靠性的连乘,不是平均
def end_to_end_success(per_step_reliability, n_steps):
return per_step_reliability ** n_steps # 乘法,不是加法
assert round(end_to_end_success(0.99, 10), 2) == 0.90 # 99%/步 × 10 步 ≈ 90%
assert round(end_to_end_success(0.99, 20), 2) == 0.82 # 20 步 ≈ 82%
assert round(end_to_end_success(0.95, 5), 2) == 0.77 # 95%/步 × 5 步 ≈ 77%
# 推论:自主长度是首要风险变量,不是单步质量。
# 治理 = 在链上插 checkpoint,给「沉默累积的误差」封顶——
# checkpoint 越靠后,能在它之前积累的 error 越多(无人打断的单轮 ~25→45 min)。
团队拿到一个单步准确率 99% 的模型,认定它"基本可靠",于是放手让它跑一条 20 步的自主工作流,不插任何 checkpoint。上线后失败率远高于预期。问题出在哪?
展开答案(先停 10 秒再点)
把单步准确率当成端到端可靠性,是复合误差盲区。0.99^20 ≈ 82%——意味着每 5 次任务就有近 1 次会在某一步出错,而这些错误在抵达 checkpoint 前是沉默的。多数工程师默认"99% 准确 = 基本可靠",但自主长度(步数)才是主导风险变量,不是单步质量。链越长,错误在被人看见前积累得越多——尤其当无人打断的单轮时长还在变长。修法不是换更准的模型,而是缩短无 checkpoint 的窗口:在不可逆动作前插审批门。
08.2审批门:可持久暂停 = 两阶段提交
审批门是一次可持久暂停:agent 在不可逆动作前 interrupt,把全量状态落盘交还调用方,人/策略批准后 resume 注入决策、继续同一次运行。
§08.1 算清了自主链需要 checkpoint。审批门就是那个 checkpoint 的工程实现——在敏感、不可逆的动作前停住,让一个人(或一条策略)批准/编辑/拒绝。Anthropic 的原话是"在 agent 执行不可逆动作之前,建立让它暂停接受人工复核的 checkpoint"。难点不在停,而在怎么停得安全。
底层机制(比"暂停一下"深一层):框架命中 interrupt()(LangGraph)或把待执行的工具调用浮现成一个 interruption(OpenAI Agents SDK),通过 checkpointer 把全量 agent 状态序列化、交还调用方;这个暂停是持久的——进程重启都活得下来,因为状态已落盘。人/策略批准后,Command(resume=...) 或 state.approve() 把决策注入、继续同一次运行。这正是两阶段提交(2PC):把 agent 当事务协调者而非 oracle——Phase 1(prepare)暂存一个可逆改动并校验不变式,人工 interrupt 就是 prepare→commit 的屏障,Phase 2 提交或回滚,全程带审计轨迹。低一层的陷阱:resume 时 LangGraph 会从顶部重跑整个 node,不是从 interrupt() 的下一行续。所以放在 interrupt 之前的任何副作用(写库、发邮件、扣款)会触发两次。
过度设闸为什么是真实的失败模式:审批门花掉的是延迟(人的周转)和过度设闸的诱惑。Anthropic 给出了一个可测量的理由——当 Claude Code 对每个动作都询问时,用户批准了约 93% 的提示,也就是橡皮图章:它提供了治理表演,却没有真实复核;同期改用 sandbox 把提示量砍掉约 84% 反而提升了安全。所以只对不可逆、高爆炸半径的动作设闸,把人的注意力省给可逆性救不回来的地方。
| 痛点 | 朴素做法 | 设计回应 / 选中 |
|---|---|---|
| resume 后副作用重复执行(双写/双扣款) | 把 interrupt() 当协程在原地 yield,写操作内联其前 | 整个 node 会重跑,内联写必然触发两次 |
| 同上 | — | 选中——interrupt 前置到所有副作用之前 + node 幂等 + 副作用挂幂等键,代价是设计纪律和偶尔一次状态校验往返 |
| 人对每步审批疲劳、无脑批准(~93%) | 每个工具调用都设闸求稳 | 监督退化成表演,延迟爆炸,真实复核反而下降 |
# LangGraph 风格审批门 = 两阶段提交。resume 会重跑整个 node,
# 所以 interrupt() 之前不能有任何副作用。
from langgraph.types import interrupt, Command
def sensitive_node(state):
# Phase 1 prepare:只暂存 + 校验,绝不写真实副作用
plan = stage_change(state.action) # 可逆暂存
preview = plan.dry_run() # 预演 diff,无副作用
# ---- 屏障:interrupt() 落盘全量状态,交还调用方 ----
# 此行之前不得有 DB 写 / 发邮件 / 扣款——node 会从顶部重跑。
decision = interrupt({"action": state.action,
"preview": preview,
"blast_radius": estimate_blast_radius(plan)})
if decision["reject"]:
return {"status": "aborted"} # 回滚:暂存从未提交
# Phase 2 commit:唯一不可逆动作,挂幂等键防重放双写
result = plan.commit(idempotency_key=state.run_id) # >= 一次安全
return {"status": "committed", "result": result}
# resume: app -> graph.invoke(Command(resume={"reject": False}), thread)
# → sensitive_node 整体重跑;stage/dry_run 再做一遍(无副作用),
# commit 因幂等键命中而 no-op,不会二次扣款。
几乎所有人都期望 interrupt()/resume 像协程一样在原地 yield、从下一行续——LangGraph 实际是重放整个 node(见 issue #6208)。朴素代码因此双重执行副作用。三条修法:把 interrupt() 前置到所有副作用之前;让 node 幂等;或把副作用包进带幂等键的 task。这是 durable replay 下唯一安全的写法。
08.3爆炸半径:sandbox 收环境,least agency 收能力
爆炸半径 = 一次坏动作能触达的最大破坏,由 agent 能碰到的一切的并集决定;两把正交的剪刀分别收窄环境与能力。
审批门是预防——在动作发生前拦下。但 prompt injection 一旦得手,预防已经失效,这时决定损失大小的是动作能触达多少。Sophos/Anthropic 的框架是"当 prompt injection 成功时,爆炸半径由 agent 能触达的东西决定"。降低爆炸半径,是 assume-breach(假设已被攻破)对预防的补全。
底层机制(比"加沙箱"深一层):两把正交的剪刀。其一,sandbox 收环境——Anthropic 的三层隔离从轻到重:claude.ai 用临时的 gVisor 容器,Claude Code 用 OS 系统调用沙箱(Seatbelt / bubblewrap),Cowork 用密封 VM 带 read-only / read-write / read-write-no-delete 三种挂载模式,凭据留在宿主 keychain、绝不进入 guest。Anthropic 用血换来的教训是"你自己造的软件往往是最弱的一环"——标准 hypervisor / 系统调用过滤器胜过自制的 allowlist 代理。其二,least privilege + least agency 收能力——把每个工具收窄到任务所需的确切资源,默认只读,签发短时、按任务收窄的凭据。least agency 比 least privilege 更进一步:不只限制能触达什么资源,而是限制能表达什么动作——暴露一个参数化查询接口而非裸 SQL、一个固定 schema 的 MCP 工具而非 shell,让 agent "能完成这份工作,却无法悄悄扩大这份工作"。
| 方案 | 优势 | 为什么没选 / 选中 |
|---|---|---|
| 给裸 SQL/shell,靠 RBAC 限范围 | 灵活、改一处即可 | 范围内的裸 SQL 被劫持后仍可任意破坏;least privilege 不等于 least agency |
| 靠 prompt 指令叮嘱"别越界" | 零工程 | 概率节点不保证遵守,注入可直接覆盖指令,约束形同虚设 |
| 自制 allowlist 代理 / bespoke 系统调用过滤器 | 看着贴合任务 | "你自己造的软件往往是最弱的一环",自制隔离引入自身 bug |
| 标准隔离(gVisor/Seatbelt/bubblewrap/VM)+ least agency 参数化接口 | 隔离久经实战,更宽动作无法被表达,注入下爆炸半径塌缩 | 选中——代价是要设计维护窄 schema、损失一些灵活性,换远小的攻击面 |
沙箱只在凭据不进入 guest 时才真正隔离。一旦把 secret 注入 guest,沙箱就不再能遏制凭据窃取——一次注入就能把它读走外带。Cowork 的做法是凭据全程留在宿主 keychain,guest 只拿到被代理转发的、已收窄的访问。同理,自制容器是反模式:标准 hypervisor / 系统调用过滤器久经实战,自制 allowlist 代理往往是最弱的一环。
一个 agent 已经做到 least privilege:它的数据库账号只读、只能访问 orders 这一张表。团队认为爆炸半径已经够小。一次 prompt injection 让它"导出全部订单到外部 URL"——它照做了。最小权限为什么没拦住?
展开答案(先停 10 秒再点)
因为最小权限收的是能触达什么资源,没收能表达什么动作。账号确实被正确收窄到只读 orders,但"读出整张表的全部行"恰恰在这个范围之内——裸 SQL/裸查询让 agent 能表达"SELECT * FROM orders 再外带"这种合法但灾难的动作。least privilege ≠ least agency。修法是 least agency:暴露一个参数化接口(如 get_order(order_id) 固定 schema),让"批量导出"这种更宽动作根本无法被表达,而不只是不被授权。多数威胁模型漏掉的正是这一层。
08.4渐进承诺:把信任做成单调阶梯
用一道单调阶梯替换二元信任决策:read-only(洞察)→ recommend-and-wait(辅助)→ act-within-guardrails(自主),信任按履历逐级挣得,不可跳级。
"这个工具/路径能不能放手让 agent 用"不该是一次性的是非题。一次性授权要么过紧(什么都要人批,回到 §08.2 的疲劳),要么过松(一上来全开,回到 §08.1 的无封顶)。渐进承诺把这个二元决策摊成一道单调阶梯,信任从履历里逐级挣得。
底层机制(比"分级授权"深一层):三个机制叠起来。其一,read-then-write 默认——先观察读行为是否可预测,再加写权限。其二,dry-run / preview-then-commit——算出动作的 diff/plan 却不施加(借自 terraform plan/apply、rsync --dry-run),人/检查批准预览后,再把同一份 plan 提交,让不可逆操作在开火前可被检视。其三,挣来的晋升——一个运行时授权平面(ABAC/RBAC + 审批阈值)输出 ALLOW / MASK / DENY / REQUIRE_APPROVAL,信任层级从运行历史里晋升,而非一上来就授予。低一层的失败模式:信任持久化(trust persistence)——一个工具/路径一旦被批准,这份授权常常活得比当初证成它的上下文更久,而一个跑过 100 次成功的 agent 又常被当作和全新 agent 完全一样处理。没有降级机制,渐进承诺就悄悄退化成一揽子承诺(blanket commitment)。
| 方案 | 优势 | 为什么没选 / 选中 |
|---|---|---|
| 一上来就给完整自主权 | 零摩擦 | 无履历兜底,第一次失误就是高爆炸半径不可逆动作 |
| 永远停在 recommend、每次都等人批 | 看似最稳 | 退回 §08.2 的审批疲劳,自主性全部花掉 |
| 单调阶梯 + dry-run 预览 + 运行时授权平面(含降级) | 信任按履历挣得、不可逆动作开火前可检视、可自动降级 | 选中——代价是要维护授权平面与晋升/降级策略,否则 trust persistence 让它退化成一揽子承诺 |
Mindgard 2026 指出:在编码 agent 里,一次批准的工具/路径/scope 常常活得比证成它的上下文更久,且没有降级——"approve once, exploit forever"。渐进承诺若只有晋升、没有降级,就静默变成一揽子承诺。两个解药:给每份授权挂过期时间(expiring grants),以及让信任层级随运行历史自动降级,而不是只升不降。
08.5可观测性:拒绝黑盒,让运行可回放
可观测性把非确定的黑盒变成可审计、可回放的工件:每次 LLM 调用、工具调用、推理步都落成结构化 span,事后能重放复现。
§08.2–§08.4 决定 agent 能做什么。但概率节点的输出不可复现,光有护栏不够——还要能在事后回答"它到底做了什么、为什么这么做"。可观测性就是这个事后验证层,它把 L37–L40 闭成一个环。它和 monitoring(盯已知指标)不同:monitoring 答不了"为什么这次 agent 这么干"这种未知故障,那需要可回放的逐步 trace。
底层机制(比"加日志"深一层):底座是结构化 tracing——每次 LLM 调用、工具调用、推理步都是一个 span。OpenTelemetry GenAI semantic conventions 把 span 的形状标准化、让 trace 厂商中立:gen_ai.operation.name(取值含 chat / invoke_agent / execute_tool / create_agent / embeddings)、gen_ai.agent.name、gen_ai.tool.name、用 gen_ai.conversation.id 把多轮会话缝起来、gen_ai.usage.input_tokens/output_tokens 算成本,以及选择性开启的内容捕获(gen_ai.input.messages / gen_ai.output.messages)——因为 prompt/输出含 PII 且存储昂贵,所以默认关闭。Span 名形如 invoke_agent {name}、execute_tool {name}。trace 之上坐着 eval(用 LLM-as-judge / code / 多轮评估器给真实生产 trace打分,而不只是离线测试集)和 replay(重跑一条捕获的 trace 以确定性复现故障)。这正好闭合治理回路。
| 方案 | 优势 | 为什么没选 / 选中 |
|---|---|---|
| 厂商私有 tracing(LangSmith / Datadog 原生)或临时日志 | 开箱即用、属性稳定 | 锁定单一后端;临时日志缺结构、无法缝多轮、难回放 |
| 只跑离线 benchmark / 人工抽查 | 回归门控清晰 | 抓不到生产分布漂移,等用户投诉才发现质量下降 |
| OTel GenAI semconv 做底座 + eval 跑生产 trace + replay | 厂商中立可移植(Datadog/Honeycomb/New Relic/LangSmith 都能 ingest),生产 trace 实时打分抓漂移 | 选中——代价:semconv 截至 2026 年中仍是 Development(未稳定),属性名会变需 shim;内容捕获按隐私/成本选择性开启;judge 调用加推理成本 |
# 用 OTel GenAI semconv 把每步落成 span;trace 之上跑 eval + replay
with tracer.start_span("invoke_agent", attrs={
"gen_ai.operation.name": "invoke_agent",
"gen_ai.agent.name": agent.name,
"gen_ai.conversation.id": session_id}): # 缝多轮会话
action = agent.propose() # L36:单步,误差跨步连乘
with tracer.start_span("execute_tool", attrs={
"gen_ai.operation.name": "execute_tool",
"gen_ai.tool.name": tool.name,
"gen_ai.usage.input_tokens": in_tok, # 算成本
"gen_ai.usage.output_tokens": out_tok}):
result = tool.commit(action.args) # 仅在审批后(L37)
# 内容捕获默认关闭:含 PII + 存储贵,按需 opt-in 并做脱敏/采样
if CAPTURE_CONTENT:
span.set_attribute("gen_ai.input.messages", redact(messages))
span.set_attribute("gen_ai.output.messages", redact(result))
emit_eval(trace=current_trace(), judge=llm_as_judge) # L40:给真实生产 trace 打分
# replay:re-run 这条 trace 即可确定性复现故障,反哺 L38/L39 的护栏与 eval。
团队声称"日志和 dashboard 都齐了,可观测性已经做好了"。一个 agent 出现了一次从未见过的诡异行为,他们盯着 dashboard 却查不出为什么。哪里出了问题?
展开答案(先停 10 秒再点)
把 observability 当成了 monitoring。Dashboard 盯的是已知指标(延迟、错误率、token 量),它答不了"为什么这次 agent 这么干"这种未知故障——那需要可回放的逐步结构化 trace,把每次 LLM 调用、工具调用、推理步重建出来。更微妙的是:OTel 的内容捕获(gen_ai.input/output.messages)因 PII 和存储成本默认关闭,所以信号最丰富的那层数据往往一开始就是 off 的——出事时才发现没存。修法是在生产里就开结构化 tracing(必要时按需开内容捕获并脱敏),让故障可被 replay 复现,而不是只堆 dashboard。
§本章 self-check
先合上教程,把你能想到的答案写在纸上或编辑器里。写完再点开答案对照——直接点开等于把这一节当再读一遍。
- 为什么 99% 单步准确率的模型跑 20 步任务并不"基本可靠"?给出端到端数字。
- LangGraph 里 resume 后为什么会出现双写/双扣款?正确的写法是什么?
- 为什么"每步都让人审批"反而让监督更差?用 Anthropic 的实测数字说明。
- least privilege 和 least agency 差在哪?举一个 least privilege 拦不住而 least agency 能拦的例子。
- 渐进承诺缺了哪个机制就会退化成一揽子承诺?observability 和 monitoring 的关键区别是什么?
答案(先做完再展开)
- 端到端成功率是连乘:0.99^20 ≈ 82%,每 5 次约 1 次失败。自主长度(步数)才是首要风险变量,且误差在抵达 checkpoint 前是沉默的。
- 因为 resume 会从顶部重跑整个 node,不是从
interrupt()下一行续;放在 interrupt 之前的副作用会执行两次。正确写法:把interrupt()前置到所有副作用之前 + node 幂等 + commit 挂幂等键。 - 人会习惯化、橡皮图章——Anthropic 实测每步询问时约 93% 的提示被无脑批准,监督退化成表演且延迟爆炸;改用 sandbox 把提示量砍掉约 84% 反而更安全。只对不可逆/高爆炸半径动作设闸。
- least privilege 收"能触达什么资源",least agency 收"能表达什么动作"。例:账号只读
orders(least privilege 已做到),裸 SQL 仍能SELECT *全表外带;换成参数化get_order(id)接口(least agency),批量导出这种更宽动作无法被表达。 - 缺降级(自动降级 + 过期授权)就退化成一揽子承诺(trust persistence)。observability 用可回放的逐步 trace 调试未知故障,monitoring 只盯已知指标,答不了"为什么这次这么干"。
设计一个会"自动降级"的渐进承诺策略
§08.4 给了 trust persistence 这个失败模式:授权只升不降,活得比证成它的上下文更久,渐进承诺静默退化成一揽子承诺。设计一条策略,让 agent 在表现良好时晋升、在出现可疑信号时自动降级。说清你的策略在哪一层介入(运行时授权平面 / dry-run / trace),降级的触发信号从哪来(提示:呼应 §08.5 的 eval),以及代价是什么。
提示(卡住再展开)
关键在运行时授权平面和可观测性两层的耦合。可考虑:给每份授权挂 TTL(expiring grants),到期不自动续、要重新挣得;把 §08.5 的生产 trace 喂给 eval(LLM-as-judge / code 评估器),当某个工具/路径的 eval 分数掉破阈值时,授权平面把该 agent 在该 scope 上的层级从 autonomous 降回 recommend(输出从 ALLOW 变 REQUIRE_APPROVAL)。代价是额外的 judge 推理成本与误降级的摩擦——judge 本身会 flaky,把好 agent 错降会重新引入审批疲劳。这正好呼应 §08.1 的曲线:每加一道控制都在花自主性,降级阈值要按用例的可逆性来调。