AI Agent 拦住了一次危险工具调用,日志却未必能证明它没有执行
很多 AI Agent 项目在加入工具调用能力后,都会很快遇到一个问题:
当 Agent 尝试删除文件、修改数据库、发送付款请求时,我们怎么确认这次操作真的被阻止了?
多数系统会记录一条日志:
tool_call blocked
或者:
permission denied
看起来似乎已经足够。
但从运行时安全角度来看,这两句话并不能回答最关键的问题:
工具处理函数到底有没有开始执行?
这也是我在开发 DHMS / AgentFuse 时,逐渐意识到的一个核心边界问题。
一、“拒绝调用”和“没有执行”不是同一件事
假设一个 Agent 准备调用下面这个工具:
def delete_database(database_name: str):
# 删除数据库
...
系统中的权限策略判断:
decision=block
这只能说明:
策略层决定不允许本次操作。
但它不能自动证明:
工具没有进入调度流程
工具处理函数没有被调用
数据库连接没有建立
外部副作用没有发生
在复杂 Agent 框架里,权限判断、工具调度和日志记录经常分散在不同组件中。
一种常见的错误结构是:
Agent 生成工具调用
→ 工具进入执行队列
→ 权限中间件进行判断
→ 系统记录 blocked
如果权限判断发生得太晚,那么即使最终日志显示“拒绝”,工具也可能已经进入了部分执行流程。
因此,真正需要区分的是两组概念:
策略决定了什么
以及:
运行时实际上发生了什么
二、decision 和 outcome 必须分开记录
AgentFuse当前采用的一个基本原则是:
decision != outcome
decision 表示策略层作出的决定,例如:
allow
block
outcome 表示运行时最终观察到的结果,例如:
executed
not_executed
execution_failed
interrupted
一条被真正阻止在工具调度前的调用,可以产生类似下面的 receipt:
decision=block
dispatch_started=false
handler_started=false
outcome=not_executed
这几项分别表示:
decision=block
策略拒绝了调用。
dispatch_started=false
工具没有进入受保护的调度路径。
handler_started=false
真正的工具处理函数没有开始运行。
outcome=not_executed
这次调用在当前受保护边界内被记录为未执行。
相比简单的一句:
tool blocked
这种记录能更清楚地回答:
工具调用到底停在了哪一个阶段?
三、为什么普通 tracing 还不够
LangSmith、Langfuse、OpenTelemetry 等 tracing 工具,可以帮助开发者查看:
-
模型调用;
-
工具调用;
-
token 消耗;
-
延迟;
-
异常;
-
Agent运行轨迹。
这些能力非常重要。
但 tracing 和 runtime enforcement 解决的不是同一个问题。
Tracing通常回答:
系统记录到了什么?
而执行边界需要回答:
系统是否允许工具处理函数开始运行?
如果 tracing 系统位于工具调用之后,它可能看到:
tool invocation
tool error
permission denied
但仅凭这些日志,不一定能确认权限检查发生在 handler 启动之前还是之后。
换句话说:
观察执行
不能天然替代:
控制执行
更不能自动证明:
某个函数从未开始执行
AgentFuse目前尝试做的,是让策略判断、调度边界和生命周期 receipt 尽量位于同一条代码路径中。
四、一个更合理的工具执行路径
一个更安全的 Agent 工具调用流程,应当类似:
Agent 生成 tool call
→ 解析工具名称和参数
→ 执行策略检查
→ 生成 allow / block decision
→ 只有 allow 才进入 dispatch
→ 记录 handler 是否启动
→ 记录最终 outcome
被拒绝的工具:
policy check
→ block
→ 不进入 handler
→ outcome=not_executed
被允许且正常完成的工具:
policy check
→ allow
→ handler started
→ handler completed
→ outcome=executed
被允许但执行失败的工具:
policy check
→ allow
→ handler started
→ exception
→ outcome=execution_failed
被允许但触发 Agent 控制流中断的工具:
policy check
→ allow
→ handler started
→ GraphInterrupt
→ outcome=interrupted
这四种结果不能全部混在一个笼统的 error 字段里。
五、GraphInterrupt 也不是工具执行失败
在 LangGraph 的 Human-in-the-loop 场景中,工具可能调用 interrupt() 暂停图执行,等待人工确认。
这时系统发生的是:
工具被允许执行
工具已经进入 handler
执行过程中触发暂停
图等待后续 resume
合理的生命周期记录应该是:
decision=allow
dispatch_started=true
handler_started=true
outcome=interrupted
execution_failed=false
这里的 interrupted 表示控制流暂停,而不是工具异常。
如果系统把 GraphInterrupt 统一记录成:
tool-error
就会丢失一个关键语义:
这次运行是在等待人工输入,还是工具真的执行失败了?
对于需要恢复运行、人工审批或检查点机制的 Agent,这个区别非常重要。
六、AgentFuse目前做到了什么
AgentFuse当前仍然是一个开源运行时原型,主要聚焦在一个较窄的问题上:
在工具 handler 启动之前应用策略,并生成区分 decision 和 outcome 的生命周期 receipt。
目前实现的能力包括:
-
工具 allowlist 和 denylist;
-
参数感知策略;
-
策略异常时 fail closed;
-
同步和异步工具处理函数;
-
LangGraph
ToolNode适配; -
blocked、executed、execution_failed 和 interrupted 的区分;
-
保留稳定的 tool call ID;
-
混合批次中,被阻止的调用不影响其他允许调用继续执行;
-
receipt 默认不保存原始工具参数;
-
使用参数 hash 和安全元数据进行关联。
项目当前并不声称提供:
-
操作系统级沙箱;
-
网络级隔离;
-
内核级执行证明;
-
密码学不可篡改证明;
-
对所有 Agent 框架的自动拦截;
-
企业级合规认证。
它目前只对经过 RuntimeGuard 或显式适配器的调用路径负责。
七、为什么不默认保存完整参数
很多 tracing 和审计产品会保存:
-
完整提示词;
-
原始工具参数;
-
Agent状态快照;
-
模型返回;
-
中间上下文。
这样做便于回放,但也会引入新的隐私和安全风险。
工具参数中可能包含:
-
数据库名称;
-
文件路径;
-
用户信息;
-
API请求内容;
-
支付信息;
-
内部系统标识;
-
私有业务数据。
AgentFuse当前选择的方向是:
保留判断和生命周期证据
尽量不保留原始敏感数据
因此 receipt 更倾向于包含:
tool_name
tool_call_id
decision
dispatch_started
handler_started
outcome
execution_failed
argument_hash
safe_metadata
而不是直接保存完整参数。
这是一个取舍:
审计信息越完整,隐私负担越大;
信息越少,必须越精准地记录关键执行状态。
八、一个值得讨论的问题
目前大量 Agent 安全产品集中在:
Prompt 注入检测
内容过滤
模型输出检查
运行轨迹追踪
这些都很重要。
但随着 Agent开始调用真实工具,另一个问题会越来越重要:
系统是否能够明确区分“策略拒绝”“没有调度”“处理函数已启动”“执行失败”和“控制流暂停”?
如果这些状态无法区分,那么事故发生后,团队可能只能看到:
工具失败了
却无法判断:
工具从未开始执行
还是:
工具已经开始,只是结果丢失
甚至:
外部副作用可能已经发生
这也是 AgentFuse当前主要研究和实现的方向。
九、项目地址和试用方式
项目仓库:
MkaliezZ/dhms-engine
当前主要开发分支:
agent-harness-v1
LangGraph interrupt receipt 示例:
examples/runtime_guard/langgraph_interrupt_receipt_demo.py
参考实现提交:
8c6ae9875b3618a529d5150c96385da7461099c2
项目 README 中提供了 RuntimeGuard 的使用方式和当前限制。
我现在最希望获得的不是 Star 数量,而是真实运行反馈。
尤其是以下场景:
-
LangGraph
ToolNode; -
Human-in-the-loop;
-
删除、写入或付款类工具;
-
工具权限 middleware;
-
工具调用取消;
-
interrupt 和普通异常混淆;
-
需要判断 handler 是否启动的 Agent。
有相关复现的开发者,可以运行最小示例,并在 GitHub 提供:
运行环境
接入方式
receipt 输出
兼容性错误
与原框架生命周期的差异
一次真实运行结果,比十条“这个项目很有意思”更有价值。
结语
Agent安全不能只回答:
这次调用最后显示了什么?
还应该回答:
这次调用是否进入调度?
工具处理函数是否开始?
最终结果是未执行、执行失败,还是等待恢复?
当 Agent只负责聊天时,这些差别可能并不明显。
但当 Agent开始删除文件、修改数据库、执行代码或调用付款工具时,它们会直接决定:
我们是在看一条普通日志,还是在面对一个真正的运行时安全边界。
更多推荐


所有评论(0)