给 Agent 加一个 Approval Gate

📅 2026/7/6 2:12:54 👁️ 阅读次数 📝 编程学习
给 Agent 加一个 Approval Gate

echo-agent 前身为 2025 年 11 月启动的个人助理项目 fubot,最初面向长期陪伴型个人智能体,围绕认知记忆、上下文延续、用户偏好沉淀、任务闭环与持续自我优化展开。随着真实场景迭代,项目逐步形成多入口接入、统一事件模型、消息总线、Agent Loop、多模型抽象、工具调用、MCP 接入、任务调度、权限审批、运行轨迹、长期记忆和受控自演进等能力。目前已支持微信、QQ、CLI、Gateway、Webhook、Cron 等入口,服务用户超过 20 万、累计下载超过 50 万,是面向长期运行、记忆增强和可持续成长智能体的开源 Agent Runtime。

项目地址:https://github.com/fuyuxiang/echo-agent

你让 Agent “帮我修复测试失败”。

它读完日志,判断需要运行测试。第一次是pytest,第二次可能是安装依赖,第三次可能是删除临时文件。每一步看起来都像正常工程动作,但它们的副作用完全不同:有的只是读取状态,有的会写文件,有的会联网,有的可能不可恢复。

这时真正的问题不是“模型能不能想到正确命令”,而是系统能不能在命令执行前回答:这一步是否允许执行、是否必须拒绝、是否需要用户授权

这就是 Approval Gate 要解决的问题。

问题入口

如果只看传统文本型 Chatbot,它的基本形态仍是:用户输入文本,模型返回文本。回答错了可以追问,理解偏了可以纠正,系统本身不会因为一句话就改文件、跑命令、发消息。

Agent 不一样。它把模型输出接到了工具系统上。模型不再只是生成答案,而是在生成行动计划:读文件、写文件、执行命令、访问网络、启动进程。行动一旦接入真实环境,安全问题就从“回答质量”变成了“执行授权”。

上一章讲的安全基础层已经能产生很多信号:工具是否暴露、命令是否危险、路径是否敏感、网络是否允许、风险等级是什么。但信号本身还不是决策。系统还需要一个统一入口,把这些信号合成最后结果。

Approval Gate 不是一个“确认弹窗”,而是 Agent 行动系统的最后决策门。

为了不停留在抽象层面,下面以 echo-agent 的实现为例。它把所有模型发起的工具调用先送入InferenceStage,再进入ApprovalGate.check。工具只有通过这道门,才会继续执行。

审批门

Approval Gate 的输出不是简单的 true / false,而是一个ApprovalCheck

它有两个核心字段:denialapproved_actions。如果denialNone,表示工具可以继续执行;如果denialToolResult,表示工具调用被拒绝或审批超时;approved_actions则记录本次已经批准的动作标识,供后续工具内部策略继续使用。

@dataclass class ApprovalCheck: denial: ToolResult | None = None approved_actions: frozenset[str] = frozenset()

这个设计有一个很重要的工程含义:审批失败不是异常退出,而是一个模型可观察到的工具结果。InferenceStage会把拒绝信息写回模型上下文,模型可以解释失败原因、请求用户授权,或者选择更安全的替代方案。

例如,用户要求“清理项目里没用的文件”。模型如果直接生成递归删除命令,Approval Gate 可以拒绝或请求审批;拒绝结果会作为 tool 消息返回。模型接下来可以改成先列出候选文件、生成删除计划,再让用户确认范围。

审批门因此不是中断对话,而是把风险决策放回 Agent Loop。

决策链

echo-agent 当前的ApprovalGate.check有清晰的判断顺序。这个顺序比单个规则更重要。

硬阻断必须最先发生。已经被 static guard 判定为危险的命令,不能再被auto_approve、trusted channel 或 Smart Approval 放行。Elevated 权限也要早于普通风险放行,因为 local、remote 或 full security 的执行范围可能超出沙箱。

可以把这条链压缩成几层:

