Policy:划清 Agent 的行动边界
旅游助手能查天气、搜景点、估路线了。现在你给它加了 book_ticket、send_email、delete_order 这些工具。问题马上出来:模型想帮忙,帮着帮着就自己下单了,或者把用户护照号发给了第三方接口。
它不是故意越界。它只是没有边界。
Policy 回答的问题很简单:
这个工具调用允不允许?这些参数合不合规?
不要只在 prompt 里写"你不能做危险的事"。模型会忘,会被绕。边界必须是 Python 代码里的硬检查。
没有 Policy 会怎么坏
| 问题 | 表面看起来 | 实际风险 |
|---|---|---|
用户问"帮我订票",模型直接调 book_ticket |
回复很快、体验好 | 用户还没确认价格就扣款了 |
| 模型把 city 参数填成海外城市 | 看起来是正常查询 | 触发了你没对接的境外 API,返回错误或产生费用 |
模型调了 delete_order "帮用户清理" |
看起来是贴心服务 | 删掉了不可恢复的订单 |
| prompt 里写了"不要调 delete" | 短期有效 | 换个 prompt 注入就绕过了 |
流程
flowchart TD
A["Agent 提出工具调用"] --> P["Policy 引擎检查工具名 + 参数"]
P -->|通过| T["Python 执行工具"]
T --> O["工具结果写回对话"]
P -->|拒绝| B["返回拒绝原因"]
B --> R["Agent 换计划或提示用户"]
完整代码
一个白名单 + 黑名单 + 参数校验的 Policy 引擎。旅游助手场景:只允许查天气和搜景点,订票只能杭州标准票且价格不超过 500,删除类工具一律禁止。
from __future__ import annotations
import json
from dataclasses import dataclass
# ========== Policy 规则定义 ==========
@dataclass
class ToolPolicy:
"""单个工具的策略规则。"""
allowed: bool = True
max_price: float | None = None
allowed_cities: list[str] | None = None
allowed_ticket_types: list[str] | None = None
reason: str = ""
# 黑名单:这些工具无论什么情况都不许调
BLACKLIST = {"delete_order", "send_passport", "drop_database"}
# 白名单 + 参数规则
TOOL_POLICIES: dict[str, ToolPolicy] = {
"get_weather": ToolPolicy(),
"search_places": ToolPolicy(),
"estimate_route": ToolPolicy(),
"book_ticket": ToolPolicy(
allowed=True,
max_price=500.0,
allowed_cities=["杭州"],
allowed_ticket_types=["standard"],
reason="订票仅限杭州标准票,单价不超过 500",
),
}
@dataclass
class PolicyResult:
allowed: bool
tool: str
args: dict
reason: str = ""
def check_policy(tool_name: str, args: dict) -> PolicyResult:
"""检查一次工具调用是否符合策略。"""
# 1. 黑名单直接拒绝
if tool_name in BLACKLIST:
return PolicyResult(
allowed=False,
tool=tool_name,
args=args,
reason=f"工具 {tool_name} 在黑名单中,禁止调用",
)
# 2. 不在白名单也拒绝
policy = TOOL_POLICIES.get(tool_name)
if policy is None:
return PolicyResult(
allowed=False,
tool=tool_name,
args=args,
reason=f"工具 {tool_name} 未注册,不在白名单中",
)
if not policy.allowed:
return PolicyResult(
allowed=False,
tool=tool_name,
args=args,
reason=f"工具 {tool_name} 当前被禁用",
)
# 3. 参数级校验
if policy.allowed_cities is not None:
city = args.get("city", "")
if city not in policy.allowed_cities:
return PolicyResult(
allowed=False,
tool=tool_name,
args=args,
reason=f"城市 {city} 不在允许范围 {policy.allowed_cities}",
)
if policy.max_price is not None:
price = args.get("price", 0)
if price > policy.max_price:
return PolicyResult(
allowed=False,
tool=tool_name,
args=args,
reason=f"价格 {price} 超过上限 {policy.max_price}",
)
if policy.allowed_ticket_types is not None:
ticket_type = args.get("ticket_type", "")
if ticket_type not in policy.allowed_ticket_types:
return PolicyResult(
allowed=False,
tool=tool_name,
args=args,
reason=f"票型 {ticket_type} 不在允许范围 {policy.allowed_ticket_types}",
)
return PolicyResult(allowed=True, tool=tool_name, args=args, reason="通过")
# ========== 模拟 Agent 调用 ==========
def simulate_agent_calls():
"""模拟一组工具调用,展示 Policy 引擎的拦截效果。"""
test_calls = [
("get_weather", {"city": "杭州"}),
("search_places", {"city": "杭州", "constraint": "下午有雨"}),
("book_ticket", {"city": "杭州", "ticket_type": "standard", "price": 280}),
("book_ticket", {"city": "杭州", "ticket_type": "vip", "price": 1200}),
("book_ticket", {"city": "东京", "ticket_type": "standard", "price": 3500}),
("delete_order", {"order_id": "ORD-20250601"}),
("hack_system", {"cmd": "rm -rf /"}),
]
print("=" * 60)
print("Policy 引擎测试")
print("=" * 60)
for tool_name, args in test_calls:
result = check_policy(tool_name, args)
status = "允许" if result.allowed else "拒绝"
print(f"\n调用: {tool_name}({json.dumps(args, ensure_ascii=False)})")
print(f" 结果: {status}")
print(f" 原因: {result.reason}")
if __name__ == "__main__":
simulate_agent_calls()
运行输出:
============================================================
Policy 引擎测试
============================================================
调用: get_weather({"city": "杭州"})
结果: 允许
原因: 通过
调用: search_places({"city": "杭州", "constraint": "下午有雨"})
结果: 允许
原因: 通过
调用: book_ticket({"city": "杭州", "ticket_type": "standard", "price": 280})
结果: 允许
原因: 通过
调用: book_ticket({"city": "杭州", "ticket_type": "vip", "price": 1200})
结果: 拒绝
原因: 票型 vip 不在允许范围 ['standard']
调用: book_ticket({"city": "东京", "ticket_type": "standard", "price": 3500})
结果: 拒绝
原因: 城市 东京 不在允许范围 ['杭州']
调用: delete_order({"order_id": "ORD-20250601"})
结果: 拒绝
原因: 工具 delete_order 在黑名单中,禁止调用
调用: hack_system({"cmd": "rm -rf /"})
结果: 拒绝
原因: 工具 hack_system 未注册,不在白名单中
代码走读
三层防线:
- 黑名单:
delete_order、send_passport这类工具,无论参数是什么,一律拒绝。这是最硬的一层。 - 白名单:没有在
TOOL_POLICIES里注册的工具名,默认拒绝。模型编造一个hack_system也会被挡住。 - 参数校验:工具允许调用,但参数不对也拒绝。
book_ticket可以用,但只能订杭州的标准票,单价不超过 500。
check_policy 返回的 PolicyResult 带着 reason 字段。这个 reason 有两个用途:
- 写进日志,方便事后审计。
- 返回给 Agent,让模型知道为什么被拒了,好调整计划。
和 Guardrails 的区别
| Policy | Guardrails | |
|---|---|---|
| 检查时机 | 工具调用之前 | 运行时任意位置 |
| 检查对象 | 工具名 + 参数 | 输入、输出、中间状态 |
| 典型问题 | 该不该调这个工具 | 内容有没有注入、泄露、超长 |
| 违规后果 | 直接拒绝,不执行 | 拦截、降级、上报 |
Policy 管权限,Guardrails 管运行时安全。两个不是同一件事。
什么时候用
- 工具会影响真实世界:付款、订票、发邮件、删数据。
- 不同用户有不同权限(普通用户不能删订单,管理员可以)。
- 工具有成本或调用频率限制。
- 你需要审计日志来追溯"为什么这个调用被允许/拒绝了"。
什么时候不需要
如果系统只生成文本、没有工具,Policy 可以先不做。但只要有一个工具能改变外部状态,就从白名单开始。不要让模型自己给自己划边界。
常见坑
| 坑 | 现象 | 修法 |
|---|---|---|
| 只在 prompt 里写"不要越界" | 模型偶尔会忘、会被注入绕过 | 用 Python 代码检查,不依赖 prompt |
| 只检查工具名不检查参数 | 工具对了但参数越界 | 加参数级规则 |
| 白名单开得太大 | 开发时方便,上线后裸奔 | 最小权限原则,逐步开放 |
| 拒绝了但不告诉 Agent 为什么 | Agent 反复重试同样的调用 | 返回 reason,让模型调整计划 |
| 不记日志 | 出事以后查不到哪一步越界 | 每次检查都写日志 |
读完以后
Policy 解决了"该不该调这个工具"。但有些问题不是权限问题——工具返回的内容里可能藏着 prompt 注入,最终输出里可能泄露敏感信息。
这些运行时的危险,读下一节 Guardrails。