上一篇划清了“模型负责判断、程序负责执行”的边界。本篇把边界落实为可验证的工具契约:模型只能提出调用建议,参数校验、权限判断和副作用控制始终留在应用侧。

一、痛点:从“能演示”走向“可托管”

function calling 的本质不是让模型获得函数执行权,而是让模型输出一个结构化意图。一个完整调用要经过四道门:工具名必须在白名单;参数满足类型与业务约束;当前用户对目标资源有权限;高风险动作得到明确批准。Schema 只解决形状,不解决授权,例如 customer_id 是字符串,并不代表当前用户可以查询任意客户。

真正可迁移的做法,是先写任务契约再选模型:输入从哪里来,成功产物是什么,哪些动作禁止自动执行,最多允许多少步、多少时间和多少费用,失败后由谁接管。契约一旦写进代码和测试,模型升级只是实现替换,不会悄悄改变业务责任边界。

二、原理:把概率判断放进确定性边界

工具描述要同时写“什么时候用”和“什么时候不能用”。字段优先使用枚举、整数和明确格式,金额使用整数分,日期使用 ISO 8601;对象应关闭额外字段。执行器不要用反射按模型给出的名称导入模块,而应从静态注册表取函数。工具返回值也要裁剪,只把判断所需字段、来源和稳定错误码送回模型。

模型输出、工具数据和记忆内容都按不可信输入处理。每个边界都执行校验、授权、裁剪和审计;所有状态变化都有明确原因。这样做看似增加一些代码,却把“偶尔答错”拆成可定位的规划错误、参数错误、工具错误或策略错误,团队才能针对性改进。

三、实现:用独立程序跑通最小闭环

下面先把一个库存查询调用当作不可信输入。程序逐项验证工具名、商品编号、仓库范围和数量,再决定是否执行。这个模式可以迁移到 CRM 查询、日程创建和退款申请:替换 Schema 与授权函数即可,验证顺序不变。

from dataclasses import dataclass
from typing import Any

@dataclass(frozen=True)
class Context:
    allowed_warehouses: frozenset[str]

def validate_call(call: dict[str, Any], ctx: Context) -> tuple[bool, str]:
    if call.get("name") != "check_inventory":
        return False, "unknown_tool"
    args = call.get("arguments")
    if not isinstance(args, dict):
        return False, "arguments_not_object"
    if set(args) != {"sku", "warehouse", "quantity"}:
        return False, "unexpected_fields"
    if not isinstance(args["sku"], str) or not args["sku"].startswith("SKU-"):
        return False, "invalid_sku"
    if args["warehouse"] not in ctx.allowed_warehouses:
        return False, "warehouse_forbidden"
    if type(args["quantity"]) is not int or not 1 <= args["quantity"] <= 20:
        return False, "invalid_quantity"
    return True, "accepted"

ctx = Context(frozenset({"SH-A", "SH-B"}))
calls = [
    {"name": "check_inventory", "arguments": {"sku": "SKU-7", "warehouse": "SH-A", "quantity": 2}},
    {"name": "check_inventory", "arguments": {"sku": "SKU-7", "warehouse": "BJ-X", "quantity": 2}},
]
for index, call in enumerate(calls, 1):
    ok, reason = validate_call(call, ctx)
    print(f"call={index} allowed={ok} reason={reason}")

运行输出:

call=1 allowed=True reason=accepted
call=2 allowed=False reason=warehouse_forbidden

校验通过后仍不要直接产生副作用。读取类工具可以立即执行;写入类工具应先生成预览,把标准化后的参数、影响范围和幂等键展示给审批者。模型看到的错误应是 warehouse_forbidden 这类稳定类别,而不是数据库栈信息。这样提示词升级、模型切换后,安全边界仍由确定性代码保持。

第二个程序补上生产中最容易遗漏的控制点。它与前一个示例完全独立,可单独保存运行,不依赖本系列其他文件;示例数据是内存假实现,因此不会访问真实账户或产生外部副作用。

from dataclasses import dataclass

@dataclass
class ToolResult:
    code: str
    summary: str
    source: str

INVENTORY = {("SKU-7", "SH-A"): 8}

def check_inventory(sku: str, warehouse: str, quantity: int) -> ToolResult:
    available = INVENTORY.get((sku, warehouse))
    if available is None:
        return ToolResult("not_found", "没有匹配库存", "inventory:v1")
    enough = available >= quantity
    text = f"requested={quantity}, available={available}, enough={str(enough).lower()}"
    return ToolResult("ok", text, "inventory:v1")

def dispatch(name: str, arguments: dict[str, object]) -> ToolResult:
    registry = {"check_inventory": check_inventory}
    function = registry.get(name)
    if function is None:
        return ToolResult("blocked", "工具不在白名单", "executor")
    return function(**arguments)

request = {"sku": "SKU-7", "warehouse": "SH-A", "quantity": 2}
result = dispatch("check_inventory", request)
print(f"code={result.code}")
print(f"summary={result.summary}")
print(f"source={result.source}")

运行输出:

code=ok
summary=requested=2, available=8, enough=true
source=inventory:v1

落地时应把示例中的内存状态替换为事务型存储,把打印日志替换为结构化事件,但不要改变契约。事件至少包含 run_id、步骤、输入摘要、策略结果、耗时、错误类别和版本;密钥、完整个人信息及未经脱敏的提示不得进入日志。

四、踩坑:失败路径决定系统上限

常见失败有三类。第一,把参数解析成功误当成业务合法,遗漏租户隔离与资源授权;第二,把工具返回的网页或数据库文本视为可信指令,导致间接提示注入;第三,失败后让模型随意改参数重试,造成越权范围逐步扩大。解决办法是授权绑定用户与资源,结果使用数据通道而非指令通道,重试只能修改预先允许的字段。

还要防止把确定逻辑模型化。金额计算、权限判断、枚举路由和状态转移应使用普通代码;模型适合处理分类、抽取、歧义消解与证据综合。每减少一个无必要的模型决策点,就减少一处延迟、成本和不可复现性,同时让测试更容易覆盖。

五、验证:用轨迹和门槛验收

验收应保存模型原始调用、标准化参数、策略判定、工具结果摘要和最终回答。测试至少覆盖未知工具、额外字段、错误类型、越权资源、空结果、下游超时和重复写入。只有非法调用拒绝率为百分之百、合法调用成功率达到门槛、日志不含敏感值,才进入下一篇的循环执行。

发布顺序固定为离线回放、影子流量、小比例灰度和有限自治。每阶段先定义通过线和回滚条件,再看结果;不能在看到分数后移动门槛。关键样本要保存初始状态、每步动作、观察、最终产物与终止原因,模型、提示和工具契约版本也必须一起记录。

下一篇会把一次安全调用放进多轮任务循环;本篇的白名单、参数校验与审计记录将成为每一轮都不可绕过的执行门。

参考来源


👍 觉得有用就点个 赞 + 收藏,方便回头查阅;有疑问直接在评论区留言,我看到都会回。

🚀 本文属于 《AI Agent 应用实战》 系列,持续更新,关注不迷路。

📌 文章里的代码都能直接跑。想要可直接 clone 的完整工程 + 配套部署脚本 / 踩坑清单?评论一声或发邮件到 cj2664@qq.com,我免费发你。
如果你正好在做类似系统、或有工程化难题想找人做,也欢迎邮件聊一句——我按实际情况评估,能落地的就接单或出方案。评论和邮件都能直接找到我,不用跳别的平台。

更多推荐