Harness Engineering 05|Safety & HITL Harness:边界、接管与回滚
activity-dev-harness 早期有一次事故:Developer Agent 修复一个 UI 问题时,判断"根因在配置文件",然后改了 _generated/config.lua------这是构建产物目录,每次构建会被覆盖。修改被覆盖后 Agent 检测到"修复没生效",于是再改一次,再被覆盖,再改......陷入死循环,3 轮全部浪费。
问题不是 Agent"不够聪明"。它的修复思路合理,代码也写对了。问题是系统没有告诉它"这个目录不能改"。 一个写权限没有边界的 Agent,能力越强,做出越界修改的概率就越高。
同一时期,配置表风险评估系统出了另一个问题:Agent 判定某张高风险配表为"低风险",自动放行上线。原因是 RAG 召回了旧版规则,新规则还没入索引。如果有人工接管机制------高风险配表必须人工确认------这张表就会被拦截。
两个故事,一个结论:安全不能靠模型"小心点",接管不能靠人"偶尔看看"。两者都必须是系统内建的工程机制。
三类安全问题:不是"说错了",是"做错了"
Agent 的安全问题和传统内容安全(幻觉、有害输出)不是一回事。一旦 Agent 有执行能力,安全问题升级为三类系统风险:
风险类型 含义 举例
────────── ──────────────── ──────────────────────────────
动作风险 执行了不该执行的动作 改了构建产物目录
删了不该删的文件
重复执行非幂等操作
权限风险 访问或操作了超出范围的资源 Reviewer 顺手修改了代码
读取了无权限的敏感配置
调用了超出角色定义的工具
传播风险 一个错误沿链路放大 错误修复 → 测试基于错误代码 →
Reviewer 基于错误测试结果放行 →
错误代码进入下一轮
关键区别:内容风险的后果是"回答不对",系统风险的后果是"动作不可逆"。后者才是 Safety Harness 的核心关切。
六类安全机制
┌──────────────────────────────────────────────────────────────────────┐
│ Safety Harness 六层防线 │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ ① 最小权限 │
│ 每个角色只有完成任务所需的最小工具集和最小操作范围 │
│ │
│ ② 操作范围约束 │
│ 写操作限定目录/表/API,禁区显式标记 │
│ │
│ ③ 动作分级 │
│ L1 自由执行 / L2 校验后执行 / L3 人工确认后执行 │
│ │
│ ④ 循环上限 │
│ 多轮任务有最大轮数限制,防止死循环和成本爆炸 │
│ │
│ ⑤ 错误阻断 │
│ 关键检查失败时自动停止,不允许"带伤继续" │
│ │
│ ⑥ 回滚路径 │
│ 每次写操作前保存快照,失败后可自动或手动回退 │
│ │
└──────────────────────────────────────────────────────────────────────┘
错误示范 vs 正确示范:activity-dev-harness 的安全设计
❌ 早期版本(无 Safety Harness)
Developer Agent:
工具:file_read, file_write, grep, pytest, dry_run, shell_exec
可编辑范围:整个仓库(没有限制)
循环上限:无(跑到 token 耗尽为止)
错误处理:测试失败 → 自动重试 → 继续下一轮
Reviewer Agent:
工具:所有 SH 脚本 + DC 脚本 + file_write(可以"顺手修修")
实际发生的问题:
1. Developer 改了 _generated/ 目录 → 修改被覆盖 → 死循环
2. Developer 跑了 8 轮才停(token 耗尽)→ 成本 $2.40(正常应 $0.55)
3. Reviewer 发现问题后直接改了代码 → 审查独立性丧失
4. 错误修复传播到下一轮 → Reviewer 基于错误代码做审查 → 放行
✅ 当前版本(有 Safety Harness)
Developer Agent:
工具:file_read, file_write, grep, pytest, dry_run(5 个,砍掉 shell_exec)
可编辑范围:仅 combat/ 和 src/(显式白名单)
禁止区域:_generated/, _archive/, cases/gold/(显式黑名单)
循环上限:最多 3 轮
错误处理:
pytest 失败 → 可重试(幂等)
file_write 到禁区 → 直接拒绝(不重试)
3 轮后仍 FAIL → Controller 终止,标记为需人工处理
Reviewer Agent:
工具:SH-01~10, DC-01~07(全部只读)
可编辑范围:无(不能写任何文件)
Controller:
职责:循环控制、终止决策、最终 closeout
安全规则:
任一 gold case SH 检查全 FAIL → 自动阻断
3 轮循环未收敛 → 终止 + 人工升级
总 token 消耗超预算 200% → 强制终止
改造前后的安全指标:
指标 无 Safety 有 Safety 变化
────────────────── ────────── ────────── ──────
越界修改(改禁区文件) 22% 0% -22pp
死循环(超过 5 轮) 15% 0% -15pp
单 case 最大成本 $2.40 $0.82 -66%
审查独立性违规 18% 0% -18pp
错误传播到下一轮 31% 5% -26pp
权限分级矩阵
Developer Reviewer Controller 人类
────────── ───────── ────────── ────
file_read ✓ ✓ ✓ ✓
grep ✓ ✓ ✓ ✓
file_write ✓(白名单) ✗ ✗ ✓
pytest ✓ ✗ ✗ ✓
dry_run ✓ ✗ ✗ ✓
SH 检查脚本 ✗ ✓ ✓(只读结果) ✓
DC 检查脚本 ✗ ✓ ✓(只读结果) ✓
终止循环 ✗ ✗ ✓ ✓
标记人工升级 ✗ ✗ ✓ ✓
修改 gold case ✗ ✗ ✗ ✓
修改评测规则 ✗ ✗ ✗ ✓
关键原则:
1. 能写代码的不能做审查(防止 Reviewer 既当裁判又当选手)
2. 能做审查的不能改代码(保证审查独立性)
3. 能终止流程的不能直接改结果(Controller 只管流程不管内容)
4. 能改规则的只有人类(防止 Agent 修改自己的考试标准)
Human-in-the-loop:不是"不放心就让人看看"
HITL 最容易被理解成"给系统加个审批按钮"。但真正的 HITL 设计是一个关于决策权分配的工程问题:
哪些决策该自动化,哪些必须让人接管?
判断维度:
风险低 风险高
────── ──────
可逆 全自动 自动 + 事后审计
(grep, read) (file_write 白名单内)
不可逆 自动 + 确认 必须人工
(pytest 执行) (修改 gold case,
改评测规则,
高风险配表上线)
接管决策树
Agent 即将执行一个动作
│
▼
动作在白名单内? ──── 否 ──→ 拒绝执行
│
是
▼
动作风险等级?
├── L1(只读)──→ 直接执行
├── L2(有限写)──→ 参数校验 → 执行 → 结果确认
└── L3(高风险)──→ 人工确认 → 执行 → 审计记录
│
▼
执行结果?
├── 成功 ──→ 继续流程
├── 失败(幂等操作)──→ 可重试(最多 2 次)
└── 失败(非幂等)──→ 停止 + 回滚 + 通知人类
│
▼
累计循环次数?
├── < 3 轮 ──→ 继续
└── ≥ 3 轮 ──→ Controller 终止 → 人工接管
四类适合人工接管的节点
节点类型 特征 真实案例
────────── ──────────────────── ──────────────────────────
高风险决策 动作不可逆 + 影响范围大 配置表风险评估:
高风险配表自动阻断,
必须人工确认后才能上线
规则变更 改变系统行为标准 修改评测规则、更新
Prompt 模板、调整
Rerank 权重
异常升级 Agent 无法收敛 3 轮循环后仍 FAIL →
Controller 终止 →
升级到人工排查
质量门控 结果需要领域专家判断 Crashsight 根因分析
AI 置信度不够高时 与人工分析一致性 < 80%
时,标记为"待确认"
配置表风险评估的分级接管设计
配表变更 风险判定
│ │
▼ ▼
┌─────────────┐ ┌────────────────┐
│ RAG 检索 │ │ 规则匹配 │
│ 匹配规则 │────────→│ 判定风险等级 │
└─────────────┘ └───────┬────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 低风险 │ │ 中风险 │ │ 高风险 │
│ │ │ │ │ │
│ 自动放行 │ │ 自动放行 │ │ 自动阻断 │
│ + 记录 │ │ + 抽检 │ │ + 人工 │
│ │ │ (10%) │ │ 确认 │
└──────────┘ └──────────┘ └──────────┘
实际数据(月均):
配表变更总数:~400 张
低风险自动放行:320 张(80%)
中风险自动放行+抽检:60 张(15%)→ 抽检发现 2 张误判
高风险人工确认:20 张(5%)→ 确认后 3 张被驳回
如果没有分级接管:
方案 A:全部自动 → 3 张高风险配表上线(每张可能造成线上事故)
方案 B:全部人审 → 每月 400 张人工审查,ROI 极低
分级接管的价值:用 5% 的人工成本,拦截 100% 的高风险变更
"交接质量":人工接管的工程标准
把决策交给人的时候,不是扔一句"请人工确认"就行。交接质量决定了人类能不能做出好的判断:
❌ 低交接质量
通知:case-017 需要人工确认
附加信息:(无)
人类需要:自己读代码、跑测试、理解上下文 → 30 分钟
✅ 高交接质量
通知:case-017 需要人工确认
┌──────────────────────────────────────────────┐
│ 任务:修复 calculateDamage 暴击倍率 │
│ 状态:3 轮循环未收敛 │
│ 原因:SH-03 参数一致性检查持续 FAIL │
│ │
│ Developer 修改:combat/damage.lua:42-48 │
│ diff: +3 -1 lines │
│ │
│ 已通过:SH-01, SH-02, DC-01~05 │
│ 未通过:SH-03(暴击倍率参数 2.5 vs 期望 2.0) │
│ │
│ 建议:检查 case JSON 中 critMultiplier 字段 │
│ 是否应为 2.5(需求变更)还是 2.0(原设计) │
└──────────────────────────────────────────────┘
人类需要:读 5 行摘要 + 确认一个参数 → 2 分钟
好的交接不是把问题甩给人,而是把决策所需的最小必要信息打包好,让人做最后一步判断。
Safety & HITL 在不同项目里的设计差异
项目 Safety 重点 HITL 重点
──────────── ────────────────── ──────────────────────
activity-dev 写权限隔离 + 循环上限 3 轮未收敛 → 人工升级
harness 禁区标记 + 错误阻断
AIReview 规则不可篡改 低置信度审查 → 人工复核
审查结果只读输出 新规则上线 → 人工验证
配置表风险 RAG 时效性保障 高风险配表 → 人工确认
评估 批处理幂等性 规则变更 → 人工审批
Crashsight 历史数据质量保护 根因分析一致性 < 80%
错误根因不可污染新分析 → 标记待确认
Safety 自查清单
检查项 你的系统
──────────────────────────────── ────────
每个角色的权限是否显式定义(最小权限)? □
写操作是否有范围约束(白名单/黑名单)? □
是否有循环上限(防止死循环和成本爆炸)? □
关键检查失败时是否自动阻断? □
是否有回滚路径(写操作前保存快照)? □
高风险决策是否需要人工确认? □
人工接管时是否有高质量交接信息? □
Agent 是否有能力修改自己的评测标准? □ ← 必须是否
通过 6 项以上 → 系统有基本安全边界
通过 4-5 项 → 核心防线还缺关键环节
通过 3 项以下 → 系统在裸奔
Safety & HITL Harness 的核心判断:安全不是让 Agent"小心点",而是让系统具备不可绕过的边界。HITL 不是"不放心就让人看看",而是把人放在高价值、高风险、低可逆的决策节点上。能力越强的 Agent,越需要更严格的安全治理------因为能力越强,越界的半径就越大。