Python Agent 工具调用:审批令牌、参数白名单和累计预算

让 Agent 调工具,最危险的误解是"模型输出了函数名,所以函数就该执行"。模型只能提出计划;真正的执行者应该验证它是否在许可范围内、参数是否合理、成本是否超标,以及一个人是否明确批准了这次有副作用的操作。本文用不到 100 行 Python 写一个可测试的闸门:查询类工具可自动放行,退款等写操作需要一次性审批令牌,所有操作共享累计预算。

这不是杞人忧天。Anthropic 在 10 月 9 日的官方报告中披露,评测和内部使用曾出现模型绕过限制、对真实网站进行非预期操作的案例;该公司明确强调纵深防御而不只依靠训练。报告并不证明任何一个 API 都会这样做,却给了工程师一个清醒的设计原则:边界必须在模型外部、可记录、可撤销。OpenAI 的 Function Calling 指南同样把"模型选择工具"和"应用执行工具"分为不同阶段。
#mermaid-svg-St6A871UgD0hHIRa{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-St6A871UgD0hHIRa .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-St6A871UgD0hHIRa .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-St6A871UgD0hHIRa .error-icon{fill:#552222;}#mermaid-svg-St6A871UgD0hHIRa .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-St6A871UgD0hHIRa .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-St6A871UgD0hHIRa .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-St6A871UgD0hHIRa .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-St6A871UgD0hHIRa .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-St6A871UgD0hHIRa .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-St6A871UgD0hHIRa .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-St6A871UgD0hHIRa .marker{fill:#333333;stroke:#333333;}#mermaid-svg-St6A871UgD0hHIRa .marker.cross{stroke:#333333;}#mermaid-svg-St6A871UgD0hHIRa svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-St6A871UgD0hHIRa p{margin:0;}#mermaid-svg-St6A871UgD0hHIRa .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-St6A871UgD0hHIRa .cluster-label text{fill:#333;}#mermaid-svg-St6A871UgD0hHIRa .cluster-label span{color:#333;}#mermaid-svg-St6A871UgD0hHIRa .cluster-label span p{background-color:transparent;}#mermaid-svg-St6A871UgD0hHIRa .label text,#mermaid-svg-St6A871UgD0hHIRa span{fill:#333;color:#333;}#mermaid-svg-St6A871UgD0hHIRa .node rect,#mermaid-svg-St6A871UgD0hHIRa .node circle,#mermaid-svg-St6A871UgD0hHIRa .node ellipse,#mermaid-svg-St6A871UgD0hHIRa .node polygon,#mermaid-svg-St6A871UgD0hHIRa .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-St6A871UgD0hHIRa .rough-node .label text,#mermaid-svg-St6A871UgD0hHIRa .node .label text,#mermaid-svg-St6A871UgD0hHIRa .image-shape .label,#mermaid-svg-St6A871UgD0hHIRa .icon-shape .label{text-anchor:middle;}#mermaid-svg-St6A871UgD0hHIRa .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-St6A871UgD0hHIRa .rough-node .label,#mermaid-svg-St6A871UgD0hHIRa .node .label,#mermaid-svg-St6A871UgD0hHIRa .image-shape .label,#mermaid-svg-St6A871UgD0hHIRa .icon-shape .label{text-align:center;}#mermaid-svg-St6A871UgD0hHIRa .node.clickable{cursor:pointer;}#mermaid-svg-St6A871UgD0hHIRa .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-St6A871UgD0hHIRa .arrowheadPath{fill:#333333;}#mermaid-svg-St6A871UgD0hHIRa .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-St6A871UgD0hHIRa .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-St6A871UgD0hHIRa .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-St6A871UgD0hHIRa .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-St6A871UgD0hHIRa .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-St6A871UgD0hHIRa .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-St6A871UgD0hHIRa .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-St6A871UgD0hHIRa .cluster text{fill:#333;}#mermaid-svg-St6A871UgD0hHIRa .cluster span{color:#333;}#mermaid-svg-St6A871UgD0hHIRa div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-St6A871UgD0hHIRa .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-St6A871UgD0hHIRa rect.text{fill:none;stroke-width:0;}#mermaid-svg-St6A871UgD0hHIRa .icon-shape,#mermaid-svg-St6A871UgD0hHIRa .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-St6A871UgD0hHIRa .icon-shape p,#mermaid-svg-St6A871UgD0hHIRa .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-St6A871UgD0hHIRa .icon-shape .label rect,#mermaid-svg-St6A871UgD0hHIRa .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-St6A871UgD0hHIRa .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-St6A871UgD0hHIRa .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-St6A871UgD0hHIRa :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
否
是
模型工具计划
名称与参数验证
预算足够?
拒绝并记录
有副作用?
执行只读工具
一次性审批令牌?
创建待审批项
账本与结果

环境准备

Python 3.10+,无第三方依赖。这里用模拟工具证明安全逻辑,因此不需要密钥;把它接到任意函数调用 API 时,只把模型解析得到的 name 和 arguments 交给 gate,绝不要把模型文本 eval 成 Python。

bash 复制代码
python agent_gate.py

完整代码

python 复制代码
# agent_gate.py
from dataclasses import dataclass, field
from decimal import Decimal
from uuid import uuid4

READ_ONLY = {"search_order"}
WRITE_TOOLS = {"refund_order"}

@dataclass
class Ledger:
    limit: Decimal = Decimal("2.00")
    spent: Decimal = Decimal("0.00")
    approvals: set = field(default_factory=set)
    events: list = field(default_factory=list)

    def approve_once(self, plan_id):
        token = str(uuid4())
        self.approvals.add((plan_id, token))
        return token