阶段作用
Static guard命中破坏性命令、敏感路径、禁用工具、network deny 时直接拒绝
Elevated check对 local、remote、full security 等高权限执行做通道和用户校验
Risk classification把工具调用分为 read-only、write、exec、dangerous
Low-risk passread-only 和 write 默认放行,但仍受路径、沙箱和工具内部策略约束
Auto paths处理 auto_approve、personal CLI、trusted channel、approval mode off
Required check根据 auto_deny、guard ask、推理确认、require_approval、dangerous 判断是否需要审批
Allowlist / unattended复用已批准 pattern,或在无人值守场景按策略拒绝
Smart / manual低风险 exec 可智能判断,否则进入人工审批

最小伪代码大致是这样:

async def check(tool_name, arguments, event): guard = evaluate_tool_call(config, tool_name, arguments) if guard.denied: return deny(guard.reason) ​ if requires_elevated(tool_name) and not elevated_allowed(event): return deny("requires elevated execution rights") ​ risk = classify_risk(tool_name, arguments) if risk in {READ_ONLY, WRITE}: return allow() ​ if auto_approved(tool_name, risk, event): return allow() ​ if not approval_required(tool_name, guard, risk): return allow() ​ pattern_key = build_pattern_key(tool_name, arguments) if risk == EXEC and allowlist.approved(event.session_key, pattern_key): return allow() ​ if unattended(event): return handle_unattended_policy(risk, pattern_key) ​ if risk == EXEC and mode == "smart": return await smart_or_escalate(tool_name, arguments, guard) ​ return await manual_approval(tool_name, arguments, event, guard)

这里最容易误解的是 read-only 和 write 的放行。它不等于“写操作没有风险”,而是说普通写操作的主要边界放在路径策略、safe write root、工具暴露策略和具体工具内部校验里。

例如write_file可以写普通用户文件,但仍应阻断~/.ssh/id_rsa/etc/passwd、系统路径和safe_write_root外路径。Approval Gate 不应该把每次正常编辑都变成弹窗,否则 Agent 会失去可用性;它要把注意力集中在 exec、dangerous 和上下文不确定的动作上。

好的审批系统不是把所有动作都交给人,而是知道哪些动作可以自动化,哪些动作必须拒绝,哪些动作需要授权。

授权范围

人工审批最怕两个极端:范围太窄,用户反复确认;范围太宽,一次批准变成长期风险。

echo-agent 用 allowlist 处理这个问题。审批级别分三种:ONCESESSIONALWAYS。一次性批准只对当前请求生效;session 批准写入当前会话;always 批准写入永久 allowlist 并保存到文件。

allowlist 不是直接保存完整命令字符串,而是通过build_pattern_key做归一化。比如exec执行python3 -m pytest,pattern 可能是exec:python3execute_code执行 Python,pattern 是code:pythonprocess启动 npm,pattern 是process:npm

这是一种折中。完整参数过细,会导致相似动作不断重复审批;只按工具名过粗,又会把完全不同风险混在一起。pattern_key让系统可以复用“同类动作”的授权,同时保留重新判断的空间。

审批通过后,approved_actions会包含工具名、guard 的pattern_keyapproval_action。后续ToolExecutionContext会把这些信息传给工具。ShellToolCodeExecToolProcessTool等工具可以再次检查:这次执行是否真的包含被批准的动作。

这解决了一个常见脱节问题:Gate 层判断allowlist_miss需要审批,用户批准后,工具内部再次扫描命令仍可能得到 ask。如果没有approved_actions,工具会再次拦住同一个动作;有了它,工具能知道这个模式已经被本轮授权覆盖。

审批范围的原则可以压成一句话:批准应覆盖完成当前任务所需的最小行动集合

人工审批

当自动路径不能放行时,系统进入 manual approval flow。

echo-agent 会通过ApprovalManager.request_approval创建审批请求。请求有签名,签名由用户、动作、工具名和参数生成;如果相同请求已经 pending,系统会返回已有请求,避免重复创建审批项。

审批请求不是临时变量,而是状态对象。它要包含请求 ID、工具名、参数、发起用户、当前状态、审批结果,并支持等待、去重、查询和持久化。

用户侧的控制命令很直接:

/approvals /approve <request_id> /approve <request_id> session /approve <request_id> always /deny <request_id> [原因]

