Agent 的工具调用格式都对,为什么还是不能放行?用 ActionRail 在执行前核对实时业务数据
给 Agent 加 JSON Schema、工具白名单和权限策略后,很多团队会觉得“写操作已经被管住了”。但在退款、转账、删改资源这类动作上,真正棘手的失败并不一定是 Agent 调了不该调的工具,而是它带着形式正确、权限也看似正确、业务值却错误的参数调用了正确工具。
例如客服 Agent 收到一段被污染的工单摘要,随后提出:给订单 order-100 退款 75 元。issue_refund 在白名单中,金额也没有超过额度,参数类型完全合法;但订单属于另一位客户,或者早已退款。传统 schema 校验只能判断 order_id 是字符串、amount 是数字,RBAC 只能判断该 Agent 是否具备退款权限,它们都无法回答一个更接近业务事实的问题:这笔退款此刻是否真的对应当前请求者和订单状态?
ActionRail 是 ToolJet 发布的 Apache-2.0 开源 Python 项目,处于 public beta。它把这一层称为 grounding:在真正执行后果性工具调用前,针对参数向实时的 system of record 做核对,再给出 allow、hold 或 block。这不是替代鉴权系统;它是在“已经获准调用”之后,为“这一个值是否正确”补上的最后一道执行边界。
先分清三种检查:结构、权限与实时事实
把所有安全检查都叫作“guardrail”,容易让设计失焦。实际落地时,至少要区分三类问题:
| 层次 | 要回答的问题 | 典型手段 | 它解决不了什么 |
| — | — | — | — |
| 结构 | 调用长得对不对? | JSON Schema、参数类型、枚举 | 合法订单号是否属于当前客户 |
| 权限 | 这个主体能不能做? | RBAC、ReBAC、Cedar、OPA | 当前订单是否已经退款、余额是否足够 |
| 实时核对 | 这次调用的值现在是否正确? | 对账库、业务 API、只读 MCP 查询 | 更高层的角色授权设计 |
ActionRail 的目标集中在第三层。它不根据模型的解释来判断“看上去是否合理”,而是在调用边界读取可信数据源,把待执行参数、受认证的 caller context 与实时记录进行比对。若订单的 customer_id 和当前用户不一致,或者状态不是可退款状态,就不应把函数真正交给 Agent 执行。
这种分层还有一个工程收益:模型可以继续负责语言理解和任务规划;业务真实性回到数据库、业务 API 与明确规则中。安全团队不必把每条业务约束硬塞进提示词,也不必相信模型会稳定地记住某个订单刚刚发生的状态变化。
从“先执行再审计”改为执行边界的同步决策
ActionRail 的决策管道先做较便宜的 policy 检查,再做 grounding,并按照 block、hold、allow 的优先级返回结果。hold 适合金额超过自动处理阈值等需要人工确认的情况;数据不匹配、对象不存在或已处理等情况则适合 block。
对于任意 Python Agent 循环、Worker 或 API handler,可以把真实业务函数包在 ActionRuntime.execute() 里。以下代码只展示边界位置;agent_id 和 api_key 应由安全的配置管理系统提供,业务身份则必须来自已认证请求,而不是模型文本:
from actionrail import ActionRuntime
runtime = ActionRuntime(
agent_id="agent_support",
api_key="从受控配置读取",
)
result = runtime.execute(
"issue_refund",
{"order_id": order_id, "amount": amount},
issue_refund,
context={"customer_id": authenticated_customer_id},
)
这里最关键的是 context 的来源。若把聊天记录里模型提到的客户号直接填进去,grounding 会失去可信锚点;正确做法是从会话认证、服务端身份或网关注入的上下文中取得它。ActionRail 在授权通过后才调用 issue_refund(**arguments),因此被阻断的建议不会变成已经发生、只能事后补救的副作用。
已使用 LangGraph 的团队还可以用 enforce() 自动发现和包装已编译 Agent 的工具。官方示例将 SQLiteSource 作为业务核对来源,并用 trusted_context() 传入客户身份。无论选择显式边界还是框架适配器,都要先列出哪些工具具有不可逆后果:付款、退款、删库、变更权限、对外发送和创建云资源,优先覆盖这些动作,而不是试图一夜之间包住所有只读查询。
规则与数据源怎样设计才不会反而扩大风险
ActionRail 支持 SQLite、PostgreSQL、MySQL/MariaDB、HTTP/REST 与 MCP gateway 等 Source。它的资料强调数据面留在客户环境中;规则使用 Source 返回的数据做比较。对生产系统而言,重点不是“能接多少数据源”,而是给验证通道单独设定最小权限。
以数据库为例,验证退款资格的账号应只具有 SELECT 权限,不能拥有退款、更新订单或读取无关表的能力。对 MCP Source,则应让网关身份只能调用明确的只读验证工具。项目会要求 readOnlyHint,但这类注解只是提示而非授权边界;真正的约束仍应在 MCP server、网关和数据源权限上落实。
还要给失败模式做产品决策。对于转账、退款、权限变更等高风险写入,如果实时核对源不可用,通常应失败关闭:宁可暂缓一笔操作,也不要因为“查不到数据”自动放行。相反,低风险、可逆的辅助动作可以有不同的可用性策略。不要把同一种 fail-open/fail-closed 策略套给所有工具。
项目的规则可以引用调用者上下文、字面量、另一参数或当前时间。例如检查余额是否大于等于退款金额、订单是否属于客户、交付时间是否满足退款窗口。这样的规则应放进代码审查与版本控制流程,而不是在控制台里靠临时手工调整;业务规则变更本身就是影响资金与权限的发布。
用 Action Tests 把“不会误放行”变成可回归验证
把规则部署到线上之前,最需要防的是两类回归:新的业务状态没覆盖,或“修规则”时意外放开了原本必须拦截的路径。ActionRail 提供 Action Tests,把 allow、hold、block 写成 YAML 测试契约,再通过同一条决策管道执行。
schema_version: 1
cases:
- name: 跨客户退款必须阻断
tool: issue_refund
args: {order_id: order-100, amount: 75}
context: {customer_id: customer-2}
expect: block
fixtures:
billing-production:
by_value:
order-100: {customer_id: customer-1, status: delivered}
运行命令是:
actionrail test
这类测试的价值不在于模拟语言模型,而在于固定“业务事实不匹配时函数不能执行”的契约。对每个高风险工具,至少准备正常路径、跨租户/跨客户、对象不存在、重复执行、额度边界、数据源超时六类用例。生产数据源不应在普通 CI 中被隐式访问;官方说明中,只有显式传入 --live-sources 才会让测试阶段使用生产 Source。
还应把测试分为两层。第一层使用固定 fixture,确保规则表达式、字段映射与优先级在每次提交后都可重复验证;第二层放在受控的预发布环境,针对脱敏副本或专门的验证账户检查连接、权限、超时和审计链路。不要用真实客户订单做“看看会不会拦住”的实验。测试报告也不能只统计拦截率:若正常退款被大量 hold,客服队列会被压垮;若异常订单长期被 allow,则说明事实模型、数据延迟或上下文绑定存在缺口。把每一次 hold 与 block 的原因分组,才能知道该修规则、修源数据,还是调整人工复核流程。
不要把它当成万能的 Agent 安全方案
ActionRail 解决的是运行时参数与实时事实不一致的问题,不会自动修复提示注入、身份认证、数据脱敏、供应链安全或模型越权规划。它也不能让权限过大的验证账号变得安全,更不能证明某个 MCP 工具真的只读。对于公共 beta 项目,还应在试点阶段锁定依赖版本、审阅升级变更,并把业务事故演练纳入上线条件。
更实际的落地顺序是:先选一条高价值、可清晰核对的写操作,例如退款或创建访问权限;再建立最小权限的只读 Source 与可信上下文;随后把“应该拦住什么”写成 Action Tests;最后观察 allow、hold、block 的结果和人工复核反馈。这样,Agent 仍然能加速流程,但最终写入业务系统之前多了一道由实时数据而非模型自信程度决定的闸门。
相关链接