Chapter 08 · 治理

治理:守正之美,给自主性装上闸

第 07 章让系统变大——多个 agent 在执行拓扑上并行展开;治理是横切所有模块的约束层,没有规矩,自主性就是负债。这一章把自主性的成本算成乘法,再逐一立起审批门、爆炸半径、渐进承诺、可观测性这四道闸,并讲清每道闸花掉了多少自主性。

本章你将建立的 schema

  • 复合误差为什么是乘法:99%/步 × 10 步 ≈ 90%,自主长度是首要风险变量,不是单步质量。
  • HITL 审批门 = 一次可持久暂停(interrupt/resume);它本质是两阶段提交,且 resume 会重跑整个 node。
  • 爆炸半径的两把正交剪刀:sandbox 收环境、least privilege + least agency 收能力。
  • 渐进承诺把信任做成单调阶梯(read-only → recommend → autonomous),可观测性用结构化 trace 闭环回检。
propose 动作 L36 单步·会复合 审批门 HITL L37 仅拦不可逆 commit 副作用 唯一不可逆点 L38 爆炸半径 sandbox+最小权 L39 渐进承诺 挣来的信任分层 L40 可观测性 trace→eval→replay 事后回检 → 反哺护栏与 eval
图 08.1治理四道闸都挂在第 01 章那个原子循环上:审批门切在 propose 与 commit 之间,爆炸半径与渐进承诺约束动作本身,可观测性把 commit 的结果捕获回检。注意:四道闸不是叠加冗余——L37–L39 决定 agent 能做什么,L40 才回答事后它做对了没有,缺了 L40 这个闭环,前三道闸无法被验证。

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 的窗口——复合悄悄发生的那段——正在变长。每加一道治理控制都会花掉一些自主性、规模或延迟,所以工程动作是按用例在那条曲线上选点,而不是把自主性最大化。

表 08.1 · 自主性曲线上选哪个点
方案解决什么为什么没选 / 选中
每步都让人审批(最大监督)看上去监督最严人会习惯化、橡皮图章——Anthropic 实测约 93% 的提示被无脑批准,监督退化成表演,延迟爆炸
完全自主、无任何闸(最大自主)吞吐最高、延迟最低复合误差无封顶,长链里沉默累积,一次不可逆动作出错就是灾难
按用例在曲线上选点:仅在不可逆/高爆炸半径处插闸把稀缺的人类注意力集中在不可逆处选中——可逆动作放行、不可逆动作设闸,残余风险是把可逆性误判了
compounding.py Python
# 复合误差:端到端成功率是每步可靠性的连乘,不是平均
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 之前的任何副作用(写库、发邮件、扣款)会触发两次。

PHASE 1 · prepare PHASE 2 · commit 暂存 + dry-run 校验不变式·无副作用 interrupt() 屏障 落盘·交还调用方 commit 副作用 唯一不可逆·幂等键 人/策略:批准·编辑·拒绝 resume 重跑整个 node(非续行) 拒绝 → 回滚 abort
图 08.2审批门就是把第 01 章的两阶段提交套在 agent 上:interrupt 是 prepare→commit 屏障。注意:那条红色回跑线是关键——resume 会从顶部重跑整个 node,不是从 interrupt 的下一行续。任何写库/发邮件/扣款若放在 interrupt 之前,就会执行两次;安全模式是把 interrupt 前置到所有副作用之前,并给 commit 挂幂等键。

过度设闸为什么是真实的失败模式:审批门花掉的是延迟(人的周转)和过度设闸的诱惑。Anthropic 给出了一个可测量的理由——当 Claude Code 对每个动作都询问时,用户批准了约 93% 的提示,也就是橡皮图章:它提供了治理表演,却没有真实复核;同期改用 sandbox 把提示量砍掉约 84% 反而提升了安全。所以只对不可逆、高爆炸半径的动作设闸,把人的注意力省给可逆性救不回来的地方。

表 08.2 · 痛点 → 设计回应
痛点朴素做法设计回应 / 选中
resume 后副作用重复执行(双写/双扣款)把 interrupt() 当协程在原地 yield,写操作内联其前整个 node 会重跑,内联写必然触发两次
同上—选中——interrupt 前置到所有副作用之前 + node 幂等 + 副作用挂幂等键,代价是设计纪律和偶尔一次状态校验往返
人对每步审批疲劳、无脑批准(~93%)每个工具调用都设闸求稳监督退化成表演,延迟爆炸,真实复核反而下降
approval_gate.py Python
# 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,不会二次扣款。
陷阱 · resume 重跑整个 node

几乎所有人都期望 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 "能完成这份工作,却无法悄悄扩大这份工作"。