这些命令是控制面命令,不需要进入完整模型推理。/approvals会列出当前用户可见的 pending 请求;/approve/deny会直接改变审批状态。

谁可以审批也不能靠模型判断。书稿里的规则是:如果配置了admin_users,只有管理员能审批;如果没有配置管理员,请求发起者可以审批自己的请求。这让授权关系绑定到用户和通道,而不是绑定到模型的一句话。

如果审批超时,ApprovalGate会返回 denial。无人值守场景也会单独处理:cron、scheduler 或带_unattendedmetadata 的事件,默认策略是 deny;allow_safe也只放行 write,或者已经命中 allowlist 的 exec。没有人在场时,系统不应该默默执行高风险动作。

Smart Approval

Smart Approval 的价值是降低误报,不是替代责任归属。

在 echo-agent 中,它只出现在较靠后的阶段:风险为EXEC、approval mode 为smart、provider 可用时才运行。它可以返回三种结果:approve、deny、escalate。approve 会把 pattern 写入本 session;deny 会返回拒绝;escalate 则继续进入人工审批。

例如,python -c "print('hello')"可能因为 inline interpreter 被标记为需要审批,但上下文显示风险很低。Smart Approval 可以减少这种低价值打断。

但它不能覆盖 static guard。破坏性删除、写块设备、格式化文件系统、读取敏感凭证、network deny 下访问外网,这些动作不应该因为另一个模型判断“看起来没问题”就被放行。

智能审批的边界是降低打扰,不是转移授权责任。

这也是为什么 Approval Gate 必须有固定时序。Smart Approval 在硬阻断之后,只处理规则无法充分判断、但又可能低风险的 exec 场景。真正有副作用、外部可见、涉及成本或凭证的动作,仍应由明确策略或用户授权承担责任。

生产可用性

判断一个 Approval Gate 是否接近生产级,不能只看“有没有 approve 按钮”。更硬的检查项应该是这些:

检查项可验证标准
执行前入口所有模型工具调用是否先进入统一 gate,再进入工具执行
硬阻断优先static guard 是否早于 auto approve、trusted channel、smart approval
权限绑定elevated 执行是否绑定 channel、sender_id、admin_users
风险分流是否区分 read-only、write、exec、dangerous,并有不同路径
审批结果拒绝是否作为 tool result 写回上下文,而不是静默失败
授权范围是否支持 once、session、always,并能解释 pattern 范围
无人值守cron、scheduler 是否默认保守,避免等待不到审批或静默执行
状态持久化pending 请求、一次性授权、审批历史是否能跨重启恢复
工具协同工具内部是否读取approved_actions,避免 gate 与工具策略脱节
回归测试是否覆盖请求创建、去重、批准、拒绝、超时、持久化和审批命令

这些检查项背后的共同原则是:审批不是 UI 功能,而是执行控制协议。它要让系统能解释每次行动为什么被允许、为什么被拒绝、为什么需要人参与,以及批准范围到哪里结束。

审批和澄清也要分开。用户说“删除没用的文件”,系统缺的首先是范围信息,其次才是删除授权。正确做法是先澄清“哪些文件算没用”,再对具体删除动作请求审批。把澄清当审批,会让用户批准一个自己并未理解的行动;把审批当澄清,又会让系统显得啰嗦。

小结

Approval Gate 的核心不是让用户多点几次确认,而是把 Agent 的行动授权显式化。

模型负责提出候选行动,系统策略负责识别风险,人类在规则无法充分判断且影响较大的地方做授权。低风险、可恢复、边界清楚的动作自动放行;硬阻断动作直接拒绝;高影响或上下文不确定的动作进入审批。

这样设计之后,Agent 不只是“会调用工具”,而是知道每次工具调用如何被授权、如何被记录、如何在失败后回到对话中继续推进任务。

(全篇完)


本文为 echo-agent 设计笔记系列第 16 篇。项目源码已开源至 GitHub。如果你对工业级 Agent 的工程落地感兴趣,欢迎加入技术交流群参与日常讨论。下一篇我们将探讨 《Agent Memory 设计笔记:从短期上下文到长期经验》,敬请期待。