背景
我们在做一个面向业务运营的 AI Agent 平台。Agent 可以调用各种工具帮用户完成任务------查数据、提单、提交审批、批量操作等等。
功能做起来很爽,但很快我们就意识到一件事:Agent 是一个自主执行体,它的"边界"在哪里?
如果用户说了一句模糊的话,Agent 是否会误解意图,去做一件用户没想到的事?
如果某个工具存在安全漏洞,Agent 会不会帮攻击者"顺手"利用?
如果一次批量操作涉及几千条记录,是否要让人先确认?
如果某个下游服务挂了,Agent 是否会无限重试把整个系统拖垮?
这些问题堆在一起,我们觉得需要一套专门针对 AI Agent 的安全防护框架,而不是简单地在业务代码里加几个 if。基于此,于是项目里面有了 Harness 层 。
什么是 Harness?
Harness,中文是"马具"------给马套上缰绳,让它跑得快但不失控。
我们用这个词来命名我们的 Agent 边界管控框架。它的核心思想是:
让 AI Agent 在可控的边界内自主执行,而不是无限制地"自由发挥"。
Harness 不是一个业务模块,它是一个横切所有工具调用链路的安全中间层,由四层 Guard 组成,采用纵深防御思路:在不同阶段分别拦截不同类型的风险。
四层防御体系
csharp
用户输入
│
▼
[L1] IntentGuard --- 意图安全防护(入口)
│
▼
[L2] SkillGuard --- 技能包安全扫描
│
▼
[L3] OperationGuard --- 操作风险控制(核心)
│
▼
[L4] ResourceGuard --- 资源保护
│
▼
工具执行
每一层都实现同一个接口:
typescript
interface IGuardLayer {
check(context: HarnessContext): Promise<HarnessDecision>;
}
// 决策结果只有三种
type Decision = 'ALLOW' | 'BLOCK' | 'HITL_WAIT';
这个设计让四层可以被统一调度,也很容易在某一层出问题时快速降级。
L1 --- IntentGuard:别让"坏话"进来
第一层在用户提交输入后立刻执行,目的是在 LLM 开始推理之前就拦截可疑请求。
为什么要在 LLM 之前拦截?因为一旦把注入攻击或者越权指令喂给 LLM,Token 就消耗了,而且上下文被污染了,后续几乎无法清除。在入口处拦截的投资回报率最高。
IntentGuard 内部是一条四环检测流水线:
css
输入文本
→ [归一化] Base64解码、Unicode还原(对抗混淆)
→ [模式匹配] 正则规则集匹配已知攻击模式
→ [上下文分析] 看历史消息,累积风险分(防分散投毒)
→ [语义分类] 仅当前三环未命中高威胁时,用 LLM 语义兜底
→ 综合最终威胁等级
设计上有几个有趣的取舍:
取舍一:语义检测是可选的。规则已经高置信度命中(HIGH/CRITICAL)时,跳过 LLM 调用------因为已经确定是攻击了,没必要再花钱确认一遍。
取舍二:整体 fail-open 。IntentGuard 出现异常或超时,默认放行,因为 L3 OperationGuard 会在工具执行前再做一次更精准的检查,两道防线之间有冗余。
取舍三:观察模式 。上线初期我们不敢直接拦截,开了 observe 模式------检测但不拦截,只记日志,先收集一段时间的误报数据,再调整规则,再开启拦截。
L2 --- SkillGuard:别让"坏工具"进来
我们平台允许接入第三方 Skill 包,这就带来了供应链安全风险------Skill 包里如果有危险 API 调用或者已知 CVE 漏洞,Agent 执行它等于帮攻击者做了事。
SkillGuard 对每个 Skill 包做四级扫描:
- 结构校验:manifest 完整性、目录规范、package.json 合法性
- 静态分析:扫代码,找危险 API 调用(比如直接执行系统命令)、硬编码凭证
- 漏洞扫描:异步调 OSV API 查 CVE,不阻塞主链路
- 权限交叉比对:Skill 声明需要的权限,和代码里实际调用的权限,是否对得上
其中漏洞扫描是异步的------它不会卡住 Skill 注册流程,而是后台跑,跑完了更新 Skill 的审核状态。运行时 isSkillAllowed() 就是一次内存级白名单查询,几乎没有性能开销。
L3 --- OperationGuard:每次工具调用都要过一关
这是整个 Harness 框架里最核心的一层,也是设计最复杂的一层。
它的时机:工具调用真正执行的前一刻。Agent 已经决定要调用什么工具、传什么参数,OperationGuard 做最后审核。
它的决策:ALLOW(直接执行)、HITL(让人来确认)、BLOCK(拒绝)。
执行链
scss
[1] PermissionChecker(六级硬保护)
敏感路径 → 工具黑名单 → 工具白名单 → 自定义路径规则 → 命令deny模式 → 权限模式
↓(通过)
[1.5] RuleEngine(动态规则,可选)
内置规则 + 数据库动态规则 → PASS / WARN / BLOCK / HITL
↓(PASS)
[2] RiskScorer(四维度加权评分 0-100)
0-39 → ALLOW
40-59 → HITL
60-79 → 高优先级 HITL
80+ → BLOCK
↓(HITL场景)
[3] HITLInterceptor
生成一次性 Token → 写库 → 推通知 → 等待人工确认
权限检查的六级设计
为什么要六级?因为我们发现"权限"这件事有很多维度:
typescript
// 优先级从高到低(更高优先级的判断覆盖低优先级)
// P1: 敏感路径(硬编码,不可绕过)
// P2: 工具黑名单
// P3: 工具白名单(在白名单里就放行,无需往下判断)
// P4: 路径规则(自定义 deny 路径)
// P5: 命令 deny 模式(正则匹配)
// P6: 权限模式(default/plan/full_auto)
default 模式下,"只读"类工具(get/list/query/search 等)直接放行,"变更"类工具(create/update/delete/batch 等)需要额外确认。这样大部分查询请求不会被打断,只有真正有副作用的操作才走确认流程。
HITL 双模式
HITL(Human-In-The-Loop)是我们整个框架里用户感知最强的机制。
我们做了两种模式:
simple 模式:适合"当前用户自己确认"的场景。比如 Agent 要提交一个审批单,弹出确认框,用户点"确认",WebSocket 实时通知 Agent 继续执行。
standard 模式:适合"需要特定审批人批准"的场景。比如某个高权限操作必须由负责人审批。这时 Harness 会向配置的审批人发送即时消息通知,审批人回调确认后,Agent 恢复执行。Token 有 TTL,超时自动拒绝。
typescript
// 规则配置示例(伪代码)
{
ruleId: 'high_risk_batch_op',
name: '高风险批量操作需审批',
condition: { toolName: 'batch_*' },
action: {
type: 'HITL',
approvalType: 'standard', // standard 模式
approvers: ['mis_id_of_approver'],
userMessage: '当前操作涉及批量变更,需要负责人审批',
hitlTtlMs: 10 * 60 * 1000, // 10分钟超时
}
}
fail-closed 策略
OperationGuard 出现异常时,默认拒绝(而不是放行)。
这和 L1 的 fail-open 策略相反,是刻意设计的:L3 是工具执行前的最后一道关卡,如果这里出了问题还放行,工具可能已经产生了实际副作用(比如真的提了单、真的删了数据),这种风险远大于"多拒绝了一次"的代价。
L4 --- ResourceGuard:别把资源跑垮
L4 关注的不是"做什么",而是"能不能做"------资源层面的边界。
月度 Token 配额:按团队维度统计本月已用 Token,超过配额则拦截。配额数据从 数据库 读取,加了 TTL 缓存避免每次都查库。
熔断器:每个工具都有一个独立的熔断器(三态:CLOSED/OPEN/HALF_OPEN),下游服务连续失败超阈值后熔断,在 OPEN 状态下直接快速失败,不再请求。恢复时走半开探测,指数退避重试。
上下文窗口保护:Session 历史如果无限增长,最终会超出 LLM 的上下文窗口。ResourceGuard 会估算当前 Session 的 Token 数,接近阈值时告警,超限时拦截。
沙箱租户配额:我们平台有代码执行沙箱,ResourceGuard 会按租户维度限制沙箱占用数,防止单个租户把全局资源耗尽(而影响其他租户)。
规则引擎:让安全策略变成数据
Harness 最初的权限规则都是硬编码的,调整起来需要发版。我们后来加了一个规则引擎,把规则从代码里抽出来,存到数据库里。
核心设计是双源合并:
markdown
L1 内置规则(代码硬编码,兜底保底)
+
L2 数据库规则(运营配置,TTL 30s 缓存)
↓
去重合并(同 rule_id 时 DB 规则覆盖内置规则参数)
↓
按 priority 排序 → 逐条评估 → 首条非PASS即短路返回
内置了七种评估器:Shell 注入检测、预算控制、频率限制、内容关键词过滤、自定义正则、工具限制、通用条件表达式。
其中"通用条件表达式"评估器是最后加的,它让非研发人员也能写规则:
json
// 通用规则示例(运营配置,无需改代码)
{
"ruleId": "budget_check_001",
"name": "预算超限审批",
"condition": {
"field": "budget",
"op": "gt",
"value": 50000
},
"action": {
"type": "HITL",
"userMessage": "申请金额较大,需要上级审批"
}
}
热开关:生产出了问题怎么办
上线初期我们非常担心误报。如果 IntentGuard 误判了一个正常请求,用户会直接被拦截,体验非常差。
所以每个 Guard 层都有三种运行状态,通过配置中心实时切换,无需重启服务:
| 状态 | 行为 |
|---|---|
enabled |
正常拦截 + 写审计日志 |
observe |
检测但不拦截,只记日志(收集误报率) |
disabled |
完全跳过,不检测不记录(紧急回滚) |
在灰度上线时,我们先把新规则设置为 observe 模式,跑一段时间后分析日志,确认没有误报再切换到 enabled。这个机制救了我们好几次。
审计日志:做了什么,为什么这么做
Harness 的每一个拦截决策、每一次 HITL 事件、每一次工具调用,都会写审计日志。但我们遇到了一个问题:日志太多。
一个普通的 ALLOW 请求,四层 Guard 都通过了,如果每层都写一条日志,存储成本很高而且意义不大------"正常放行"的日志大部分时候没人看。
我们的解决方案是分层写入策略:
arduino
BLOCK 决策 → 只写触发拦截的那一层日志(精确定位原因)
ALLOW 决策 → 只写一条汇总日志("所有层通过",降噪)
HITL 事件 → 始终写完整事件日志(合规要求)
工具调用 → 始终写调用记录(操作留痕)
同时加了双队列机制:合规敏感日志(BLOCK/HITL)走高优先级队列,运营分析日志走普通队列。极端情况下数据库写入失败,合规日志会 fallback 写本地文件,普通日志则丢弃。
整体架构回顾
把上面所有东西放在一起,看起来是这样的:
scss
用户请求
│
▼
HarnessOrchestrator(编排器)
│
├── [L1+L2 并行] IntentGuard + SkillGuard
│ │
│ └── BLOCK/HITL → 短路,跳过后续层
│
├── [L3 串行] OperationGuard
│ ├── PermissionChecker(硬保护)
│ ├── RuleEngine(动态规则)
│ ├── RiskScorer(风险评分)
│ └── HITLInterceptor(人工确认)
│
└── [L4 串行] ResourceGuard
├── BudgetInterceptor
├── CircuitBreakerRegistry
├── ContextEfficiencyOptimizer
└── SandboxQuotaCheck
│
▼
工具执行
│
▼
AuditLogger(异步写审计日志)
L1 和 L2 并行执行(互相不依赖),L3 和 L4 串行执行(L3 要先出决策,L4 才检查资源)。任意一层 BLOCK 或 HITL_WAIT,后续层跳过------这是短路求值。
一些经验总结
做完这套框架,有几点感受比较深:
1. fail-open 和 fail-closed 是两个哲学,要根据层级选择
L1 意图检测失败 → fail-open(放行):因为 L3 还有兜底,误拦截的代价更高。
L3 操作权限检查失败 → fail-closed(拒绝):因为这是最后一关,放行可能产生实际副作用。
L4 资源检查失败 → fail-closed:资源不确定时放行可能触发雪崩。
2. 可观测性比拦截能力更重要(初期)
上线前三个月,observe 模式的价值远大于 enabled 模式。看到哪些请求会被命中规则,才能判断规则是否合理,才敢真正开启拦截。急着上 enabled 只会积累大量投诉。
3. HITL 要设计得让人愿意点
HITL 的用户体验非常关键。如果审批流程太繁琐,用户会绕着走(比如把批量操作拆成多次小操作)。我们花了很多时间打磨通知的文案和确认界面,让"需要确认"这件事本身不成为负担。
4. 规则数据化是必要的,但要留好兜底
把规则放数据库里,运营团队可以实时调整,非常灵活。但一定要保留代码层面的内置规则作为兜底------数据库出问题、缓存失效,都不应该导致安全规则完全失效。
结语
Harness 从一个"临时加的安全校验"演化成今天这个四层防护框架,大概花了三个月时间,经历了好几轮重构。
最大的收获不是框架本身,而是在做这件事的过程中,把"AI Agent 的边界在哪里"这个问题想清楚了。
Agent 不应该是无限制的自动化执行器,它应该是一个在清晰边界内工作的可信助手------知道什么能做、什么要确认、什么绝对不能做。Harness 就是这套边界的技术实现。
如果你们团队也在做 Agent 平台,希望这篇文章对你有所启发。欢迎交流。