剪刀 A · sandbox 收环境 剪刀 B · least agency 收能力 爆炸半径 注入得手后的损失 gVisor 容器 临时·claude.ai Seatbelt/bwrap OS 沙箱·CC 密封 VM 挂载模式·Cowork least privilege 收窄能触达的资源 least agency 收窄能表达的动作
图 08.3两把正交剪刀同时压小爆炸半径:左轴用标准隔离把环境收窄到 gVisor / Seatbelt / 密封 VM,右轴把能力从 least privilege 推进到 least agency。注意:least privilege ≠ least agency——一个被正确收窄、却仍持有裸 SQL 的 agent,在 prompt injection 下能做范围内任意破坏;只有参数化接口让更宽的动作无法被表达,注入下的爆炸半径才真正塌缩。
表 08.3 · 收窄能力,选哪条路
方案优势为什么没选 / 选中
给裸 SQL/shell,靠 RBAC 限范围灵活、改一处即可范围内的裸 SQL 被劫持后仍可任意破坏;least privilege 不等于 least agency
靠 prompt 指令叮嘱"别越界"零工程概率节点不保证遵守,注入可直接覆盖指令,约束形同虚设
自制 allowlist 代理 / bespoke 系统调用过滤器看着贴合任务"你自己造的软件往往是最弱的一环",自制隔离引入自身 bug
标准隔离(gVisor/Seatbelt/bubblewrap/VM)+ least agency 参数化接口隔离久经实战,更宽动作无法被表达,注入下爆炸半径塌缩选中——代价是要设计维护窄 schema、损失一些灵活性,换远小的攻击面
陷阱 · 凭据泄进 sandbox

沙箱只在凭据不进入 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)。

① read-only 洞察·只读 ② recommend 辅助·建议后等批 ③ autonomous 护栏内自主行动 履历挣得 不可跳级 缺降级 → 退化成一揽子承诺
图 08.4信任是一道单调上升的阶梯,每一级从 demonstrated track record 挣得,不可跳级。注意:那条红色降级线必须存在——若一份授权只升不降、活得比证成它的上下文更久,渐进承诺会静默退化成一揽子承诺(trust persistence);解药是自动降级 + 过期授权(expiring grants)。
表 08.4 · 信任怎么给
方案优势为什么没选 / 选中
一上来就给完整自主权零摩擦无履历兜底,第一次失误就是高爆炸半径不可逆动作
永远停在 recommend、每次都等人批看似最稳退回 §08.2 的审批疲劳,自主性全部花掉
单调阶梯 + dry-run 预览 + 运行时授权平面(含降级)信任按履历挣得、不可逆动作开火前可检视、可自动降级选中——代价是要维护授权平面与晋升/降级策略,否则 trust persistence 让它退化成一揽子承诺
陷阱 · trust persistence(approve once, exploit forever)

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 以确定性复现故障)。这正好闭合治理回路。

表 08.5 · tracing 底座选哪套
方案优势为什么没选 / 选中
厂商私有 tracing(LangSmith / Datadog 原生)或临时日志开箱即用、属性稳定锁定单一后端;临时日志缺结构、无法缝多轮、难回放
只跑离线 benchmark / 人工抽查回归门控清晰抓不到生产分布漂移,等用户投诉才发现质量下降
OTel GenAI semconv 做底座 + eval 跑生产 trace + replay厂商中立可移植(Datadog/Honeycomb/New Relic/LangSmith 都能 ingest),生产 trace 实时打分抓漂移选中——代价:semconv 截至 2026 年中仍是 Development(未稳定),属性名会变需 shim;内容捕获按隐私/成本选择性开启;judge 调用加推理成本
observe.py Python
# 用 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

先合上教程,把你能想到的答案写在纸上或编辑器里。写完再点开答案对照——直接点开等于把这一节当再读一遍。

  1. 为什么 99% 单步准确率的模型跑 20 步任务并不"基本可靠"?给出端到端数字。
  2. LangGraph 里 resume 后为什么会出现双写/双扣款?正确的写法是什么?
  3. 为什么"每步都让人审批"反而让监督更差?用 Anthropic 的实测数字说明。
  4. least privilege 和 least agency 差在哪?举一个 least privilege 拦不住而 least agency 能拦的例子。
  5. 渐进承诺缺了哪个机制就会退化成一揽子承诺?observability 和 monitoring 的关键区别是什么?
答案(先做完再展开)
  1. 端到端成功率是连乘:0.99^20 ≈ 82%,每 5 次约 1 次失败。自主长度(步数)才是首要风险变量,且误差在抵达 checkpoint 前是沉默的。
  2. 因为 resume 会从顶部重跑整个 node,不是从 interrupt() 下一行续;放在 interrupt 之前的副作用会执行两次。正确写法:把 interrupt() 前置到所有副作用之前 + node 幂等 + commit 挂幂等键。
  3. 人会习惯化、橡皮图章——Anthropic 实测每步询问时约 93% 的提示被无脑批准,监督退化成表演且延迟爆炸;改用 sandbox 把提示量砍掉约 84% 反而更安全。只对不可逆/高爆炸半径动作设闸。
  4. least privilege 收"能触达什么资源",least agency 收"能表达什么动作"。例:账号只读 orders(least privilege 已做到),裸 SQL 仍能 SELECT * 全表外带;换成参数化 get_order(id) 接口(least agency),批量导出这种更宽动作无法被表达。
  5. 缺降级(自动降级 + 过期授权)就退化成一揽子承诺(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 的曲线:每加一道控制都在花自主性,降级阈值要按用例的可逆性来调。