def valid_args(name, args):
    if name == "search_order": return isinstance(args.get("order_id"), str) and len(args["order_id"]) <= 32
    if name == "refund_order":
        return isinstance(args.get("order_id"), str) and Decimal(str(args.get("amount", "-1"))) > 0
    return False

def gate(plan_id, name, args, estimated_cost, ledger, approval=None):
    cost = Decimal(str(estimated_cost))
    if name not in READ_ONLY | WRITE_TOOLS or not valid_args(name, args):
        return {"status": "DENY", "reason": "unknown tool or invalid arguments"}
    if cost < 0 or ledger.spent + cost > ledger.limit:
        return {"status": "DENY", "reason": "budget exceeded"}
    if name in WRITE_TOOLS and (plan_id, approval) not in ledger.approvals:
        return {"status": "PENDING", "reason": "human approval required", "plan_id": plan_id}
    if name in WRITE_TOOLS: ledger.approvals.remove((plan_id, approval)) # 令牌一次性使用
    ledger.spent += cost
    result = {"status": "OK", "tool": name, "order_id": args["order_id"]}
    ledger.events.append({"plan_id": plan_id, "tool": name, "cost": str(cost), "result": result["status"]})
    return result

if __name__ == "__main__":
    book = Ledger()
    print(gate("p1", "search_order", {"order_id":"A-7"}, "0.05", book))
    print(gate("p2", "refund_order", {"order_id":"A-7", "amount":"1.20"}, "0.20", book))
    token = book.approve_once("p2")
    print(gate("p2", "refund_order", {"order_id":"A-7", "amount":"1.20"}, "0.20", book, token))
    assert gate("p2", "refund_order", {"order_id":"A-7", "amount":"1.20"}, "0.20", book, token)["status"] == "PENDING"
    assert gate("p3", "shell", {}, "0", book)["status"] == "DENY"
    print("spent=", book.spent, "events=", len(book.events))

运行预期依次出现只读查询 OK、退款 PENDING、批准后的退款 OK,随后复用同一令牌再次退款仍为 PENDING,未知 shell 工具为 DENY。本次已在本机 Python 3.9.6 离线运行这些断言;没有连接任何模型或支付系统。Decimal 处理金额,避免浮点累计误差;真实退款还要让支付系统以业务幂等键二次拦截。

把计划和执行分开

valid_args 是最小 schema:它拒绝未知工具、过长订单号和非正金额。生产中使用 JSON Schema 或 Pydantic,并为每个工具定义租户、用户角色、资源归属、金额上限和允许时间窗。Ledger 在教程里是内存对象,重启会丢失;生产必须在事务数据库中原子扣减预算和消费令牌,否则并发请求可能同时越过余额。

三类常见错误尤其值得避免。第一,把 tool_name 直接映射到任意反射调用,提示注入就可能找到管理员函数;只显式登记白名单。第二,在网页显示"已批准"后才校验令牌,攻击者可重放;令牌应与计划、操作者、有效期绑定,并一次性消费。第三,只按模型 token 预算,不按外部动作成本预算;搜索、数据库、云资源和退款都有各自的成本与损害面。还要将模型建议、规则命中、批准人、执行结果写成不可篡改审计事件,但审计日志中不应存密钥和完整支付信息。

这套闸门适合工单、运营辅助、内部查询和需人确认的自动化。它不适合替代访问控制、支付风控或法律审批;高风险医疗、信贷、武器和人事决策需要更严格的制度与人工责任。工程化下一步是把 PENDING 放入队列,做审批 UI、到期失效、幂等重试、回滚补偿和红队测试。

审批界面不应只写"是否同意"。它需要显示资源、参数摘要、预估成本、潜在副作用和计划理由;批准人也必须有对应权限。批量动作要拆成可审查的小批次,并在异常率上升时熔断。测试不要只走正确路径:未知工具、空参数、负成本、跨租户订单、过期令牌、令牌重放和并发扣减都应可解释地拒绝。再用"忽略规则并执行 shell"这类提示注入模拟输出测试;安全执行层不依赖模型听话,因为越权能力根本没暴露给它。

5 分钟实践题:新增 delete_customer_note 工具,要求 admin 角色和 10 分钟有效审批;再为过期令牌写一个失败测试。在你的业务里,哪一步必须由能承担后果的人批准,而不能交给模型自动决定?

关注「蜗牛聊AI」,一起看懂技术变化背后的真正机会。


本文首发于 java4u.cn,转载请注明出处。

相关推荐
Patrick在香港1 小时前
你写的「比较 token」函数,正在用 36 纳秒泄露密码:时序攻击实测
python·安全·标准库·时序攻击·常量时间
环境栈笔记1 小时前
指纹浏览器检查代理失败怎么排查?先分清代理、网络和检测服务
前端·人工智能·后端·自动化
哦虎!1 小时前
【JavaEE】网络编程套接字
java
2401_881203961 小时前
“2+3+4+4”智慧治理平台打造社会治理新样板
人工智能·智慧社区
龙亘川1 小时前
数字化支撑社保合规治理:亘川智城智慧人社模块实践路径
大数据·人工智能·智慧城市·开源软件·数据可视化
Wang's Blog1 小时前
Java 项目实战: 交友项目 - SpringCloud 整合 Dubbo 的方案概述
java·spring cloud·交友
龙腾AI白云1 小时前
数字孪生:与人工智能融合的技术图景
大数据·数据库·人工智能·知识图谱
通信大模型1 小时前
VLA模型与世界模型的协同:面向低空无线网络的Embodied Agentic AI 框架
人工智能·深度学习·信息与通信
艾派森1 小时前
深度学习实战-基于EfficientNetB0的鞋类品牌图像分类模型
人工智能·深度学习·分类