很多 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开始删除文件、修改数据库、执行代码或调用付款工具时,它们会直接决定:

我们是在看一条普通日志,还是在面对一个真正的运行时安全边界。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