Function Calling 容易给人一种错觉:模型已经可以操作系统了。实际情况是,模型只生成“建议调用哪个工具、使用什么参数”。真正执行函数的仍是应用代码。
这个区别决定了安全责任。模型可以提出查询订单,但它不能绕过登录状态,也不能因为用户一句话就重启服务。
一次完整调用有两次模型请求
以只读工具 get_order_status 为例:
- 应用把用户问题和工具 Schema 发给模型;
- 模型返回工具名与 JSON 参数;
- 应用校验工具名、参数、用户权限和调用频率;
- 应用执行查询,把结构化结果作为 tool 消息回传;
- 模型根据工具结果生成面向用户的答复。
少了第二次请求,用户只能看到函数结果,模型无法把结果组织成自然回复。少了应用校验,则相当于把外部输入直接接到了内部函数。
Schema 是输入约束,不代表绝对安全
工具定义可以限制 order_id 必须是字符串并要求该字段存在,但模型仍可能返回不存在的工具、额外参数或越权订单号。应用需要使用允许名单,并按当前登录用户重新检查资源归属。
import logging
LOGGER = logging.getLogger(__name__)
ALLOWED_TOOLS = {"get_order_status"}
def validate_tool_call(name: str, arguments: dict, user_id: str) -> str:
"""校验工具调用;arguments 是模型参数,user_id 是已认证用户。"""
try:
# 工具名必须来自应用允许名单,不能按模型文本动态查找函数。
if name not in ALLOWED_TOOLS:
raise ValueError(f"不允许的工具:{name}")
order_id = arguments.get("order_id", "").strip()
if not order_id or len(order_id) > 64:
raise ValueError("order_id 格式无效")
# 此处还要查询数据库,确认订单属于当前已认证用户。
if not order_belongs_to_user(order_id, user_id):
raise PermissionError("当前用户无权查询该订单")
return order_id
except (ValueError, PermissionError):
LOGGER.warning("工具参数或权限校验失败", exc_info=True)
raise
不要使用 globals()[name]() 或拼接 shell 命令来执行模型返回值。工具路由应是显式字典,每个入口都有固定参数模型、超时和错误处理。
高风险动作要多一道门
查询天气和查询订单通常是只读操作。删除文件、恢复备份、重启服务和发送邮件会改变外部状态,需要更严格的流程:展示将要执行的对象和影响,取得明确确认,再由有权限的应用身份执行。
“模型判断有必要”不能替代授权。确认信息也不能只写“是否继续”,应包含精确对象,例如服务名、环境、动作和预期影响,避免用户确认了另一件事。
工具结果也不能直接相信
工具可能超时、返回空值,或者产生与 Schema 不一致的数据。回传模型前先做结构校验,并标记数据来源和时间。错误信息只提供解决问题所需内容,不把数据库连接串、内部堆栈和密钥交给模型。
日志至少记录请求关联 ID、工具名、校验结果、耗时和错误类型。参数中如果有个人信息,应做脱敏。高风险动作还要记录谁确认、确认了什么,但不要把认证凭据写进日志。
验收这条链路
准备正常参数、缺失参数、越权资源、不存在工具、工具超时和用户拒绝确认等样例。检查应用是否在正确阶段停止,并确认模型无论怎样输出,都不能绕过允许名单和权限检查。
能通过这些失败样例,Function Calling 才算一条可控的应用链路。